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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CONFORMANCE.md +109 -0
  3. data/Cargo.lock +975 -0
  4. data/Cargo.toml +40 -0
  5. data/LICENSE +21 -0
  6. data/NOTICE +7 -0
  7. data/README.ja.md +161 -0
  8. data/README.md +173 -0
  9. data/config/default.yml +6305 -0
  10. data/exe/sonicop +7 -0
  11. data/ext/sonicop/extconf.rb +38 -0
  12. data/lib/sonicop/runner.rb +67 -0
  13. data/lib/sonicop/version.rb +6 -0
  14. data/lib/sonicop.rb +8 -0
  15. data/licenses/RUBOCOP.txt +20 -0
  16. data/licenses/TREE_SITTER_RUBY.txt +21 -0
  17. data/src/cli.rs +940 -0
  18. data/src/config/inheritance.rs +332 -0
  19. data/src/config/loader.rs +117 -0
  20. data/src/config/mod.rs +461 -0
  21. data/src/config/paths.rs +436 -0
  22. data/src/config/plugin.rs +247 -0
  23. data/src/config/store.rs +141 -0
  24. data/src/cop_name.rs +71 -0
  25. data/src/diagnostic.rs +205 -0
  26. data/src/directives.rs +305 -0
  27. data/src/engine.rs +734 -0
  28. data/src/formatter.rs +684 -0
  29. data/src/lib.rs +42 -0
  30. data/src/main.rs +3 -0
  31. data/src/ruby_version.rs +393 -0
  32. data/src/rules/layout/empty_line_after_magic_comment.rs +49 -0
  33. data/src/rules/layout/end_of_line.rs +35 -0
  34. data/src/rules/layout/line_length.rs +167 -0
  35. data/src/rules/layout/mod.rs +13 -0
  36. data/src/rules/layout/space_after_comma.rs +32 -0
  37. data/src/rules/layout/space_around_operators.rs +104 -0
  38. data/src/rules/layout/space_inside_parens.rs +100 -0
  39. data/src/rules/layout/support.rs +23 -0
  40. data/src/rules/layout/trailing_empty_lines.rs +38 -0
  41. data/src/rules/layout/trailing_whitespace.rs +26 -0
  42. data/src/rules/lint/duplicate_methods.rs +72 -0
  43. data/src/rules/lint/mod.rs +7 -0
  44. data/src/rules/lint/syntax.rs +200 -0
  45. data/src/rules/lint/unused_block_argument.rs +74 -0
  46. data/src/rules/lint/useless_assignment.rs +104 -0
  47. data/src/rules/metrics/block_length.rs +39 -0
  48. data/src/rules/metrics/class_length.rs +34 -0
  49. data/src/rules/metrics/method_length.rs +10 -0
  50. data/src/rules/metrics/mod.rs +10 -0
  51. data/src/rules/metrics/module_length.rs +17 -0
  52. data/src/rules/metrics/parameter_lists.rs +21 -0
  53. data/src/rules/metrics/support.rs +105 -0
  54. data/src/rules/mod.rs +380 -0
  55. data/src/rules/naming/ascii_identifiers.rs +17 -0
  56. data/src/rules/naming/constant_name.rs +49 -0
  57. data/src/rules/naming/method_name.rs +51 -0
  58. data/src/rules/naming/mod.rs +9 -0
  59. data/src/rules/naming/support.rs +22 -0
  60. data/src/rules/naming/variable_name.rs +50 -0
  61. data/src/rules/security/eval.rs +170 -0
  62. data/src/rules/security/mod.rs +6 -0
  63. data/src/rules/style/frozen_string_literal_comment.rs +56 -0
  64. data/src/rules/style/hash_syntax.rs +63 -0
  65. data/src/rules/style/mod.rs +9 -0
  66. data/src/rules/style/numeric_literals.rs +59 -0
  67. data/src/rules/style/redundant_return.rs +143 -0
  68. data/src/rules/style/semicolon.rs +48 -0
  69. data/src/rules/style/string_literals.rs +74 -0
  70. data/src/rules/support.rs +31 -0
  71. data/src/source.rs +118 -0
  72. 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
+ }