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
data/src/config/mod.rs ADDED
@@ -0,0 +1,461 @@
1
+ mod inheritance;
2
+ mod loader;
3
+ mod paths;
4
+ mod plugin;
5
+ mod store;
6
+
7
+ use std::collections::{HashMap, HashSet};
8
+ use std::fs;
9
+ use std::path::{Path, PathBuf};
10
+
11
+ use anyhow::{Context, Result, bail};
12
+ use serde::de::DeserializeOwned;
13
+ use serde_yaml_ng::{Mapping, Value};
14
+
15
+ use crate::cop_name;
16
+ use crate::ruby_version::{
17
+ ResolvedTargetRuby, RubyVersion, resolve_target_ruby, validate_supported,
18
+ };
19
+
20
+ use inheritance::{load_with_inheritance, merge_config};
21
+ use loader::{find_config, find_project_root};
22
+ use paths::{PathPatterns, compile_excludes, cop_patterns, has_hidden_component};
23
+ use plugin::{belongs_to_plugin, configured_plugin_departments};
24
+
25
+ pub use store::ConfigStore;
26
+
27
+ const DEFAULT_CONFIG: &str = include_str!("../../config/default.yml");
28
+
29
+ #[derive(Clone, Debug)]
30
+ pub struct Config {
31
+ raw: Value,
32
+ user: Value,
33
+ project_root: PathBuf,
34
+ config_path: Option<PathBuf>,
35
+ target_ruby: ResolvedTargetRuby,
36
+ known_cops: HashSet<String>,
37
+ unrecognized_cops: Vec<String>,
38
+ includes: PathPatterns,
39
+ excludes: HashMap<String, PathPatterns>,
40
+ }
41
+
42
+ impl Config {
43
+ pub fn load(explicit: Option<&Path>, cwd: &Path) -> Result<Self> {
44
+ Self::load_with_options(explicit, cwd, false)
45
+ }
46
+
47
+ pub fn load_with_options(
48
+ explicit: Option<&Path>,
49
+ cwd: &Path,
50
+ force_default: bool,
51
+ ) -> Result<Self> {
52
+ let default: Value = serde_yaml_ng::from_str(DEFAULT_CONFIG)
53
+ .context("embedded RuboCop default configuration is invalid")?;
54
+ let mut known_cops = cop_names(&default);
55
+ let config_path = if force_default {
56
+ None
57
+ } else {
58
+ match explicit {
59
+ Some(path) => Some(fs::canonicalize(path).with_context(|| {
60
+ format!("configuration file not found: {}", path.display())
61
+ })?),
62
+ None => find_config(cwd),
63
+ }
64
+ };
65
+
66
+ let (raw, user, project_root, unrecognized_cops) = if let Some(path) = &config_path {
67
+ let mut visited = HashSet::new();
68
+ let user = load_with_inheritance(path, &mut visited)?;
69
+ let configured_cops = cop_names(&user);
70
+ let plugin_departments = configured_plugin_departments(&user);
71
+ let plugin_cops = configured_cops
72
+ .iter()
73
+ .filter(|name| belongs_to_plugin(name, &plugin_departments))
74
+ .cloned()
75
+ .collect::<HashSet<_>>();
76
+ let mut unknown = configured_cops
77
+ .difference(&known_cops)
78
+ .filter(|name| !plugin_cops.contains(*name))
79
+ .cloned()
80
+ .collect::<Vec<_>>();
81
+ unknown.sort();
82
+ known_cops.extend(plugin_cops);
83
+ let root = find_project_root(path.parent().unwrap_or(cwd))
84
+ .unwrap_or_else(|| path.parent().unwrap_or(cwd).to_path_buf());
85
+ (merge_config(default, user.clone()), user, root, unknown)
86
+ } else {
87
+ (
88
+ default,
89
+ Value::Mapping(Mapping::new()),
90
+ cwd.to_path_buf(),
91
+ Vec::new(),
92
+ )
93
+ };
94
+
95
+ if all_cops_bool(&raw, "EnabledByDefault") && all_cops_bool(&raw, "DisabledByDefault") {
96
+ bail!("AllCops/EnabledByDefault and AllCops/DisabledByDefault cannot both be true");
97
+ }
98
+
99
+ let target_base = config_path.as_deref().and_then(Path::parent).unwrap_or(cwd);
100
+ let configured_target = configured_target_ruby(&raw)?;
101
+ let target_ruby = resolve_target_ruby(configured_target, target_base)?;
102
+ validate_supported(target_ruby.version)?;
103
+
104
+ let includes = cop_patterns(&raw, "AllCops", "Include").unwrap_or_default();
105
+ let excludes = compile_excludes(&raw);
106
+
107
+ Ok(Self {
108
+ raw,
109
+ user,
110
+ project_root,
111
+ config_path,
112
+ target_ruby,
113
+ known_cops,
114
+ unrecognized_cops,
115
+ includes,
116
+ excludes,
117
+ })
118
+ }
119
+
120
+ pub fn project_root(&self) -> &Path {
121
+ &self.project_root
122
+ }
123
+
124
+ pub fn config_path(&self) -> Option<&Path> {
125
+ self.config_path.as_deref()
126
+ }
127
+
128
+ pub fn target_ruby_version(&self) -> RubyVersion {
129
+ self.target_ruby.version
130
+ }
131
+
132
+ pub fn display_cop_names(&self) -> bool {
133
+ self.all_cops_value("DisplayCopNames").unwrap_or(true)
134
+ }
135
+
136
+ pub fn rule_enabled(&self, name: &str) -> bool {
137
+ self.rule_enabled_with_pending(name, false, false)
138
+ }
139
+
140
+ pub fn rule_enabled_with_pending(
141
+ &self,
142
+ name: &str,
143
+ enable_pending: bool,
144
+ disable_pending: bool,
145
+ ) -> bool {
146
+ if name == "Lint/Syntax" {
147
+ return true;
148
+ }
149
+
150
+ let configured = self.user_cop_mapping(name);
151
+ let configured_enabled = configured.and_then(|cop| cop.get("Enabled"));
152
+ let department = self.user_department_mapping(name);
153
+ let department_enabled = department.and_then(|cop| cop.get("Enabled"));
154
+
155
+ // An explicitly enabled cop overrides a disabled department.
156
+ if configured_enabled == Some(&Value::Bool(true)) {
157
+ return true;
158
+ }
159
+ if department_enabled == Some(&Value::Bool(false)) {
160
+ return false;
161
+ }
162
+
163
+ if self.all_cops_bool_value("DisabledByDefault") {
164
+ if let Some(configured) = configured {
165
+ return configured.get("Enabled").is_none_or(|enabled| {
166
+ self.resolve_enabled_value(enabled, name, enable_pending, disable_pending)
167
+ });
168
+ }
169
+ if department_enabled == Some(&Value::Bool(true)) {
170
+ return self.default_enabled(name, enable_pending, disable_pending);
171
+ }
172
+ return false;
173
+ }
174
+
175
+ if self.all_cops_bool_value("EnabledByDefault") {
176
+ return configured_enabled.is_none_or(|enabled| {
177
+ self.resolve_enabled_value(enabled, name, enable_pending, disable_pending)
178
+ });
179
+ }
180
+
181
+ configured_enabled.map_or_else(
182
+ || self.default_enabled(name, enable_pending, disable_pending),
183
+ |enabled| self.resolve_enabled_value(enabled, name, enable_pending, disable_pending),
184
+ )
185
+ }
186
+
187
+ pub fn rule_safe(&self, name: &str) -> bool {
188
+ self.cop_raw_value(name, "Safe")
189
+ .and_then(Value::as_bool)
190
+ .unwrap_or(true)
191
+ }
192
+
193
+ pub fn rule_safe_autocorrect(&self, name: &str) -> bool {
194
+ self.cop_raw_value(name, "SafeAutoCorrect")
195
+ .and_then(Value::as_bool)
196
+ .unwrap_or(true)
197
+ }
198
+
199
+ pub fn cop_value<T: DeserializeOwned>(&self, name: &str, key: &str) -> Option<T> {
200
+ serde_yaml_ng::from_value(self.cop_raw_value(name, key)?.clone()).ok()
201
+ }
202
+
203
+ pub fn all_cops_value<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
204
+ self.cop_value("AllCops", key)
205
+ }
206
+
207
+ pub fn path_included(&self, path: &Path) -> bool {
208
+ self.includes.is_empty() || self.includes.matches_includes(path, &self.project_root)
209
+ }
210
+
211
+ pub fn path_excluded(&self, path: &Path) -> bool {
212
+ self.excluded_by("AllCops", path)
213
+ }
214
+
215
+ pub fn possibly_include_hidden(&self) -> bool {
216
+ let patterns: Vec<String> = self.all_cops_value("Include").unwrap_or_default();
217
+ patterns
218
+ .iter()
219
+ .any(|pattern| pattern.starts_with('.') || pattern.contains("/."))
220
+ }
221
+
222
+ pub fn path_hidden(&self, path: &Path) -> bool {
223
+ let relative =
224
+ paths::project_relative(path, &self.project_root).unwrap_or_else(|| path.to_path_buf());
225
+ let relative = relative.as_path();
226
+ has_hidden_component(relative)
227
+ }
228
+
229
+ pub fn rule_excluded(&self, name: &str, path: &Path) -> bool {
230
+ self.excluded_by(name, path)
231
+ }
232
+
233
+ fn excluded_by(&self, name: &str, path: &Path) -> bool {
234
+ self.excludes
235
+ .get(name)
236
+ .is_some_and(|patterns| patterns.matches_any(path, &self.project_root))
237
+ }
238
+
239
+ pub fn known_cop_names(&self) -> impl Iterator<Item = &str> {
240
+ self.known_cops.iter().map(String::as_str)
241
+ }
242
+
243
+ pub fn unrecognized_cop_names(&self) -> &[String] {
244
+ &self.unrecognized_cops
245
+ }
246
+
247
+ pub fn description(&self, name: &str) -> Option<String> {
248
+ self.cop_value(name, "Description")
249
+ }
250
+
251
+ fn cop_mapping(&self, name: &str) -> Option<&Mapping> {
252
+ self.raw.as_mapping()?.get(name)?.as_mapping()
253
+ }
254
+
255
+ fn cop_raw_value(&self, name: &str, key: &str) -> Option<&Value> {
256
+ self.cop_mapping(name)?.get(key)
257
+ }
258
+
259
+ fn user_cop_mapping(&self, name: &str) -> Option<&Mapping> {
260
+ self.user.as_mapping()?.get(name)?.as_mapping()
261
+ }
262
+
263
+ fn user_department_mapping(&self, name: &str) -> Option<&Mapping> {
264
+ self.user
265
+ .as_mapping()?
266
+ .get(cop_name::department(name))?
267
+ .as_mapping()
268
+ }
269
+
270
+ fn default_enabled(&self, name: &str, enable_pending: bool, disable_pending: bool) -> bool {
271
+ self.cop_raw_value(name, "Enabled").is_none_or(|enabled| {
272
+ self.resolve_enabled_value(enabled, name, enable_pending, disable_pending)
273
+ })
274
+ }
275
+
276
+ fn resolve_enabled_value(
277
+ &self,
278
+ enabled: &Value,
279
+ name: &str,
280
+ enable_pending: bool,
281
+ disable_pending: bool,
282
+ ) -> bool {
283
+ match enabled {
284
+ Value::Bool(value) => *value,
285
+ Value::String(value) if value == "pending" => {
286
+ if enable_pending {
287
+ true
288
+ } else if disable_pending {
289
+ false
290
+ } else {
291
+ let department_new_cops = self
292
+ .cop_raw_value(cop_name::department(name), "NewCops")
293
+ .and_then(Value::as_str);
294
+ department_new_cops.map_or_else(
295
+ || {
296
+ self.cop_raw_value("AllCops", "NewCops")
297
+ .and_then(Value::as_str)
298
+ == Some("enable")
299
+ },
300
+ |setting| setting == "enable",
301
+ )
302
+ }
303
+ }
304
+ _ => true,
305
+ }
306
+ }
307
+
308
+ fn all_cops_bool_value(&self, key: &str) -> bool {
309
+ self.cop_raw_value("AllCops", key)
310
+ .and_then(Value::as_bool)
311
+ .unwrap_or(false)
312
+ }
313
+ }
314
+
315
+ fn all_cops_bool(config: &Value, key: &str) -> bool {
316
+ all_cops_mapping(config)
317
+ .and_then(|mapping| mapping.get(key))
318
+ .and_then(Value::as_bool)
319
+ .unwrap_or(false)
320
+ }
321
+
322
+ fn all_cops_mapping(config: &Value) -> Option<&Mapping> {
323
+ config.as_mapping()?.get("AllCops")?.as_mapping()
324
+ }
325
+
326
+ fn configured_target_ruby(config: &Value) -> Result<Option<RubyVersion>> {
327
+ let value = all_cops_mapping(config).and_then(|mapping| mapping.get("TargetRubyVersion"));
328
+ let Some(value) = value else {
329
+ return Ok(None);
330
+ };
331
+ let text = match value {
332
+ Value::Null => return Ok(None),
333
+ Value::Number(number) => number.to_string(),
334
+ Value::String(string) => string.clone(),
335
+ _ => bail!("AllCops/TargetRubyVersion must be a major.minor version"),
336
+ };
337
+ RubyVersion::parse(&text)
338
+ .map(Some)
339
+ .with_context(|| format!("invalid AllCops/TargetRubyVersion: {text}"))
340
+ }
341
+
342
+ fn cop_names(value: &Value) -> HashSet<String> {
343
+ value
344
+ .as_mapping()
345
+ .into_iter()
346
+ .flat_map(Mapping::keys)
347
+ .filter_map(Value::as_str)
348
+ .filter(|name| name.contains('/'))
349
+ .map(ToOwned::to_owned)
350
+ .collect()
351
+ }
352
+
353
+ #[cfg(test)]
354
+ mod tests {
355
+ use std::fs;
356
+
357
+ use tempfile::tempdir;
358
+
359
+ use super::Config;
360
+
361
+ #[test]
362
+ fn recognizes_all_upstream_cops() {
363
+ let directory = tempdir().unwrap();
364
+ let config = Config::load(None, directory.path()).unwrap();
365
+ assert_eq!(config.known_cop_names().count(), 609);
366
+ assert!(!config.rule_enabled("Style/ArrayFirstLast"));
367
+ }
368
+
369
+ #[test]
370
+ fn disabled_by_default_enables_only_configured_cops() {
371
+ let directory = tempdir().unwrap();
372
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
373
+ fs::write(
374
+ directory.path().join(".rubocop.yml"),
375
+ "AllCops:\n DisabledByDefault: true\nLayout/TrailingWhitespace:\n Enabled: true\nStyle/StringLiterals:\n EnforcedStyle: double_quotes\n",
376
+ )
377
+ .unwrap();
378
+
379
+ let config = Config::load(None, directory.path()).unwrap();
380
+
381
+ assert!(config.rule_enabled("Lint/Syntax"));
382
+ assert!(config.rule_enabled("Layout/TrailingWhitespace"));
383
+ assert!(config.rule_enabled("Style/StringLiterals"));
384
+ assert!(!config.rule_enabled("Layout/SpaceAfterComma"));
385
+ }
386
+
387
+ #[test]
388
+ fn explicitly_enabled_cop_overrides_disabled_department() {
389
+ let directory = tempdir().unwrap();
390
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
391
+ fs::write(
392
+ directory.path().join(".rubocop.yml"),
393
+ "Layout:\n Enabled: false\nLayout/TrailingWhitespace:\n Enabled: true\n",
394
+ )
395
+ .unwrap();
396
+
397
+ let config = Config::load(None, directory.path()).unwrap();
398
+
399
+ assert!(config.rule_enabled("Layout/TrailingWhitespace"));
400
+ assert!(!config.rule_enabled("Layout/SpaceAfterComma"));
401
+ }
402
+
403
+ #[test]
404
+ fn enabled_by_default_preserves_explicit_disables() {
405
+ let directory = tempdir().unwrap();
406
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
407
+ fs::write(
408
+ directory.path().join(".rubocop.yml"),
409
+ "AllCops:\n EnabledByDefault: true\nStyle/ArrayFirstLast:\n Enabled: false\n",
410
+ )
411
+ .unwrap();
412
+
413
+ let config = Config::load(None, directory.path()).unwrap();
414
+
415
+ assert!(config.rule_enabled("Style/HashSyntax"));
416
+ assert!(!config.rule_enabled("Style/ArrayFirstLast"));
417
+ }
418
+
419
+ #[test]
420
+ fn still_reports_unknown_core_cops() {
421
+ let directory = tempdir().unwrap();
422
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
423
+ fs::write(
424
+ directory.path().join(".rubocop.yml"),
425
+ "Style/DefinitelyNotACop:\n Enabled: true\n",
426
+ )
427
+ .unwrap();
428
+
429
+ let config = Config::load(None, directory.path()).unwrap();
430
+
431
+ assert_eq!(
432
+ config.unrecognized_cop_names(),
433
+ &["Style/DefinitelyNotACop"]
434
+ );
435
+ }
436
+
437
+ #[test]
438
+ fn rejects_conflicting_default_modes() {
439
+ let directory = tempdir().unwrap();
440
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
441
+ fs::write(
442
+ directory.path().join(".rubocop.yml"),
443
+ "AllCops:\n EnabledByDefault: true\n DisabledByDefault: true\n",
444
+ )
445
+ .unwrap();
446
+
447
+ assert!(Config::load(None, directory.path()).is_err());
448
+ }
449
+
450
+ #[test]
451
+ fn relative_excludes_do_not_match_paths_outside_the_project_root() {
452
+ let project = tempdir().unwrap();
453
+ let external = tempdir().unwrap();
454
+ let config = Config::load(None, project.path()).unwrap();
455
+ let local_gemspec = project.path().join("local.gemspec");
456
+ let external_gemspec = external.path().join("external.gemspec");
457
+
458
+ assert!(config.rule_excluded("Metrics/BlockLength", &local_gemspec));
459
+ assert!(!config.rule_excluded("Metrics/BlockLength", &external_gemspec));
460
+ }
461
+ }