@owenlamont/ryl 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/bin/ryl.js +195 -1
- package/package.json +35 -13
- package/.github/CODEOWNERS +0 -1
- package/.github/dependabot.yml +0 -13
- package/.github/workflows/ci.yml +0 -107
- package/.github/workflows/release.yml +0 -613
- package/.github/workflows/update_dependencies.yml +0 -61
- package/.github/workflows/update_linters.yml +0 -56
- package/.pre-commit-config.yaml +0 -87
- package/.yamllint +0 -4
- package/AGENTS.md +0 -200
- package/Cargo.lock +0 -908
- package/Cargo.toml +0 -32
- package/clippy.toml +0 -1
- package/docs/config-presets.md +0 -100
- package/img/benchmark-5x5-5runs.svg +0 -2176
- package/pyproject.toml +0 -42
- package/ruff.toml +0 -107
- package/rumdl.toml +0 -20
- package/rust-toolchain.toml +0 -3
- package/rustfmt.toml +0 -3
- package/scripts/benchmark_perf_vs_yamllint.py +0 -400
- package/scripts/coverage-missing.ps1 +0 -80
- package/scripts/coverage-missing.sh +0 -60
- package/src/bin/discover_config_bin.rs +0 -24
- package/src/cli_support.rs +0 -33
- package/src/conf/mod.rs +0 -85
- package/src/config.rs +0 -2099
- package/src/decoder.rs +0 -326
- package/src/discover.rs +0 -31
- package/src/lib.rs +0 -19
- package/src/lint.rs +0 -558
- package/src/main.rs +0 -535
- package/src/migrate.rs +0 -233
- package/src/rules/anchors.rs +0 -517
- package/src/rules/braces.rs +0 -77
- package/src/rules/brackets.rs +0 -77
- package/src/rules/colons.rs +0 -475
- package/src/rules/commas.rs +0 -372
- package/src/rules/comments.rs +0 -299
- package/src/rules/comments_indentation.rs +0 -243
- package/src/rules/document_end.rs +0 -175
- package/src/rules/document_start.rs +0 -84
- package/src/rules/empty_lines.rs +0 -152
- package/src/rules/empty_values.rs +0 -255
- package/src/rules/float_values.rs +0 -259
- package/src/rules/flow_collection.rs +0 -562
- package/src/rules/hyphens.rs +0 -104
- package/src/rules/indentation.rs +0 -803
- package/src/rules/key_duplicates.rs +0 -218
- package/src/rules/key_ordering.rs +0 -303
- package/src/rules/line_length.rs +0 -326
- package/src/rules/mod.rs +0 -25
- package/src/rules/new_line_at_end_of_file.rs +0 -23
- package/src/rules/new_lines.rs +0 -95
- package/src/rules/octal_values.rs +0 -121
- package/src/rules/quoted_strings.rs +0 -577
- package/src/rules/span_utils.rs +0 -37
- package/src/rules/trailing_spaces.rs +0 -65
- package/src/rules/truthy.rs +0 -420
- package/tests/brackets_carriage_return.rs +0 -114
- package/tests/build_global_cfg_error.rs +0 -23
- package/tests/cli_anchors_rule.rs +0 -143
- package/tests/cli_braces_rule.rs +0 -104
- package/tests/cli_brackets_rule.rs +0 -104
- package/tests/cli_colons_rule.rs +0 -65
- package/tests/cli_commas_rule.rs +0 -104
- package/tests/cli_comments_indentation_rule.rs +0 -61
- package/tests/cli_comments_rule.rs +0 -67
- package/tests/cli_config_data_error.rs +0 -30
- package/tests/cli_config_flags.rs +0 -66
- package/tests/cli_config_migrate.rs +0 -229
- package/tests/cli_document_end_rule.rs +0 -92
- package/tests/cli_document_start_rule.rs +0 -92
- package/tests/cli_empty_lines_rule.rs +0 -87
- package/tests/cli_empty_values_rule.rs +0 -68
- package/tests/cli_env_config.rs +0 -34
- package/tests/cli_exit_and_errors.rs +0 -41
- package/tests/cli_file_encoding.rs +0 -203
- package/tests/cli_float_values_rule.rs +0 -64
- package/tests/cli_format_options.rs +0 -316
- package/tests/cli_global_cfg_relaxed.rs +0 -20
- package/tests/cli_hyphens_rule.rs +0 -104
- package/tests/cli_indentation_rule.rs +0 -65
- package/tests/cli_invalid_project_config.rs +0 -39
- package/tests/cli_key_duplicates_rule.rs +0 -104
- package/tests/cli_key_ordering_rule.rs +0 -59
- package/tests/cli_line_length_rule.rs +0 -85
- package/tests/cli_list_files.rs +0 -29
- package/tests/cli_new_line_rule.rs +0 -141
- package/tests/cli_new_lines_rule.rs +0 -119
- package/tests/cli_octal_values_rule.rs +0 -60
- package/tests/cli_quoted_strings_rule.rs +0 -47
- package/tests/cli_toml_config.rs +0 -119
- package/tests/cli_trailing_spaces_rule.rs +0 -77
- package/tests/cli_truthy_rule.rs +0 -83
- package/tests/cli_yaml_files_negation.rs +0 -45
- package/tests/colons_rule.rs +0 -303
- package/tests/common/compat.rs +0 -114
- package/tests/common/fake_env.rs +0 -93
- package/tests/common/mod.rs +0 -1
- package/tests/conf_builtin.rs +0 -9
- package/tests/config_anchors.rs +0 -84
- package/tests/config_braces.rs +0 -121
- package/tests/config_brackets.rs +0 -127
- package/tests/config_commas.rs +0 -79
- package/tests/config_comments.rs +0 -65
- package/tests/config_comments_indentation.rs +0 -20
- package/tests/config_deep_merge_nonstring_key.rs +0 -24
- package/tests/config_document_end.rs +0 -54
- package/tests/config_document_start.rs +0 -55
- package/tests/config_empty_lines.rs +0 -48
- package/tests/config_empty_values.rs +0 -35
- package/tests/config_env_errors.rs +0 -23
- package/tests/config_env_invalid_inline.rs +0 -15
- package/tests/config_env_missing.rs +0 -63
- package/tests/config_env_shim.rs +0 -301
- package/tests/config_explicit_file_parse_error.rs +0 -55
- package/tests/config_extended_features.rs +0 -225
- package/tests/config_extends_inline.rs +0 -185
- package/tests/config_extends_sequence.rs +0 -18
- package/tests/config_find_project_home_boundary.rs +0 -54
- package/tests/config_find_project_two_files_in_cwd.rs +0 -47
- package/tests/config_float_values.rs +0 -34
- package/tests/config_from_yaml_paths.rs +0 -32
- package/tests/config_hyphens.rs +0 -51
- package/tests/config_ignore_errors.rs +0 -243
- package/tests/config_ignore_overrides.rs +0 -83
- package/tests/config_indentation.rs +0 -65
- package/tests/config_invalid_globs.rs +0 -16
- package/tests/config_invalid_types.rs +0 -19
- package/tests/config_key_duplicates.rs +0 -34
- package/tests/config_key_ordering.rs +0 -70
- package/tests/config_line_length.rs +0 -65
- package/tests/config_locale.rs +0 -111
- package/tests/config_merge.rs +0 -26
- package/tests/config_new_lines.rs +0 -89
- package/tests/config_octal_values.rs +0 -33
- package/tests/config_quoted_strings.rs +0 -195
- package/tests/config_rule_level.rs +0 -147
- package/tests/config_rules_non_string_keys.rs +0 -23
- package/tests/config_scalar_overrides.rs +0 -27
- package/tests/config_to_toml.rs +0 -110
- package/tests/config_toml_coverage.rs +0 -80
- package/tests/config_toml_discovery.rs +0 -304
- package/tests/config_trailing_spaces.rs +0 -152
- package/tests/config_truthy.rs +0 -77
- package/tests/config_yaml_files.rs +0 -62
- package/tests/config_yaml_files_all_non_string.rs +0 -15
- package/tests/config_yaml_files_empty.rs +0 -30
- package/tests/coverage_commas.rs +0 -46
- package/tests/decoder_decode.rs +0 -338
- package/tests/discover_config_bin_all.rs +0 -66
- package/tests/discover_config_bin_env_invalid_yaml.rs +0 -26
- package/tests/discover_config_bin_project_config_parse_error.rs +0 -24
- package/tests/discover_config_bin_user_global_error.rs +0 -26
- package/tests/discover_module.rs +0 -30
- package/tests/discover_per_file_dir.rs +0 -10
- package/tests/discover_per_file_project_config_error.rs +0 -21
- package/tests/float_values.rs +0 -43
- package/tests/lint_multi_errors.rs +0 -32
- package/tests/main_yaml_ok_filtering.rs +0 -30
- package/tests/migrate_module.rs +0 -259
- package/tests/resolve_ctx_empty_parent.rs +0 -16
- package/tests/rule_anchors.rs +0 -442
- package/tests/rule_braces.rs +0 -258
- package/tests/rule_brackets.rs +0 -217
- package/tests/rule_commas.rs +0 -205
- package/tests/rule_comments.rs +0 -197
- package/tests/rule_comments_indentation.rs +0 -127
- package/tests/rule_document_end.rs +0 -118
- package/tests/rule_document_start.rs +0 -60
- package/tests/rule_empty_lines.rs +0 -96
- package/tests/rule_empty_values.rs +0 -102
- package/tests/rule_float_values.rs +0 -109
- package/tests/rule_hyphens.rs +0 -65
- package/tests/rule_indentation.rs +0 -455
- package/tests/rule_key_duplicates.rs +0 -76
- package/tests/rule_key_ordering.rs +0 -207
- package/tests/rule_line_length.rs +0 -200
- package/tests/rule_new_lines.rs +0 -51
- package/tests/rule_octal_values.rs +0 -53
- package/tests/rule_quoted_strings.rs +0 -290
- package/tests/rule_trailing_spaces.rs +0 -41
- package/tests/rule_truthy.rs +0 -236
- package/tests/user_global_invalid_yaml.rs +0 -32
- package/tests/yamllint_compat_anchors.rs +0 -280
- package/tests/yamllint_compat_braces.rs +0 -411
- package/tests/yamllint_compat_brackets.rs +0 -364
- package/tests/yamllint_compat_colons.rs +0 -298
- package/tests/yamllint_compat_colors.rs +0 -80
- package/tests/yamllint_compat_commas.rs +0 -375
- package/tests/yamllint_compat_comments.rs +0 -167
- package/tests/yamllint_compat_comments_indentation.rs +0 -281
- package/tests/yamllint_compat_config.rs +0 -170
- package/tests/yamllint_compat_document_end.rs +0 -243
- package/tests/yamllint_compat_document_start.rs +0 -136
- package/tests/yamllint_compat_empty_lines.rs +0 -117
- package/tests/yamllint_compat_empty_values.rs +0 -179
- package/tests/yamllint_compat_float_values.rs +0 -216
- package/tests/yamllint_compat_hyphens.rs +0 -223
- package/tests/yamllint_compat_indentation.rs +0 -398
- package/tests/yamllint_compat_key_duplicates.rs +0 -139
- package/tests/yamllint_compat_key_ordering.rs +0 -170
- package/tests/yamllint_compat_line_length.rs +0 -375
- package/tests/yamllint_compat_list.rs +0 -127
- package/tests/yamllint_compat_new_line.rs +0 -133
- package/tests/yamllint_compat_newline_types.rs +0 -185
- package/tests/yamllint_compat_octal_values.rs +0 -172
- package/tests/yamllint_compat_quoted_strings.rs +0 -154
- package/tests/yamllint_compat_syntax.rs +0 -200
- package/tests/yamllint_compat_trailing_spaces.rs +0 -162
- package/tests/yamllint_compat_truthy.rs +0 -130
- package/tests/yamllint_compat_yaml_files.rs +0 -81
- package/typos.toml +0 -2
package/src/config.rs
DELETED
|
@@ -1,2099 +0,0 @@
|
|
|
1
|
-
use std::borrow::Cow;
|
|
2
|
-
use std::collections::BTreeMap;
|
|
3
|
-
use std::env;
|
|
4
|
-
use std::fs;
|
|
5
|
-
use std::path::{Path, PathBuf};
|
|
6
|
-
|
|
7
|
-
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
|
8
|
-
use regex::Regex;
|
|
9
|
-
use saphyr::{LoadableYamlNode, MappingOwned, ScalarOwned, YamlOwned};
|
|
10
|
-
use toml::{Table as TomlTable, Value as TomlValue};
|
|
11
|
-
|
|
12
|
-
use crate::{conf, decoder};
|
|
13
|
-
|
|
14
|
-
/// Abstraction over environment/filesystem to enable full test coverage.
|
|
15
|
-
/// Minimal environment abstraction used by tests to cover file system and env-var behavior.
|
|
16
|
-
pub trait Env {
|
|
17
|
-
/// Current working directory.
|
|
18
|
-
fn current_dir(&self) -> PathBuf;
|
|
19
|
-
/// Platform configuration directory (e.g., XDG config dir).
|
|
20
|
-
fn config_dir(&self) -> Option<PathBuf>;
|
|
21
|
-
/// Home directory for tilde expansion.
|
|
22
|
-
fn home_dir(&self) -> Option<PathBuf>;
|
|
23
|
-
/// Read file contents.
|
|
24
|
-
///
|
|
25
|
-
/// # Errors
|
|
26
|
-
/// Returns an error string when the file cannot be read.
|
|
27
|
-
fn read_to_string(&self, p: &Path) -> Result<String, String>;
|
|
28
|
-
fn path_exists(&self, p: &Path) -> bool;
|
|
29
|
-
fn env_var(&self, key: &str) -> Option<String>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
#[derive(Debug, Default, Clone, Copy)]
|
|
33
|
-
pub struct SystemEnv;
|
|
34
|
-
|
|
35
|
-
impl Env for SystemEnv {
|
|
36
|
-
fn current_dir(&self) -> PathBuf {
|
|
37
|
-
PathBuf::from(".")
|
|
38
|
-
}
|
|
39
|
-
fn config_dir(&self) -> Option<PathBuf> {
|
|
40
|
-
// Check XDG_CONFIG_HOME first (for cross-platform compatibility)
|
|
41
|
-
env::var("XDG_CONFIG_HOME")
|
|
42
|
-
.ok()
|
|
43
|
-
.map(PathBuf::from)
|
|
44
|
-
.or_else(dirs_next::config_dir)
|
|
45
|
-
}
|
|
46
|
-
fn home_dir(&self) -> Option<PathBuf> {
|
|
47
|
-
dirs_next::home_dir()
|
|
48
|
-
}
|
|
49
|
-
fn read_to_string(&self, p: &Path) -> Result<String, String> {
|
|
50
|
-
let bytes = match fs::read(p) {
|
|
51
|
-
Ok(data) => data,
|
|
52
|
-
Err(err) => {
|
|
53
|
-
return Err(format!(
|
|
54
|
-
"failed to read config file {}: {err}",
|
|
55
|
-
p.display()
|
|
56
|
-
));
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
match decoder::decode_bytes(&bytes) {
|
|
60
|
-
Ok(text) => Ok(text),
|
|
61
|
-
Err(err) => {
|
|
62
|
-
Err(format!("failed to read config file {}: {err}", p.display()))
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
fn path_exists(&self, p: &Path) -> bool {
|
|
67
|
-
p.exists()
|
|
68
|
-
}
|
|
69
|
-
fn env_var(&self, key: &str) -> Option<String> {
|
|
70
|
-
env::var(key).ok()
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
struct ClosureEnv<'a> {
|
|
75
|
-
get: &'a dyn Fn(&str) -> Option<String>,
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
impl Env for ClosureEnv<'_> {
|
|
79
|
-
fn current_dir(&self) -> PathBuf {
|
|
80
|
-
SystemEnv.current_dir()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
fn config_dir(&self) -> Option<PathBuf> {
|
|
84
|
-
SystemEnv.config_dir()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
fn home_dir(&self) -> Option<PathBuf> {
|
|
88
|
-
(self.get)("HOME")
|
|
89
|
-
.or_else(|| (self.get)("USERPROFILE"))
|
|
90
|
-
.map(PathBuf::from)
|
|
91
|
-
.or_else(|| SystemEnv.home_dir())
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
fn read_to_string(&self, p: &Path) -> Result<String, String> {
|
|
95
|
-
SystemEnv.read_to_string(p)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
fn path_exists(&self, p: &Path) -> bool {
|
|
99
|
-
SystemEnv.path_exists(p)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
fn env_var(&self, key: &str) -> Option<String> {
|
|
103
|
-
(self.get)(key)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/// Minimal configuration model compatible with yamllint discovery precedence.
|
|
108
|
-
#[derive(Debug, Clone)]
|
|
109
|
-
pub struct YamlLintConfig {
|
|
110
|
-
ignore_patterns: Vec<String>,
|
|
111
|
-
ignore_from_files: Vec<String>,
|
|
112
|
-
#[allow(clippy::struct_field_names)]
|
|
113
|
-
ignore_matcher: Option<Gitignore>,
|
|
114
|
-
rule_names: Vec<String>,
|
|
115
|
-
rules: std::collections::BTreeMap<String, YamlOwned>,
|
|
116
|
-
rule_filters: std::collections::BTreeMap<String, RuleFilter>,
|
|
117
|
-
yaml_file_patterns: Vec<String>,
|
|
118
|
-
yaml_matcher: Option<Gitignore>,
|
|
119
|
-
locale: Option<String>,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const DEFAULT_YAML_FILE_PATTERNS: [&str; 3] = ["*.yaml", "*.yml", ".yamllint"];
|
|
123
|
-
|
|
124
|
-
const TRUTHY_ALLOWED_VALUES: [&str; 18] = [
|
|
125
|
-
"YES", "Yes", "yes", "NO", "No", "no", "TRUE", "True", "true", "FALSE", "False",
|
|
126
|
-
"false", "ON", "On", "on", "OFF", "Off", "off",
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
const TRUTHY_ALLOWED_VALUES_DISPLAY: &str = "['YES', 'Yes', 'yes', 'NO', 'No', 'no', 'TRUE', 'True', 'true', 'FALSE', 'False', 'false', 'ON', 'On', 'on', 'OFF', 'Off', 'off']";
|
|
130
|
-
|
|
131
|
-
#[derive(Debug, Clone, Default)]
|
|
132
|
-
struct RuleFilter {
|
|
133
|
-
patterns: Vec<String>,
|
|
134
|
-
from_files: Vec<String>,
|
|
135
|
-
matcher: Option<Gitignore>,
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
139
|
-
pub enum RuleLevel {
|
|
140
|
-
Error,
|
|
141
|
-
Warning,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
impl RuleLevel {
|
|
145
|
-
fn parse(value: &str) -> Option<Self> {
|
|
146
|
-
match value {
|
|
147
|
-
"error" => Some(Self::Error),
|
|
148
|
-
"warning" => Some(Self::Warning),
|
|
149
|
-
_ => None,
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
impl Default for YamlLintConfig {
|
|
155
|
-
fn default() -> Self {
|
|
156
|
-
Self {
|
|
157
|
-
ignore_patterns: Vec::new(),
|
|
158
|
-
ignore_from_files: Vec::new(),
|
|
159
|
-
ignore_matcher: None,
|
|
160
|
-
rule_names: Vec::new(),
|
|
161
|
-
rules: std::collections::BTreeMap::new(),
|
|
162
|
-
rule_filters: std::collections::BTreeMap::new(),
|
|
163
|
-
yaml_file_patterns: DEFAULT_YAML_FILE_PATTERNS
|
|
164
|
-
.iter()
|
|
165
|
-
.map(|s| (*s).to_string())
|
|
166
|
-
.collect(),
|
|
167
|
-
yaml_matcher: None,
|
|
168
|
-
locale: None,
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
#[derive(Debug, Clone, Default)]
|
|
174
|
-
pub struct Overrides {
|
|
175
|
-
pub config_file: Option<PathBuf>,
|
|
176
|
-
pub config_data: Option<String>,
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
impl YamlLintConfig {
|
|
180
|
-
/// Parse configuration data without filesystem access.
|
|
181
|
-
///
|
|
182
|
-
/// # Errors
|
|
183
|
-
/// Returns an error when `extends` is used and the config requires filesystem access.
|
|
184
|
-
pub fn from_yaml_str(s: &str) -> Result<Self, String> {
|
|
185
|
-
Self::from_yaml_str_with_env(s, None, None)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
fn apply_extends(
|
|
189
|
-
&mut self,
|
|
190
|
-
node: &YamlOwned,
|
|
191
|
-
envx: Option<&dyn Env>,
|
|
192
|
-
base_dir: Option<&Path>,
|
|
193
|
-
) -> Result<(), String> {
|
|
194
|
-
let base_path = base_dir.unwrap_or_else(|| Path::new(""));
|
|
195
|
-
|
|
196
|
-
match node {
|
|
197
|
-
YamlOwned::Value(value) => {
|
|
198
|
-
if let Some(ext) = value.as_str() {
|
|
199
|
-
self.extend_from_entry(ext, envx, base_path)?;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
YamlOwned::Sequence(seq) => {
|
|
203
|
-
for item in seq {
|
|
204
|
-
if let Some(ext) = item.as_str() {
|
|
205
|
-
self.extend_from_entry(ext, envx, base_path)?;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
_ => {}
|
|
210
|
-
}
|
|
211
|
-
Ok(())
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
fn extend_from_entry(
|
|
215
|
-
&mut self,
|
|
216
|
-
entry: &str,
|
|
217
|
-
envx: Option<&dyn Env>,
|
|
218
|
-
base_dir: &Path,
|
|
219
|
-
) -> Result<(), String> {
|
|
220
|
-
if let Some(builtin) = conf::builtin(entry) {
|
|
221
|
-
let base = Self::from_yaml_str(builtin).expect("builtin preset must parse");
|
|
222
|
-
self.merge_from(base);
|
|
223
|
-
return Ok(());
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let Some(envx) = envx else {
|
|
227
|
-
return Err(format!(
|
|
228
|
-
"invalid config: extends '{entry}' requires filesystem access for resolution"
|
|
229
|
-
));
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
let resolved = resolve_extend_path(entry, envx, Some(base_dir));
|
|
233
|
-
if is_toml_path(&resolved) {
|
|
234
|
-
return Err(format!(
|
|
235
|
-
"invalid config: extends cannot reference TOML configuration {}",
|
|
236
|
-
resolved.display()
|
|
237
|
-
));
|
|
238
|
-
}
|
|
239
|
-
let data = match envx.read_to_string(&resolved) {
|
|
240
|
-
Ok(text) => text,
|
|
241
|
-
Err(err) => {
|
|
242
|
-
return Err(format!(
|
|
243
|
-
"failed to read extended config {}: {err}",
|
|
244
|
-
resolved.display()
|
|
245
|
-
));
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
let parent_dir = resolved
|
|
249
|
-
.parent()
|
|
250
|
-
.map_or_else(|| base_dir.to_path_buf(), Path::to_path_buf);
|
|
251
|
-
let base = Self::from_yaml_str_with_env(&data, Some(envx), Some(&parent_dir))?;
|
|
252
|
-
self.merge_from(base);
|
|
253
|
-
Ok(())
|
|
254
|
-
}
|
|
255
|
-
#[must_use]
|
|
256
|
-
pub fn ignore_patterns(&self) -> &[String] {
|
|
257
|
-
&self.ignore_patterns
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
#[must_use]
|
|
261
|
-
pub fn rule_names(&self) -> &[String] {
|
|
262
|
-
&self.rule_names
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
#[must_use]
|
|
266
|
-
pub fn rule_level(&self, rule: &str) -> Option<RuleLevel> {
|
|
267
|
-
let value = self.rules.get(rule)?;
|
|
268
|
-
determine_rule_level(value)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
#[must_use]
|
|
272
|
-
pub fn rule_option_str(&self, rule: &str, option: &str) -> Option<&str> {
|
|
273
|
-
let node = self.rules.get(rule)?;
|
|
274
|
-
let map = node.as_mapping()?;
|
|
275
|
-
for (key, value) in map {
|
|
276
|
-
if key.as_str() == Some(option) {
|
|
277
|
-
return value.as_str();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
None
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
#[must_use]
|
|
284
|
-
pub fn rule_option(&self, rule: &str, option: &str) -> Option<&YamlOwned> {
|
|
285
|
-
let node = self.rules.get(rule)?;
|
|
286
|
-
let map = node.as_mapping()?;
|
|
287
|
-
for (key, value) in map {
|
|
288
|
-
if key.as_str() == Some(option) {
|
|
289
|
-
return Some(value);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
None
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
#[must_use]
|
|
296
|
-
pub fn locale(&self) -> Option<&str> {
|
|
297
|
-
self.locale.as_deref()
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
fn build_yaml_matcher(&mut self, base_dir: &Path) {
|
|
301
|
-
if self.yaml_file_patterns.is_empty() {
|
|
302
|
-
self.yaml_matcher = None;
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let mut builder = GitignoreBuilder::new(base_dir);
|
|
307
|
-
builder.allow_unclosed_class(false);
|
|
308
|
-
for pat in &self.yaml_file_patterns {
|
|
309
|
-
let normalized = pat.trim_end_matches(['\r']);
|
|
310
|
-
let _ = builder.add_line(None, normalized);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
self.yaml_matcher = builder.build().ok();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
fn refresh_rule_filter(&mut self, rule: &str) {
|
|
317
|
-
let node = self
|
|
318
|
-
.rules
|
|
319
|
-
.get(rule)
|
|
320
|
-
.expect("refresh_rule_filter should only be called for existing rules");
|
|
321
|
-
|
|
322
|
-
if node.as_mapping().is_none() {
|
|
323
|
-
self.rule_filters.remove(rule);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let patterns = node
|
|
328
|
-
.as_mapping_get("ignore")
|
|
329
|
-
.map(|n| {
|
|
330
|
-
load_ignore_patterns(n)
|
|
331
|
-
.expect("ignore patterns validated during parsing")
|
|
332
|
-
})
|
|
333
|
-
.unwrap_or_default();
|
|
334
|
-
let from_files = node
|
|
335
|
-
.as_mapping_get("ignore-from-file")
|
|
336
|
-
.map(|n| {
|
|
337
|
-
load_ignore_from_files(n)
|
|
338
|
-
.expect("ignore-from-file entries validated during parsing")
|
|
339
|
-
})
|
|
340
|
-
.unwrap_or_default();
|
|
341
|
-
|
|
342
|
-
let filter = self.rule_filters.entry(rule.to_owned()).or_default();
|
|
343
|
-
filter.patterns = patterns;
|
|
344
|
-
filter.from_files = from_files;
|
|
345
|
-
filter.matcher = None;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/// Returns true when `path` should be ignored according to config patterns.
|
|
349
|
-
/// Matching is performed on the path relative to `base_dir`.
|
|
350
|
-
#[must_use]
|
|
351
|
-
pub fn is_file_ignored(&self, path: &Path, base_dir: &Path) -> bool {
|
|
352
|
-
let Some(matcher) = &self.ignore_matcher else {
|
|
353
|
-
return false;
|
|
354
|
-
};
|
|
355
|
-
let rel = path.strip_prefix(base_dir).unwrap_or(path);
|
|
356
|
-
let direct = matcher.matched(rel, false);
|
|
357
|
-
if direct.is_whitelist() {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
if direct.is_ignore() {
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
matcher.matched_path_or_any_parents(rel, false).is_ignore()
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
#[must_use]
|
|
367
|
-
pub fn is_rule_ignored(&self, rule: &str, path: &Path, base_dir: &Path) -> bool {
|
|
368
|
-
let Some(filter) = self.rule_filters.get(rule) else {
|
|
369
|
-
return false;
|
|
370
|
-
};
|
|
371
|
-
let Some(matcher) = &filter.matcher else {
|
|
372
|
-
return false;
|
|
373
|
-
};
|
|
374
|
-
let rel = path.strip_prefix(base_dir).unwrap_or(path);
|
|
375
|
-
let direct = matcher.matched(rel, false);
|
|
376
|
-
if direct.is_whitelist() {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
if direct.is_ignore() {
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
matcher.matched_path_or_any_parents(rel, false).is_ignore()
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
#[must_use]
|
|
386
|
-
pub fn is_yaml_candidate(&self, path: &Path, base_dir: &Path) -> bool {
|
|
387
|
-
if let Some(matcher) = &self.yaml_matcher {
|
|
388
|
-
let rel: Cow<'_, Path> = path.strip_prefix(base_dir).map_or_else(
|
|
389
|
-
|_| Cow::Owned(path.file_name().map(PathBuf::from).unwrap_or_default()),
|
|
390
|
-
Cow::Borrowed,
|
|
391
|
-
);
|
|
392
|
-
let matched =
|
|
393
|
-
matcher.matched_path_or_any_parents(rel.as_ref(), path.is_dir());
|
|
394
|
-
return matched.is_ignore();
|
|
395
|
-
}
|
|
396
|
-
crate::discover::is_yaml_path(path)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
fn from_yaml_str_with_env(
|
|
400
|
-
s: &str,
|
|
401
|
-
envx: Option<&dyn Env>,
|
|
402
|
-
base_dir: Option<&Path>,
|
|
403
|
-
) -> Result<Self, String> {
|
|
404
|
-
let docs = YamlOwned::load_from_str(s)
|
|
405
|
-
.map_err(|e| format!("failed to parse config data: {e}"))?;
|
|
406
|
-
Self::from_doc_with_env(&docs[0], envx, base_dir, true)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
fn from_toml_str_with_env(
|
|
410
|
-
s: &str,
|
|
411
|
-
envx: Option<&dyn Env>,
|
|
412
|
-
base_dir: Option<&Path>,
|
|
413
|
-
pyproject: bool,
|
|
414
|
-
) -> Result<Option<Self>, String> {
|
|
415
|
-
let toml = s
|
|
416
|
-
.parse::<TomlTable>()
|
|
417
|
-
.map_err(|e| format!("failed to parse config data: {e}"))?;
|
|
418
|
-
|
|
419
|
-
if pyproject {
|
|
420
|
-
let Some(doc) = toml
|
|
421
|
-
.get("tool")
|
|
422
|
-
.and_then(|tool| tool.get("ryl"))
|
|
423
|
-
.map(toml_value_to_yaml_owned)
|
|
424
|
-
else {
|
|
425
|
-
return Ok(None);
|
|
426
|
-
};
|
|
427
|
-
return Self::from_doc_with_env(&doc, envx, base_dir, false).map(Some);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
let doc = toml_value_to_yaml_owned(&TomlValue::Table(toml));
|
|
431
|
-
Self::from_doc_with_env(&doc, envx, base_dir, false).map(Some)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
fn from_doc_with_env(
|
|
435
|
-
doc: &YamlOwned,
|
|
436
|
-
envx: Option<&dyn Env>,
|
|
437
|
-
base_dir: Option<&Path>,
|
|
438
|
-
allow_extends: bool,
|
|
439
|
-
) -> Result<Self, String> {
|
|
440
|
-
let mut cfg = Self::default();
|
|
441
|
-
|
|
442
|
-
if doc.as_mapping().is_none() {
|
|
443
|
-
return Err("invalid config: not a mapping".to_string());
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if let Some(extends) = doc.as_mapping_get("extends") {
|
|
447
|
-
if !allow_extends {
|
|
448
|
-
return Err(
|
|
449
|
-
"invalid config: extends is not supported in TOML configuration"
|
|
450
|
-
.into(),
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
cfg.apply_extends(extends, envx, base_dir)?;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
let ignore = doc.as_mapping_get("ignore");
|
|
457
|
-
let ignore_from_file = doc.as_mapping_get("ignore-from-file");
|
|
458
|
-
if ignore.is_some() && ignore_from_file.is_some() {
|
|
459
|
-
return Err(
|
|
460
|
-
"invalid config: ignore and ignore-from-file keys cannot be used together"
|
|
461
|
-
.to_string(),
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if let Some(node) = ignore {
|
|
466
|
-
cfg.ignore_patterns.clear();
|
|
467
|
-
cfg.ignore_from_files.clear();
|
|
468
|
-
let mut patterns = load_ignore_patterns(node)?;
|
|
469
|
-
cfg.ignore_patterns.append(&mut patterns);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if let Some(node) = ignore_from_file {
|
|
473
|
-
cfg.ignore_patterns.clear();
|
|
474
|
-
cfg.ignore_from_files = load_ignore_from_files(node)?;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
let yaml_files = doc.as_mapping_get("yaml-files");
|
|
478
|
-
if let Some(yf) = yaml_files
|
|
479
|
-
&& let Some(seq) = yf.as_sequence()
|
|
480
|
-
{
|
|
481
|
-
cfg.yaml_file_patterns.clear();
|
|
482
|
-
for it in seq {
|
|
483
|
-
let Some(s) = it.as_str() else {
|
|
484
|
-
return Err(
|
|
485
|
-
"invalid config: yaml-files should be a list of file patterns"
|
|
486
|
-
.to_string(),
|
|
487
|
-
);
|
|
488
|
-
};
|
|
489
|
-
cfg.yaml_file_patterns.push(s.to_owned());
|
|
490
|
-
}
|
|
491
|
-
} else if yaml_files.is_some() {
|
|
492
|
-
return Err(
|
|
493
|
-
"invalid config: yaml-files should be a list of file patterns"
|
|
494
|
-
.to_string(),
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if let Some(locale) = doc.as_mapping_get("locale") {
|
|
499
|
-
let Some(loc) = locale.as_str() else {
|
|
500
|
-
return Err("invalid config: locale should be a string".to_string());
|
|
501
|
-
};
|
|
502
|
-
cfg.locale = Some(loc.to_owned());
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if let Some(rules) = doc.as_mapping_get("rules")
|
|
506
|
-
&& let Some(map) = rules.as_mapping()
|
|
507
|
-
{
|
|
508
|
-
for (k, v) in map {
|
|
509
|
-
let Some(name) = k.as_str() else {
|
|
510
|
-
continue;
|
|
511
|
-
};
|
|
512
|
-
validate_rule_value(name, v)?;
|
|
513
|
-
if let Some(dst) = cfg.rules.get_mut(name) {
|
|
514
|
-
deep_merge_yaml_owned(dst, v);
|
|
515
|
-
} else {
|
|
516
|
-
cfg.rules.insert(name.to_owned(), v.clone());
|
|
517
|
-
}
|
|
518
|
-
cfg.refresh_rule_filter(name);
|
|
519
|
-
let mut seen = false;
|
|
520
|
-
for e in &cfg.rule_names {
|
|
521
|
-
if e == name {
|
|
522
|
-
seen = true;
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if !seen {
|
|
527
|
-
cfg.rule_names.push(name.to_owned());
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
Ok(cfg)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
fn merge_from(&mut self, mut other: Self) {
|
|
536
|
-
// Merge ignore patterns (append, then dedup later during matcher build)
|
|
537
|
-
self.ignore_patterns.append(&mut other.ignore_patterns);
|
|
538
|
-
self.ignore_from_files.append(&mut other.ignore_from_files);
|
|
539
|
-
// Merge rules deeply and accumulate names
|
|
540
|
-
for (name, val) in other.rules {
|
|
541
|
-
if let Some(dst) = self.rules.get_mut(&name) {
|
|
542
|
-
deep_merge_yaml_owned(dst, &val);
|
|
543
|
-
} else {
|
|
544
|
-
self.rules.insert(name.clone(), val.clone());
|
|
545
|
-
}
|
|
546
|
-
self.refresh_rule_filter(&name);
|
|
547
|
-
if !self.rule_names.iter().any(|e| e == &name) {
|
|
548
|
-
self.rule_names.push(name);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
if !other.yaml_file_patterns.is_empty() {
|
|
552
|
-
self.yaml_file_patterns = other.yaml_file_patterns;
|
|
553
|
-
}
|
|
554
|
-
if self.locale.is_none() {
|
|
555
|
-
self.locale = other.locale;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/// Render the effective configuration as TOML.
|
|
560
|
-
///
|
|
561
|
-
/// # Errors
|
|
562
|
-
/// Returns an error if a value cannot be represented in TOML.
|
|
563
|
-
///
|
|
564
|
-
/// # Panics
|
|
565
|
-
/// Panics if serializing a valid TOML value fails unexpectedly.
|
|
566
|
-
pub fn to_toml_string(&self) -> Result<String, String> {
|
|
567
|
-
let mut root = toml::map::Map::new();
|
|
568
|
-
|
|
569
|
-
root.insert(
|
|
570
|
-
"yaml-files".to_string(),
|
|
571
|
-
TomlValue::Array(
|
|
572
|
-
self.yaml_file_patterns
|
|
573
|
-
.iter()
|
|
574
|
-
.map(|item| TomlValue::String(item.clone()))
|
|
575
|
-
.collect(),
|
|
576
|
-
),
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
if !self.ignore_from_files.is_empty() {
|
|
580
|
-
root.insert(
|
|
581
|
-
"ignore-from-file".to_string(),
|
|
582
|
-
TomlValue::Array(
|
|
583
|
-
self.ignore_from_files
|
|
584
|
-
.iter()
|
|
585
|
-
.map(|item| TomlValue::String(item.clone()))
|
|
586
|
-
.collect(),
|
|
587
|
-
),
|
|
588
|
-
);
|
|
589
|
-
} else if !self.ignore_patterns.is_empty() {
|
|
590
|
-
root.insert(
|
|
591
|
-
"ignore".to_string(),
|
|
592
|
-
TomlValue::Array(
|
|
593
|
-
self.ignore_patterns
|
|
594
|
-
.iter()
|
|
595
|
-
.map(|item| TomlValue::String(item.clone()))
|
|
596
|
-
.collect(),
|
|
597
|
-
),
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if let Some(locale) = &self.locale {
|
|
602
|
-
root.insert("locale".to_string(), TomlValue::String(locale.clone()));
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
let mut rules: BTreeMap<String, TomlValue> = BTreeMap::new();
|
|
606
|
-
for (name, value) in &self.rules {
|
|
607
|
-
let converted = yaml_owned_to_toml_value(value)?;
|
|
608
|
-
rules.insert(name.clone(), converted);
|
|
609
|
-
}
|
|
610
|
-
if !rules.is_empty() {
|
|
611
|
-
root.insert(
|
|
612
|
-
"rules".to_string(),
|
|
613
|
-
TomlValue::Table(toml::map::Map::from_iter(rules)),
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
Ok(toml::to_string_pretty(&TomlValue::Table(root))
|
|
618
|
-
.expect("serializing TOML Value should not fail"))
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
fn finalize(&mut self, envx: &dyn Env, base_dir: &Path) -> Result<(), String> {
|
|
622
|
-
let mut builder = GitignoreBuilder::new(base_dir);
|
|
623
|
-
builder.allow_unclosed_class(false);
|
|
624
|
-
let mut any_pattern = false;
|
|
625
|
-
|
|
626
|
-
for pat in &self.ignore_patterns {
|
|
627
|
-
let normalized = pat.trim_end_matches(['\r']);
|
|
628
|
-
if let Err(err) = builder.add_line(None, normalized) {
|
|
629
|
-
return Err(format!(
|
|
630
|
-
"invalid config: ignore pattern '{normalized}' is invalid: {err}"
|
|
631
|
-
));
|
|
632
|
-
}
|
|
633
|
-
any_pattern = true;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
let mut extra_patterns: Vec<String> = Vec::new();
|
|
637
|
-
for source in &self.ignore_from_files {
|
|
638
|
-
let source_path = Path::new(source);
|
|
639
|
-
let resolved = if source_path.is_absolute() {
|
|
640
|
-
source_path.to_path_buf()
|
|
641
|
-
} else {
|
|
642
|
-
base_dir.join(source_path)
|
|
643
|
-
};
|
|
644
|
-
let data = match envx.read_to_string(&resolved) {
|
|
645
|
-
Ok(text) => text,
|
|
646
|
-
Err(err) => {
|
|
647
|
-
return Err(format!(
|
|
648
|
-
"failed to read ignore-from-file {}: {err}",
|
|
649
|
-
resolved.display()
|
|
650
|
-
));
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
for line in data.lines() {
|
|
654
|
-
let normalized = line.trim_end_matches(['\r']);
|
|
655
|
-
if normalized.trim().is_empty() {
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
if let Err(err) = builder.add_line(Some(resolved.clone()), normalized) {
|
|
659
|
-
return Err(format!(
|
|
660
|
-
"invalid config: ignore-from-file pattern in {} is invalid: {err}",
|
|
661
|
-
resolved.display()
|
|
662
|
-
));
|
|
663
|
-
}
|
|
664
|
-
extra_patterns.push(normalized.to_string());
|
|
665
|
-
any_pattern = true;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if !extra_patterns.is_empty() {
|
|
670
|
-
self.ignore_patterns.extend(extra_patterns);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
self.ignore_matcher = if any_pattern {
|
|
674
|
-
Some(
|
|
675
|
-
builder
|
|
676
|
-
.build()
|
|
677
|
-
.expect("ignore matcher build should not fail after validation"),
|
|
678
|
-
)
|
|
679
|
-
} else {
|
|
680
|
-
None
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
self.build_yaml_matcher(base_dir);
|
|
684
|
-
|
|
685
|
-
for filter in self.rule_filters.values_mut() {
|
|
686
|
-
build_rule_filter(filter, envx, base_dir)?;
|
|
687
|
-
}
|
|
688
|
-
Ok(())
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
fn build_rule_filter(
|
|
693
|
-
filter: &mut RuleFilter,
|
|
694
|
-
envx: &dyn Env,
|
|
695
|
-
base_dir: &Path,
|
|
696
|
-
) -> Result<(), String> {
|
|
697
|
-
if filter.patterns.is_empty() && filter.from_files.is_empty() {
|
|
698
|
-
filter.matcher = None;
|
|
699
|
-
return Ok(());
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
let mut builder = GitignoreBuilder::new(base_dir);
|
|
703
|
-
builder.allow_unclosed_class(false);
|
|
704
|
-
let mut any_pattern = false;
|
|
705
|
-
|
|
706
|
-
for pat in &filter.patterns {
|
|
707
|
-
let normalized = pat.trim_end_matches(['\r']);
|
|
708
|
-
if let Err(err) = builder.add_line(None, normalized) {
|
|
709
|
-
return Err(format!(
|
|
710
|
-
"invalid config: ignore pattern '{normalized}' is invalid: {err}"
|
|
711
|
-
));
|
|
712
|
-
}
|
|
713
|
-
any_pattern = true;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
let mut extra_patterns: Vec<String> = Vec::new();
|
|
717
|
-
for source in &filter.from_files {
|
|
718
|
-
let source_path = Path::new(source);
|
|
719
|
-
let resolved = if source_path.is_absolute() {
|
|
720
|
-
source_path.to_path_buf()
|
|
721
|
-
} else {
|
|
722
|
-
base_dir.join(source_path)
|
|
723
|
-
};
|
|
724
|
-
let data = match envx.read_to_string(&resolved) {
|
|
725
|
-
Ok(text) => text,
|
|
726
|
-
Err(err) => {
|
|
727
|
-
return Err(format!(
|
|
728
|
-
"failed to read ignore-from-file {}: {err}",
|
|
729
|
-
resolved.display()
|
|
730
|
-
));
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
for line in data.lines() {
|
|
734
|
-
let normalized = line.trim_end_matches(['\r']);
|
|
735
|
-
if normalized.trim().is_empty() {
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
if let Err(err) = builder.add_line(Some(resolved.clone()), normalized) {
|
|
739
|
-
return Err(format!(
|
|
740
|
-
"invalid config: ignore-from-file pattern in {} is invalid: {err}",
|
|
741
|
-
resolved.display()
|
|
742
|
-
));
|
|
743
|
-
}
|
|
744
|
-
extra_patterns.push(normalized.to_string());
|
|
745
|
-
any_pattern = true;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if !extra_patterns.is_empty() {
|
|
750
|
-
filter.patterns.extend(extra_patterns);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
filter.matcher = if any_pattern {
|
|
754
|
-
Some(
|
|
755
|
-
builder
|
|
756
|
-
.build()
|
|
757
|
-
.expect("rule ignore matcher build should not fail after validation"),
|
|
758
|
-
)
|
|
759
|
-
} else {
|
|
760
|
-
None
|
|
761
|
-
};
|
|
762
|
-
Ok(())
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
fn load_ignore_patterns(node: &YamlOwned) -> Result<Vec<String>, String> {
|
|
766
|
-
let mut out = Vec::new();
|
|
767
|
-
if let Some(seq) = node.as_sequence() {
|
|
768
|
-
for it in seq {
|
|
769
|
-
let Some(s) = it.as_str() else {
|
|
770
|
-
return Err(
|
|
771
|
-
"invalid config: ignore should contain file patterns".to_string()
|
|
772
|
-
);
|
|
773
|
-
};
|
|
774
|
-
out.extend(patterns_from_scalar(s));
|
|
775
|
-
}
|
|
776
|
-
} else if let Some(s) = node.as_str() {
|
|
777
|
-
out.extend(patterns_from_scalar(s));
|
|
778
|
-
} else {
|
|
779
|
-
return Err("invalid config: ignore should contain file patterns".to_string());
|
|
780
|
-
}
|
|
781
|
-
Ok(out)
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
fn load_ignore_from_files(node: &YamlOwned) -> Result<Vec<String>, String> {
|
|
785
|
-
if let Some(seq) = node.as_sequence() {
|
|
786
|
-
let mut files = Vec::new();
|
|
787
|
-
for it in seq {
|
|
788
|
-
let Some(s) = it.as_str() else {
|
|
789
|
-
return Err(
|
|
790
|
-
"invalid config: ignore-from-file should contain filename(s), either as a list or string"
|
|
791
|
-
.to_string(),
|
|
792
|
-
);
|
|
793
|
-
};
|
|
794
|
-
files.push(s.to_owned());
|
|
795
|
-
}
|
|
796
|
-
Ok(files)
|
|
797
|
-
} else if let Some(s) = node.as_str() {
|
|
798
|
-
Ok(vec![s.to_owned()])
|
|
799
|
-
} else {
|
|
800
|
-
Err(
|
|
801
|
-
"invalid config: ignore-from-file should contain filename(s), either as a list or string"
|
|
802
|
-
.to_string(),
|
|
803
|
-
)
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
fn patterns_from_scalar(value: &str) -> Vec<String> {
|
|
808
|
-
value
|
|
809
|
-
.lines()
|
|
810
|
-
.map(|line| line.trim_end_matches(['\r']))
|
|
811
|
-
.filter(|line| !line.trim().is_empty())
|
|
812
|
-
.map(std::string::ToString::to_string)
|
|
813
|
-
.collect()
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
fn determine_rule_level(node: &YamlOwned) -> Option<RuleLevel> {
|
|
817
|
-
if let Some(s) = node.as_str() {
|
|
818
|
-
return if s == "disable" {
|
|
819
|
-
None
|
|
820
|
-
} else {
|
|
821
|
-
Some(RuleLevel::Error)
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if let Some(flag) = node.as_bool() {
|
|
826
|
-
return flag.then_some(RuleLevel::Error);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
node.as_mapping()
|
|
830
|
-
.and_then(|map| {
|
|
831
|
-
map.iter().find_map(|(key, value)| {
|
|
832
|
-
(key.as_str() == Some("level"))
|
|
833
|
-
.then(|| value.as_str().and_then(RuleLevel::parse))
|
|
834
|
-
})
|
|
835
|
-
})
|
|
836
|
-
.flatten()
|
|
837
|
-
.or(Some(RuleLevel::Error))
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
fn validate_rule_value(name: &str, value: &YamlOwned) -> Result<(), String> {
|
|
841
|
-
if let Some(text) = value.as_str() {
|
|
842
|
-
return match text {
|
|
843
|
-
"enable" | "disable" => Ok(()),
|
|
844
|
-
_ => Err(format!(
|
|
845
|
-
"invalid config: rule '{name}' should be 'enable', 'disable', or a mapping"
|
|
846
|
-
)),
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if value.as_bool().is_some() {
|
|
851
|
-
return Ok(());
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if let Some(map) = value.as_mapping() {
|
|
855
|
-
if name == "quoted-strings" {
|
|
856
|
-
validate_quoted_strings_rule(map)?;
|
|
857
|
-
return Ok(());
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
for (key, val) in map {
|
|
861
|
-
if handle_common_rule_key(name, key, val)? {
|
|
862
|
-
continue;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
match name {
|
|
866
|
-
"anchors" => validate_anchors_option(key, val)?,
|
|
867
|
-
"braces" => validate_brace_like_option("braces", key, val)?,
|
|
868
|
-
"brackets" => validate_brace_like_option("brackets", key, val)?,
|
|
869
|
-
"document-end" => validate_document_end_option(key, val)?,
|
|
870
|
-
"document-start" => validate_document_start_option(key, val)?,
|
|
871
|
-
"empty-lines" => validate_empty_lines_option(key, val)?,
|
|
872
|
-
"commas" => validate_commas_option(key, val)?,
|
|
873
|
-
"comments" => validate_comments_option(key, val)?,
|
|
874
|
-
"new-lines" => validate_new_lines_option(key, val)?,
|
|
875
|
-
"octal-values" => validate_octal_values_option(key, val)?,
|
|
876
|
-
"float-values" => validate_float_values_option(key, val)?,
|
|
877
|
-
"empty-values" => validate_empty_values_option(key, val)?,
|
|
878
|
-
"key-duplicates" => validate_key_duplicates_option(key, val)?,
|
|
879
|
-
"hyphens" => validate_hyphens_option(key, val)?,
|
|
880
|
-
"truthy" => validate_truthy_option(key, val)?,
|
|
881
|
-
"key-ordering" => validate_key_ordering_option(key, val)?,
|
|
882
|
-
"indentation" => validate_indentation_option(key, val)?,
|
|
883
|
-
"line-length" => validate_line_length_option(key, val)?,
|
|
884
|
-
"trailing-spaces" => {
|
|
885
|
-
let key_name = describe_rule_option_key(key);
|
|
886
|
-
return Err(format!(
|
|
887
|
-
"invalid config: unknown option \"{key_name}\" for rule \"trailing-spaces\""
|
|
888
|
-
));
|
|
889
|
-
}
|
|
890
|
-
"comments-indentation" => {
|
|
891
|
-
let key_name = describe_rule_option_key(key);
|
|
892
|
-
return Err(format!(
|
|
893
|
-
"invalid config: unknown option \"{key_name}\" for rule \"comments-indentation\""
|
|
894
|
-
));
|
|
895
|
-
}
|
|
896
|
-
_ => {}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
return Ok(());
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
Err(format!(
|
|
903
|
-
"invalid config: rule '{name}' should be 'enable', 'disable', or a mapping"
|
|
904
|
-
))
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
fn handle_common_rule_key(
|
|
908
|
-
rule: &str,
|
|
909
|
-
key: &YamlOwned,
|
|
910
|
-
val: &YamlOwned,
|
|
911
|
-
) -> Result<bool, String> {
|
|
912
|
-
if key.as_str() == Some("level") {
|
|
913
|
-
let Some(level_text) = val.as_str() else {
|
|
914
|
-
return Err(format!(
|
|
915
|
-
"invalid config: rule '{rule}' level should be \"error\" or \"warning\""
|
|
916
|
-
));
|
|
917
|
-
};
|
|
918
|
-
if RuleLevel::parse(level_text).is_none() {
|
|
919
|
-
return Err(format!(
|
|
920
|
-
"invalid config: rule '{rule}' level should be \"error\" or \"warning\""
|
|
921
|
-
));
|
|
922
|
-
}
|
|
923
|
-
return Ok(true);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if key.as_str() == Some("ignore") {
|
|
927
|
-
load_ignore_patterns(val)?;
|
|
928
|
-
return Ok(true);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if key.as_str() == Some("ignore-from-file") {
|
|
932
|
-
load_ignore_from_files(val)?;
|
|
933
|
-
return Ok(true);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
Ok(false)
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
fn validate_document_end_option(
|
|
940
|
-
key: &YamlOwned,
|
|
941
|
-
val: &YamlOwned,
|
|
942
|
-
) -> Result<(), String> {
|
|
943
|
-
match key.as_str() {
|
|
944
|
-
Some("present") => validate_bool_option(val, "document-end", "present"),
|
|
945
|
-
Some(other) => Err(format!(
|
|
946
|
-
"invalid config: unknown option \"{other}\" for rule \"document-end\""
|
|
947
|
-
)),
|
|
948
|
-
None => {
|
|
949
|
-
let key_name = describe_rule_option_key(key);
|
|
950
|
-
Err(format!(
|
|
951
|
-
"invalid config: unknown option \"{key_name}\" for rule \"document-end\""
|
|
952
|
-
))
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
fn validate_document_start_option(
|
|
958
|
-
key: &YamlOwned,
|
|
959
|
-
val: &YamlOwned,
|
|
960
|
-
) -> Result<(), String> {
|
|
961
|
-
match key.as_str() {
|
|
962
|
-
Some("present") => validate_bool_option(val, "document-start", "present"),
|
|
963
|
-
Some(other) => Err(format!(
|
|
964
|
-
"invalid config: unknown option \"{other}\" for rule \"document-start\""
|
|
965
|
-
)),
|
|
966
|
-
None => {
|
|
967
|
-
let key_name = describe_rule_option_key(key);
|
|
968
|
-
Err(format!(
|
|
969
|
-
"invalid config: unknown option \"{key_name}\" for rule \"document-start\""
|
|
970
|
-
))
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
fn validate_brace_like_option(
|
|
976
|
-
rule: &str,
|
|
977
|
-
key: &YamlOwned,
|
|
978
|
-
val: &YamlOwned,
|
|
979
|
-
) -> Result<(), String> {
|
|
980
|
-
let Some(name) = key.as_str() else {
|
|
981
|
-
let key_name = describe_rule_option_key(key);
|
|
982
|
-
return Err(format!(
|
|
983
|
-
"invalid config: unknown option \"{key_name}\" for rule \"{rule}\""
|
|
984
|
-
));
|
|
985
|
-
};
|
|
986
|
-
|
|
987
|
-
match name {
|
|
988
|
-
"forbid" => {
|
|
989
|
-
if val.as_bool().is_some() || matches!(val.as_str(), Some("non-empty")) {
|
|
990
|
-
Ok(())
|
|
991
|
-
} else {
|
|
992
|
-
Err(format!(
|
|
993
|
-
"invalid config: option \"forbid\" of \"{rule}\" should be bool or \"non-empty\""
|
|
994
|
-
))
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
"min-spaces-inside" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
998
|
-
format!("invalid config: option \"min-spaces-inside\" of \"{rule}\" should be int")
|
|
999
|
-
}),
|
|
1000
|
-
"max-spaces-inside" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1001
|
-
format!("invalid config: option \"max-spaces-inside\" of \"{rule}\" should be int")
|
|
1002
|
-
}),
|
|
1003
|
-
"min-spaces-inside-empty" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1004
|
-
format!(
|
|
1005
|
-
"invalid config: option \"min-spaces-inside-empty\" of \"{rule}\" should be int"
|
|
1006
|
-
)
|
|
1007
|
-
}),
|
|
1008
|
-
"max-spaces-inside-empty" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1009
|
-
format!(
|
|
1010
|
-
"invalid config: option \"max-spaces-inside-empty\" of \"{rule}\" should be int"
|
|
1011
|
-
)
|
|
1012
|
-
}),
|
|
1013
|
-
other => Err(format!(
|
|
1014
|
-
"invalid config: unknown option \"{other}\" for rule \"{rule}\""
|
|
1015
|
-
)),
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
fn validate_anchors_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1020
|
-
let Some(name) = key.as_str() else {
|
|
1021
|
-
let key_name = describe_rule_option_key(key);
|
|
1022
|
-
return Err(format!(
|
|
1023
|
-
"invalid config: unknown option \"{key_name}\" for rule \"anchors\""
|
|
1024
|
-
));
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
match name {
|
|
1028
|
-
"forbid-undeclared-aliases"
|
|
1029
|
-
| "forbid-duplicated-anchors"
|
|
1030
|
-
| "forbid-unused-anchors" => {
|
|
1031
|
-
if val.as_bool().is_some() {
|
|
1032
|
-
Ok(())
|
|
1033
|
-
} else {
|
|
1034
|
-
Err(format!(
|
|
1035
|
-
"invalid config: option \"{name}\" of \"anchors\" should be bool"
|
|
1036
|
-
))
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
other => Err(format!(
|
|
1040
|
-
"invalid config: unknown option \"{other}\" for rule \"anchors\""
|
|
1041
|
-
)),
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
fn validate_hyphens_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1046
|
-
match key.as_str() {
|
|
1047
|
-
Some("max-spaces-after") => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1048
|
-
"invalid config: option \"max-spaces-after\" of \"hyphens\" should be int".to_string()
|
|
1049
|
-
}),
|
|
1050
|
-
Some(other) => Err(format!(
|
|
1051
|
-
"invalid config: unknown option \"{other}\" for rule \"hyphens\""
|
|
1052
|
-
)),
|
|
1053
|
-
None => {
|
|
1054
|
-
let key_name = describe_rule_option_key(key);
|
|
1055
|
-
Err(format!(
|
|
1056
|
-
"invalid config: unknown option \"{key_name}\" for rule \"hyphens\""
|
|
1057
|
-
))
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
fn validate_commas_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1063
|
-
let Some(name) = key.as_str() else {
|
|
1064
|
-
let key_name = describe_rule_option_key(key);
|
|
1065
|
-
return Err(format!(
|
|
1066
|
-
"invalid config: unknown option \"{key_name}\" for rule \"commas\""
|
|
1067
|
-
));
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
match name {
|
|
1071
|
-
"max-spaces-before" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1072
|
-
"invalid config: option \"max-spaces-before\" of \"commas\" should be int".to_string()
|
|
1073
|
-
}),
|
|
1074
|
-
"min-spaces-after" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1075
|
-
"invalid config: option \"min-spaces-after\" of \"commas\" should be int".to_string()
|
|
1076
|
-
}),
|
|
1077
|
-
"max-spaces-after" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1078
|
-
"invalid config: option \"max-spaces-after\" of \"commas\" should be int".to_string()
|
|
1079
|
-
}),
|
|
1080
|
-
other => Err(format!(
|
|
1081
|
-
"invalid config: unknown option \"{other}\" for rule \"commas\""
|
|
1082
|
-
)),
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
fn validate_comments_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1087
|
-
let Some(name) = key.as_str() else {
|
|
1088
|
-
// Non-string keys are ignored during deep merge, matching yamllint.
|
|
1089
|
-
return Ok(());
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
match name {
|
|
1093
|
-
"require-starting-space" => validate_bool_option(val, "comments", "require-starting-space"),
|
|
1094
|
-
"ignore-shebangs" => validate_bool_option(val, "comments", "ignore-shebangs"),
|
|
1095
|
-
"min-spaces-from-content" => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1096
|
-
"invalid config: option \"min-spaces-from-content\" of \"comments\" should be int"
|
|
1097
|
-
.to_string()
|
|
1098
|
-
}),
|
|
1099
|
-
other => Err(format!(
|
|
1100
|
-
"invalid config: unknown option \"{other}\" for rule \"comments\""
|
|
1101
|
-
)),
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
fn validate_empty_lines_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1106
|
-
match key.as_str() {
|
|
1107
|
-
Some("max") => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1108
|
-
"invalid config: option \"max\" of \"empty-lines\" should be int"
|
|
1109
|
-
.to_string()
|
|
1110
|
-
}),
|
|
1111
|
-
Some("max-start") => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1112
|
-
"invalid config: option \"max-start\" of \"empty-lines\" should be int"
|
|
1113
|
-
.to_string()
|
|
1114
|
-
}),
|
|
1115
|
-
Some("max-end") => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1116
|
-
"invalid config: option \"max-end\" of \"empty-lines\" should be int"
|
|
1117
|
-
.to_string()
|
|
1118
|
-
}),
|
|
1119
|
-
Some(other) => Err(format!(
|
|
1120
|
-
"invalid config: unknown option \"{other}\" for rule \"empty-lines\""
|
|
1121
|
-
)),
|
|
1122
|
-
None => {
|
|
1123
|
-
let key_name = describe_rule_option_key(key);
|
|
1124
|
-
Err(format!(
|
|
1125
|
-
"invalid config: unknown option \"{key_name}\" for rule \"empty-lines\""
|
|
1126
|
-
))
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
fn validate_line_length_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1132
|
-
match key.as_str() {
|
|
1133
|
-
Some("max") => val.as_integer().map(|_| ()).ok_or_else(|| {
|
|
1134
|
-
"invalid config: option \"max\" of \"line-length\" should be int"
|
|
1135
|
-
.to_string()
|
|
1136
|
-
}),
|
|
1137
|
-
Some("allow-non-breakable-words") => {
|
|
1138
|
-
validate_bool_option(val, "line-length", "allow-non-breakable-words")
|
|
1139
|
-
}
|
|
1140
|
-
Some("allow-non-breakable-inline-mappings") => validate_bool_option(
|
|
1141
|
-
val,
|
|
1142
|
-
"line-length",
|
|
1143
|
-
"allow-non-breakable-inline-mappings",
|
|
1144
|
-
),
|
|
1145
|
-
Some(other) => Err(format!(
|
|
1146
|
-
"invalid config: unknown option \"{other}\" for rule \"line-length\""
|
|
1147
|
-
)),
|
|
1148
|
-
None => {
|
|
1149
|
-
let key_name = describe_rule_option_key(key);
|
|
1150
|
-
Err(format!(
|
|
1151
|
-
"invalid config: unknown option \"{key_name}\" for rule \"line-length\""
|
|
1152
|
-
))
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
fn validate_new_lines_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1158
|
-
if key.as_str() != Some("type") {
|
|
1159
|
-
let key_name = describe_rule_option_key(key);
|
|
1160
|
-
return Err(format!(
|
|
1161
|
-
"invalid config: unknown option \"{key_name}\" for rule \"new-lines\""
|
|
1162
|
-
));
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
let Some(kind) = val.as_str() else {
|
|
1166
|
-
return Err(
|
|
1167
|
-
"invalid config: option \"type\" of \"new-lines\" should be in ('unix', 'dos', 'platform')"
|
|
1168
|
-
.to_string(),
|
|
1169
|
-
);
|
|
1170
|
-
};
|
|
1171
|
-
|
|
1172
|
-
if matches!(kind, "unix" | "dos" | "platform") {
|
|
1173
|
-
Ok(())
|
|
1174
|
-
} else {
|
|
1175
|
-
Err(
|
|
1176
|
-
"invalid config: option \"type\" of \"new-lines\" should be in ('unix', 'dos', 'platform')"
|
|
1177
|
-
.to_string(),
|
|
1178
|
-
)
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
fn validate_octal_values_option(
|
|
1183
|
-
key: &YamlOwned,
|
|
1184
|
-
val: &YamlOwned,
|
|
1185
|
-
) -> Result<(), String> {
|
|
1186
|
-
match key.as_str() {
|
|
1187
|
-
Some("forbid-implicit-octal") => {
|
|
1188
|
-
validate_bool_option(val, "octal-values", "forbid-implicit-octal")
|
|
1189
|
-
}
|
|
1190
|
-
Some("forbid-explicit-octal") => {
|
|
1191
|
-
validate_bool_option(val, "octal-values", "forbid-explicit-octal")
|
|
1192
|
-
}
|
|
1193
|
-
Some(other) => Err(format!(
|
|
1194
|
-
"invalid config: unknown option \"{other}\" for rule \"octal-values\""
|
|
1195
|
-
)),
|
|
1196
|
-
None => {
|
|
1197
|
-
let key_name = describe_rule_option_key(key);
|
|
1198
|
-
Err(format!(
|
|
1199
|
-
"invalid config: unknown option \"{key_name}\" for rule \"octal-values\""
|
|
1200
|
-
))
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
fn validate_empty_values_option(
|
|
1206
|
-
key: &YamlOwned,
|
|
1207
|
-
val: &YamlOwned,
|
|
1208
|
-
) -> Result<(), String> {
|
|
1209
|
-
match key.as_str() {
|
|
1210
|
-
Some("forbid-in-block-mappings") => {
|
|
1211
|
-
validate_bool_option(val, "empty-values", "forbid-in-block-mappings")
|
|
1212
|
-
}
|
|
1213
|
-
Some("forbid-in-flow-mappings") => {
|
|
1214
|
-
validate_bool_option(val, "empty-values", "forbid-in-flow-mappings")
|
|
1215
|
-
}
|
|
1216
|
-
Some("forbid-in-block-sequences") => {
|
|
1217
|
-
validate_bool_option(val, "empty-values", "forbid-in-block-sequences")
|
|
1218
|
-
}
|
|
1219
|
-
Some(other) => Err(format!(
|
|
1220
|
-
"invalid config: unknown option \"{other}\" for rule \"empty-values\""
|
|
1221
|
-
)),
|
|
1222
|
-
None => {
|
|
1223
|
-
let key_name = describe_rule_option_key(key);
|
|
1224
|
-
Err(format!(
|
|
1225
|
-
"invalid config: unknown option \"{key_name}\" for rule \"empty-values\""
|
|
1226
|
-
))
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
fn validate_float_values_option(
|
|
1232
|
-
key: &YamlOwned,
|
|
1233
|
-
val: &YamlOwned,
|
|
1234
|
-
) -> Result<(), String> {
|
|
1235
|
-
match key.as_str() {
|
|
1236
|
-
Some("require-numeral-before-decimal") => {
|
|
1237
|
-
validate_bool_option(val, "float-values", "require-numeral-before-decimal")
|
|
1238
|
-
}
|
|
1239
|
-
Some("forbid-scientific-notation") => {
|
|
1240
|
-
validate_bool_option(val, "float-values", "forbid-scientific-notation")
|
|
1241
|
-
}
|
|
1242
|
-
Some("forbid-nan") => validate_bool_option(val, "float-values", "forbid-nan"),
|
|
1243
|
-
Some("forbid-inf") => validate_bool_option(val, "float-values", "forbid-inf"),
|
|
1244
|
-
Some(other) => Err(format!(
|
|
1245
|
-
"invalid config: unknown option \"{other}\" for rule \"float-values\""
|
|
1246
|
-
)),
|
|
1247
|
-
None => {
|
|
1248
|
-
let key_name = describe_rule_option_key(key);
|
|
1249
|
-
Err(format!(
|
|
1250
|
-
"invalid config: unknown option \"{key_name}\" for rule \"float-values\""
|
|
1251
|
-
))
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
fn validate_key_duplicates_option(
|
|
1257
|
-
key: &YamlOwned,
|
|
1258
|
-
val: &YamlOwned,
|
|
1259
|
-
) -> Result<(), String> {
|
|
1260
|
-
match key.as_str() {
|
|
1261
|
-
Some("forbid-duplicated-merge-keys") => {
|
|
1262
|
-
validate_bool_option(val, "key-duplicates", "forbid-duplicated-merge-keys")
|
|
1263
|
-
}
|
|
1264
|
-
Some(other) => Err(format!(
|
|
1265
|
-
"invalid config: unknown option \"{other}\" for rule \"key-duplicates\""
|
|
1266
|
-
)),
|
|
1267
|
-
None => {
|
|
1268
|
-
let key_name = describe_rule_option_key(key);
|
|
1269
|
-
Err(format!(
|
|
1270
|
-
"invalid config: unknown option \"{key_name}\" for rule \"key-duplicates\""
|
|
1271
|
-
))
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
fn validate_truthy_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1277
|
-
match key.as_str() {
|
|
1278
|
-
Some("allowed-values") => {
|
|
1279
|
-
let Some(seq) = val.as_sequence() else {
|
|
1280
|
-
return Err(format!(
|
|
1281
|
-
"invalid config: option \"allowed-values\" of \"truthy\" should only contain values in {TRUTHY_ALLOWED_VALUES_DISPLAY}"
|
|
1282
|
-
));
|
|
1283
|
-
};
|
|
1284
|
-
for item in seq {
|
|
1285
|
-
let Some(text) = item.as_str() else {
|
|
1286
|
-
return Err(format!(
|
|
1287
|
-
"invalid config: option \"allowed-values\" of \"truthy\" should only contain values in {TRUTHY_ALLOWED_VALUES_DISPLAY}"
|
|
1288
|
-
));
|
|
1289
|
-
};
|
|
1290
|
-
if !TRUTHY_ALLOWED_VALUES.iter().any(|allowed| allowed == &text) {
|
|
1291
|
-
return Err(format!(
|
|
1292
|
-
"invalid config: option \"allowed-values\" of \"truthy\" should only contain values in {TRUTHY_ALLOWED_VALUES_DISPLAY}"
|
|
1293
|
-
));
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
Ok(())
|
|
1297
|
-
}
|
|
1298
|
-
Some("check-keys") => {
|
|
1299
|
-
if val.as_bool().is_none() {
|
|
1300
|
-
Err(
|
|
1301
|
-
"invalid config: option \"check-keys\" of \"truthy\" should be bool"
|
|
1302
|
-
.to_string(),
|
|
1303
|
-
)
|
|
1304
|
-
} else {
|
|
1305
|
-
Ok(())
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
Some(other) => Err(format!(
|
|
1309
|
-
"invalid config: unknown option \"{other}\" for rule \"truthy\""
|
|
1310
|
-
)),
|
|
1311
|
-
None => {
|
|
1312
|
-
let key_name = describe_rule_option_key(key);
|
|
1313
|
-
Err(format!(
|
|
1314
|
-
"invalid config: unknown option \"{key_name}\" for rule \"truthy\""
|
|
1315
|
-
))
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
fn validate_key_ordering_option(
|
|
1321
|
-
key: &YamlOwned,
|
|
1322
|
-
val: &YamlOwned,
|
|
1323
|
-
) -> Result<(), String> {
|
|
1324
|
-
match key.as_str() {
|
|
1325
|
-
Some("ignored-keys") => {
|
|
1326
|
-
if let Some(seq) = val.as_sequence() {
|
|
1327
|
-
for entry in seq {
|
|
1328
|
-
let Some(text) = entry.as_str() else {
|
|
1329
|
-
return Err(
|
|
1330
|
-
"invalid config: option \"ignored-keys\" of \"key-ordering\" should contain regex strings"
|
|
1331
|
-
.to_string(),
|
|
1332
|
-
);
|
|
1333
|
-
};
|
|
1334
|
-
Regex::new(text).map_err(|err| {
|
|
1335
|
-
format!(
|
|
1336
|
-
"invalid config: option \"ignored-keys\" of \"key-ordering\" contains invalid regex '{text}': {err}"
|
|
1337
|
-
)
|
|
1338
|
-
})?;
|
|
1339
|
-
}
|
|
1340
|
-
Ok(())
|
|
1341
|
-
} else if let Some(text) = val.as_str() {
|
|
1342
|
-
Regex::new(text).map_err(|err| {
|
|
1343
|
-
format!(
|
|
1344
|
-
"invalid config: option \"ignored-keys\" of \"key-ordering\" contains invalid regex '{text}': {err}"
|
|
1345
|
-
)
|
|
1346
|
-
})?;
|
|
1347
|
-
Ok(())
|
|
1348
|
-
} else {
|
|
1349
|
-
Err(
|
|
1350
|
-
"invalid config: option \"ignored-keys\" of \"key-ordering\" should contain regex strings"
|
|
1351
|
-
.to_string(),
|
|
1352
|
-
)
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
Some(other) => Err(format!(
|
|
1356
|
-
"invalid config: unknown option \"{other}\" for rule \"key-ordering\""
|
|
1357
|
-
)),
|
|
1358
|
-
None => {
|
|
1359
|
-
let key_name = describe_rule_option_key(key);
|
|
1360
|
-
Err(format!(
|
|
1361
|
-
"invalid config: unknown option \"{key_name}\" for rule \"key-ordering\""
|
|
1362
|
-
))
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
fn validate_indentation_option(key: &YamlOwned, val: &YamlOwned) -> Result<(), String> {
|
|
1368
|
-
match key.as_str() {
|
|
1369
|
-
Some("spaces") => {
|
|
1370
|
-
if val.as_integer().is_some() || val.as_str() == Some("consistent") {
|
|
1371
|
-
Ok(())
|
|
1372
|
-
} else {
|
|
1373
|
-
Err(
|
|
1374
|
-
"invalid config: option \"spaces\" of \"indentation\" should be in (<class 'int'>, 'consistent')"
|
|
1375
|
-
.to_string(),
|
|
1376
|
-
)
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
Some("indent-sequences") => {
|
|
1380
|
-
if val.as_bool().is_some()
|
|
1381
|
-
|| matches!(val.as_str(), Some("whatever" | "consistent"))
|
|
1382
|
-
{
|
|
1383
|
-
Ok(())
|
|
1384
|
-
} else {
|
|
1385
|
-
Err(
|
|
1386
|
-
"invalid config: option \"indent-sequences\" of \"indentation\" should be in (<class 'bool'>, 'whatever', 'consistent')"
|
|
1387
|
-
.to_string(),
|
|
1388
|
-
)
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
Some("check-multi-line-strings") => {
|
|
1392
|
-
if val.as_bool().is_some() {
|
|
1393
|
-
Ok(())
|
|
1394
|
-
} else {
|
|
1395
|
-
Err(
|
|
1396
|
-
"invalid config: option \"check-multi-line-strings\" of \"indentation\" should be bool"
|
|
1397
|
-
.to_string(),
|
|
1398
|
-
)
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
Some(other) => Err(format!(
|
|
1402
|
-
"invalid config: unknown option \"{other}\" for rule \"indentation\""
|
|
1403
|
-
)),
|
|
1404
|
-
None => {
|
|
1405
|
-
let key_name = describe_rule_option_key(key);
|
|
1406
|
-
Err(format!(
|
|
1407
|
-
"invalid config: unknown option \"{key_name}\" for rule \"indentation\""
|
|
1408
|
-
))
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
fn validate_quoted_strings_rule(map: &MappingOwned) -> Result<(), String> {
|
|
1414
|
-
let mut state = QuotedStringsValidationState::default();
|
|
1415
|
-
for (key, val) in map {
|
|
1416
|
-
if handle_common_rule_key("quoted-strings", key, val)? {
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
validate_quoted_strings_option(key, val, &mut state)?;
|
|
1420
|
-
}
|
|
1421
|
-
state.finish()
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
#[derive(Default)]
|
|
1425
|
-
struct QuotedStringsValidationState {
|
|
1426
|
-
required: Option<QuotedStringsRequired>,
|
|
1427
|
-
extra_required_count: Option<usize>,
|
|
1428
|
-
extra_allowed_count: Option<usize>,
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
1432
|
-
enum QuotedStringsRequired {
|
|
1433
|
-
True,
|
|
1434
|
-
False,
|
|
1435
|
-
OnlyWhenNeeded,
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
impl QuotedStringsValidationState {
|
|
1439
|
-
fn finish(&self) -> Result<(), String> {
|
|
1440
|
-
let required = self.required.unwrap_or(QuotedStringsRequired::True);
|
|
1441
|
-
let extra_required = self.extra_required_count.unwrap_or(0);
|
|
1442
|
-
let extra_allowed = self.extra_allowed_count.unwrap_or(0);
|
|
1443
|
-
|
|
1444
|
-
if matches!(required, QuotedStringsRequired::True) && extra_allowed > 0 {
|
|
1445
|
-
return Err(
|
|
1446
|
-
"invalid config: quoted-strings: cannot use both \"required: true\" and \"extra-allowed\""
|
|
1447
|
-
.to_string(),
|
|
1448
|
-
);
|
|
1449
|
-
}
|
|
1450
|
-
if matches!(required, QuotedStringsRequired::True) && extra_required > 0 {
|
|
1451
|
-
return Err(
|
|
1452
|
-
"invalid config: quoted-strings: cannot use both \"required: true\" and \"extra-required\""
|
|
1453
|
-
.to_string(),
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
if matches!(required, QuotedStringsRequired::False) && extra_allowed > 0 {
|
|
1457
|
-
return Err(
|
|
1458
|
-
"invalid config: quoted-strings: cannot use both \"required: false\" and \"extra-allowed\""
|
|
1459
|
-
.to_string(),
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
Ok(())
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
fn validate_quoted_strings_option(
|
|
1468
|
-
key: &YamlOwned,
|
|
1469
|
-
val: &YamlOwned,
|
|
1470
|
-
state: &mut QuotedStringsValidationState,
|
|
1471
|
-
) -> Result<(), String> {
|
|
1472
|
-
match key.as_str() {
|
|
1473
|
-
Some("quote-type") => validate_quote_type_option(val),
|
|
1474
|
-
Some("required") => validate_required_option(val, state),
|
|
1475
|
-
Some("extra-required") => validate_regex_list_option(
|
|
1476
|
-
val,
|
|
1477
|
-
"extra-required",
|
|
1478
|
-
&mut state.extra_required_count,
|
|
1479
|
-
),
|
|
1480
|
-
Some("extra-allowed") => validate_regex_list_option(
|
|
1481
|
-
val,
|
|
1482
|
-
"extra-allowed",
|
|
1483
|
-
&mut state.extra_allowed_count,
|
|
1484
|
-
),
|
|
1485
|
-
Some("allow-quoted-quotes") => {
|
|
1486
|
-
validate_bool_option(val, "quoted-strings", "allow-quoted-quotes")
|
|
1487
|
-
}
|
|
1488
|
-
Some("check-keys") => validate_bool_option(val, "quoted-strings", "check-keys"),
|
|
1489
|
-
Some(other) => Err(format!(
|
|
1490
|
-
"invalid config: unknown option \"{other}\" for rule \"quoted-strings\""
|
|
1491
|
-
)),
|
|
1492
|
-
None => {
|
|
1493
|
-
let key_name = describe_rule_option_key(key);
|
|
1494
|
-
Err(format!(
|
|
1495
|
-
"invalid config: unknown option \"{key_name}\" for rule \"quoted-strings\""
|
|
1496
|
-
))
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
fn validate_quote_type_option(val: &YamlOwned) -> Result<(), String> {
|
|
1502
|
-
let Some(text) = val.as_str() else {
|
|
1503
|
-
return Err(
|
|
1504
|
-
"invalid config: option \"quote-type\" of \"quoted-strings\" should be in ('any', 'single', 'double')"
|
|
1505
|
-
.to_string(),
|
|
1506
|
-
);
|
|
1507
|
-
};
|
|
1508
|
-
if matches!(text, "any" | "single" | "double") {
|
|
1509
|
-
Ok(())
|
|
1510
|
-
} else {
|
|
1511
|
-
Err(
|
|
1512
|
-
"invalid config: option \"quote-type\" of \"quoted-strings\" should be in ('any', 'single', 'double')"
|
|
1513
|
-
.to_string(),
|
|
1514
|
-
)
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
fn validate_required_option(
|
|
1519
|
-
val: &YamlOwned,
|
|
1520
|
-
state: &mut QuotedStringsValidationState,
|
|
1521
|
-
) -> Result<(), String> {
|
|
1522
|
-
if let Some(flag) = val.as_bool() {
|
|
1523
|
-
state.required = Some(if flag {
|
|
1524
|
-
QuotedStringsRequired::True
|
|
1525
|
-
} else {
|
|
1526
|
-
QuotedStringsRequired::False
|
|
1527
|
-
});
|
|
1528
|
-
Ok(())
|
|
1529
|
-
} else if val.as_str() == Some("only-when-needed") {
|
|
1530
|
-
state.required = Some(QuotedStringsRequired::OnlyWhenNeeded);
|
|
1531
|
-
Ok(())
|
|
1532
|
-
} else {
|
|
1533
|
-
Err(
|
|
1534
|
-
"invalid config: option \"required\" of \"quoted-strings\" should be in (True, False, 'only-when-needed')"
|
|
1535
|
-
.to_string(),
|
|
1536
|
-
)
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
fn validate_regex_list_option(
|
|
1541
|
-
val: &YamlOwned,
|
|
1542
|
-
option_name: &str,
|
|
1543
|
-
count_slot: &mut Option<usize>,
|
|
1544
|
-
) -> Result<(), String> {
|
|
1545
|
-
let Some(seq) = val.as_sequence() else {
|
|
1546
|
-
return Err(format!(
|
|
1547
|
-
"invalid config: option \"{option_name}\" of \"quoted-strings\" should only contain values in [<class 'str'>]"
|
|
1548
|
-
));
|
|
1549
|
-
};
|
|
1550
|
-
*count_slot = Some(seq.len());
|
|
1551
|
-
for entry in seq {
|
|
1552
|
-
let Some(text) = entry.as_str() else {
|
|
1553
|
-
return Err(format!(
|
|
1554
|
-
"invalid config: option \"{option_name}\" of \"quoted-strings\" should only contain values in [<class 'str'>]"
|
|
1555
|
-
));
|
|
1556
|
-
};
|
|
1557
|
-
Regex::new(text).map_err(|err| {
|
|
1558
|
-
format!(
|
|
1559
|
-
"invalid config: regex \"{text}\" in option \"{option_name}\" of \"quoted-strings\" is invalid: {err}"
|
|
1560
|
-
)
|
|
1561
|
-
})?;
|
|
1562
|
-
}
|
|
1563
|
-
Ok(())
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
fn validate_bool_option(
|
|
1567
|
-
val: &YamlOwned,
|
|
1568
|
-
rule_name: &str,
|
|
1569
|
-
option_name: &str,
|
|
1570
|
-
) -> Result<(), String> {
|
|
1571
|
-
if val.as_bool().is_some() {
|
|
1572
|
-
Ok(())
|
|
1573
|
-
} else {
|
|
1574
|
-
Err(format!(
|
|
1575
|
-
"invalid config: option \"{option_name}\" of \"{rule_name}\" should be bool"
|
|
1576
|
-
))
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
fn resolve_extend_path(
|
|
1581
|
-
entry: &str,
|
|
1582
|
-
envx: &dyn Env,
|
|
1583
|
-
base_dir: Option<&Path>,
|
|
1584
|
-
) -> PathBuf {
|
|
1585
|
-
let candidate = PathBuf::from(entry);
|
|
1586
|
-
if candidate.is_absolute() {
|
|
1587
|
-
return candidate;
|
|
1588
|
-
}
|
|
1589
|
-
if let Some(joined) = base_dir
|
|
1590
|
-
.map(|base| base.join(&candidate))
|
|
1591
|
-
.filter(|candidate| envx.path_exists(candidate))
|
|
1592
|
-
{
|
|
1593
|
-
return joined;
|
|
1594
|
-
}
|
|
1595
|
-
let cwd = envx.current_dir();
|
|
1596
|
-
let fallback = cwd.join(&candidate);
|
|
1597
|
-
if envx.path_exists(&fallback) {
|
|
1598
|
-
fallback
|
|
1599
|
-
} else {
|
|
1600
|
-
candidate
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
fn deep_merge_yaml_owned(dst: &mut YamlOwned, src: &YamlOwned) {
|
|
1605
|
-
if let (Some(_), Some(src_map)) = (dst.as_mapping(), src.as_mapping()) {
|
|
1606
|
-
for (k, v) in src_map {
|
|
1607
|
-
let Some(key) = k.as_str() else {
|
|
1608
|
-
continue;
|
|
1609
|
-
};
|
|
1610
|
-
let merged = dst.as_mapping_get_mut(key).is_some_and(|dv| {
|
|
1611
|
-
deep_merge_yaml_owned(dv, v);
|
|
1612
|
-
true
|
|
1613
|
-
});
|
|
1614
|
-
if !merged {
|
|
1615
|
-
let map = dst.as_mapping_mut().expect("checked mapping above");
|
|
1616
|
-
map.insert(
|
|
1617
|
-
YamlOwned::Value(ScalarOwned::String(key.to_owned())),
|
|
1618
|
-
v.clone(),
|
|
1619
|
-
);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
} else {
|
|
1623
|
-
*dst = src.clone();
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
fn describe_rule_option_key(key: &YamlOwned) -> String {
|
|
1628
|
-
match (
|
|
1629
|
-
key.as_integer(),
|
|
1630
|
-
key.as_floating_point(),
|
|
1631
|
-
key.as_bool(),
|
|
1632
|
-
key.is_null(),
|
|
1633
|
-
key.as_str(),
|
|
1634
|
-
) {
|
|
1635
|
-
(Some(num), _, _, _, _) => num.to_string(),
|
|
1636
|
-
(None, Some(float), _, _, _) => float.to_string(),
|
|
1637
|
-
(None, None, Some(flag), _, _) => flag.to_string(),
|
|
1638
|
-
(None, None, None, true, _) => "None".to_string(),
|
|
1639
|
-
(None, None, None, false, Some(text)) => text.to_owned(),
|
|
1640
|
-
_ => format!("{key:?}"),
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
fn toml_value_to_yaml_owned(value: &TomlValue) -> YamlOwned {
|
|
1645
|
-
match value {
|
|
1646
|
-
TomlValue::String(text) => YamlOwned::Value(ScalarOwned::String(text.clone())),
|
|
1647
|
-
TomlValue::Integer(num) => YamlOwned::Value(ScalarOwned::Integer(*num)),
|
|
1648
|
-
TomlValue::Float(num) => {
|
|
1649
|
-
let rendered = num.to_string();
|
|
1650
|
-
YamlOwned::load_from_str(&rendered)
|
|
1651
|
-
.ok()
|
|
1652
|
-
.and_then(|docs| docs.into_iter().next())
|
|
1653
|
-
.unwrap_or(YamlOwned::Value(ScalarOwned::String(rendered)))
|
|
1654
|
-
}
|
|
1655
|
-
TomlValue::Boolean(flag) => YamlOwned::Value(ScalarOwned::Boolean(*flag)),
|
|
1656
|
-
TomlValue::Datetime(dt) => {
|
|
1657
|
-
YamlOwned::Value(ScalarOwned::String(dt.to_string()))
|
|
1658
|
-
}
|
|
1659
|
-
TomlValue::Array(items) => {
|
|
1660
|
-
YamlOwned::Sequence(items.iter().map(toml_value_to_yaml_owned).collect())
|
|
1661
|
-
}
|
|
1662
|
-
TomlValue::Table(table) => {
|
|
1663
|
-
let mut map = MappingOwned::new();
|
|
1664
|
-
for (key, val) in table {
|
|
1665
|
-
map.insert(
|
|
1666
|
-
YamlOwned::Value(ScalarOwned::String(key.clone())),
|
|
1667
|
-
toml_value_to_yaml_owned(val),
|
|
1668
|
-
);
|
|
1669
|
-
}
|
|
1670
|
-
YamlOwned::Mapping(map)
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
fn yaml_owned_to_toml_value(value: &YamlOwned) -> Result<TomlValue, String> {
|
|
1676
|
-
if let Some(text) = value.as_str() {
|
|
1677
|
-
return Ok(TomlValue::String(text.to_string()));
|
|
1678
|
-
}
|
|
1679
|
-
if let Some(flag) = value.as_bool() {
|
|
1680
|
-
return Ok(TomlValue::Boolean(flag));
|
|
1681
|
-
}
|
|
1682
|
-
if let Some(num) = value.as_integer() {
|
|
1683
|
-
return Ok(TomlValue::Integer(num));
|
|
1684
|
-
}
|
|
1685
|
-
if let Some(num) = value.as_floating_point() {
|
|
1686
|
-
return Ok(TomlValue::Float(num));
|
|
1687
|
-
}
|
|
1688
|
-
if value.is_null() {
|
|
1689
|
-
return Err(
|
|
1690
|
-
"cannot convert null values to TOML (TOML has no null type)".to_string()
|
|
1691
|
-
);
|
|
1692
|
-
}
|
|
1693
|
-
if let Some(items) = value.as_sequence() {
|
|
1694
|
-
let out: Result<Vec<_>, _> =
|
|
1695
|
-
items.iter().map(yaml_owned_to_toml_value).collect();
|
|
1696
|
-
return out.map(TomlValue::Array);
|
|
1697
|
-
}
|
|
1698
|
-
if let Some(map) = value.as_mapping() {
|
|
1699
|
-
let mut out = toml::map::Map::new();
|
|
1700
|
-
for (key, val) in map {
|
|
1701
|
-
let Some(key_text) = key.as_str() else {
|
|
1702
|
-
return Err(format!("cannot convert non-string TOML key: {key:?}"));
|
|
1703
|
-
};
|
|
1704
|
-
out.insert(key_text.to_string(), yaml_owned_to_toml_value(val)?);
|
|
1705
|
-
}
|
|
1706
|
-
return Ok(TomlValue::Table(out));
|
|
1707
|
-
}
|
|
1708
|
-
Err("cannot convert this YAML node to TOML".to_string())
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
/// Result of configuration discovery.
|
|
1712
|
-
#[derive(Debug, Clone)]
|
|
1713
|
-
pub struct ConfigContext {
|
|
1714
|
-
pub config: YamlLintConfig,
|
|
1715
|
-
pub base_dir: PathBuf,
|
|
1716
|
-
pub source: Option<PathBuf>,
|
|
1717
|
-
pub notices: Vec<String>,
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
fn finalize_context(
|
|
1721
|
-
envx: &dyn Env,
|
|
1722
|
-
mut cfg: YamlLintConfig,
|
|
1723
|
-
base_dir: impl Into<PathBuf>,
|
|
1724
|
-
source: Option<PathBuf>,
|
|
1725
|
-
notices: Vec<String>,
|
|
1726
|
-
) -> Result<ConfigContext, String> {
|
|
1727
|
-
let base_dir = base_dir.into();
|
|
1728
|
-
cfg.finalize(envx, &base_dir)?;
|
|
1729
|
-
Ok(ConfigContext {
|
|
1730
|
-
config: cfg,
|
|
1731
|
-
base_dir,
|
|
1732
|
-
source,
|
|
1733
|
-
notices,
|
|
1734
|
-
})
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
/// Discover configuration with precedence:
|
|
1738
|
-
/// config-data > config-file > project (TOML-first, YAML fallback) > env var > user-global > defaults.
|
|
1739
|
-
///
|
|
1740
|
-
/// # Errors
|
|
1741
|
-
/// Returns an error when a config file cannot be read or parsed.
|
|
1742
|
-
pub fn discover_config(
|
|
1743
|
-
inputs: &[PathBuf],
|
|
1744
|
-
overrides: &Overrides,
|
|
1745
|
-
) -> Result<ConfigContext, String> {
|
|
1746
|
-
discover_config_with(inputs, overrides, &SystemEnv)
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
/// Discover configuration using a provided `Env` implementation.
|
|
1750
|
-
///
|
|
1751
|
-
/// # Errors
|
|
1752
|
-
/// Returns an error when a configuration file cannot be read or parsed.
|
|
1753
|
-
///
|
|
1754
|
-
/// # Panics
|
|
1755
|
-
/// Panics only if built-in preset YAML cannot be parsed, which indicates a programming error.
|
|
1756
|
-
pub fn discover_config_with(
|
|
1757
|
-
inputs: &[PathBuf],
|
|
1758
|
-
overrides: &Overrides,
|
|
1759
|
-
envx: &dyn Env,
|
|
1760
|
-
) -> Result<ConfigContext, String> {
|
|
1761
|
-
// Global config resolution: inline > file > project > env var.
|
|
1762
|
-
if let Some(ref data) = overrides.config_data {
|
|
1763
|
-
let base_dir = envx.current_dir();
|
|
1764
|
-
let cfg =
|
|
1765
|
-
YamlLintConfig::from_yaml_str_with_env(data, Some(envx), Some(&base_dir))?;
|
|
1766
|
-
return finalize_context(envx, cfg, base_dir, None, Vec::new());
|
|
1767
|
-
}
|
|
1768
|
-
if let Some(ref file) = overrides.config_file {
|
|
1769
|
-
return ctx_from_config_path_core(envx, file, false, Vec::new());
|
|
1770
|
-
}
|
|
1771
|
-
let discovered = find_project_config_core(envx, inputs)?;
|
|
1772
|
-
if let Some(discovered) = discovered {
|
|
1773
|
-
return ctx_from_config_path_core(
|
|
1774
|
-
envx,
|
|
1775
|
-
&discovered.cfg_path,
|
|
1776
|
-
true,
|
|
1777
|
-
discovered.notices,
|
|
1778
|
-
);
|
|
1779
|
-
}
|
|
1780
|
-
if let Some(ctx) = try_env_config_core(envx)? {
|
|
1781
|
-
return Ok(ctx);
|
|
1782
|
-
}
|
|
1783
|
-
let cwd = envx.current_dir();
|
|
1784
|
-
try_user_global_core(envx, &cwd)?.map_or_else(
|
|
1785
|
-
move || {
|
|
1786
|
-
finalize_context(
|
|
1787
|
-
envx,
|
|
1788
|
-
YamlLintConfig::from_yaml_str(conf::builtin("default").unwrap())
|
|
1789
|
-
.expect("builtin preset must parse"),
|
|
1790
|
-
cwd,
|
|
1791
|
-
None,
|
|
1792
|
-
Vec::new(),
|
|
1793
|
-
)
|
|
1794
|
-
},
|
|
1795
|
-
Ok,
|
|
1796
|
-
)
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
/// Variant of `discover_config` with injectable environment access to keep tests safe.
|
|
1800
|
-
///
|
|
1801
|
-
/// # Errors
|
|
1802
|
-
/// Returns an error when a config file cannot be read or parsed.
|
|
1803
|
-
///
|
|
1804
|
-
/// # Panics
|
|
1805
|
-
/// Panics only if the built-in default preset is not embedded (programming error).
|
|
1806
|
-
pub fn discover_config_with_env(
|
|
1807
|
-
inputs: &[PathBuf],
|
|
1808
|
-
overrides: &Overrides,
|
|
1809
|
-
env_get: &dyn Fn(&str) -> Option<String>,
|
|
1810
|
-
) -> Result<ConfigContext, String> {
|
|
1811
|
-
discover_config_with(inputs, overrides, &ClosureEnv { get: env_get })
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
/// Discover the config for a single file path, ignoring env/global overrides.
|
|
1815
|
-
/// Precedence: nearest project config up-tree (TOML-first, YAML fallback),
|
|
1816
|
-
/// then user-global, then defaults.
|
|
1817
|
-
///
|
|
1818
|
-
/// # Errors
|
|
1819
|
-
/// Returns an error when a config file cannot be read or parsed.
|
|
1820
|
-
/// Discover the effective config for a single file.
|
|
1821
|
-
///
|
|
1822
|
-
/// # Errors
|
|
1823
|
-
/// Returns an error when a config file cannot be read or parsed.
|
|
1824
|
-
///
|
|
1825
|
-
/// # Panics
|
|
1826
|
-
/// Panics only if the built-in default preset is not embedded (programming error).
|
|
1827
|
-
pub fn discover_per_file(path: &Path) -> Result<ConfigContext, String> {
|
|
1828
|
-
discover_per_file_with(path, &SystemEnv)
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
/// Discover the effective config for a single file using a provided `Env`.
|
|
1832
|
-
///
|
|
1833
|
-
/// # Errors
|
|
1834
|
-
/// Returns an error when a configuration file cannot be read or parsed.
|
|
1835
|
-
///
|
|
1836
|
-
/// # Panics
|
|
1837
|
-
/// Panics only if the built-in default preset cannot be parsed.
|
|
1838
|
-
pub fn discover_per_file_with(
|
|
1839
|
-
path: &Path,
|
|
1840
|
-
envx: &dyn Env,
|
|
1841
|
-
) -> Result<ConfigContext, String> {
|
|
1842
|
-
let start_dir = if path.is_dir() {
|
|
1843
|
-
path
|
|
1844
|
-
} else {
|
|
1845
|
-
path.parent().unwrap_or(path)
|
|
1846
|
-
};
|
|
1847
|
-
|
|
1848
|
-
let discovered = find_project_config_core(envx, &[start_dir.to_path_buf()])?;
|
|
1849
|
-
if let Some(discovered) = discovered {
|
|
1850
|
-
return ctx_from_config_path_core(
|
|
1851
|
-
envx,
|
|
1852
|
-
&discovered.cfg_path,
|
|
1853
|
-
true,
|
|
1854
|
-
discovered.notices,
|
|
1855
|
-
);
|
|
1856
|
-
}
|
|
1857
|
-
try_user_global_core(envx, start_dir)?.map_or_else(
|
|
1858
|
-
|| {
|
|
1859
|
-
finalize_context(
|
|
1860
|
-
envx,
|
|
1861
|
-
YamlLintConfig::from_yaml_str(conf::builtin("default").unwrap())
|
|
1862
|
-
.expect("builtin preset must parse"),
|
|
1863
|
-
envx.current_dir(),
|
|
1864
|
-
None,
|
|
1865
|
-
Vec::new(),
|
|
1866
|
-
)
|
|
1867
|
-
},
|
|
1868
|
-
Ok,
|
|
1869
|
-
)
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
// Testable core helpers below.
|
|
1873
|
-
fn ctx_from_config_path_core(
|
|
1874
|
-
envx: &dyn Env,
|
|
1875
|
-
p: &Path,
|
|
1876
|
-
allow_missing_pyproject: bool,
|
|
1877
|
-
notices: Vec<String>,
|
|
1878
|
-
) -> Result<ConfigContext, String> {
|
|
1879
|
-
let base = p
|
|
1880
|
-
.parent()
|
|
1881
|
-
.map_or_else(|| envx.current_dir(), Path::to_path_buf);
|
|
1882
|
-
let cfg = load_config_from_path_core(envx, p, &base, allow_missing_pyproject)?
|
|
1883
|
-
.expect("missing [tool.ryl] should be filtered or returned as an error before this point");
|
|
1884
|
-
finalize_context(envx, cfg, base, Some(p.to_path_buf()), notices)
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
fn expand_user_path(envx: &dyn Env, raw: &str) -> PathBuf {
|
|
1888
|
-
if let Some(rest) = raw.strip_prefix('~') {
|
|
1889
|
-
let trimmed = rest.trim_start_matches(['/', '\\']);
|
|
1890
|
-
return envx
|
|
1891
|
-
.home_dir()
|
|
1892
|
-
.map_or_else(|| PathBuf::from(raw), |home| home.join(trimmed));
|
|
1893
|
-
}
|
|
1894
|
-
PathBuf::from(raw)
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
fn try_env_config_core(envx: &dyn Env) -> Result<Option<ConfigContext>, String> {
|
|
1898
|
-
envx.env_var("YAMLLINT_CONFIG_FILE")
|
|
1899
|
-
.map(|raw| expand_user_path(envx, &raw))
|
|
1900
|
-
.filter(|p| envx.path_exists(p))
|
|
1901
|
-
.map(|p| ctx_from_config_path_core(envx, &p, false, Vec::new()))
|
|
1902
|
-
.transpose()
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
// no separate try_env_config_with; discover_config_with_env uses ClosureEnv + discover_config_with
|
|
1906
|
-
|
|
1907
|
-
fn try_user_global_core(
|
|
1908
|
-
envx: &dyn Env,
|
|
1909
|
-
base_dir: &Path,
|
|
1910
|
-
) -> Result<Option<ConfigContext>, String> {
|
|
1911
|
-
envx.config_dir()
|
|
1912
|
-
.map(|base| base.join("yamllint").join("config"))
|
|
1913
|
-
.filter(|p| envx.path_exists(p))
|
|
1914
|
-
.map(|p| {
|
|
1915
|
-
let data = envx.read_to_string(&p)?;
|
|
1916
|
-
let cfg = YamlLintConfig::from_yaml_str_with_env(
|
|
1917
|
-
&data,
|
|
1918
|
-
Some(envx),
|
|
1919
|
-
Some(base_dir),
|
|
1920
|
-
)?;
|
|
1921
|
-
finalize_context(envx, cfg, base_dir.to_path_buf(), Some(p), Vec::new())
|
|
1922
|
-
})
|
|
1923
|
-
.transpose()
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
const TOML_PROJECT_CONFIG_CANDIDATES: [&str; 3] =
|
|
1927
|
-
[".ryl.toml", "ryl.toml", "pyproject.toml"];
|
|
1928
|
-
const YAML_PROJECT_CONFIG_CANDIDATES: [&str; 3] =
|
|
1929
|
-
[".yamllint", ".yamllint.yaml", ".yamllint.yml"];
|
|
1930
|
-
|
|
1931
|
-
#[derive(Debug, Clone)]
|
|
1932
|
-
struct ProjectConfigDiscovery {
|
|
1933
|
-
cfg_path: PathBuf,
|
|
1934
|
-
notices: Vec<String>,
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
fn load_config_from_path_core(
|
|
1938
|
-
envx: &dyn Env,
|
|
1939
|
-
path: &Path,
|
|
1940
|
-
base_dir: &Path,
|
|
1941
|
-
allow_missing_pyproject: bool,
|
|
1942
|
-
) -> Result<Option<YamlLintConfig>, String> {
|
|
1943
|
-
let data = envx.read_to_string(path)?;
|
|
1944
|
-
if path
|
|
1945
|
-
.file_name()
|
|
1946
|
-
.is_some_and(|name| name == "pyproject.toml")
|
|
1947
|
-
{
|
|
1948
|
-
let cfg = YamlLintConfig::from_toml_str_with_env(
|
|
1949
|
-
&data,
|
|
1950
|
-
Some(envx),
|
|
1951
|
-
Some(base_dir),
|
|
1952
|
-
true,
|
|
1953
|
-
)?;
|
|
1954
|
-
if cfg.is_none() && !allow_missing_pyproject {
|
|
1955
|
-
return Err(format!(
|
|
1956
|
-
"failed to parse config file {}: missing [tool.ryl] section",
|
|
1957
|
-
path.display()
|
|
1958
|
-
));
|
|
1959
|
-
}
|
|
1960
|
-
return Ok(cfg);
|
|
1961
|
-
}
|
|
1962
|
-
if is_toml_path(path) {
|
|
1963
|
-
let cfg = YamlLintConfig::from_toml_str_with_env(
|
|
1964
|
-
&data,
|
|
1965
|
-
Some(envx),
|
|
1966
|
-
Some(base_dir),
|
|
1967
|
-
false,
|
|
1968
|
-
)?;
|
|
1969
|
-
return Ok(cfg);
|
|
1970
|
-
}
|
|
1971
|
-
let cfg =
|
|
1972
|
-
YamlLintConfig::from_yaml_str_with_env(&data, Some(envx), Some(base_dir))?;
|
|
1973
|
-
Ok(Some(cfg))
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
fn is_toml_path(path: &Path) -> bool {
|
|
1977
|
-
path.extension().is_some_and(|ext| ext == "toml")
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
fn build_project_search_starts(envx: &dyn Env, inputs: &[PathBuf]) -> Vec<PathBuf> {
|
|
1981
|
-
let cwd = envx.current_dir();
|
|
1982
|
-
let mut starts = Vec::new();
|
|
1983
|
-
if inputs.is_empty() {
|
|
1984
|
-
starts.push(cwd.clone());
|
|
1985
|
-
return starts;
|
|
1986
|
-
}
|
|
1987
|
-
for path in inputs {
|
|
1988
|
-
let start = if path.is_dir() {
|
|
1989
|
-
path.clone()
|
|
1990
|
-
} else {
|
|
1991
|
-
path.parent().map_or_else(|| cwd.clone(), Path::to_path_buf)
|
|
1992
|
-
};
|
|
1993
|
-
let abs = if start.is_absolute() {
|
|
1994
|
-
start
|
|
1995
|
-
} else {
|
|
1996
|
-
cwd.join(start)
|
|
1997
|
-
};
|
|
1998
|
-
if !starts.iter().any(|existing| existing == &abs) {
|
|
1999
|
-
starts.push(abs);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
starts
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
fn find_first_yaml_candidate(
|
|
2006
|
-
envx: &dyn Env,
|
|
2007
|
-
start: &Path,
|
|
2008
|
-
home_abs: Option<&PathBuf>,
|
|
2009
|
-
) -> Option<PathBuf> {
|
|
2010
|
-
let mut dir = start.to_path_buf();
|
|
2011
|
-
loop {
|
|
2012
|
-
for name in YAML_PROJECT_CONFIG_CANDIDATES {
|
|
2013
|
-
let candidate = dir.join(name);
|
|
2014
|
-
if envx.path_exists(&candidate) {
|
|
2015
|
-
return Some(candidate);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
if home_abs.is_some_and(|home| home == &dir) {
|
|
2019
|
-
break;
|
|
2020
|
-
}
|
|
2021
|
-
match dir.parent() {
|
|
2022
|
-
Some(parent) if parent != dir => dir = parent.to_path_buf(),
|
|
2023
|
-
_ => break,
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
None
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
fn find_project_config_core(
|
|
2030
|
-
envx: &dyn Env,
|
|
2031
|
-
inputs: &[PathBuf],
|
|
2032
|
-
) -> Result<Option<ProjectConfigDiscovery>, String> {
|
|
2033
|
-
let starts = build_project_search_starts(envx, inputs);
|
|
2034
|
-
let cwd = envx.current_dir();
|
|
2035
|
-
let home_abs = envx
|
|
2036
|
-
.env_var("HOME")
|
|
2037
|
-
.map(PathBuf::from)
|
|
2038
|
-
.or_else(dirs_next::home_dir)
|
|
2039
|
-
.map(|home| {
|
|
2040
|
-
if home.is_absolute() {
|
|
2041
|
-
home
|
|
2042
|
-
} else {
|
|
2043
|
-
cwd.join(home)
|
|
2044
|
-
}
|
|
2045
|
-
});
|
|
2046
|
-
|
|
2047
|
-
for start in &starts {
|
|
2048
|
-
let mut dir = start.clone();
|
|
2049
|
-
loop {
|
|
2050
|
-
for name in TOML_PROJECT_CONFIG_CANDIDATES {
|
|
2051
|
-
let candidate = dir.join(name);
|
|
2052
|
-
if !envx.path_exists(&candidate) {
|
|
2053
|
-
continue;
|
|
2054
|
-
}
|
|
2055
|
-
if name == "pyproject.toml" {
|
|
2056
|
-
let loaded =
|
|
2057
|
-
load_config_from_path_core(envx, &candidate, &dir, true)?;
|
|
2058
|
-
if loaded.is_none() {
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
let notices = find_first_yaml_candidate(envx, start, home_abs.as_ref())
|
|
2063
|
-
.map(|yaml_path| {
|
|
2064
|
-
format!(
|
|
2065
|
-
"warning: ignoring legacy YAML config discovery because TOML config {} was found (legacy candidate: {})",
|
|
2066
|
-
candidate.display(),
|
|
2067
|
-
yaml_path.display()
|
|
2068
|
-
)
|
|
2069
|
-
})
|
|
2070
|
-
.into_iter()
|
|
2071
|
-
.collect();
|
|
2072
|
-
return Ok(Some(ProjectConfigDiscovery {
|
|
2073
|
-
cfg_path: candidate,
|
|
2074
|
-
notices,
|
|
2075
|
-
}));
|
|
2076
|
-
}
|
|
2077
|
-
if home_abs.as_ref().is_some_and(|home| home == &dir) {
|
|
2078
|
-
break;
|
|
2079
|
-
}
|
|
2080
|
-
match dir.parent() {
|
|
2081
|
-
Some(parent) if parent != dir => dir = parent.to_path_buf(),
|
|
2082
|
-
_ => break,
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
for start in starts {
|
|
2088
|
-
if let Some(candidate) =
|
|
2089
|
-
find_first_yaml_candidate(envx, &start, home_abs.as_ref())
|
|
2090
|
-
{
|
|
2091
|
-
return Ok(Some(ProjectConfigDiscovery {
|
|
2092
|
-
cfg_path: candidate,
|
|
2093
|
-
notices: Vec::new(),
|
|
2094
|
-
}));
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
Ok(None)
|
|
2099
|
-
}
|