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/rules/mod.rs
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::ops::Range;
|
|
3
|
+
|
|
4
|
+
use tree_sitter::Node;
|
|
5
|
+
|
|
6
|
+
use crate::config::Config;
|
|
7
|
+
use crate::diagnostic::{Offense, Severity};
|
|
8
|
+
use crate::ruby_version::RubyVersion;
|
|
9
|
+
use crate::source::SourceFile;
|
|
10
|
+
|
|
11
|
+
/// Registers one department's cops. Each entry names the module the cop lives in, the cop's own
|
|
12
|
+
/// name within the department, and the severity it reports at unless the configuration overrides
|
|
13
|
+
/// it.
|
|
14
|
+
///
|
|
15
|
+
/// This is the department's only source of truth: the module declaration, the qualified cop name
|
|
16
|
+
/// and the default severity all come from the single line here, so a cop file never repeats its
|
|
17
|
+
/// own name. A cop that spelled its name a second time could disagree with the registry, and
|
|
18
|
+
/// nothing in the type system would catch it -- the offense would simply be attributed to a cop
|
|
19
|
+
/// that never ran, and directives and severity overrides would both consult the wrong entry.
|
|
20
|
+
macro_rules! department_rules {
|
|
21
|
+
($department:literal; $($module:ident => ($cop:literal, $severity:ident)),+ $(,)?) => {
|
|
22
|
+
$(mod $module;)+
|
|
23
|
+
|
|
24
|
+
pub(crate) static RULES: &[$crate::rules::Rule] = &[
|
|
25
|
+
$($crate::rules::Rule::new(
|
|
26
|
+
concat!($department, "/", $cop),
|
|
27
|
+
$crate::diagnostic::Severity::$severity,
|
|
28
|
+
$module::check,
|
|
29
|
+
),)+
|
|
30
|
+
];
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
mod layout;
|
|
35
|
+
mod lint;
|
|
36
|
+
mod metrics;
|
|
37
|
+
mod naming;
|
|
38
|
+
mod security;
|
|
39
|
+
mod style;
|
|
40
|
+
mod support;
|
|
41
|
+
|
|
42
|
+
pub(crate) use support::{first_identifier, push_named_children, walk_named};
|
|
43
|
+
|
|
44
|
+
/// A cop: its qualified name, the severity it reports at by default, and the function that
|
|
45
|
+
/// inspects one file.
|
|
46
|
+
#[derive(Clone, Copy)]
|
|
47
|
+
pub(crate) struct Rule {
|
|
48
|
+
pub name: &'static str,
|
|
49
|
+
pub severity: Severity,
|
|
50
|
+
pub check: fn(&RuleContext<'_>, &mut Vec<Offense>),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl Rule {
|
|
54
|
+
pub(crate) const fn new(
|
|
55
|
+
name: &'static str,
|
|
56
|
+
severity: Severity,
|
|
57
|
+
check: fn(&RuleContext<'_>, &mut Vec<Offense>),
|
|
58
|
+
) -> Self {
|
|
59
|
+
Self {
|
|
60
|
+
name,
|
|
61
|
+
severity,
|
|
62
|
+
check,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Every department's registry, in the order cops run. Offenses are sorted before they are
|
|
68
|
+
/// reported, so this order is not user-visible; it only has to stay deterministic.
|
|
69
|
+
static RULE_GROUPS: &[&[Rule]] = &[
|
|
70
|
+
layout::RULES,
|
|
71
|
+
lint::RULES,
|
|
72
|
+
metrics::RULES,
|
|
73
|
+
naming::RULES,
|
|
74
|
+
security::RULES,
|
|
75
|
+
style::RULES,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
pub(crate) fn rules() -> impl Iterator<Item = &'static Rule> {
|
|
79
|
+
RULE_GROUPS.iter().copied().flatten()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn rule_names() -> impl Iterator<Item = &'static str> {
|
|
83
|
+
rules().map(|rule| rule.name)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// What one cop sees of one file: the source, the indexed syntax tree, and the configuration
|
|
87
|
+
/// resolved for that cop.
|
|
88
|
+
///
|
|
89
|
+
/// The cop's identity lives here rather than in the cop's own code, so [`Self::setting`] and
|
|
90
|
+
/// [`Self::offense`] address the right configuration key and stamp the right name without the cop
|
|
91
|
+
/// ever naming itself.
|
|
92
|
+
pub(crate) struct RuleContext<'a> {
|
|
93
|
+
pub source: &'a SourceFile,
|
|
94
|
+
ast: &'a AstIndex<'a>,
|
|
95
|
+
config: &'a Config,
|
|
96
|
+
rule: &'static Rule,
|
|
97
|
+
/// `rule.severity` unless the configuration overrode it for this cop.
|
|
98
|
+
severity: Severity,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl<'a> RuleContext<'a> {
|
|
102
|
+
pub(crate) fn new(
|
|
103
|
+
source: &'a SourceFile,
|
|
104
|
+
ast: &'a AstIndex<'a>,
|
|
105
|
+
config: &'a Config,
|
|
106
|
+
rule: &'static Rule,
|
|
107
|
+
severity: Severity,
|
|
108
|
+
) -> Self {
|
|
109
|
+
Self {
|
|
110
|
+
source,
|
|
111
|
+
ast,
|
|
112
|
+
config,
|
|
113
|
+
rule,
|
|
114
|
+
severity,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl RuleContext<'_> {
|
|
120
|
+
/// One of the cop's own configuration parameters, such as `Max` or `EnforcedStyle`.
|
|
121
|
+
pub fn setting<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
|
|
122
|
+
self.config.cop_value(self.rule.name, key)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// The Ruby version the run analyzes as, which version-gated cops compare against.
|
|
126
|
+
pub fn target_ruby_version(&self) -> RubyVersion {
|
|
127
|
+
self.config.target_ruby_version()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Reports `range` under this cop's name and severity. Every cop offense is built here.
|
|
131
|
+
pub fn offense(&self, message: impl Into<String>, range: Range<usize>) -> Offense {
|
|
132
|
+
Offense::new(
|
|
133
|
+
self.rule.name,
|
|
134
|
+
self.severity,
|
|
135
|
+
message,
|
|
136
|
+
range.start,
|
|
137
|
+
range.end,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub fn root_node(&self) -> Node<'_> {
|
|
142
|
+
self.ast.root
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub fn nodes(&self) -> impl Iterator<Item = Node<'_>> + '_ {
|
|
146
|
+
self.ast.nodes.iter().copied()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// The named nodes of one kind, in source order. A cop that inspects a single kind should
|
|
150
|
+
/// reach for this rather than filtering every node in the file: with hundreds of cops running
|
|
151
|
+
/// per file, a full walk each is what turns inspection quadratic.
|
|
152
|
+
pub fn nodes_of(&self, kind: &str) -> impl Iterator<Item = Node<'_>> + '_ {
|
|
153
|
+
self.ast
|
|
154
|
+
.of_kind(kind)
|
|
155
|
+
.map(|index| self.ast.named_node(index))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// The named nodes of any of `kinds`, in source order. The kinds are indexed separately, so
|
|
159
|
+
/// their positions have to be merged to put the nodes back in the order a cop that scans the
|
|
160
|
+
/// whole file would have seen them in.
|
|
161
|
+
pub fn nodes_of_any(&self, kinds: &[&str]) -> impl Iterator<Item = Node<'_>> + '_ {
|
|
162
|
+
let mut indices: Vec<u32> = kinds
|
|
163
|
+
.iter()
|
|
164
|
+
.flat_map(|kind| self.ast.of_kind(kind))
|
|
165
|
+
.collect();
|
|
166
|
+
indices.sort_unstable();
|
|
167
|
+
indices.into_iter().map(|index| self.ast.named_node(index))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn protected_ranges(&self) -> &[Range<usize>] {
|
|
171
|
+
&self.ast.protected_ranges
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
pub fn in_heredoc(&self, range: Range<usize>) -> bool {
|
|
175
|
+
self.heredoc_count(range) > 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
pub fn heredoc_count(&self, range: Range<usize>) -> usize {
|
|
179
|
+
self.ast
|
|
180
|
+
.heredoc_ranges
|
|
181
|
+
.iter()
|
|
182
|
+
.filter(|heredoc| heredoc.start < range.end && range.start < heredoc.end)
|
|
183
|
+
.count()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Node kinds whose byte range spans literal text rather than code. The
|
|
188
|
+
/// byte-scanning cops (`Style/Semicolon`, `Layout/SpaceAfterComma`,
|
|
189
|
+
/// `Layout/SpaceInsideParens`) must not report punctuation found inside them.
|
|
190
|
+
const PROTECTED_LITERAL_KINDS: &[&str] = &[
|
|
191
|
+
"comment",
|
|
192
|
+
"string",
|
|
193
|
+
"symbol",
|
|
194
|
+
"simple_symbol",
|
|
195
|
+
"heredoc_body",
|
|
196
|
+
"regex",
|
|
197
|
+
"subshell",
|
|
198
|
+
"bare_string",
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
pub(crate) struct AstIndex<'tree> {
|
|
202
|
+
root: Node<'tree>,
|
|
203
|
+
nodes: Vec<Node<'tree>>,
|
|
204
|
+
named_nodes: Vec<Node<'tree>>,
|
|
205
|
+
/// Positions in `named_nodes` grouped by node kind, each list in source order. Indices rather
|
|
206
|
+
/// than nodes because a `Node` is eight times the size of the `u32` that finds it.
|
|
207
|
+
by_kind: HashMap<&'static str, Vec<u32>>,
|
|
208
|
+
protected_ranges: Vec<Range<usize>>,
|
|
209
|
+
heredoc_ranges: Vec<Range<usize>>,
|
|
210
|
+
comment_ranges: Vec<Range<usize>>,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
impl<'tree> AstIndex<'tree> {
|
|
214
|
+
pub fn new(root: Node<'tree>) -> Self {
|
|
215
|
+
let mut index = Self {
|
|
216
|
+
root,
|
|
217
|
+
nodes: Vec::new(),
|
|
218
|
+
named_nodes: Vec::new(),
|
|
219
|
+
by_kind: HashMap::new(),
|
|
220
|
+
protected_ranges: Vec::new(),
|
|
221
|
+
heredoc_ranges: Vec::new(),
|
|
222
|
+
comment_ranges: Vec::new(),
|
|
223
|
+
};
|
|
224
|
+
index.collect(root);
|
|
225
|
+
index.protected_ranges.sort_by_key(|range| range.start);
|
|
226
|
+
merge_touching_ranges(&mut index.protected_ranges);
|
|
227
|
+
index.heredoc_ranges.sort_by_key(|range| range.start);
|
|
228
|
+
index
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
pub fn comment_ranges(&self) -> &[Range<usize>] {
|
|
232
|
+
&self.comment_ranges
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn of_kind(&self, kind: &str) -> impl Iterator<Item = u32> + '_ {
|
|
236
|
+
self.by_kind
|
|
237
|
+
.get(kind)
|
|
238
|
+
.map_or(&[][..], Vec::as_slice)
|
|
239
|
+
.iter()
|
|
240
|
+
.copied()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn named_node(&self, index: u32) -> Node<'tree> {
|
|
244
|
+
self.named_nodes[index as usize]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Visits every node in depth-first pre-order. Iterative on purpose: rayon
|
|
248
|
+
/// worker stacks are far smaller than the main thread's, and a recursive
|
|
249
|
+
/// walk aborts the whole process on deeply nested input.
|
|
250
|
+
fn collect(&mut self, root: Node<'tree>) {
|
|
251
|
+
let mut cursor = root.walk();
|
|
252
|
+
loop {
|
|
253
|
+
self.visit(cursor.node());
|
|
254
|
+
if cursor.goto_first_child() {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
loop {
|
|
258
|
+
if cursor.goto_next_sibling() {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
if !cursor.goto_parent() {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn visit(&mut self, node: Node<'tree>) {
|
|
269
|
+
self.nodes.push(node);
|
|
270
|
+
if node.is_named() {
|
|
271
|
+
// A file with more than u32::MAX named nodes would need tens of gigabytes of source;
|
|
272
|
+
// the cast cannot lose information for anything a parser will accept.
|
|
273
|
+
let index = self.named_nodes.len() as u32;
|
|
274
|
+
self.named_nodes.push(node);
|
|
275
|
+
self.by_kind.entry(node.kind()).or_default().push(index);
|
|
276
|
+
}
|
|
277
|
+
if PROTECTED_LITERAL_KINDS.contains(&node.kind()) {
|
|
278
|
+
self.protected_ranges.push(node.byte_range());
|
|
279
|
+
}
|
|
280
|
+
if node.kind() == "heredoc_body" {
|
|
281
|
+
self.heredoc_ranges.push(node.byte_range());
|
|
282
|
+
}
|
|
283
|
+
if node.kind() == "comment" {
|
|
284
|
+
self.comment_ranges.push(node.byte_range());
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Collapses overlapping and touching ranges of a start-sorted list so that no
|
|
290
|
+
/// offset is covered by more than one entry. `source::is_protected` inspects
|
|
291
|
+
/// only the last range starting at or before its offset, so an inner range that
|
|
292
|
+
/// outlived its enclosing one would make the enclosed offsets look unprotected.
|
|
293
|
+
fn merge_touching_ranges(ranges: &mut Vec<Range<usize>>) {
|
|
294
|
+
if ranges.is_empty() {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
let mut merged = 0;
|
|
298
|
+
for index in 1..ranges.len() {
|
|
299
|
+
if ranges[index].start <= ranges[merged].end {
|
|
300
|
+
ranges[merged].end = ranges[merged].end.max(ranges[index].end);
|
|
301
|
+
} else {
|
|
302
|
+
merged += 1;
|
|
303
|
+
ranges[merged] = ranges[index].clone();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
ranges.truncate(merged + 1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[cfg(test)]
|
|
310
|
+
mod tests {
|
|
311
|
+
use std::collections::HashSet;
|
|
312
|
+
|
|
313
|
+
use super::{merge_touching_ranges, rule_names, rules};
|
|
314
|
+
use crate::config::Config;
|
|
315
|
+
use crate::source::is_protected;
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn merges_nested_and_touching_ranges() {
|
|
319
|
+
let mut ranges = vec![0..10, 3..6, 10..14, 20..25];
|
|
320
|
+
merge_touching_ranges(&mut ranges);
|
|
321
|
+
assert_eq!(ranges, vec![0..14, 20..25]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn leaves_disjoint_ranges_alone() {
|
|
326
|
+
let mut ranges = vec![0..2, 5..7];
|
|
327
|
+
merge_touching_ranges(&mut ranges);
|
|
328
|
+
assert_eq!(ranges, vec![0..2, 5..7]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// `is_protected` only consults the last range starting at or before the
|
|
332
|
+
// offset, so an unmerged inner range hides the offsets after it.
|
|
333
|
+
#[test]
|
|
334
|
+
fn merging_keeps_offsets_under_an_inner_range_protected() {
|
|
335
|
+
let nested = vec![0..10, 3..6];
|
|
336
|
+
assert!(!is_protected(7, &nested));
|
|
337
|
+
let mut merged = nested;
|
|
338
|
+
merge_touching_ranges(&mut merged);
|
|
339
|
+
assert!(is_protected(7, &merged));
|
|
340
|
+
assert!(!is_protected(10, &merged));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn every_cop_is_registered_once() {
|
|
345
|
+
let mut seen = HashSet::new();
|
|
346
|
+
for name in rule_names() {
|
|
347
|
+
assert!(seen.insert(name), "{name} is registered twice");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// A registered name that the bundled RuboCop configuration does not know would be
|
|
352
|
+
/// unreachable: `--only` rejects it, and it would carry no defaults.
|
|
353
|
+
#[test]
|
|
354
|
+
fn every_registered_cop_exists_in_the_default_configuration() {
|
|
355
|
+
let directory = tempfile::tempdir().unwrap();
|
|
356
|
+
let config = Config::load(None, directory.path()).unwrap();
|
|
357
|
+
let known: HashSet<&str> = config.known_cop_names().collect();
|
|
358
|
+
for name in rule_names() {
|
|
359
|
+
assert!(known.contains(name), "{name} is not a RuboCop cop");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[test]
|
|
364
|
+
fn every_cop_name_is_qualified_by_a_department() {
|
|
365
|
+
for name in rule_names() {
|
|
366
|
+
let (department, cop) = name.split_once('/').expect("{name} has no department");
|
|
367
|
+
assert!(!department.is_empty(), "{name} has an empty department");
|
|
368
|
+
assert!(!cop.is_empty(), "{name} has an empty cop name");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// The registry is a static built from the department tables, so iteration order cannot vary
|
|
373
|
+
/// between runs; autocorrect ordering and `--debug` output both rely on that.
|
|
374
|
+
#[test]
|
|
375
|
+
fn registration_order_is_stable() {
|
|
376
|
+
let first: Vec<&str> = rule_names().collect();
|
|
377
|
+
let second: Vec<&str> = rules().map(|rule| rule.name).collect();
|
|
378
|
+
assert_eq!(first, second);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
use crate::diagnostic::Offense;
|
|
2
|
+
use crate::rules::RuleContext;
|
|
3
|
+
|
|
4
|
+
pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
5
|
+
for node in context.nodes_of_any(&[
|
|
6
|
+
"identifier",
|
|
7
|
+
"constant",
|
|
8
|
+
"instance_variable",
|
|
9
|
+
"class_variable",
|
|
10
|
+
"global_variable",
|
|
11
|
+
]) {
|
|
12
|
+
if context.source.node_text(node).is_ascii() {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
offenses.push(context.offense("Use only ASCII symbols in identifiers.", node.byte_range()));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
use std::sync::LazyLock;
|
|
2
|
+
|
|
3
|
+
use regex::Regex;
|
|
4
|
+
use tree_sitter::Node;
|
|
5
|
+
|
|
6
|
+
use crate::diagnostic::Offense;
|
|
7
|
+
use crate::rules::RuleContext;
|
|
8
|
+
|
|
9
|
+
static CONSTANT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Z][A-Z0-9_]*$").unwrap());
|
|
10
|
+
|
|
11
|
+
pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
12
|
+
for node in context.nodes_of("assignment") {
|
|
13
|
+
let Some(left) = node.child_by_field_name("left") else {
|
|
14
|
+
continue;
|
|
15
|
+
};
|
|
16
|
+
if left.kind() != "constant" {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
let name = context.source.node_text(left);
|
|
20
|
+
// Only a literal makes the name a constant in RuboCop's sense; anything computed may well
|
|
21
|
+
// be a class or module the author named in CamelCase on purpose.
|
|
22
|
+
let allowed_assignment = node
|
|
23
|
+
.child_by_field_name("right")
|
|
24
|
+
.is_none_or(|right| !literal_constant_value(right));
|
|
25
|
+
if allowed_assignment || CONSTANT.is_match(name) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
offenses
|
|
29
|
+
.push(context.offense("Use SCREAMING_SNAKE_CASE for constants.", left.byte_range()));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn literal_constant_value(node: Node<'_>) -> bool {
|
|
34
|
+
matches!(
|
|
35
|
+
node.kind(),
|
|
36
|
+
"integer"
|
|
37
|
+
| "float"
|
|
38
|
+
| "rational"
|
|
39
|
+
| "complex"
|
|
40
|
+
| "string"
|
|
41
|
+
| "symbol"
|
|
42
|
+
| "array"
|
|
43
|
+
| "hash"
|
|
44
|
+
| "true"
|
|
45
|
+
| "false"
|
|
46
|
+
| "nil"
|
|
47
|
+
| "regex"
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
use super::support::valid_name;
|
|
2
|
+
use crate::diagnostic::Offense;
|
|
3
|
+
use crate::rules::RuleContext;
|
|
4
|
+
|
|
5
|
+
pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
6
|
+
let style: String = context
|
|
7
|
+
.setting("EnforcedStyle")
|
|
8
|
+
.unwrap_or_else(|| "snake_case".to_owned());
|
|
9
|
+
for node in context.nodes_of_any(&["method", "singleton_method"]) {
|
|
10
|
+
let Some(name_node) = node.child_by_field_name("name") else {
|
|
11
|
+
continue;
|
|
12
|
+
};
|
|
13
|
+
let name = context.source.node_text(name_node);
|
|
14
|
+
if operator_method(name) || valid_name(name, &style) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
offenses.push(context.offense(
|
|
18
|
+
format!("Use {style} for method names."),
|
|
19
|
+
name_node.byte_range(),
|
|
20
|
+
));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Operators are defined with `def` too, and none of them can be spelled in the enforced style.
|
|
25
|
+
fn operator_method(name: &str) -> bool {
|
|
26
|
+
matches!(
|
|
27
|
+
name,
|
|
28
|
+
"+" | "-"
|
|
29
|
+
| "*"
|
|
30
|
+
| "/"
|
|
31
|
+
| "%"
|
|
32
|
+
| "**"
|
|
33
|
+
| "=="
|
|
34
|
+
| "==="
|
|
35
|
+
| "!="
|
|
36
|
+
| "<=>"
|
|
37
|
+
| "<"
|
|
38
|
+
| "<="
|
|
39
|
+
| ">"
|
|
40
|
+
| ">="
|
|
41
|
+
| "[]"
|
|
42
|
+
| "[]="
|
|
43
|
+
| "<<"
|
|
44
|
+
| ">>"
|
|
45
|
+
| "&"
|
|
46
|
+
| "|"
|
|
47
|
+
| "^"
|
|
48
|
+
| "~"
|
|
49
|
+
| "`"
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//! Identifier spelling shared by the cops that enforce `EnforcedStyle`.
|
|
2
|
+
|
|
3
|
+
use std::sync::LazyLock;
|
|
4
|
+
|
|
5
|
+
use regex::Regex;
|
|
6
|
+
|
|
7
|
+
static SNAKE_CASE: LazyLock<Regex> =
|
|
8
|
+
LazyLock::new(|| Regex::new(r"^[a-z_][a-zA-Z0-9_]*[!?=]?$").unwrap());
|
|
9
|
+
static CAMEL_CASE: LazyLock<Regex> =
|
|
10
|
+
LazyLock::new(|| Regex::new(r"^[a-z][a-zA-Z0-9]*[!?=]?$").unwrap());
|
|
11
|
+
|
|
12
|
+
pub(super) fn valid_name(name: &str, style: &str) -> bool {
|
|
13
|
+
if style == "camelCase" {
|
|
14
|
+
CAMEL_CASE.is_match(name) && !name.contains('_')
|
|
15
|
+
} else {
|
|
16
|
+
SNAKE_CASE.is_match(name)
|
|
17
|
+
&& !name
|
|
18
|
+
.trim_matches(['?', '!', '='])
|
|
19
|
+
.chars()
|
|
20
|
+
.any(char::is_uppercase)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use tree_sitter::Node;
|
|
4
|
+
|
|
5
|
+
use super::support::valid_name;
|
|
6
|
+
use crate::diagnostic::Offense;
|
|
7
|
+
use crate::rules::{RuleContext, first_identifier};
|
|
8
|
+
|
|
9
|
+
pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
10
|
+
let style: String = context
|
|
11
|
+
.setting("EnforcedStyle")
|
|
12
|
+
.unwrap_or_else(|| "snake_case".to_owned());
|
|
13
|
+
let mut seen = HashSet::new();
|
|
14
|
+
for node in context.nodes_of("identifier") {
|
|
15
|
+
if !is_variable_definition(node) || !seen.insert(node.start_byte()) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
let name = context.source.node_text(node);
|
|
19
|
+
if valid_name(name, &style) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
offenses.push(context.offense(
|
|
23
|
+
format!("Use {style} for variable names."),
|
|
24
|
+
node.byte_range(),
|
|
25
|
+
));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Whether the identifier introduces a variable rather than reading one.
|
|
30
|
+
fn is_variable_definition(node: Node<'_>) -> bool {
|
|
31
|
+
let Some(parent) = node.parent() else {
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
match parent.kind() {
|
|
35
|
+
"assignment" | "operator_assignment" => parent
|
|
36
|
+
.child_by_field_name("left")
|
|
37
|
+
.is_some_and(|left| left.byte_range() == node.byte_range()),
|
|
38
|
+
"method_parameters" | "block_parameters" | "lambda_parameters" => true,
|
|
39
|
+
"optional_parameter"
|
|
40
|
+
| "keyword_parameter"
|
|
41
|
+
| "splat_parameter"
|
|
42
|
+
| "hash_splat_parameter"
|
|
43
|
+
| "block_parameter"
|
|
44
|
+
| "destructured_parameter"
|
|
45
|
+
| "rescue" => {
|
|
46
|
+
first_identifier(parent).is_some_and(|first| first.byte_range() == node.byte_range())
|
|
47
|
+
}
|
|
48
|
+
_ => false,
|
|
49
|
+
}
|
|
50
|
+
}
|