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
data/src/directives.rs
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use std::ops::Range;
|
|
3
|
+
|
|
4
|
+
use crate::cop_name::department;
|
|
5
|
+
use crate::diagnostic::Offense;
|
|
6
|
+
use crate::source::SourceFile;
|
|
7
|
+
|
|
8
|
+
#[derive(Clone, Debug, Default)]
|
|
9
|
+
struct Snapshot {
|
|
10
|
+
all: bool,
|
|
11
|
+
all_reason: Option<String>,
|
|
12
|
+
cops: HashMap<String, Option<String>>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[derive(Debug, Default)]
|
|
16
|
+
pub struct DirectiveState {
|
|
17
|
+
line_states: Vec<Snapshot>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl DirectiveState {
|
|
21
|
+
pub fn parse(source: &SourceFile, comment_ranges: &[Range<usize>]) -> Self {
|
|
22
|
+
let mut current = Snapshot::default();
|
|
23
|
+
let mut stack = Vec::new();
|
|
24
|
+
let mut line_states = Vec::with_capacity(source.line_count());
|
|
25
|
+
let comment_starts: HashSet<usize> =
|
|
26
|
+
comment_ranges.iter().map(|range| range.start).collect();
|
|
27
|
+
|
|
28
|
+
for line_number in 1..=source.line_count() {
|
|
29
|
+
let line = source.line(line_number);
|
|
30
|
+
let before = current.clone();
|
|
31
|
+
let directive = find_comment_start(line)
|
|
32
|
+
.filter(|start| comment_starts.contains(&(source.line_start(line_number) + start)))
|
|
33
|
+
.and_then(|_| parse_directive(line));
|
|
34
|
+
|
|
35
|
+
if let Some(directive) = directive {
|
|
36
|
+
if directive.inline && matches!(directive.action, Action::Disable) {
|
|
37
|
+
let mut line_only = before;
|
|
38
|
+
apply_disable(&mut line_only, &directive.cops, directive.reason);
|
|
39
|
+
line_states.push(line_only);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
match directive.action {
|
|
44
|
+
Action::Disable => {
|
|
45
|
+
apply_disable(&mut current, &directive.cops, directive.reason);
|
|
46
|
+
}
|
|
47
|
+
Action::Enable => apply_enable(&mut current, &directive.cops),
|
|
48
|
+
Action::Push => {
|
|
49
|
+
stack.push(current.clone());
|
|
50
|
+
for (enable, cop) in directive.push_operations {
|
|
51
|
+
if enable {
|
|
52
|
+
apply_enable(&mut current, &[cop]);
|
|
53
|
+
} else {
|
|
54
|
+
apply_disable(&mut current, &[cop], directive.reason.clone());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
Action::Pop => {
|
|
59
|
+
if let Some(snapshot) = stack.pop() {
|
|
60
|
+
current = snapshot;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
line_states.push(current.clone());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Self { line_states }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub fn suppression(&self, offense: &Offense, source: &SourceFile) -> Option<Option<String>> {
|
|
73
|
+
let (line, _) = source.line_column(offense.start);
|
|
74
|
+
let state = self.line_states.get(line.saturating_sub(1))?;
|
|
75
|
+
if let Some(reason) = state.cops.get(offense.cop_name) {
|
|
76
|
+
return Some(reason.clone());
|
|
77
|
+
}
|
|
78
|
+
if let Some(reason) = state.cops.get(department(offense.cop_name)) {
|
|
79
|
+
return Some(reason.clone());
|
|
80
|
+
}
|
|
81
|
+
state.all.then(|| state.all_reason.clone())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn apply_disable(state: &mut Snapshot, cops: &[String], reason: Option<String>) {
|
|
86
|
+
if cops.iter().any(|cop| cop == "all") {
|
|
87
|
+
state.all = true;
|
|
88
|
+
state.all_reason = reason;
|
|
89
|
+
} else {
|
|
90
|
+
state
|
|
91
|
+
.cops
|
|
92
|
+
.extend(cops.iter().cloned().map(|cop| (cop, reason.clone())));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn apply_enable(state: &mut Snapshot, cops: &[String]) {
|
|
97
|
+
if cops.iter().any(|cop| cop == "all") {
|
|
98
|
+
state.all = false;
|
|
99
|
+
state.all_reason = None;
|
|
100
|
+
state.cops.clear();
|
|
101
|
+
} else {
|
|
102
|
+
for cop in cops {
|
|
103
|
+
state.cops.remove(cop);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[derive(Clone, Copy, Debug)]
|
|
109
|
+
enum Action {
|
|
110
|
+
Disable,
|
|
111
|
+
Enable,
|
|
112
|
+
Push,
|
|
113
|
+
Pop,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[derive(Debug)]
|
|
117
|
+
struct Directive {
|
|
118
|
+
action: Action,
|
|
119
|
+
cops: Vec<String>,
|
|
120
|
+
push_operations: Vec<(bool, String)>,
|
|
121
|
+
reason: Option<String>,
|
|
122
|
+
inline: bool,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn parse_directive(line: &str) -> Option<Directive> {
|
|
126
|
+
let comment_start = find_comment_start(line)?;
|
|
127
|
+
let comment = &line[comment_start..];
|
|
128
|
+
let marker_end = marker_end(comment)?;
|
|
129
|
+
let command = comment[marker_end..].trim_start();
|
|
130
|
+
let mode_end = command.find(char::is_whitespace).unwrap_or(command.len());
|
|
131
|
+
let mode = &command[..mode_end];
|
|
132
|
+
let remainder = command[mode_end..].trim();
|
|
133
|
+
let (arguments, reason) =
|
|
134
|
+
remainder
|
|
135
|
+
.split_once("--")
|
|
136
|
+
.map_or((remainder, None), |(arguments, reason)| {
|
|
137
|
+
let reason = reason.trim();
|
|
138
|
+
(
|
|
139
|
+
arguments.trim(),
|
|
140
|
+
(!reason.is_empty()).then(|| reason.to_owned()),
|
|
141
|
+
)
|
|
142
|
+
});
|
|
143
|
+
let action = match mode {
|
|
144
|
+
"disable" | "todo" => Action::Disable,
|
|
145
|
+
"enable" => Action::Enable,
|
|
146
|
+
"push" => Action::Push,
|
|
147
|
+
"pop" => Action::Pop,
|
|
148
|
+
_ => return None,
|
|
149
|
+
};
|
|
150
|
+
let inline = !line[..comment_start].trim().is_empty();
|
|
151
|
+
|
|
152
|
+
if matches!(action, Action::Push) {
|
|
153
|
+
let push_operations = arguments
|
|
154
|
+
.split_whitespace()
|
|
155
|
+
.filter_map(|specification| {
|
|
156
|
+
let (operator, name) = specification.split_at_checked(1)?;
|
|
157
|
+
matches!(operator, "+" | "-").then(|| (operator == "+", name.to_owned()))
|
|
158
|
+
})
|
|
159
|
+
.collect();
|
|
160
|
+
return Some(Directive {
|
|
161
|
+
action,
|
|
162
|
+
cops: Vec::new(),
|
|
163
|
+
push_operations,
|
|
164
|
+
reason,
|
|
165
|
+
inline,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if matches!(action, Action::Pop) {
|
|
169
|
+
return Some(Directive {
|
|
170
|
+
action,
|
|
171
|
+
cops: Vec::new(),
|
|
172
|
+
push_operations: Vec::new(),
|
|
173
|
+
reason,
|
|
174
|
+
inline,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if arguments.is_empty() {
|
|
178
|
+
return None;
|
|
179
|
+
}
|
|
180
|
+
let cops = arguments
|
|
181
|
+
.split(',')
|
|
182
|
+
.map(str::trim)
|
|
183
|
+
.filter(|value| !value.is_empty())
|
|
184
|
+
.map(ToOwned::to_owned)
|
|
185
|
+
.collect();
|
|
186
|
+
Some(Directive {
|
|
187
|
+
action,
|
|
188
|
+
cops,
|
|
189
|
+
push_operations: Vec::new(),
|
|
190
|
+
reason,
|
|
191
|
+
inline,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn find_comment_start(line: &str) -> Option<usize> {
|
|
196
|
+
let mut escaped = false;
|
|
197
|
+
let mut quote = None;
|
|
198
|
+
for (index, character) in line.char_indices() {
|
|
199
|
+
if escaped {
|
|
200
|
+
escaped = false;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if character == '\\' {
|
|
204
|
+
escaped = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if matches!(character, '\'' | '"') {
|
|
208
|
+
if quote == Some(character) {
|
|
209
|
+
quote = None;
|
|
210
|
+
} else if quote.is_none() {
|
|
211
|
+
quote = Some(character);
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if character == '#' && quote.is_none() {
|
|
216
|
+
return Some(index);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
None
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn marker_end(comment: &str) -> Option<usize> {
|
|
223
|
+
let bytes = comment.as_bytes();
|
|
224
|
+
let mut index = 0;
|
|
225
|
+
if bytes.get(index) != Some(&b'#') {
|
|
226
|
+
return None;
|
|
227
|
+
}
|
|
228
|
+
index += 1;
|
|
229
|
+
skip_ascii_whitespace(bytes, &mut index);
|
|
230
|
+
if !comment[index..].starts_with("rubocop") {
|
|
231
|
+
return None;
|
|
232
|
+
}
|
|
233
|
+
index += "rubocop".len();
|
|
234
|
+
skip_ascii_whitespace(bytes, &mut index);
|
|
235
|
+
if bytes.get(index) != Some(&b':') {
|
|
236
|
+
return None;
|
|
237
|
+
}
|
|
238
|
+
Some(index + 1)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn skip_ascii_whitespace(bytes: &[u8], index: &mut usize) {
|
|
242
|
+
while bytes
|
|
243
|
+
.get(*index)
|
|
244
|
+
.is_some_and(|byte| byte.is_ascii_whitespace())
|
|
245
|
+
{
|
|
246
|
+
*index += 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[cfg(test)]
|
|
251
|
+
mod tests {
|
|
252
|
+
use crate::diagnostic::{Offense, Severity};
|
|
253
|
+
use crate::source::SourceFile;
|
|
254
|
+
|
|
255
|
+
use super::DirectiveState;
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn handles_block_inline_reason_and_push_pop_directives() {
|
|
259
|
+
let source = SourceFile::new(
|
|
260
|
+
"test.rb",
|
|
261
|
+
"# rubocop:disable Layout -- legacy\n\
|
|
262
|
+
a = 1 \n\
|
|
263
|
+
# rubocop:enable Layout\n\
|
|
264
|
+
b = 2 # rubocop:disable Layout/TrailingWhitespace\n\
|
|
265
|
+
# rubocop:push -Layout/TrailingWhitespace\n\
|
|
266
|
+
c = 3 \n\
|
|
267
|
+
# rubocop:pop\n\
|
|
268
|
+
d = 4 \n"
|
|
269
|
+
.to_owned(),
|
|
270
|
+
);
|
|
271
|
+
let comment_ranges: Vec<_> = (1..=source.line_count())
|
|
272
|
+
.filter_map(|line_number| {
|
|
273
|
+
let line = source.line(line_number);
|
|
274
|
+
let local_start = line.find('#')?;
|
|
275
|
+
let start = source.line_start(line_number) + local_start;
|
|
276
|
+
Some(start..start + line[local_start..].trim_end().len())
|
|
277
|
+
})
|
|
278
|
+
.collect();
|
|
279
|
+
let directives = DirectiveState::parse(&source, &comment_ranges);
|
|
280
|
+
for (line, expected) in [(2, true), (4, true), (6, true), (8, false)] {
|
|
281
|
+
let offense = Offense::new(
|
|
282
|
+
"Layout/TrailingWhitespace",
|
|
283
|
+
Severity::Convention,
|
|
284
|
+
"test",
|
|
285
|
+
source.line_start(line),
|
|
286
|
+
source.line_start(line) + 1,
|
|
287
|
+
);
|
|
288
|
+
assert_eq!(
|
|
289
|
+
directives.suppression(&offense, &source).is_some(),
|
|
290
|
+
expected
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
let offense = Offense::new(
|
|
294
|
+
"Layout/TrailingWhitespace",
|
|
295
|
+
Severity::Convention,
|
|
296
|
+
"test",
|
|
297
|
+
source.line_start(2),
|
|
298
|
+
source.line_start(2) + 1,
|
|
299
|
+
);
|
|
300
|
+
assert_eq!(
|
|
301
|
+
directives.suppression(&offense, &source),
|
|
302
|
+
Some(Some("legacy".to_owned()))
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|