sonicop 26.8.101
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.
- checksums.yaml +7 -0
- data/CONFORMANCE.md +109 -0
- data/Cargo.lock +975 -0
- data/Cargo.toml +40 -0
- data/LICENSE +21 -0
- data/NOTICE +7 -0
- data/README.ja.md +161 -0
- data/README.md +173 -0
- data/config/default.yml +6305 -0
- data/exe/sonicop +7 -0
- data/ext/sonicop/extconf.rb +38 -0
- data/lib/sonicop/runner.rb +67 -0
- data/lib/sonicop/version.rb +6 -0
- data/lib/sonicop.rb +8 -0
- data/licenses/RUBOCOP.txt +20 -0
- data/licenses/TREE_SITTER_RUBY.txt +21 -0
- data/src/cli.rs +940 -0
- data/src/config/inheritance.rs +332 -0
- data/src/config/loader.rs +117 -0
- data/src/config/mod.rs +461 -0
- data/src/config/paths.rs +436 -0
- data/src/config/plugin.rs +247 -0
- data/src/config/store.rs +141 -0
- data/src/cop_name.rs +71 -0
- data/src/diagnostic.rs +205 -0
- data/src/directives.rs +305 -0
- data/src/engine.rs +734 -0
- data/src/formatter.rs +684 -0
- data/src/lib.rs +42 -0
- data/src/main.rs +3 -0
- data/src/ruby_version.rs +393 -0
- data/src/rules/layout/empty_line_after_magic_comment.rs +49 -0
- data/src/rules/layout/end_of_line.rs +35 -0
- data/src/rules/layout/line_length.rs +167 -0
- data/src/rules/layout/mod.rs +13 -0
- data/src/rules/layout/space_after_comma.rs +32 -0
- data/src/rules/layout/space_around_operators.rs +104 -0
- data/src/rules/layout/space_inside_parens.rs +100 -0
- data/src/rules/layout/support.rs +23 -0
- data/src/rules/layout/trailing_empty_lines.rs +38 -0
- data/src/rules/layout/trailing_whitespace.rs +26 -0
- data/src/rules/lint/duplicate_methods.rs +72 -0
- data/src/rules/lint/mod.rs +7 -0
- data/src/rules/lint/syntax.rs +200 -0
- data/src/rules/lint/unused_block_argument.rs +74 -0
- data/src/rules/lint/useless_assignment.rs +104 -0
- data/src/rules/metrics/block_length.rs +39 -0
- data/src/rules/metrics/class_length.rs +34 -0
- data/src/rules/metrics/method_length.rs +10 -0
- data/src/rules/metrics/mod.rs +10 -0
- data/src/rules/metrics/module_length.rs +17 -0
- data/src/rules/metrics/parameter_lists.rs +21 -0
- data/src/rules/metrics/support.rs +105 -0
- data/src/rules/mod.rs +380 -0
- data/src/rules/naming/ascii_identifiers.rs +17 -0
- data/src/rules/naming/constant_name.rs +49 -0
- data/src/rules/naming/method_name.rs +51 -0
- data/src/rules/naming/mod.rs +9 -0
- data/src/rules/naming/support.rs +22 -0
- data/src/rules/naming/variable_name.rs +50 -0
- data/src/rules/security/eval.rs +170 -0
- data/src/rules/security/mod.rs +6 -0
- data/src/rules/style/frozen_string_literal_comment.rs +56 -0
- data/src/rules/style/hash_syntax.rs +63 -0
- data/src/rules/style/mod.rs +9 -0
- data/src/rules/style/numeric_literals.rs +59 -0
- data/src/rules/style/redundant_return.rs +143 -0
- data/src/rules/style/semicolon.rs +48 -0
- data/src/rules/style/string_literals.rs +74 -0
- data/src/rules/support.rs +31 -0
- data/src/source.rs +118 -0
- metadata +117 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
use std::process::Command;
|
|
5
|
+
use std::time::Duration;
|
|
6
|
+
|
|
7
|
+
use anyhow::{Context, Result, bail};
|
|
8
|
+
use serde_yaml_ng::{Mapping, Value};
|
|
9
|
+
|
|
10
|
+
pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>) -> Result<Value> {
|
|
11
|
+
let canonical = fs::canonicalize(path)
|
|
12
|
+
.with_context(|| format!("configuration file not found: {}", path.display()))?;
|
|
13
|
+
if !visited.insert(canonical.clone()) {
|
|
14
|
+
bail!("circular inherit_from detected at {}", canonical.display());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let contents = fs::read_to_string(&canonical)
|
|
18
|
+
.with_context(|| format!("failed to read configuration: {}", canonical.display()))?;
|
|
19
|
+
let mut current: Value = serde_yaml_ng::from_str(&contents)
|
|
20
|
+
.with_context(|| format!("invalid YAML in {}", canonical.display()))?;
|
|
21
|
+
let inherit = take_mapping_key(&mut current, "inherit_from");
|
|
22
|
+
let inherit_gem = take_mapping_key(&mut current, "inherit_gem");
|
|
23
|
+
let parent = canonical.parent().unwrap_or(Path::new("."));
|
|
24
|
+
let mut inherited_paths = resolve_inherit_gems(inherit_gem)?;
|
|
25
|
+
inherited_paths.extend(parse_inherit_paths(inherit)?);
|
|
26
|
+
let mut merged = Value::Mapping(Mapping::new());
|
|
27
|
+
|
|
28
|
+
for inherited in inherited_paths {
|
|
29
|
+
if inherited.starts_with("http://") || inherited.starts_with("https://") {
|
|
30
|
+
let mut remote_visited = HashSet::new();
|
|
31
|
+
merged = merge_config(
|
|
32
|
+
merged,
|
|
33
|
+
load_remote_with_inheritance(&inherited, &mut remote_visited)?,
|
|
34
|
+
);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let inherited = PathBuf::from(inherited);
|
|
38
|
+
let inherited = if inherited.is_absolute() {
|
|
39
|
+
inherited
|
|
40
|
+
} else {
|
|
41
|
+
parent.join(inherited)
|
|
42
|
+
};
|
|
43
|
+
merged = merge_config(merged, load_with_inheritance(&inherited, visited)?);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
visited.remove(&canonical);
|
|
47
|
+
Ok(merge_config(merged, current))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn load_remote_with_inheritance(url: &str, visited: &mut HashSet<String>) -> Result<Value> {
|
|
51
|
+
if !visited.insert(url.to_owned()) {
|
|
52
|
+
bail!("circular remote inherit_from detected at {url}");
|
|
53
|
+
}
|
|
54
|
+
let contents = fetch_remote_config(url)?;
|
|
55
|
+
let mut current: Value =
|
|
56
|
+
serde_yaml_ng::from_str(&contents).with_context(|| format!("invalid YAML from {url}"))?;
|
|
57
|
+
let inherit = take_mapping_key(&mut current, "inherit_from");
|
|
58
|
+
let mut merged = Value::Mapping(Mapping::new());
|
|
59
|
+
for inherited in parse_inherit_paths(inherit)? {
|
|
60
|
+
let inherited_url = if inherited.starts_with("http://") || inherited.starts_with("https://")
|
|
61
|
+
{
|
|
62
|
+
inherited
|
|
63
|
+
} else {
|
|
64
|
+
join_remote_url(url, &inherited)?
|
|
65
|
+
};
|
|
66
|
+
merged = merge_config(
|
|
67
|
+
merged,
|
|
68
|
+
load_remote_with_inheritance(&inherited_url, visited)?,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
visited.remove(url);
|
|
72
|
+
Ok(merge_config(merged, current))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn fetch_remote_config(url: &str) -> Result<String> {
|
|
76
|
+
use ureq::tls::{RootCerts, TlsConfig, TlsProvider};
|
|
77
|
+
|
|
78
|
+
let agent: ureq::Agent = ureq::Agent::config_builder()
|
|
79
|
+
.timeout_global(Some(Duration::from_secs(30)))
|
|
80
|
+
.tls_config(
|
|
81
|
+
TlsConfig::builder()
|
|
82
|
+
.provider(TlsProvider::NativeTls)
|
|
83
|
+
.root_certs(RootCerts::PlatformVerifier)
|
|
84
|
+
.build(),
|
|
85
|
+
)
|
|
86
|
+
.build()
|
|
87
|
+
.into();
|
|
88
|
+
let mut response = agent
|
|
89
|
+
.get(url)
|
|
90
|
+
.call()
|
|
91
|
+
.with_context(|| format!("failed to fetch remote configuration: {url}"))?;
|
|
92
|
+
response
|
|
93
|
+
.body_mut()
|
|
94
|
+
.with_config()
|
|
95
|
+
.limit(5 * 1024 * 1024)
|
|
96
|
+
.read_to_string()
|
|
97
|
+
.with_context(|| format!("failed to read remote configuration: {url}"))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn join_remote_url(base: &str, relative: &str) -> Result<String> {
|
|
101
|
+
if relative.starts_with('/') {
|
|
102
|
+
let scheme_end = base
|
|
103
|
+
.find("://")
|
|
104
|
+
.context("remote configuration URL has no scheme")?
|
|
105
|
+
+ 3;
|
|
106
|
+
let host_end = base[scheme_end..]
|
|
107
|
+
.find('/')
|
|
108
|
+
.map_or(base.len(), |offset| scheme_end + offset);
|
|
109
|
+
return Ok(format!("{}{}", &base[..host_end], relative));
|
|
110
|
+
}
|
|
111
|
+
let directory_end = base
|
|
112
|
+
.rfind('/')
|
|
113
|
+
.context("remote configuration URL has no directory")?
|
|
114
|
+
+ 1;
|
|
115
|
+
Ok(format!("{}{}", &base[..directory_end], relative))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn parse_inherit_paths(value: Option<Value>) -> Result<Vec<String>> {
|
|
119
|
+
match value {
|
|
120
|
+
None => Ok(Vec::new()),
|
|
121
|
+
Some(Value::String(path)) => Ok(vec![path]),
|
|
122
|
+
Some(Value::Sequence(values)) => values
|
|
123
|
+
.into_iter()
|
|
124
|
+
.map(|value| match value {
|
|
125
|
+
Value::String(path) => Ok(path),
|
|
126
|
+
_ => bail!("inherit_from entries must be paths"),
|
|
127
|
+
})
|
|
128
|
+
.collect(),
|
|
129
|
+
Some(_) => bail!("inherit_from must be a path or a list of paths"),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn resolve_inherit_gems(value: Option<Value>) -> Result<Vec<String>> {
|
|
134
|
+
let Some(Value::Mapping(gems)) = value else {
|
|
135
|
+
return Ok(Vec::new());
|
|
136
|
+
};
|
|
137
|
+
let mut paths = Vec::new();
|
|
138
|
+
for (gem, values) in gems {
|
|
139
|
+
let Some(gem) = gem.as_str() else {
|
|
140
|
+
bail!("inherit_gem keys must be gem names");
|
|
141
|
+
};
|
|
142
|
+
let relative_paths = parse_inherit_paths(Some(values))?;
|
|
143
|
+
for relative in relative_paths {
|
|
144
|
+
let script = "spec = Gem::Specification.find_by_name(ARGV.shift); puts File.join(spec.full_gem_path, ARGV.shift)";
|
|
145
|
+
let output = Command::new("ruby")
|
|
146
|
+
.args(["-rrubygems", "-e", script, gem, &relative])
|
|
147
|
+
.output()
|
|
148
|
+
.with_context(|| format!("failed to resolve inherit_gem {gem}"))?;
|
|
149
|
+
if !output.status.success() {
|
|
150
|
+
bail!(
|
|
151
|
+
"failed to resolve inherit_gem {gem}: {}",
|
|
152
|
+
String::from_utf8_lossy(&output.stderr).trim()
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
let resolved = String::from_utf8(output.stdout)
|
|
156
|
+
.with_context(|| format!("inherit_gem {gem} resolved to a non-UTF-8 path"))?;
|
|
157
|
+
paths.push(resolved.trim().to_owned());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
Ok(paths)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn take_mapping_key(value: &mut Value, key: &str) -> Option<Value> {
|
|
164
|
+
value
|
|
165
|
+
.as_mapping_mut()?
|
|
166
|
+
.remove(Value::String(key.to_owned()))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub(super) fn merge_config(base: Value, overlay: Value) -> Value {
|
|
170
|
+
let global_merge = inherit_merge_keys(&overlay);
|
|
171
|
+
match (base, overlay) {
|
|
172
|
+
(Value::Mapping(mut base), Value::Mapping(overlay)) => {
|
|
173
|
+
for (key, value) in overlay {
|
|
174
|
+
let local_merge = inherit_merge_keys(&value);
|
|
175
|
+
let merge_keys = if local_merge.is_empty() {
|
|
176
|
+
&global_merge
|
|
177
|
+
} else {
|
|
178
|
+
&local_merge
|
|
179
|
+
};
|
|
180
|
+
// `map_or` evaluates its default eagerly, so every key paid for a deep
|
|
181
|
+
// clone even though most keys are absent from `base`.
|
|
182
|
+
let merged = match base.remove(&key) {
|
|
183
|
+
Some(old) => deep_merge(old, value, merge_keys),
|
|
184
|
+
None => value,
|
|
185
|
+
};
|
|
186
|
+
base.insert(key, merged);
|
|
187
|
+
}
|
|
188
|
+
Value::Mapping(base)
|
|
189
|
+
}
|
|
190
|
+
(_, overlay) => overlay,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn deep_merge(base: Value, overlay: Value, merge_keys: &HashSet<String>) -> Value {
|
|
195
|
+
match (base, overlay) {
|
|
196
|
+
(Value::Mapping(mut base), Value::Mapping(overlay)) => {
|
|
197
|
+
for (key, value) in overlay {
|
|
198
|
+
let should_merge_sequence =
|
|
199
|
+
key.as_str().is_some_and(|name| merge_keys.contains(name));
|
|
200
|
+
let merged = match (base.remove(&key), value) {
|
|
201
|
+
(Some(Value::Sequence(mut old)), Value::Sequence(new))
|
|
202
|
+
if should_merge_sequence =>
|
|
203
|
+
{
|
|
204
|
+
old.extend(new);
|
|
205
|
+
Value::Sequence(old)
|
|
206
|
+
}
|
|
207
|
+
(Some(old), new) => deep_merge(old, new, merge_keys),
|
|
208
|
+
(None, new) => new,
|
|
209
|
+
};
|
|
210
|
+
base.insert(key, merged);
|
|
211
|
+
}
|
|
212
|
+
Value::Mapping(base)
|
|
213
|
+
}
|
|
214
|
+
(_, overlay) => overlay,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn inherit_merge_keys(value: &Value) -> HashSet<String> {
|
|
219
|
+
let Some(mode) = value
|
|
220
|
+
.as_mapping()
|
|
221
|
+
.and_then(|mapping| mapping.get("inherit_mode"))
|
|
222
|
+
.and_then(Value::as_mapping)
|
|
223
|
+
.and_then(|mapping| mapping.get("merge"))
|
|
224
|
+
.and_then(Value::as_sequence)
|
|
225
|
+
else {
|
|
226
|
+
return HashSet::new();
|
|
227
|
+
};
|
|
228
|
+
mode.iter()
|
|
229
|
+
.filter_map(Value::as_str)
|
|
230
|
+
.map(ToOwned::to_owned)
|
|
231
|
+
.collect()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[cfg(test)]
|
|
235
|
+
mod tests {
|
|
236
|
+
use std::fs;
|
|
237
|
+
|
|
238
|
+
use serde_yaml_ng::Value;
|
|
239
|
+
use tempfile::tempdir;
|
|
240
|
+
|
|
241
|
+
use super::{deep_merge, join_remote_url, merge_config};
|
|
242
|
+
use crate::config::Config;
|
|
243
|
+
|
|
244
|
+
fn yaml(text: &str) -> Value {
|
|
245
|
+
serde_yaml_ng::from_str(text).unwrap()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn joins_remote_inherit_urls() {
|
|
250
|
+
assert_eq!(
|
|
251
|
+
join_remote_url("https://example.com/team/base.yml", "shared.yml").unwrap(),
|
|
252
|
+
"https://example.com/team/shared.yml"
|
|
253
|
+
);
|
|
254
|
+
assert_eq!(
|
|
255
|
+
join_remote_url("https://example.com/team/base.yml", "/root.yml").unwrap(),
|
|
256
|
+
"https://example.com/root.yml"
|
|
257
|
+
);
|
|
258
|
+
assert_eq!(
|
|
259
|
+
join_remote_url("https://example.com", "/root.yml").unwrap(),
|
|
260
|
+
"https://example.com/root.yml"
|
|
261
|
+
);
|
|
262
|
+
assert!(join_remote_url("example.com/base.yml", "/root.yml").is_err());
|
|
263
|
+
assert!(join_remote_url("no-directory", "shared.yml").is_err());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn merges_configurations_without_losing_untouched_keys() {
|
|
268
|
+
let merged = merge_config(
|
|
269
|
+
yaml("Style/A:\n Max: 1\nStyle/B:\n Max: 2\n"),
|
|
270
|
+
yaml("Style/A:\n Enabled: false\nStyle/C:\n Max: 3\n"),
|
|
271
|
+
);
|
|
272
|
+
assert_eq!(
|
|
273
|
+
merged,
|
|
274
|
+
yaml("Style/A:\n Max: 1\n Enabled: false\nStyle/B:\n Max: 2\nStyle/C:\n Max: 3\n")
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn merge_replaces_sequences_unless_inherit_mode_requests_merging() {
|
|
280
|
+
let replaced = merge_config(
|
|
281
|
+
yaml("Style/A:\n Exclude: [a.rb]\n"),
|
|
282
|
+
yaml("Style/A:\n Exclude: [b.rb]\n"),
|
|
283
|
+
);
|
|
284
|
+
assert_eq!(replaced, yaml("Style/A:\n Exclude: [b.rb]\n"));
|
|
285
|
+
|
|
286
|
+
let appended = merge_config(
|
|
287
|
+
yaml("Style/A:\n Exclude: [a.rb]\n"),
|
|
288
|
+
yaml("inherit_mode:\n merge:\n - Exclude\nStyle/A:\n Exclude: [b.rb]\n"),
|
|
289
|
+
);
|
|
290
|
+
assert_eq!(
|
|
291
|
+
appended.as_mapping().unwrap().get("Style/A").unwrap(),
|
|
292
|
+
&yaml("Exclude: [a.rb, b.rb]\n")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn merge_prefers_scalar_overlays_over_mappings() {
|
|
298
|
+
assert_eq!(merge_config(yaml("A:\n x: 1\n"), yaml("3")), yaml("3"));
|
|
299
|
+
let keys = std::collections::HashSet::new();
|
|
300
|
+
assert_eq!(deep_merge(yaml("[1, 2]"), yaml("[3]"), &keys), yaml("[3]"));
|
|
301
|
+
assert_eq!(
|
|
302
|
+
deep_merge(yaml("a: 1\n"), yaml("b: 2\n"), &keys),
|
|
303
|
+
yaml("a: 1\nb: 2\n")
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn loads_inherited_configuration_and_merges_selected_arrays() {
|
|
309
|
+
let directory = tempdir().unwrap();
|
|
310
|
+
fs::write(directory.path().join("Gemfile"), "").unwrap();
|
|
311
|
+
fs::write(
|
|
312
|
+
directory.path().join("base.yml"),
|
|
313
|
+
"Layout/LineLength:\n Max: 90\n Exclude: [a.rb]\n",
|
|
314
|
+
)
|
|
315
|
+
.unwrap();
|
|
316
|
+
fs::write(
|
|
317
|
+
directory.path().join(".rubocop.yml"),
|
|
318
|
+
"inherit_from: base.yml\ninherit_mode:\n merge:\n - Exclude\nLayout/LineLength:\n Enabled: false\n Exclude: [b.rb]\n",
|
|
319
|
+
)
|
|
320
|
+
.unwrap();
|
|
321
|
+
let config = Config::load(None, directory.path()).unwrap();
|
|
322
|
+
assert_eq!(
|
|
323
|
+
config.cop_value::<usize>("Layout/LineLength", "Max"),
|
|
324
|
+
Some(90)
|
|
325
|
+
);
|
|
326
|
+
assert_eq!(
|
|
327
|
+
config.cop_value::<Vec<String>>("Layout/LineLength", "Exclude"),
|
|
328
|
+
Some(vec!["a.rb".to_owned(), "b.rb".to_owned()])
|
|
329
|
+
);
|
|
330
|
+
assert!(!config.rule_enabled("Layout/LineLength"));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
use std::ffi::OsString;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
|
|
5
|
+
pub(super) fn find_config(start: &Path) -> Option<PathBuf> {
|
|
6
|
+
let start = fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf());
|
|
7
|
+
let project_root = find_project_root(&start);
|
|
8
|
+
|
|
9
|
+
for directory in start.ancestors() {
|
|
10
|
+
let candidate = directory.join(".rubocop.yml");
|
|
11
|
+
if candidate.is_file() {
|
|
12
|
+
return fs::canonicalize(candidate).ok();
|
|
13
|
+
}
|
|
14
|
+
if project_root.as_deref().is_none_or(|root| directory == root) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if let Some(root) = project_root {
|
|
20
|
+
for candidate in [
|
|
21
|
+
root.join(".config/.rubocop.yml"),
|
|
22
|
+
root.join(".config/rubocop/config.yml"),
|
|
23
|
+
] {
|
|
24
|
+
if candidate.is_file() {
|
|
25
|
+
return fs::canonicalize(candidate).ok();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let home = home_directory();
|
|
31
|
+
if let Some(candidate) = home.as_ref().map(|home| home.join(".rubocop.yml"))
|
|
32
|
+
&& candidate.is_file()
|
|
33
|
+
{
|
|
34
|
+
return fs::canonicalize(candidate).ok();
|
|
35
|
+
}
|
|
36
|
+
let xdg = std::env::var_os("XDG_CONFIG_HOME")
|
|
37
|
+
.map(PathBuf::from)
|
|
38
|
+
.or_else(|| home.map(|home| home.join(".config")));
|
|
39
|
+
if let Some(candidate) = xdg.map(|root| root.join("rubocop/config.yml"))
|
|
40
|
+
&& candidate.is_file()
|
|
41
|
+
{
|
|
42
|
+
return fs::canonicalize(candidate).ok();
|
|
43
|
+
}
|
|
44
|
+
None
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn home_directory() -> Option<PathBuf> {
|
|
48
|
+
resolve_home_directory(|key| std::env::var_os(key))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// RuboCop expands `~` through `Dir.home`, which on Windows falls back to
|
|
52
|
+
/// `USERPROFILE` and then `HOMEDRIVE`+`HOMEPATH` because `HOME` is normally unset
|
|
53
|
+
/// there. Reading `HOME` alone hides the user-global configuration on Windows.
|
|
54
|
+
fn resolve_home_directory(lookup: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
|
|
55
|
+
let present = |key: &str| lookup(key).filter(|value| !value.is_empty());
|
|
56
|
+
if let Some(home) = present("HOME").or_else(|| present("USERPROFILE")) {
|
|
57
|
+
return Some(PathBuf::from(home));
|
|
58
|
+
}
|
|
59
|
+
let mut home = present("HOMEDRIVE")?;
|
|
60
|
+
home.push(present("HOMEPATH")?);
|
|
61
|
+
Some(PathBuf::from(home))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub(super) fn find_project_root(start: &Path) -> Option<PathBuf> {
|
|
65
|
+
start
|
|
66
|
+
.ancestors()
|
|
67
|
+
.filter(|directory| {
|
|
68
|
+
directory.join("Gemfile").is_file() || directory.join("gems.rb").is_file()
|
|
69
|
+
})
|
|
70
|
+
.last()
|
|
71
|
+
.map(Path::to_path_buf)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[cfg(test)]
|
|
75
|
+
mod tests {
|
|
76
|
+
use std::ffi::OsString;
|
|
77
|
+
|
|
78
|
+
use super::resolve_home_directory;
|
|
79
|
+
|
|
80
|
+
fn environment<'a>(pairs: &'a [(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + 'a {
|
|
81
|
+
move |key| {
|
|
82
|
+
pairs
|
|
83
|
+
.iter()
|
|
84
|
+
.find(|(name, _)| *name == key)
|
|
85
|
+
.map(|(_, value)| OsString::from(*value))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn resolves_home_directory_across_platforms() {
|
|
91
|
+
assert_eq!(
|
|
92
|
+
resolve_home_directory(environment(&[("HOME", "/home/dev")])),
|
|
93
|
+
Some("/home/dev".into())
|
|
94
|
+
);
|
|
95
|
+
// Windows leaves HOME unset, so RuboCop falls through to USERPROFILE.
|
|
96
|
+
assert_eq!(
|
|
97
|
+
resolve_home_directory(environment(&[("USERPROFILE", r"C:\Users\dev")])),
|
|
98
|
+
Some(r"C:\Users\dev".into())
|
|
99
|
+
);
|
|
100
|
+
assert_eq!(
|
|
101
|
+
resolve_home_directory(environment(&[("HOME", ""), ("USERPROFILE", r"D:\dev")])),
|
|
102
|
+
Some(r"D:\dev".into())
|
|
103
|
+
);
|
|
104
|
+
assert_eq!(
|
|
105
|
+
resolve_home_directory(environment(&[
|
|
106
|
+
("HOMEDRIVE", "C:"),
|
|
107
|
+
("HOMEPATH", r"\Users\dev")
|
|
108
|
+
])),
|
|
109
|
+
Some(r"C:\Users\dev".into())
|
|
110
|
+
);
|
|
111
|
+
assert_eq!(
|
|
112
|
+
resolve_home_directory(environment(&[("HOMEDRIVE", "C:")])),
|
|
113
|
+
None
|
|
114
|
+
);
|
|
115
|
+
assert_eq!(resolve_home_directory(environment(&[])), None);
|
|
116
|
+
}
|
|
117
|
+
}
|