sonicop 26.8.113 → 26.9.100
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 +4 -4
- data/Cargo.lock +35 -43
- data/Cargo.toml +5 -5
- data/lib/sonicop/version.rb +1 -1
- data/src/rules/gemspec/require_mfa.rs +1 -1
- data/src/rules/layout/access_modifier_indentation.rs +1 -1
- data/src/rules/layout/block_alignment.rs +1 -1
- data/src/rules/layout/block_end_newline.rs +1 -1
- data/src/rules/layout/empty_lines_around_arguments.rs +1 -1
- data/src/rules/layout/empty_lines_around_exception_handling_keywords.rs +1 -1
- data/src/rules/layout/first_argument_indentation.rs +1 -1
- data/src/rules/layout/first_array_element_indentation.rs +1 -1
- data/src/rules/layout/first_hash_element_indentation.rs +1 -1
- data/src/rules/layout/heredoc_argument_closing_parenthesis.rs +1 -1
- data/src/rules/layout/indentation_width.rs +1 -1
- data/src/rules/layout/multiline_brace.rs +1 -1
- data/src/rules/layout/multiline_expression.rs +2 -2
- data/src/rules/layout/space_inside_array_percent_literal.rs +1 -1
- data/src/rules/layout/space_inside_block_braces.rs +1 -1
- data/src/rules/layout/space_inside_hash_literal_braces.rs +1 -1
- data/src/rules/layout/space_inside_percent_literal_delimiters.rs +1 -1
- data/src/rules/layout/space_inside_string_interpolation.rs +1 -1
- data/src/rules/layout/support.rs +2 -5
- data/src/rules/layout/tokens.rs +1 -3
- data/src/rules/lint/access_modifier.rs +1 -1
- data/src/rules/lint/empty_conditional_body.rs +1 -1
- data/src/rules/lint/literal_in_interpolation.rs +2 -2
- data/src/rules/lint/literals.rs +89 -48
- data/src/rules/lint/loop.rs +1 -2
- data/src/rules/lint/nil_receiver.rs +1 -1
- data/src/rules/lint/node_equality.rs +5 -2
- data/src/rules/lint/non_atomic_file_operation.rs +1 -1
- data/src/rules/lint/regexp.rs +1 -1
- data/src/rules/lint/suppressed_exception.rs +1 -1
- data/src/rules/lint/syntax.rs +1 -1
- data/src/rules/lint/unreachable_loop.rs +27 -11
- data/src/rules/lint/variable_force.rs +1 -1
- data/src/rules/metrics/locals.rs +1 -1
- data/src/rules/mod.rs +83 -4
- data/src/rules/naming/support.rs +1 -1
- data/src/rules/node_ext.rs +45 -14
- data/src/rules/send_node.rs +2 -2
- data/src/rules/style/access_modifier_declarations.rs +11 -4
- data/src/rules/style/class_and_module_children.rs +2 -2
- data/src/rules/style/command_literal.rs +1 -1
- data/src/rules/style/concat_array_literals.rs +1 -1
- data/src/rules/style/conditional_assignment.rs +1 -1
- data/src/rules/style/def_with_parentheses.rs +1 -1
- data/src/rules/style/empty_else.rs +1 -1
- data/src/rules/style/format_string_token.rs +2 -2
- data/src/rules/style/hash_as_last_array_item.rs +1 -1
- data/src/rules/style/hash_conversion.rs +1 -1
- data/src/rules/style/hash_fetch_chain.rs +1 -1
- data/src/rules/style/infinite_loop.rs +1 -1
- data/src/rules/style/lambda.rs +1 -1
- data/src/rules/style/literal.rs +2 -2
- data/src/rules/style/missing_else.rs +3 -3
- data/src/rules/style/multiline_block_chain.rs +1 -1
- data/src/rules/style/multiline_memoization.rs +1 -1
- data/src/rules/style/non_nil_check.rs +1 -1
- data/src/rules/style/percent.rs +1 -1
- data/src/rules/style/percent_array.rs +3 -3
- data/src/rules/style/redundant_format.rs +2 -2
- data/src/rules/style/redundant_regexp_character_class.rs +1 -1
- data/src/rules/style/redundant_regexp_constructor.rs +1 -1
- data/src/rules/style/redundant_regexp_escape.rs +1 -1
- data/src/rules/style/regexp_literal.rs +1 -1
- data/src/rules/style/require_order.rs +6 -2
- data/src/rules/style/single_line_methods.rs +1 -1
- data/src/rules/style/string_concatenation.rs +2 -2
- data/src/rules/style/struct_inheritance.rs +1 -1
- data/src/rules/style/trailing_body_on_method_definition.rs +1 -1
- data/src/rules/style/trailing_comma.rs +1 -1
- data/src/rules/style/trailing_method_end_statement.rs +1 -1
- metadata +1 -1
data/src/rules/lint/literals.rs
CHANGED
|
@@ -55,58 +55,99 @@ const RECURSIVE_METHODS: &[&str] = &["==", "===", "!=", "<=", ">=", ">", "<", "*
|
|
|
55
55
|
/// Reachable from `style` too: `Style/YodaCondition` asks the same question of a comparison's two
|
|
56
56
|
/// operands.
|
|
57
57
|
pub(crate) fn recursive_basic_literal(node: Node<'_>, context: &RuleContext<'_>) -> bool {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
58
|
+
// The predicate is a conjunction over a subtree, so it is answered with a stack rather than by
|
|
59
|
+
// recursing: a nested literal nests the tree once per bracket, and this is asked of every hash
|
|
60
|
+
// key of every file. `AstIndex::collect` is iterative for the same reason -- a rayon worker's
|
|
61
|
+
// stack is far smaller than the main thread's, and a recursion deep enough to exhaust it aborts
|
|
62
|
+
// the process rather than failing one file. Nothing below has a side effect, so answering the
|
|
63
|
+
// queued questions in any order gives the same answer the nested `&&` gave.
|
|
64
|
+
let mut stack = vec![Question::Literal(node)];
|
|
65
|
+
while let Some(question) = stack.pop() {
|
|
66
|
+
match question {
|
|
67
|
+
Question::Literal(node) => match node.kind_str() {
|
|
68
|
+
kind if BASIC.contains(&kind) => {}
|
|
69
|
+
// `emit_file_line_as_literals`: the parser resolves these before a cop sees them,
|
|
70
|
+
// so what reaches one is the `str` or the `int` they stood for rather than the
|
|
71
|
+
// keyword.
|
|
72
|
+
"identifier" => {
|
|
73
|
+
if !matches!(context.source.node_text(node), "__FILE__" | "__LINE__") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// A quoted literal interpolates or it does not, and only the plain one is basic --
|
|
78
|
+
// but a `dstr` and a `dsym` are composite literals, so both answers come out the
|
|
79
|
+
// same here as long as everything interpolated into them is a literal too.
|
|
80
|
+
"string" | "delimited_symbol" => {
|
|
81
|
+
if has_interpolation(node) {
|
|
82
|
+
stack.push(Question::Children(node));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
kind if COMPOSITE.contains(&kind) => stack.push(Question::Children(node)),
|
|
86
|
+
// `a && b` and `a || b` are `and`/`or` upstream, which recurse; every other binary
|
|
87
|
+
// operator is a `send`, which recurses only for the ten that keep a literal
|
|
88
|
+
// literal.
|
|
89
|
+
"binary" => {
|
|
90
|
+
let Some(operator) = node.field("operator") else {
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
let text = context.source.node_text(operator);
|
|
94
|
+
if !matches!(text, "&&" | "and" | "||" | "or")
|
|
95
|
+
&& !RECURSIVE_METHODS.contains(&text)
|
|
96
|
+
{
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
stack.push(Question::Children(node));
|
|
100
|
+
}
|
|
101
|
+
// The parser folds a leading sign into the literal it precedes; `!x` stays a
|
|
102
|
+
// `send`.
|
|
103
|
+
"unary" => {
|
|
104
|
+
let Some(operator) = node.field("operator") else {
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
if !matches!(context.source.node_text(operator), "-" | "+" | "!") {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
let Some(operand) = node.field("operand") else {
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
stack.push(Question::Literal(operand));
|
|
114
|
+
}
|
|
115
|
+
"call" => {
|
|
116
|
+
let literal_carrying = node.field("method").is_some_and(|method| {
|
|
117
|
+
RECURSIVE_METHODS.contains(&context.source.node_text(method))
|
|
118
|
+
});
|
|
119
|
+
if !literal_carrying {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
stack.push(Question::Children(node));
|
|
123
|
+
}
|
|
124
|
+
_ => return false,
|
|
125
|
+
},
|
|
126
|
+
// `children.compact.all?(&:recursive_basic_literal?)`.
|
|
127
|
+
Question::Children(node) => {
|
|
128
|
+
for child in named_children_of(node, context) {
|
|
129
|
+
match child.kind_str() {
|
|
130
|
+
"comment" => {}
|
|
131
|
+
// The parts a quoted literal is written from are not nodes upstream at all:
|
|
132
|
+
// the text between the delimiters is the value, and only what is
|
|
133
|
+
// interpolated is a child.
|
|
134
|
+
"string_content" | "escape_sequence" | "heredoc_content" => {}
|
|
135
|
+
"interpolation" => stack.push(Question::Children(child)),
|
|
136
|
+
_ => stack.push(Question::Literal(child)),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
92
140
|
}
|
|
93
|
-
_ => false,
|
|
94
141
|
}
|
|
142
|
+
true
|
|
95
143
|
}
|
|
96
144
|
|
|
97
|
-
/// `
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.all(|child| match child.kind_str() {
|
|
104
|
-
// The parts a quoted literal is written from are not nodes upstream at all: the text
|
|
105
|
-
// between the delimiters is the value, and only what is interpolated is a child.
|
|
106
|
-
"string_content" | "escape_sequence" | "heredoc_content" => true,
|
|
107
|
-
"interpolation" => all_children(child, context),
|
|
108
|
-
_ => recursive_basic_literal(child, context),
|
|
109
|
-
})
|
|
145
|
+
/// One half of [`recursive_basic_literal`]'s question, queued rather than recursed into.
|
|
146
|
+
enum Question<'tree> {
|
|
147
|
+
/// `node.recursive_basic_literal?`.
|
|
148
|
+
Literal(Node<'tree>),
|
|
149
|
+
/// `node.children.compact.all?(&:recursive_basic_literal?)`.
|
|
150
|
+
Children(Node<'tree>),
|
|
110
151
|
}
|
|
111
152
|
|
|
112
153
|
/// `node.const_type?`: a constant, however it was reached.
|
data/src/rules/lint/loop.rs
CHANGED
|
@@ -24,8 +24,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
24
24
|
Some(keyword) => keyword,
|
|
25
25
|
None => continue,
|
|
26
26
|
};
|
|
27
|
-
let last =
|
|
28
|
-
.unwrap_or(0)
|
|
27
|
+
let last = body.child_count()
|
|
29
28
|
.saturating_sub(1);
|
|
30
29
|
let (Some(open), Some(close)) = (body.child(0), body.child(last)) else {
|
|
31
30
|
continue;
|
|
@@ -394,7 +394,7 @@ fn csend_root_receiver<'tree>(node: Node<'tree>, context: &RuleContext<'_>) -> O
|
|
|
394
394
|
fn is_safe_navigation(node: Node<'_>, context: &RuleContext<'_>) -> bool {
|
|
395
395
|
node.kind_str() == "call"
|
|
396
396
|
&& (0..node.child_count())
|
|
397
|
-
.filter_map(|index| node.child(index
|
|
397
|
+
.filter_map(|index| node.child(index))
|
|
398
398
|
.any(|child| context.source.node_text(child) == "&.")
|
|
399
399
|
}
|
|
400
400
|
|
|
@@ -174,7 +174,10 @@ fn named_children_with_fields<'tree>(
|
|
|
174
174
|
}
|
|
175
175
|
loop {
|
|
176
176
|
if cursor.node().is_named() {
|
|
177
|
-
children.push((
|
|
177
|
+
children.push((
|
|
178
|
+
super::super::field_name_for_id(cursor.field_id()),
|
|
179
|
+
cursor.node(),
|
|
180
|
+
));
|
|
178
181
|
}
|
|
179
182
|
if !cursor.goto_next_sibling() {
|
|
180
183
|
return children;
|
|
@@ -299,7 +302,7 @@ fn quoted_value(node: Node<'_>, context: &RuleContext<'_>) -> Option<Vec<u8>> {
|
|
|
299
302
|
return None;
|
|
300
303
|
}
|
|
301
304
|
let open = node.child(0)?;
|
|
302
|
-
let close = node.child(
|
|
305
|
+
let close = node.child(node.child_count().saturating_sub(1))?;
|
|
303
306
|
if open.id() == close.id() || close.start_byte() < open.end_byte() {
|
|
304
307
|
return None;
|
|
305
308
|
}
|
|
@@ -176,7 +176,7 @@ fn corrections(
|
|
|
176
176
|
safe: true,
|
|
177
177
|
});
|
|
178
178
|
} else if let Some(end) = conditional
|
|
179
|
-
.child(conditional.child_count().saturating_sub(1)
|
|
179
|
+
.child(conditional.child_count().saturating_sub(1))
|
|
180
180
|
.filter(|end| context.source.node_text(*end) == "end")
|
|
181
181
|
{
|
|
182
182
|
edits.push(Edit {
|
data/src/rules/lint/regexp.rs
CHANGED
|
@@ -20,7 +20,7 @@ pub(super) struct Captures {
|
|
|
20
20
|
/// The text between a regexp literal's delimiters, and whether it was written with the `x` flag.
|
|
21
21
|
pub(super) fn pattern<'a>(node: Node<'_>, context: &'a RuleContext<'_>) -> Option<(&'a str, bool)> {
|
|
22
22
|
let opening = node.child(0)?;
|
|
23
|
-
let closing = node.child(
|
|
23
|
+
let closing = node.child(node.child_count().checked_sub(1)?)?;
|
|
24
24
|
if closing.start_byte() < opening.end_byte() {
|
|
25
25
|
return None;
|
|
26
26
|
}
|
|
@@ -28,7 +28,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
28
28
|
// The offense covers the `rescue` keyword and what follows it, not the guarded body.
|
|
29
29
|
"rescue_modifier" => {
|
|
30
30
|
let keyword = (0..node.child_count())
|
|
31
|
-
.filter_map(|index| node.child(index
|
|
31
|
+
.filter_map(|index| node.child(index))
|
|
32
32
|
.find(|child| context.source.node_text(*child) == "rescue");
|
|
33
33
|
match keyword {
|
|
34
34
|
Some(keyword) => keyword.start_byte()..node.end_byte(),
|
data/src/rules/lint/syntax.rs
CHANGED
|
@@ -833,7 +833,7 @@ fn endless_in_block_recovery(node: Node<'_>, context: &RuleContext<'_>, out: &mu
|
|
|
833
833
|
return;
|
|
834
834
|
};
|
|
835
835
|
let Some(brace) = block
|
|
836
|
-
.child(block.child_count().saturating_sub(1)
|
|
836
|
+
.child(block.child_count().saturating_sub(1))
|
|
837
837
|
.filter(|last| last.kind_str() == "}")
|
|
838
838
|
else {
|
|
839
839
|
return;
|
|
@@ -268,11 +268,24 @@ fn is_loop_shape(node: Node<'_>, context: &RuleContext<'_>, allowed: &[&'static
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
/// `each_descendant(:next, :redo).any?`, without descending into `skip`.
|
|
271
|
+
///
|
|
272
|
+
/// Iterative rather than recursive, for the reason `AstIndex::collect` is: a rayon worker's stack
|
|
273
|
+
/// is far smaller than the main thread's, and a `while` condition written as one long chain nests
|
|
274
|
+
/// as deeply as it is long -- a recursive walk aborts the whole process on it rather than failing
|
|
275
|
+
/// one file.
|
|
271
276
|
fn has_continue(node: Node<'_>, skip: Option<Node<'_>>) -> bool {
|
|
272
|
-
let mut
|
|
273
|
-
node
|
|
274
|
-
|
|
275
|
-
.
|
|
277
|
+
let mut stack = Vec::new();
|
|
278
|
+
crate::rules::push_named_children(node, &mut stack);
|
|
279
|
+
while let Some(current) = stack.pop() {
|
|
280
|
+
if skip.is_some_and(|skip| skip.id() == current.id()) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if CONTINUE.contains(¤t.kind_str()) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
crate::rules::push_named_children(current, &mut stack);
|
|
287
|
+
}
|
|
288
|
+
false
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
/// `conditional_continue_keyword?`: the last `or` written anywhere in the break statement, when its
|
|
@@ -285,16 +298,19 @@ fn conditional_continue(node: Node<'_>) -> bool {
|
|
|
285
298
|
.is_some_and(|right| CONTINUE.contains(&right.kind_str()))
|
|
286
299
|
}
|
|
287
300
|
|
|
301
|
+
/// The last `or` of `node`'s subtree in depth-first pre-order, `node` itself excluded.
|
|
302
|
+
///
|
|
303
|
+
/// Iterative for the same reason [`has_continue`] is: `a or b or c or …` nests once per operand,
|
|
304
|
+
/// and the recursion this replaced was as deep as the chain is long.
|
|
288
305
|
fn last_or<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
|
|
289
306
|
let mut found = None;
|
|
290
|
-
let mut
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if let Some(inner) = last_or(child) {
|
|
296
|
-
found = Some(inner);
|
|
307
|
+
let mut stack = Vec::new();
|
|
308
|
+
crate::rules::push_named_children(node, &mut stack);
|
|
309
|
+
while let Some(current) = stack.pop() {
|
|
310
|
+
if is_or(current) {
|
|
311
|
+
found = Some(current);
|
|
297
312
|
}
|
|
313
|
+
crate::rules::push_named_children(current, &mut stack);
|
|
298
314
|
}
|
|
299
315
|
found
|
|
300
316
|
}
|
data/src/rules/metrics/locals.rs
CHANGED
data/src/rules/mod.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::cell::OnceCell;
|
|
2
2
|
use std::collections::HashMap;
|
|
3
|
+
use std::num::NonZeroU16;
|
|
3
4
|
use std::ops::Range;
|
|
4
5
|
use std::sync::LazyLock;
|
|
5
6
|
|
|
@@ -572,12 +573,20 @@ impl<'a> Iterator for Children<'a> {
|
|
|
572
573
|
/// Every field name the grammar has, by field id, so a recorded id can be turned back into the
|
|
573
574
|
/// `&'static str` the cops compare against.
|
|
574
575
|
static FIELD_NAMES: LazyLock<Vec<Option<&'static str>>> = LazyLock::new(|| {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
.map(|id| language.field_name_for_id(id))
|
|
576
|
+
(0..=node_ext::LANGUAGE.field_count() as u16)
|
|
577
|
+
.map(|id| node_ext::LANGUAGE.field_name_for_id(id))
|
|
578
578
|
.collect()
|
|
579
579
|
});
|
|
580
580
|
|
|
581
|
+
/// The field name a field id stands for, for a node the index does not know.
|
|
582
|
+
///
|
|
583
|
+
/// `TreeCursor::field_name` answers the same question, but since tree-sitter 0.27 its answer
|
|
584
|
+
/// borrows from the tree, and the callers hand the name on as `&'static str`. Going through the
|
|
585
|
+
/// table keeps the lifetime and skips the C lookup besides.
|
|
586
|
+
pub(in crate::rules) fn field_name_for_id(id: Option<NonZeroU16>) -> Option<&'static str> {
|
|
587
|
+
FIELD_NAMES.get(id?.get() as usize).copied().flatten()
|
|
588
|
+
}
|
|
589
|
+
|
|
581
590
|
/// The value [`AstIndex::parent_of`] carries for the root.
|
|
582
591
|
const NO_PARENT: u32 = u32::MAX;
|
|
583
592
|
|
|
@@ -917,7 +926,9 @@ fn merge_touching_ranges(ranges: &mut Vec<Range<usize>>) {
|
|
|
917
926
|
mod tests {
|
|
918
927
|
use std::collections::HashSet;
|
|
919
928
|
|
|
920
|
-
use super::{
|
|
929
|
+
use super::{
|
|
930
|
+
AstIndex, RuleContext, field_name_for_id, merge_touching_ranges, rule_names, rules,
|
|
931
|
+
};
|
|
921
932
|
use crate::config::Config;
|
|
922
933
|
use crate::rules::node_ext::NodeExt;
|
|
923
934
|
use crate::source::is_protected;
|
|
@@ -1079,6 +1090,27 @@ mod tests {
|
|
|
1079
1090
|
index.parent_in_tree(call).map(|found| found.id()),
|
|
1080
1091
|
call.parent().map(|found| found.id())
|
|
1081
1092
|
);
|
|
1093
|
+
|
|
1094
|
+
// The same question asked through a context rather than the index. `named_children_of`
|
|
1095
|
+
// recursed into itself here rather than walking, so a cop handing it a fragment node never
|
|
1096
|
+
// returned.
|
|
1097
|
+
let source = crate::source::SourceFile::new("test.rb", "foo(1)\n".to_owned());
|
|
1098
|
+
let config = Config::load_with_options(None, std::path::Path::new("/"), true)
|
|
1099
|
+
.expect("the vendored default configuration loads");
|
|
1100
|
+
let rule = rules().next().expect("the registry is not empty");
|
|
1101
|
+
let context = RuleContext::new(
|
|
1102
|
+
&source,
|
|
1103
|
+
&index,
|
|
1104
|
+
&config,
|
|
1105
|
+
rule,
|
|
1106
|
+
crate::diagnostic::Severity::Convention,
|
|
1107
|
+
false,
|
|
1108
|
+
);
|
|
1109
|
+
assert!(context.named_children(call).is_none());
|
|
1110
|
+
assert_eq!(
|
|
1111
|
+
crate::rules::send_node::named_children_of(call, &context),
|
|
1112
|
+
expected
|
|
1113
|
+
);
|
|
1082
1114
|
}
|
|
1083
1115
|
|
|
1084
1116
|
/// The registry is a static built from the department tables, so iteration order cannot vary
|
|
@@ -1089,4 +1121,51 @@ mod tests {
|
|
|
1089
1121
|
let second: Vec<&str> = rules().map(|rule| rule.name).collect();
|
|
1090
1122
|
assert_eq!(first, second);
|
|
1091
1123
|
}
|
|
1124
|
+
|
|
1125
|
+
/// The table has to answer what `TreeCursor::field_name` answers, for every node of a real
|
|
1126
|
+
/// file and for a node of a tree the index never saw.
|
|
1127
|
+
///
|
|
1128
|
+
/// Five cops read the field a node fills out of the table rather than off the cursor, because
|
|
1129
|
+
/// since tree-sitter 0.27 the cursor's answer borrows from the tree while theirs is
|
|
1130
|
+
/// `&'static str`. A field the table named differently would make a cop tell the two sides of
|
|
1131
|
+
/// a range apart wrongly, which is what the field is there for.
|
|
1132
|
+
#[test]
|
|
1133
|
+
fn the_field_table_answers_what_the_cursor_answers() {
|
|
1134
|
+
let mut parser = tree_sitter::Parser::new();
|
|
1135
|
+
parser
|
|
1136
|
+
.set_language(&tree_sitter_ruby::LANGUAGE.into())
|
|
1137
|
+
.expect("the Ruby grammar loads");
|
|
1138
|
+
let tree = parser
|
|
1139
|
+
.parse(
|
|
1140
|
+
"class Foo\n def bar(a = 1, &block)\n @x ||= a.map { |v| v[1..] }\n\
|
|
1141
|
+
rescue StandardError => e\n raise e if 10.. === a\n end\nend\n",
|
|
1142
|
+
None,
|
|
1143
|
+
)
|
|
1144
|
+
.expect("the source parses");
|
|
1145
|
+
|
|
1146
|
+
let mut stack = vec![tree.root_node()];
|
|
1147
|
+
let (mut seen, mut named) = (0, 0);
|
|
1148
|
+
while let Some(node) = stack.pop() {
|
|
1149
|
+
let mut cursor = node.walk();
|
|
1150
|
+
if !cursor.goto_first_child() {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
loop {
|
|
1154
|
+
assert_eq!(
|
|
1155
|
+
field_name_for_id(cursor.field_id()),
|
|
1156
|
+
cursor.field_name(),
|
|
1157
|
+
"field of {:?} under {node:?}",
|
|
1158
|
+
cursor.node()
|
|
1159
|
+
);
|
|
1160
|
+
seen += 1;
|
|
1161
|
+
named += usize::from(cursor.field_name().is_some());
|
|
1162
|
+
stack.push(cursor.node());
|
|
1163
|
+
if !cursor.goto_next_sibling() {
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
assert!(seen > 30, "the sample has to reach a variety of children");
|
|
1169
|
+
assert!(named > 10, "and enough of them have to fill a field");
|
|
1170
|
+
}
|
|
1092
1171
|
}
|
data/src/rules/naming/support.rs
CHANGED
data/src/rules/node_ext.rs
CHANGED
|
@@ -17,9 +17,14 @@ use tree_sitter::{Language, Node};
|
|
|
17
17
|
|
|
18
18
|
use crate::rules::RuleContext;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/// The grammar itself, resolved once and kept for the life of the process.
|
|
21
|
+
///
|
|
22
|
+
/// Since tree-sitter 0.27 the name tables borrow from the `Language` they were read out of rather
|
|
23
|
+
/// than being `&'static str` outright, so a `Language` built per call cannot outlive the name it
|
|
24
|
+
/// hands back. Holding it in a `static` is what lets [`NodeExt::kind_str`] and `FIELD_NAMES` keep
|
|
25
|
+
/// answering `&'static str`, which the several thousand call sites compare against.
|
|
26
|
+
pub(in crate::rules) static LANGUAGE: LazyLock<Language> =
|
|
27
|
+
LazyLock::new(|| tree_sitter_ruby::LANGUAGE.into());
|
|
23
28
|
|
|
24
29
|
/// The field ids the codebase asks for, resolved from the grammar once.
|
|
25
30
|
///
|
|
@@ -31,9 +36,8 @@ macro_rules! field_ids {
|
|
|
31
36
|
struct FieldIds { $($field: u16,)+ }
|
|
32
37
|
|
|
33
38
|
static FIELD_IDS: LazyLock<FieldIds> = LazyLock::new(|| {
|
|
34
|
-
let language = language();
|
|
35
39
|
FieldIds {
|
|
36
|
-
$($field:
|
|
40
|
+
$($field: LANGUAGE
|
|
37
41
|
.field_id_for_name($name)
|
|
38
42
|
.map_or(0, NonZeroU16::get),)+
|
|
39
43
|
}
|
|
@@ -79,10 +83,9 @@ field_ids! {
|
|
|
79
83
|
|
|
80
84
|
/// Every node kind the grammar can produce, indexed by the id `Node::kind_id` answers with.
|
|
81
85
|
static KIND_NAMES: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
|
|
82
|
-
|
|
83
|
-
(0..language.node_kind_count())
|
|
86
|
+
(0..LANGUAGE.node_kind_count())
|
|
84
87
|
.map(|id| {
|
|
85
|
-
|
|
88
|
+
LANGUAGE
|
|
86
89
|
.node_kind_for_id(id as u16)
|
|
87
90
|
.expect("every id below the count names a kind")
|
|
88
91
|
})
|
|
@@ -106,10 +109,14 @@ impl<'tree> NodeExt<'tree> for Node<'tree> {
|
|
|
106
109
|
fn kind_str(&self) -> &'static str {
|
|
107
110
|
match KIND_NAMES.get(self.kind_id() as usize) {
|
|
108
111
|
Some(kind) => kind,
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// the
|
|
112
|
-
|
|
112
|
+
// `ERROR` and `_ERROR` carry ids above the kind count -- 65535 and 65534 against 368
|
|
113
|
+
// kinds -- so this arm is the ordinary path for them, and `ts_language_symbol_name`
|
|
114
|
+
// names both. It asks the grammar the same question `Node::kind` does, through the
|
|
115
|
+
// `static` so the answer is still `'static`. The `unwrap_or` is for an id neither the
|
|
116
|
+
// table nor the grammar knows, which no node this grammar produced can carry: `ERROR`
|
|
117
|
+
// is the honest name for a node that cannot be placed, and `Node::kind` would abort
|
|
118
|
+
// the run there.
|
|
119
|
+
None => LANGUAGE.node_kind_for_id(self.kind_id()).unwrap_or("ERROR"),
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
|
|
@@ -130,12 +137,12 @@ impl<'tree> NodeExt<'tree> for Node<'tree> {
|
|
|
130
137
|
|
|
131
138
|
#[cfg(test)]
|
|
132
139
|
mod tests {
|
|
133
|
-
use super::{
|
|
140
|
+
use super::{LANGUAGE, NodeExt};
|
|
134
141
|
use tree_sitter::Parser;
|
|
135
142
|
|
|
136
143
|
fn tree(source: &str) -> tree_sitter::Tree {
|
|
137
144
|
let mut parser = Parser::new();
|
|
138
|
-
parser.set_language(&
|
|
145
|
+
parser.set_language(&LANGUAGE).unwrap();
|
|
139
146
|
parser.parse(source, None).unwrap()
|
|
140
147
|
}
|
|
141
148
|
|
|
@@ -187,4 +194,28 @@ mod tests {
|
|
|
187
194
|
let tree = tree("a.b(1)\n");
|
|
188
195
|
assert!(tree.root_node().field("nonexistent").is_none());
|
|
189
196
|
}
|
|
197
|
+
|
|
198
|
+
/// `ERROR` carries a kind id above the kind count, so it is the fallback arm of
|
|
199
|
+
/// [`NodeExt::kind_str`] that names it -- and `Lint/Syntax` rests on that name. The arm was a
|
|
200
|
+
/// call to `Node::kind` until tree-sitter 0.27 tied that answer's lifetime to the tree; this
|
|
201
|
+
/// holds the grammar's table to the same answer.
|
|
202
|
+
#[test]
|
|
203
|
+
fn an_error_node_is_named_the_way_the_parser_names_it() {
|
|
204
|
+
let tree = tree("def foo(\n");
|
|
205
|
+
let mut stack = vec![tree.root_node()];
|
|
206
|
+
let mut errors = 0;
|
|
207
|
+
while let Some(node) = stack.pop() {
|
|
208
|
+
if node.is_error() {
|
|
209
|
+
assert!(
|
|
210
|
+
(node.kind_id() as usize) >= LANGUAGE.node_kind_count(),
|
|
211
|
+
"an id the table carries would not reach the fallback arm"
|
|
212
|
+
);
|
|
213
|
+
assert_eq!(node.kind_str(), node.kind(), "kind of {node:?}");
|
|
214
|
+
errors += 1;
|
|
215
|
+
}
|
|
216
|
+
let mut cursor = node.walk();
|
|
217
|
+
stack.extend(node.children(&mut cursor));
|
|
218
|
+
}
|
|
219
|
+
assert!(errors > 0, "the sample has to reach an ERROR node");
|
|
220
|
+
}
|
|
190
221
|
}
|
data/src/rules/send_node.rs
CHANGED
|
@@ -207,7 +207,7 @@ pub(crate) fn string_text<'a>(node: Node<'_>, context: &'a RuleContext<'_>) -> &
|
|
|
207
207
|
let text = context.source.node_text(node);
|
|
208
208
|
let (Some(open), Some(close)) = (
|
|
209
209
|
node.child(0),
|
|
210
|
-
node.child(node.child_count().saturating_sub(1)
|
|
210
|
+
node.child(node.child_count().saturating_sub(1)),
|
|
211
211
|
) else {
|
|
212
212
|
return text;
|
|
213
213
|
};
|
|
@@ -419,7 +419,7 @@ pub(crate) fn named_children_of<'tree>(
|
|
|
419
419
|
) -> Vec<Node<'tree>> {
|
|
420
420
|
match context.named_children(node) {
|
|
421
421
|
Some(children) => children.to_vec(),
|
|
422
|
-
None =>
|
|
422
|
+
None => named_children(node),
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
|
|
@@ -142,7 +142,10 @@ impl<'tree> Cop<'_, 'tree> {
|
|
|
142
142
|
if splat.kind_str() != "splat_argument" {
|
|
143
143
|
return false;
|
|
144
144
|
}
|
|
145
|
-
let Some(value) = send_node::named_children_of(*splat, self.context)
|
|
145
|
+
let Some(value) = send_node::named_children_of(*splat, self.context)
|
|
146
|
+
.first()
|
|
147
|
+
.copied()
|
|
148
|
+
else {
|
|
146
149
|
return false;
|
|
147
150
|
};
|
|
148
151
|
match value.kind_str() {
|
|
@@ -384,7 +387,7 @@ impl<'tree> Cop<'_, 'tree> {
|
|
|
384
387
|
let mut current = node;
|
|
385
388
|
while let Some(parent) = current.parent() {
|
|
386
389
|
if matches!(parent.kind_str(), "class" | "module" | "singleton_class") {
|
|
387
|
-
let last =
|
|
390
|
+
let last = parent.child_count().checked_sub(1)?;
|
|
388
391
|
return parent.child(last).filter(|end| end.kind_str() == "end");
|
|
389
392
|
}
|
|
390
393
|
current = parent;
|
|
@@ -459,8 +462,12 @@ impl<'tree> Cop<'_, 'tree> {
|
|
|
459
462
|
/// code instead and never travels forward.
|
|
460
463
|
fn leading_comments(&self, node: Node<'tree>) -> Vec<Range<usize>> {
|
|
461
464
|
let source = self.context.source;
|
|
462
|
-
let (line,
|
|
463
|
-
|
|
465
|
+
let (line, _) = source.line_column(node.start_byte());
|
|
466
|
+
// The column `line_column` reports counts characters, and the line is sliced by bytes, so
|
|
467
|
+
// what stands before the node is measured against the line's own start rather than that
|
|
468
|
+
// column -- a line opening with a multi-byte character would otherwise be sliced inside it.
|
|
469
|
+
let before = &source.line(line)[..node.start_byte() - source.line_start(line)];
|
|
470
|
+
if !before.trim().is_empty() {
|
|
464
471
|
return Vec::new();
|
|
465
472
|
}
|
|
466
473
|
let mut comments = Vec::new();
|
|
@@ -164,7 +164,7 @@ fn nested_correction(
|
|
|
164
164
|
name: Node<'_>,
|
|
165
165
|
) -> Option<Vec<Edit>> {
|
|
166
166
|
let keyword = node.child(0)?;
|
|
167
|
-
let closing = node.child(node.child_count().saturating_sub(1)
|
|
167
|
+
let closing = node.child(node.child_count().saturating_sub(1))?;
|
|
168
168
|
if closing.kind_str() != "end" {
|
|
169
169
|
return None;
|
|
170
170
|
}
|
|
@@ -229,7 +229,7 @@ fn compact_correction(
|
|
|
229
229
|
}
|
|
230
230
|
let keyword = node.child(0)?;
|
|
231
231
|
let inner_name = inner.field("name")?;
|
|
232
|
-
let inner_end = inner.child(inner.child_count().saturating_sub(1)
|
|
232
|
+
let inner_end = inner.child(inner.child_count().saturating_sub(1))?;
|
|
233
233
|
if inner_end.kind_str() != "end" {
|
|
234
234
|
return None;
|
|
235
235
|
}
|
|
@@ -18,7 +18,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
18
18
|
// `node.heredoc?`: a `<<`CMD`` opens a heredoc, which the grammar spells as its own node.
|
|
19
19
|
let (Some(open), Some(close)) = (
|
|
20
20
|
node.child(0),
|
|
21
|
-
node.child(node.child_count().saturating_sub(1)
|
|
21
|
+
node.child(node.child_count().saturating_sub(1)),
|
|
22
22
|
) else {
|
|
23
23
|
continue;
|
|
24
24
|
};
|
|
@@ -128,7 +128,7 @@ fn preferred_method(arrays: &[(ArrayKind, Node<'_>)], context: &RuleContext<'_>)
|
|
|
128
128
|
/// The `[` and the `]` of an array literal, which the correction drops.
|
|
129
129
|
fn brackets(node: Node<'_>) -> Option<(std::ops::Range<usize>, std::ops::Range<usize>)> {
|
|
130
130
|
let open = node.child(0)?;
|
|
131
|
-
let close = node.child(node.child_count().checked_sub(1)?
|
|
131
|
+
let close = node.child(node.child_count().checked_sub(1)?)?;
|
|
132
132
|
(open.id() != close.id()).then(|| (open.byte_range(), close.byte_range()))
|
|
133
133
|
}
|
|
134
134
|
|