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,141 @@
1
+ use std::collections::HashMap;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+ use std::sync::{Arc, Mutex};
5
+
6
+ use anyhow::{Result, bail};
7
+
8
+ use super::Config;
9
+ use super::loader::find_config;
10
+
11
+ type ConfigCache = Mutex<HashMap<PathBuf, Arc<Config>>>;
12
+
13
+ #[derive(Debug)]
14
+ pub struct ConfigStore {
15
+ root: Arc<Config>,
16
+ discover_per_path: bool,
17
+ ignore_unrecognized: bool,
18
+ directories: ConfigCache,
19
+ cache: ConfigCache,
20
+ }
21
+
22
+ impl ConfigStore {
23
+ pub fn new(config: Config, discover_per_path: bool, ignore_unrecognized: bool) -> Self {
24
+ Self {
25
+ root: Arc::new(config),
26
+ discover_per_path,
27
+ ignore_unrecognized,
28
+ directories: Mutex::new(HashMap::new()),
29
+ cache: Mutex::new(HashMap::new()),
30
+ }
31
+ }
32
+
33
+ pub fn root(&self) -> &Config {
34
+ &self.root
35
+ }
36
+
37
+ pub fn for_path(&self, path: &Path) -> Result<Arc<Config>> {
38
+ if !self.discover_per_path {
39
+ return Ok(Arc::clone(&self.root));
40
+ }
41
+
42
+ let start = if path.is_dir() {
43
+ path
44
+ } else {
45
+ path.parent()
46
+ .filter(|parent| !parent.as_os_str().is_empty())
47
+ .unwrap_or(Path::new("."))
48
+ };
49
+ // `find_config` canonicalizes and stats every ancestor directory, so keying only
50
+ // the resolved configuration path made every file pay that cost again. Directories
51
+ // are far fewer than the files below them, so memoize on the directory itself.
52
+ if let Some(config) = cached(&self.directories, start)? {
53
+ return Ok(config);
54
+ }
55
+
56
+ let discovered = find_config(start);
57
+ let config = if discovered.as_deref() == self.root.config_path() {
58
+ Arc::clone(&self.root)
59
+ } else {
60
+ let key = discovered
61
+ .clone()
62
+ .unwrap_or_else(|| fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()));
63
+ match cached(&self.cache, &key)? {
64
+ Some(config) => config,
65
+ None => {
66
+ let config = Arc::new(Config::load(discovered.as_deref(), start)?);
67
+ if !self.ignore_unrecognized && !config.unrecognized_cop_names().is_empty() {
68
+ bail!(
69
+ "unrecognized cop(s): {}",
70
+ config.unrecognized_cop_names().join(", ")
71
+ );
72
+ }
73
+ store(&self.cache, key, &config)?;
74
+ config
75
+ }
76
+ }
77
+ };
78
+ store(&self.directories, start.to_path_buf(), &config)?;
79
+ Ok(config)
80
+ }
81
+ }
82
+
83
+ fn cached(cache: &ConfigCache, key: &Path) -> Result<Option<Arc<Config>>> {
84
+ Ok(lock(cache)?.get(key).cloned())
85
+ }
86
+
87
+ fn store(cache: &ConfigCache, key: PathBuf, config: &Arc<Config>) -> Result<()> {
88
+ lock(cache)?.insert(key, Arc::clone(config));
89
+ Ok(())
90
+ }
91
+
92
+ fn lock(cache: &ConfigCache) -> Result<std::sync::MutexGuard<'_, HashMap<PathBuf, Arc<Config>>>> {
93
+ cache
94
+ .lock()
95
+ .map_err(|_| anyhow::anyhow!("configuration cache lock is poisoned"))
96
+ }
97
+
98
+ #[cfg(test)]
99
+ mod tests {
100
+ use std::fs;
101
+
102
+ use tempfile::tempdir;
103
+
104
+ use super::{Config, ConfigStore};
105
+
106
+ #[test]
107
+ fn store_resolves_nested_configuration_from_target_path() {
108
+ let directory = tempdir().unwrap();
109
+ let nested = directory.path().join("nested");
110
+ fs::create_dir(&nested).unwrap();
111
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
112
+ fs::write(
113
+ directory.path().join(".rubocop.yml"),
114
+ "AllCops:\n DisabledByDefault: true\nLayout/TrailingWhitespace:\n Enabled: true\n",
115
+ )
116
+ .unwrap();
117
+ fs::write(
118
+ nested.join(".rubocop.yml"),
119
+ "inherit_from: ../.rubocop.yml\nLayout/TrailingWhitespace:\n Enabled: false\n",
120
+ )
121
+ .unwrap();
122
+
123
+ let root = Config::load(None, directory.path()).unwrap();
124
+ let store = ConfigStore::new(root, true, false);
125
+ let root_config = store.for_path(&directory.path().join("root.rb")).unwrap();
126
+ let nested_config = store.for_path(&nested.join("nested.rb")).unwrap();
127
+
128
+ assert!(root_config.rule_enabled("Layout/TrailingWhitespace"));
129
+ assert!(!nested_config.rule_enabled("Layout/TrailingWhitespace"));
130
+ assert_eq!(
131
+ nested_config.config_path(),
132
+ Some(
133
+ nested
134
+ .join(".rubocop.yml")
135
+ .canonicalize()
136
+ .unwrap()
137
+ .as_path()
138
+ )
139
+ );
140
+ }
141
+ }
data/src/cop_name.rs ADDED
@@ -0,0 +1,71 @@
1
+ //! How a cop's qualified name decomposes, and when a user-supplied selector names it.
2
+ //!
3
+ //! RuboCop settles both questions in `Cop::Badge` and `Cop::Registry`. Keeping them in one place
4
+ //! here is what stops `--only`, `# rubocop:disable`, configuration inheritance and `--show-cops`
5
+ //! from disagreeing about a nested cop such as `Chef/Correctness/ServiceResource`.
6
+
7
+ /// Everything before the last `/`, or the whole name when it has none.
8
+ ///
9
+ /// `Badge#initialize` joins every segment but the last, so the department of
10
+ /// `Chef/Correctness/ServiceResource` is `Chef/Correctness` rather than `Chef`.
11
+ pub fn department(cop_name: &str) -> &str {
12
+ cop_name
13
+ .rsplit_once('/')
14
+ .map_or(cop_name, |(department, _)| department)
15
+ }
16
+
17
+ /// The cop's department and every namespace enclosing it, the department itself first.
18
+ ///
19
+ /// Only plugin ownership widens like this: a gem that declares `I18n` also ships the nested
20
+ /// `I18n/GetText` department. Selectors and configuration lookups must keep using [`department`],
21
+ /// which stops at the one department the cop actually belongs to.
22
+ pub fn department_ancestors(cop_name: &str) -> impl Iterator<Item = &str> {
23
+ let department = department(cop_name);
24
+ std::iter::once(department).chain(
25
+ department
26
+ .match_indices('/')
27
+ .map(move |(offset, _)| &department[..offset]),
28
+ )
29
+ }
30
+
31
+ /// Whether a selector the user wrote -- in `--only`, `--except`, or a `rubocop:disable` comment --
32
+ /// names this cop.
33
+ ///
34
+ /// `Badge#match_name?` compares the qualified name and the department, both in full, so an outer
35
+ /// namespace does not reach a nested cop: `Chef` leaves `Chef/Correctness/ServiceResource` alone
36
+ /// and only `Chef/Correctness` selects it.
37
+ pub fn selector_matches(selector: &str, cop_name: &str) -> bool {
38
+ selector == cop_name || selector == department(cop_name)
39
+ }
40
+
41
+ #[cfg(test)]
42
+ mod tests {
43
+ use super::{department, selector_matches};
44
+
45
+ #[test]
46
+ fn department_is_every_segment_but_the_last() {
47
+ assert_eq!(department("Layout/LineLength"), "Layout");
48
+ assert_eq!(
49
+ department("Chef/Correctness/ServiceResource"),
50
+ "Chef/Correctness"
51
+ );
52
+ assert_eq!(department("Syntax"), "Syntax");
53
+ }
54
+
55
+ #[test]
56
+ fn a_selector_names_a_cop_by_full_name_or_whole_department() {
57
+ assert!(selector_matches("Layout/LineLength", "Layout/LineLength"));
58
+ assert!(selector_matches("Layout", "Layout/LineLength"));
59
+ assert!(!selector_matches("Lay", "Layout/LineLength"));
60
+ assert!(!selector_matches("Layout/Line", "Layout/LineLength"));
61
+ }
62
+
63
+ /// An outer namespace is not a department of its own, so it selects nothing below it.
64
+ #[test]
65
+ fn an_outer_namespace_does_not_select_a_nested_cop() {
66
+ let cop = "Chef/Correctness/ServiceResource";
67
+ assert!(selector_matches("Chef/Correctness", cop));
68
+ assert!(!selector_matches("Chef", cop));
69
+ assert!(selector_matches(cop, cop));
70
+ }
71
+ }
data/src/diagnostic.rs ADDED
@@ -0,0 +1,205 @@
1
+ use std::fmt;
2
+
3
+ use serde::Serialize;
4
+
5
+ use crate::source::SourceFile;
6
+
7
+ #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
8
+ #[serde(rename_all = "lowercase")]
9
+ pub enum Severity {
10
+ Info,
11
+ Refactor,
12
+ Convention,
13
+ Warning,
14
+ Error,
15
+ Fatal,
16
+ }
17
+
18
+ impl Severity {
19
+ /// The canonical RuboCop name (`Severity#to_s`). Every textual rendering derives from this so
20
+ /// that renaming a variant cannot silently change user-visible output.
21
+ pub fn as_str(self) -> &'static str {
22
+ match self {
23
+ Self::Info => "info",
24
+ Self::Refactor => "refactor",
25
+ Self::Convention => "convention",
26
+ Self::Warning => "warning",
27
+ Self::Error => "error",
28
+ Self::Fatal => "fatal",
29
+ }
30
+ }
31
+
32
+ pub fn code(self) -> char {
33
+ match self {
34
+ Self::Info => 'I',
35
+ Self::Refactor => 'R',
36
+ Self::Convention => 'C',
37
+ Self::Warning => 'W',
38
+ Self::Error => 'E',
39
+ Self::Fatal => 'F',
40
+ }
41
+ }
42
+
43
+ pub fn parse(value: &str) -> Option<Self> {
44
+ match value.to_ascii_lowercase().as_str() {
45
+ "info" | "i" => Some(Self::Info),
46
+ "refactor" | "r" => Some(Self::Refactor),
47
+ "convention" | "c" => Some(Self::Convention),
48
+ "warning" | "w" => Some(Self::Warning),
49
+ "error" | "e" => Some(Self::Error),
50
+ "fatal" | "f" => Some(Self::Fatal),
51
+ _ => None,
52
+ }
53
+ }
54
+ }
55
+
56
+ impl fmt::Display for Severity {
57
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58
+ formatter.write_str(self.as_str())
59
+ }
60
+ }
61
+
62
+ #[derive(Clone, Debug)]
63
+ pub struct Edit {
64
+ pub start: usize,
65
+ pub end: usize,
66
+ pub replacement: String,
67
+ pub safe: bool,
68
+ }
69
+
70
+ /// Position and source line captured from the text an offense was found in. An autocorrect pass
71
+ /// rewrites that text, so a corrected offense carried into the final report can no longer resolve
72
+ /// its byte offsets against the report it travels with.
73
+ #[derive(Clone, Debug)]
74
+ pub struct OffenseSnapshot {
75
+ pub location: Location,
76
+ pub source_line: String,
77
+ }
78
+
79
+ #[derive(Clone, Debug)]
80
+ pub struct Offense {
81
+ pub cop_name: &'static str,
82
+ pub severity: Severity,
83
+ pub message: String,
84
+ pub start: usize,
85
+ pub end: usize,
86
+ pub corrected: bool,
87
+ pub correctable: bool,
88
+ pub suppressed: bool,
89
+ pub justification: Option<String>,
90
+ pub correction: Option<Edit>,
91
+ pub snapshot: Option<OffenseSnapshot>,
92
+ }
93
+
94
+ impl Offense {
95
+ /// Cops do not call this: `RuleContext::offense` supplies the name and severity from the
96
+ /// registry so that a cop never names itself. It stays reachable inside the crate for the
97
+ /// engine's own offenses, which belong to no cop -- a file that is not valid UTF-8 never
98
+ /// reaches one.
99
+ pub(crate) fn new(
100
+ cop_name: &'static str,
101
+ severity: Severity,
102
+ message: impl Into<String>,
103
+ start: usize,
104
+ end: usize,
105
+ ) -> Self {
106
+ Self {
107
+ cop_name,
108
+ severity,
109
+ message: message.into(),
110
+ start,
111
+ end: end.max(start),
112
+ corrected: false,
113
+ correctable: false,
114
+ suppressed: false,
115
+ justification: None,
116
+ correction: None,
117
+ snapshot: None,
118
+ }
119
+ }
120
+
121
+ pub fn corrected_by(mut self, edit: Edit) -> Self {
122
+ self.correctable = true;
123
+ self.correction = Some(edit);
124
+ self
125
+ }
126
+
127
+ /// RuboCop derives `correctable?` from the offense status, so an offense a directive comment
128
+ /// suppressed is never correctable however the cop flagged it. Every count, filter and exit
129
+ /// code decision goes through here rather than reading the raw flag.
130
+ pub fn is_correctable(&self) -> bool {
131
+ self.correctable && !self.suppressed
132
+ }
133
+
134
+ /// Freeze the position and source line so the offense keeps reporting against the text it was
135
+ /// found in once autocorrect replaces that text.
136
+ pub fn freeze_location(&mut self, source: &SourceFile) {
137
+ if self.snapshot.is_some() {
138
+ return;
139
+ }
140
+ let location = self.location(source);
141
+ self.snapshot = Some(OffenseSnapshot {
142
+ location,
143
+ source_line: source.line(location.line).to_owned(),
144
+ });
145
+ }
146
+
147
+ /// Where the offense starts. Ordering and identity only ever need this, and resolving it alone
148
+ /// avoids touching the end of the range, which callers may not have placed on a char boundary.
149
+ pub fn start_position(&self, source: &SourceFile) -> (usize, usize) {
150
+ match &self.snapshot {
151
+ Some(snapshot) => (snapshot.location.line, snapshot.location.column),
152
+ None => source.line_column(self.start),
153
+ }
154
+ }
155
+
156
+ /// The source line the offense points at, taken from the frozen snapshot when the report's own
157
+ /// text has since been rewritten.
158
+ pub fn source_line<'a>(&'a self, source: &'a SourceFile) -> &'a str {
159
+ match &self.snapshot {
160
+ Some(snapshot) => &snapshot.source_line,
161
+ None => source.line(self.location(source).line),
162
+ }
163
+ }
164
+
165
+ pub fn location(&self, source: &SourceFile) -> Location {
166
+ if let Some(snapshot) = &self.snapshot {
167
+ return snapshot.location;
168
+ }
169
+ let (start_line, start_column) = source.line_column(self.start);
170
+ // Stepping back one byte from the exclusive end can land inside a multibyte character,
171
+ // which `line_column` cannot slice at; walk back to the character that byte belongs to.
172
+ let mut inclusive_end = self.end.saturating_sub(1).max(self.start);
173
+ while inclusive_end > self.start && !source.text().is_char_boundary(inclusive_end) {
174
+ inclusive_end -= 1;
175
+ }
176
+ let (last_line, last_column) = source.line_column(inclusive_end);
177
+ Location {
178
+ start_line,
179
+ start_column,
180
+ last_line,
181
+ last_column,
182
+ length: self.end.saturating_sub(self.start),
183
+ line: start_line,
184
+ column: start_column,
185
+ }
186
+ }
187
+ }
188
+
189
+ #[derive(Clone, Copy, Debug, Serialize)]
190
+ pub struct Location {
191
+ pub start_line: usize,
192
+ pub start_column: usize,
193
+ pub last_line: usize,
194
+ pub last_column: usize,
195
+ pub length: usize,
196
+ pub line: usize,
197
+ pub column: usize,
198
+ }
199
+
200
+ #[derive(Clone, Debug)]
201
+ pub struct FileReport {
202
+ pub path: std::path::PathBuf,
203
+ pub source: SourceFile,
204
+ pub offenses: Vec<Offense>,
205
+ }