sonicop 26.8.105 → 26.8.106
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 +1 -1
- data/Cargo.toml +1 -1
- data/lib/sonicop/version.rb +1 -1
- data/src/rules/layout/empty_lines_around_arguments.rs +1 -1
- data/src/rules/layout/multiline_block_layout.rs +1 -1
- data/src/rules/layout/redundant_line_break.rs +1 -0
- data/src/rules/layout/space_around_block_parameters.rs +1 -1
- data/src/rules/layout/space_before_block_braces.rs +1 -1
- data/src/rules/layout/space_before_first_arg.rs +1 -1
- data/src/rules/lint/locals.rs +7 -0
- data/src/rules/lint/redundant_cop_disable_directive.rs +2 -2
- data/src/rules/lint/redundant_cop_enable_directive.rs +1 -1
- data/src/rules/lint/trailing_comma_in_attribute_declaration.rs +1 -1
- data/src/rules/lint/variable_force.rs +34 -0
- data/src/rules/style/collection_querying.rs +1 -1
- data/src/rules/style/combinable_defined.rs +1 -1
- data/src/rules/style/data_inheritance.rs +4 -4
- data/src/rules/style/map_into_array.rs +4 -4
- data/src/rules/style/map_to_hash.rs +1 -1
- data/src/rules/style/map_to_set.rs +1 -1
- data/src/rules/style/method_call_with_args_parentheses.rs +40 -16
- data/src/rules/style/multiline_in_pattern_then.rs +1 -1
- data/src/rules/style/multiline_method_signature.rs +1 -1
- data/src/rules/style/nested_parenthesized_calls.rs +8 -7
- data/src/rules/style/nil_lambda.rs +2 -2
- data/src/rules/style/operator_method_call.rs +2 -2
- data/src/rules/style/parens.rs +16 -7
- data/src/rules/style/redundant_argument.rs +2 -2
- data/src/rules/support.rs +98 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c90ed3a9d03214ba6922d2aecd35c70b28e09791c870f06c22848ef05b336054
|
|
4
|
+
data.tar.gz: 0557036a886b2023e5151d8b18b1df8bdd66725faf957d19013412a0bc62c6ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63191904f82e06d89200c5b81a22a9ad6a4ebcbf07ed86039966605a2d87225d7984a2dfd7a3b0a7b38ca54aaebc1b36518fdb1eeb5dead16318168e3441d692
|
|
7
|
+
data.tar.gz: deb43652b8dfd0c2c9fac7ed27a1a33fb1097e76bd2d455851ba04d30d2fc6751025fadfdc4012d06f6a45140a983a5686b7bdfc3e5436e5d9ea0e238da26ddd
|
data/Cargo.lock
CHANGED
data/Cargo.toml
CHANGED
data/lib/sonicop/version.rb
CHANGED
|
@@ -43,7 +43,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
43
43
|
/// `empty_range_for_starting_point`: the blank line immediately before `start`, when the run of
|
|
44
44
|
/// whitespace reaching back from it spans more than one line break.
|
|
45
45
|
fn empty_range_before(context: &RuleContext<'_>, start: usize) -> Option<Range<usize>> {
|
|
46
|
-
let begin = final_pos(context.source.text(), start, false, true, true);
|
|
46
|
+
let begin = final_pos(context.source.text(), start, false, false, true, true);
|
|
47
47
|
let first_line = context.source.line_column(begin).0;
|
|
48
48
|
let last_line = context.source.line_column(start).0;
|
|
49
49
|
(last_line.checked_sub(first_line)? > 1).then(|| context.source.line_range(last_line - 1))
|
|
@@ -35,7 +35,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
35
35
|
let mut edits = Vec::new();
|
|
36
36
|
let mut expression_before_body = open.start_byte();
|
|
37
37
|
if let Some(arguments) = arguments.filter(|_| !on_beginning_line) {
|
|
38
|
-
let end = final_pos(text, arguments.end_byte(), true, false, false);
|
|
38
|
+
let end = final_pos(text, arguments.end_byte(), true, false, false, false);
|
|
39
39
|
edits.push(Edit {
|
|
40
40
|
start: open.end_byte(),
|
|
41
41
|
end,
|
|
@@ -96,7 +96,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
96
96
|
let start = argument.start_byte();
|
|
97
97
|
no_space(
|
|
98
98
|
context,
|
|
99
|
-
final_pos(text, start, false, true, false)..start.saturating_sub(1),
|
|
99
|
+
final_pos(text, start, false, false, true, false)..start.saturating_sub(1),
|
|
100
100
|
"Extra space before",
|
|
101
101
|
offenses,
|
|
102
102
|
);
|
|
@@ -36,7 +36,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
let space_start = final_pos(text, left.start_byte(), false, true, false);
|
|
39
|
+
let space_start = final_pos(text, left.start_byte(), false, false, true, false);
|
|
40
40
|
let used_space = space_start != left.start_byte();
|
|
41
41
|
let empty = left.end_byte() == right.start_byte();
|
|
42
42
|
let wanted_space = if empty {
|
|
@@ -50,7 +50,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
let space = final_pos(text, first.start, false, true, false)..first.start;
|
|
53
|
+
let space = final_pos(text, first.start, false, false, true, false)..first.start;
|
|
54
54
|
if text[space.clone()].chars().count() == 1 {
|
|
55
55
|
continue;
|
|
56
56
|
}
|
data/src/rules/lint/locals.rs
CHANGED
|
@@ -31,6 +31,13 @@ impl<'ctx, 'tree> LocalVariables<'ctx, 'tree> {
|
|
|
31
31
|
self.context.variable_analysis().is_variable_reference(node)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/// Whether this `foo()` would read a local variable instead of calling anything once its
|
|
35
|
+
/// parentheses were gone. The call itself is a call whatever the name holds, so [`Self::is_lvar`]
|
|
36
|
+
/// says no about every part of it; the question is about the bare name a correction would leave.
|
|
37
|
+
pub(in crate::rules) fn shadows_a_local(&self, node: Node<'_>) -> bool {
|
|
38
|
+
self.context.variable_analysis().shadows_a_local(node)
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
/// Where each variable in the file is written and where it is read.
|
|
35
42
|
///
|
|
36
43
|
/// A cop that joins `VariableForce` through `after_leaving_scope` sees the whole table rather
|
|
@@ -439,12 +439,12 @@ fn trailing_range(source: &SourceFile, ranges: &[Range<usize>], range: &Range<us
|
|
|
439
439
|
|
|
440
440
|
/// `range_with_surrounding_space(side: :left, newlines: true)`.
|
|
441
441
|
fn grow_left(source: &SourceFile, range: Range<usize>) -> Range<usize> {
|
|
442
|
-
final_pos(source.text(), range.start, false, true, false)..range.end
|
|
442
|
+
final_pos(source.text(), range.start, false, false, true, false)..range.end
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
/// `range_with_surrounding_space(side: :right)`, with the newline eaten only when asked.
|
|
446
446
|
fn grow_right(source: &SourceFile, range: Range<usize>, newlines: bool) -> Range<usize> {
|
|
447
|
-
range.start..final_pos(source.text(), range.end, true, newlines, false)
|
|
447
|
+
range.start..final_pos(source.text(), range.end, true, false, newlines, false)
|
|
448
448
|
}
|
|
449
449
|
|
|
450
450
|
fn grow_right_spaces_only(source: &SourceFile, range: Range<usize>) -> Range<usize> {
|
|
@@ -142,6 +142,10 @@ pub(in crate::rules) struct Analysis<'tree> {
|
|
|
142
142
|
/// local from a receiverless call, and only the analysis knows which one the parser upstream
|
|
143
143
|
/// would have built.
|
|
144
144
|
lvars: HashSet<usize>,
|
|
145
|
+
/// The `foo()` calls whose name a local variable already holds. The parentheses make these
|
|
146
|
+
/// calls whatever the name resolves to, so nothing here is an `lvar` -- but writing the same
|
|
147
|
+
/// name without them would be one, which is the question a cop that drops parentheses asks.
|
|
148
|
+
shadowed_calls: HashSet<usize>,
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
impl<'tree> Analysis<'tree> {
|
|
@@ -150,6 +154,12 @@ impl<'tree> Analysis<'tree> {
|
|
|
150
154
|
self.lvars.contains(&node.id())
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
/// Whether this `foo()` would stop being a call if its parentheses went away, because a local
|
|
158
|
+
/// variable of that name is in scope. Only the walk knows what is in scope at a given node.
|
|
159
|
+
pub(in crate::rules) fn shadows_a_local(&self, node: Node<'_>) -> bool {
|
|
160
|
+
self.shadowed_calls.contains(&node.id())
|
|
161
|
+
}
|
|
162
|
+
|
|
153
163
|
pub(in crate::rules) fn run(root: Node<'tree>, source: &SourceFile) -> Self {
|
|
154
164
|
let mut force = Force {
|
|
155
165
|
source,
|
|
@@ -161,6 +171,7 @@ impl<'tree> Analysis<'tree> {
|
|
|
161
171
|
heredocs: heredoc_bodies(root),
|
|
162
172
|
scanned: HashSet::new(),
|
|
163
173
|
lvars: HashSet::new(),
|
|
174
|
+
shadowed_calls: HashSet::new(),
|
|
164
175
|
};
|
|
165
176
|
force.push_scope(root, true);
|
|
166
177
|
force.process_children(root);
|
|
@@ -169,6 +180,7 @@ impl<'tree> Analysis<'tree> {
|
|
|
169
180
|
scopes: force.scopes,
|
|
170
181
|
variables: force.variables,
|
|
171
182
|
lvars: force.lvars,
|
|
183
|
+
shadowed_calls: force.shadowed_calls,
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
}
|
|
@@ -207,6 +219,7 @@ struct Force<'tree, 'a> {
|
|
|
207
219
|
/// Nodes already walked in an outer scope, which the scope they sit in must not walk again.
|
|
208
220
|
scanned: HashSet<usize>,
|
|
209
221
|
lvars: HashSet<usize>,
|
|
222
|
+
shadowed_calls: HashSet<usize>,
|
|
210
223
|
}
|
|
211
224
|
|
|
212
225
|
// ---------------------------------------------------------------------------
|
|
@@ -779,6 +792,9 @@ impl<'tree> Force<'tree, '_> {
|
|
|
779
792
|
if self.binary_operator_on_a_local(node) {
|
|
780
793
|
return;
|
|
781
794
|
}
|
|
795
|
+
if self.name_is_taken_by_a_local(node) {
|
|
796
|
+
self.shadowed_calls.insert(node.id());
|
|
797
|
+
}
|
|
782
798
|
if let Some(method) = node.field("method")
|
|
783
799
|
&& self.text(method) == "binding"
|
|
784
800
|
&& opaque_binding_argument(node)
|
|
@@ -804,6 +820,24 @@ impl<'tree> Force<'tree, '_> {
|
|
|
804
820
|
}
|
|
805
821
|
}
|
|
806
822
|
|
|
823
|
+
/// Whether `foo()` is written where a local variable already holds the name `foo`.
|
|
824
|
+
///
|
|
825
|
+
/// The empty parentheses make it a call either way, so no `lvar` is built here and the name is
|
|
826
|
+
/// not a read of the variable. Dropping them would leave a bare name, and that one _is_ the
|
|
827
|
+
/// variable -- which is why a cop that offers to drop them has to leave this call alone.
|
|
828
|
+
fn name_is_taken_by_a_local(&self, node: Node<'tree>) -> bool {
|
|
829
|
+
if node.field("receiver").is_some() {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
let (Some(method), Some(arguments)) = (node.field("method"), node.field("arguments"))
|
|
833
|
+
else {
|
|
834
|
+
return false;
|
|
835
|
+
};
|
|
836
|
+
method.kind_str() == "identifier"
|
|
837
|
+
&& arguments.named_child_count() == 0
|
|
838
|
+
&& self.find_variable(self.text(method)).is_some()
|
|
839
|
+
}
|
|
840
|
+
|
|
807
841
|
/// Whether the call is really `local & expr` or `local * expr`. Ruby resolves the ambiguity by
|
|
808
842
|
/// what it has already seen: once the name is a local variable, the `&` is an operator and not
|
|
809
843
|
/// the start of a block-pass argument. tree-sitter has no scope to consult and always reads the
|
|
@@ -47,7 +47,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
47
47
|
// `removal_range`: everything from the comparison's own dot or operator, with the blank in
|
|
48
48
|
// front of it.
|
|
49
49
|
let removal =
|
|
50
|
-
final_pos(context.source.text(), dot.start, false, true, false)..node.end_byte();
|
|
50
|
+
final_pos(context.source.text(), dot.start, false, false, true, false)..node.end_byte();
|
|
51
51
|
offenses.push(
|
|
52
52
|
context
|
|
53
53
|
.offense(format!("Use `{replacement}` instead."), range)
|
|
@@ -101,7 +101,7 @@ fn removal(context: &RuleContext<'_>, subject: Node<'_>) -> Option<std::ops::Ran
|
|
|
101
101
|
term.start_byte()..position
|
|
102
102
|
};
|
|
103
103
|
// `range_with_surrounding_space(side: :right, newlines: false)`.
|
|
104
|
-
Some(range.start..final_pos(text, range.end, true, false, false))
|
|
104
|
+
Some(range.start..final_pos(text, range.end, true, false, false, false))
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/// `node.parent.and_type?`: `&&` and `and`, which are one node type upstream.
|
|
@@ -39,8 +39,8 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
39
39
|
let mut edits = vec![
|
|
40
40
|
// `range_with_surrounding_space(node.loc.keyword, newlines: false)`.
|
|
41
41
|
Edit {
|
|
42
|
-
start: final_pos(text, keyword.start_byte(), false, false, false),
|
|
43
|
-
end: final_pos(text, keyword.end_byte(), true, false, false),
|
|
42
|
+
start: final_pos(text, keyword.start_byte(), false, false, false, false),
|
|
43
|
+
end: final_pos(text, keyword.end_byte(), true, false, false, false),
|
|
44
44
|
replacement: String::new(),
|
|
45
45
|
safe: true,
|
|
46
46
|
},
|
|
@@ -65,8 +65,8 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
65
65
|
}
|
|
66
66
|
if let Some(close) = super::conditional::token(block, &["}", "end"]) {
|
|
67
67
|
edits.push(Edit {
|
|
68
|
-
start: final_pos(text, close.start_byte(), false, false, false),
|
|
69
|
-
end: final_pos(text, close.end_byte(), true, false, false),
|
|
68
|
+
start: final_pos(text, close.start_byte(), false, false, false, false),
|
|
69
|
+
end: final_pos(text, close.end_byte(), true, false, false, false),
|
|
70
70
|
replacement: String::new(),
|
|
71
71
|
safe: true,
|
|
72
72
|
});
|
|
@@ -93,14 +93,14 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
93
93
|
continue;
|
|
94
94
|
};
|
|
95
95
|
edits.push(removal(
|
|
96
|
-
final_pos(text, close.start_byte(), false, true, false)..close.end_byte(),
|
|
96
|
+
final_pos(text, close.start_byte(), false, false, true, false)..close.end_byte(),
|
|
97
97
|
));
|
|
98
98
|
}
|
|
99
99
|
// `remove_assignment`: the blank after it goes too, line break included.
|
|
100
100
|
(None, Some(assignment)) => {
|
|
101
|
-
let end = final_pos(text, assignment.end_byte(), true, true, false);
|
|
101
|
+
let end = final_pos(text, assignment.end_byte(), true, false, true, false);
|
|
102
102
|
edits.push(removal(
|
|
103
|
-
assignment.start_byte()..final_pos(text, end, true, false, false),
|
|
103
|
+
assignment.start_byte()..final_pos(text, end, true, false, false, false),
|
|
104
104
|
));
|
|
105
105
|
}
|
|
106
106
|
(None, None) => continue,
|
|
@@ -128,7 +128,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
128
128
|
&& context.source.node_text(sibling) == variable.name
|
|
129
129
|
{
|
|
130
130
|
edits.push(removal(
|
|
131
|
-
final_pos(text, sibling.start_byte(), false, true, false)..sibling.end_byte(),
|
|
131
|
+
final_pos(text, sibling.start_byte(), false, false, true, false)..sibling.end_byte(),
|
|
132
132
|
));
|
|
133
133
|
}
|
|
134
134
|
edits.push(Edit {
|
|
@@ -40,7 +40,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
40
40
|
continue;
|
|
41
41
|
};
|
|
42
42
|
let dot_source = context.source.node_text(dot);
|
|
43
|
-
let start = final_pos(context.source.text(), dot.start_byte(), false, true, false);
|
|
43
|
+
let start = final_pos(context.source.text(), dot.start_byte(), false, false, true, false);
|
|
44
44
|
let mut edits = vec![
|
|
45
45
|
Edit {
|
|
46
46
|
start,
|
|
@@ -34,7 +34,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
34
34
|
};
|
|
35
35
|
// `range_with_surrounding_space(side: :left)`: the conversion goes with the blank in front
|
|
36
36
|
// of it, so a chain written over two lines closes up.
|
|
37
|
-
let start = final_pos(context.source.text(), dot.start_byte(), false, true, false);
|
|
37
|
+
let start = final_pos(context.source.text(), dot.start_byte(), false, false, true, false);
|
|
38
38
|
offenses.push(
|
|
39
39
|
context
|
|
40
40
|
.offense(
|
|
@@ -46,7 +46,7 @@ fn require_parentheses(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
46
46
|
continue;
|
|
47
47
|
};
|
|
48
48
|
let written = super::nodes::children(arguments);
|
|
49
|
-
if written.is_empty() || is_parenthesized(arguments) {
|
|
49
|
+
if written.is_empty() || is_parenthesized(node, arguments, context) {
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
52
|
// `args_begin`: the character after the selector, which is the blank before the arguments.
|
|
@@ -164,10 +164,23 @@ fn argument_list<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
|
|
|
164
164
|
|
|
165
165
|
/// `parenthesized?`: the parentheses belong to the call rather than to its first argument, which is
|
|
166
166
|
/// what `foo(1)` has and `foo (1)` does not.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
///
|
|
168
|
+
/// Upstream settles this while lexing -- a `(` written straight against the selector opens the call
|
|
169
|
+
/// -- so the answer is adjacency, not shape. Reading the shape instead is wrong in one direction:
|
|
170
|
+
/// the grammar here usually gives the call's own parentheses as `(` and `)` tokens of the argument
|
|
171
|
+
/// list, but for some receivers it wraps them in a `parenthesized_statements` node, which then reads
|
|
172
|
+
/// as `foo (1)` and makes a properly parenthesized call look bare.
|
|
173
|
+
///
|
|
174
|
+
/// `expect(x).to receive(:y) do ... end.at_least(:once)` is such a receiver: the `at_least(:once)`
|
|
175
|
+
/// there parses to the wrapped shape while the same call after a simpler receiver does not.
|
|
176
|
+
fn is_parenthesized(node: Node<'_>, arguments: Node<'_>, context: &RuleContext<'_>) -> bool {
|
|
177
|
+
let Some(selector) = selector_end(node) else {
|
|
178
|
+
return false;
|
|
179
|
+
};
|
|
180
|
+
// The argument list begins where the arguments do, so a `(` of its own is the call's only when
|
|
181
|
+
// nothing separates it from the selector.
|
|
182
|
+
arguments.start_byte() == selector
|
|
183
|
+
&& context.source.text().as_bytes().get(selector) == Some(&b'(')
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
/// `omit_parentheses`.
|
|
@@ -195,7 +208,13 @@ fn omit_parentheses(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
195
208
|
|node| omission_edits(*node, context),
|
|
196
209
|
|node| omission_range(*node),
|
|
197
210
|
// 本家の `verified_by_reparse(@pending_omit_offenses)` は追加のオプションを渡さない。
|
|
198
|
-
|
|
211
|
+
// `fold_empty_call_parentheses` は本家の設定ではなく、`foo()` と `foo` を 1 つの
|
|
212
|
+
// ノードにする本家のパーサに合わせるためのもの。名前を局所変数が持っている呼び出しは
|
|
213
|
+
// `omits` が先に外している。
|
|
214
|
+
crate::rules::support::Verification {
|
|
215
|
+
fold_empty_call_parentheses: true,
|
|
216
|
+
..Default::default()
|
|
217
|
+
},
|
|
199
218
|
);
|
|
200
219
|
verified.extend(with_heredoc);
|
|
201
220
|
verified.sort_by_key(tree_sitter::Node::start_byte);
|
|
@@ -243,11 +262,15 @@ impl Omission {
|
|
|
243
262
|
let Some(arguments) = argument_list(node) else {
|
|
244
263
|
return false;
|
|
245
264
|
};
|
|
246
|
-
if !is_parenthesized(arguments) {
|
|
265
|
+
if !is_parenthesized(node, arguments, context) {
|
|
247
266
|
return false;
|
|
248
267
|
}
|
|
249
268
|
let written = super::nodes::children(arguments);
|
|
250
|
-
|
|
269
|
+
// Upstream's reparse settles this one: `foo()` is a call, `foo` is the local variable of
|
|
270
|
+
// that name, and the two trees do not match. The comparison here cannot tell them apart,
|
|
271
|
+
// so the candidate never reaches it.
|
|
272
|
+
!locals.shadows_a_local(node)
|
|
273
|
+
&& !inside_endless_method_def(node, &written, context)
|
|
251
274
|
&& !hash_value_omission_needs_parentheses(node, &written, context)
|
|
252
275
|
&& !syntax_like_method_call(node, context)
|
|
253
276
|
&& !before_constant_resolution(node, context)
|
|
@@ -272,7 +295,7 @@ impl Omission {
|
|
|
272
295
|
|| call_in_optional_arguments(node, context)
|
|
273
296
|
|| call_in_single_line_inheritance(node, context)
|
|
274
297
|
|| (self.multiline && is_multiline(node, arguments, context))
|
|
275
|
-
|| (self.chaining && chained_with_parentheses(node))
|
|
298
|
+
|| (self.chaining && chained_with_parentheses(node, context))
|
|
276
299
|
|| assignment_in_condition(node, context)
|
|
277
300
|
|| forwards_anonymous_rest_arguments(written)
|
|
278
301
|
}
|
|
@@ -488,7 +511,9 @@ fn call_with_ambiguous_arguments(
|
|
|
488
511
|
return true;
|
|
489
512
|
}
|
|
490
513
|
let _ = arguments;
|
|
491
|
-
if hash_literal_in_arguments(node, written, context, locals)
|
|
514
|
+
if hash_literal_in_arguments(node, written, context, locals)
|
|
515
|
+
|| ambiguous_range_argument(written)
|
|
516
|
+
{
|
|
492
517
|
return true;
|
|
493
518
|
}
|
|
494
519
|
descendants(node, context)
|
|
@@ -567,7 +592,7 @@ fn call_in_single_line_inheritance(node: Node<'_>, context: &RuleContext<'_>) ->
|
|
|
567
592
|
}
|
|
568
593
|
|
|
569
594
|
/// `allowed_chained_call_with_parentheses?`: the call it hangs off already writes its own.
|
|
570
|
-
fn chained_with_parentheses(node: Node<'_>) -> bool {
|
|
595
|
+
fn chained_with_parentheses(node: Node<'_>, context: &RuleContext<'_>) -> bool {
|
|
571
596
|
let mut current = node;
|
|
572
597
|
loop {
|
|
573
598
|
let Some(previous) = current
|
|
@@ -576,7 +601,9 @@ fn chained_with_parentheses(node: Node<'_>) -> bool {
|
|
|
576
601
|
else {
|
|
577
602
|
return false;
|
|
578
603
|
};
|
|
579
|
-
if argument_list(previous)
|
|
604
|
+
if argument_list(previous)
|
|
605
|
+
.is_some_and(|arguments| is_parenthesized(previous, arguments, context))
|
|
606
|
+
{
|
|
580
607
|
return true;
|
|
581
608
|
}
|
|
582
609
|
current = previous;
|
|
@@ -697,10 +724,7 @@ fn inside_string_interpolation(node: Node<'_>, context: &RuleContext<'_>) -> boo
|
|
|
697
724
|
|
|
698
725
|
/// The node's descendants, leaving out the block a call carries: upstream's parser wraps the block
|
|
699
726
|
/// around the call rather than hanging it off it, so a block is never a call's descendant there.
|
|
700
|
-
fn descendants<'tree>(
|
|
701
|
-
node: Node<'tree>,
|
|
702
|
-
context: &'tree RuleContext<'_>,
|
|
703
|
-
) -> Vec<Node<'tree>> {
|
|
727
|
+
fn descendants<'tree>(node: Node<'tree>, context: &'tree RuleContext<'_>) -> Vec<Node<'tree>> {
|
|
704
728
|
let mut out = Vec::new();
|
|
705
729
|
let block = node.field("block").map(|block| block.id());
|
|
706
730
|
for child in super::nodes::children(node) {
|
|
@@ -61,7 +61,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
61
61
|
}
|
|
62
62
|
// `range_with_surrounding_space(arguments_range(node), side: :left)`.
|
|
63
63
|
let arguments_range =
|
|
64
|
-
support::final_pos(text, first.start_byte(), false, true, false)..last.end_byte();
|
|
64
|
+
support::final_pos(text, first.start_byte(), false, false, true, false)..last.end_byte();
|
|
65
65
|
// A list that starts on a later line leaves the name alone on the first one.
|
|
66
66
|
if context.source.line_column(arguments_range.start).0 != opening_line {
|
|
67
67
|
let prefix = node.start_byte() + DEF_LENGTH..parameters.start_byte();
|
|
@@ -122,12 +122,13 @@ fn reads_as_binary_and(
|
|
|
122
122
|
.ends_with([' ', '\t', '\n'])
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
///
|
|
125
|
+
/// `range_with_surrounding_space(first_arg.begin, side: :left, whitespace: true,
|
|
126
|
+
/// continuations: true)`.
|
|
127
|
+
///
|
|
128
|
+
/// The line continuation is the part that matters: walking back over whitespace alone stops at the
|
|
129
|
+
/// `\` and leaves it in the source, so the `(` that replaces the run lands after it and writes
|
|
130
|
+
/// `foo \(bar)` -- which is not Ruby. The shared walk takes the `\` and its line break together,
|
|
131
|
+
/// since a backslash only ends a line when the break follows it.
|
|
126
132
|
fn leading_space(context: &RuleContext<'_>, start: usize) -> usize {
|
|
127
|
-
|
|
128
|
-
let mut position = start;
|
|
129
|
-
while position > 0 && bytes[position - 1].is_ascii_whitespace() {
|
|
130
|
-
position -= 1;
|
|
131
|
-
}
|
|
132
|
-
position
|
|
133
|
+
crate::rules::support::final_pos(context.source.text(), start, false, true, true, true)
|
|
133
134
|
}
|
|
@@ -38,8 +38,8 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
38
38
|
== context.source.line_column(block.end_byte()).0
|
|
39
39
|
{
|
|
40
40
|
// `range_with_surrounding_space(body.source_range)`.
|
|
41
|
-
support::final_pos(text, body.start_byte(), false, true, false)
|
|
42
|
-
..support::final_pos(text, body.end_byte(), true, true, false)
|
|
41
|
+
support::final_pos(text, body.start_byte(), false, false, true, false)
|
|
42
|
+
..support::final_pos(text, body.end_byte(), true, false, true, false)
|
|
43
43
|
} else {
|
|
44
44
|
// `range_by_whole_lines(body.source_range, include_final_newline: true)`.
|
|
45
45
|
whole_lines(body.byte_range(), context)
|
|
@@ -76,10 +76,10 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
76
76
|
// `ParenthesesCorrector.correct`: the argument's own parentheses go away.
|
|
77
77
|
edits.push(remove(
|
|
78
78
|
list.start_byte()
|
|
79
|
-
..support::final_pos(text, list.start_byte() + 1, true, false, true),
|
|
79
|
+
..support::final_pos(text, list.start_byte() + 1, true, false, false, true),
|
|
80
80
|
));
|
|
81
81
|
edits.push(remove(
|
|
82
|
-
support::final_pos(text, list.end_byte() - 1, false, true, false)..list.end_byte(),
|
|
82
|
+
support::final_pos(text, list.end_byte() - 1, false, false, true, false)..list.end_byte(),
|
|
83
83
|
));
|
|
84
84
|
edits.push(insert(selector.end_byte(), " "));
|
|
85
85
|
edits.push(insert(node.start_byte(), "("));
|
data/src/rules/style/parens.rs
CHANGED
|
@@ -31,6 +31,11 @@ pub(super) fn correct(context: &RuleContext<'_>, node: Node<'_>) -> Vec<Edit> {
|
|
|
31
31
|
safe: true,
|
|
32
32
|
},
|
|
33
33
|
Edit {
|
|
34
|
+
// `remove_close_paren`: `range_with_surrounding_space(side: :left, newlines: newlines)`.
|
|
35
|
+
// `whitespace` and `continuations` both stay at their default of `false`, so the walk
|
|
36
|
+
// stops at a `\` that ends the line and leaves it standing. The `continuations: true`
|
|
37
|
+
// walk belongs to `parens_range`, which is a *different* range that upstream builds
|
|
38
|
+
// only when a comma would be left stranded -- see `orphaned_comma_start` below.
|
|
34
39
|
start: super::ranges::extended_left(text, close, newlines),
|
|
35
40
|
end: range.end,
|
|
36
41
|
replacement: String::new(),
|
|
@@ -72,13 +77,17 @@ fn orphaned_comma_start(context: &RuleContext<'_>, close: usize) -> Option<usize
|
|
|
72
77
|
return None;
|
|
73
78
|
}
|
|
74
79
|
// `range_with_surrounding_space(side: :left, newlines: true, whitespace: true,
|
|
75
|
-
// continuations: true)`.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// continuations: true)`. The `continuations` step is the one that matters: a `\` ending the
|
|
81
|
+
// line would otherwise be left behind once the whitespace around it goes, stranding the
|
|
82
|
+
// backslash in front of the comma and producing source that does not parse.
|
|
83
|
+
Some(crate::rules::support::final_pos(
|
|
84
|
+
context.source.text(),
|
|
85
|
+
close,
|
|
86
|
+
false,
|
|
87
|
+
true,
|
|
88
|
+
true,
|
|
89
|
+
true,
|
|
90
|
+
))
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
/// `ternary_condition?(node) && next_char_is_question_mark?(node)`.
|
|
@@ -66,8 +66,8 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
66
66
|
let range = if context.source.node_text(arguments).starts_with('(') {
|
|
67
67
|
arguments.byte_range()
|
|
68
68
|
} else {
|
|
69
|
-
support::final_pos(text, only.start_byte(), false, false, false)
|
|
70
|
-
..support::final_pos(text, only.end_byte(), true, false, false)
|
|
69
|
+
support::final_pos(text, only.start_byte(), false, false, false, false)
|
|
70
|
+
..support::final_pos(text, only.end_byte(), true, false, false, false)
|
|
71
71
|
};
|
|
72
72
|
offenses.push(
|
|
73
73
|
context
|
data/src/rules/support.rs
CHANGED
|
@@ -16,18 +16,53 @@ pub(crate) fn final_pos(
|
|
|
16
16
|
text: &str,
|
|
17
17
|
position: usize,
|
|
18
18
|
forward: bool,
|
|
19
|
+
continuations: bool,
|
|
19
20
|
newlines: bool,
|
|
20
21
|
whitespace: bool,
|
|
21
22
|
) -> usize {
|
|
22
23
|
let mut position = move_pos(text, position, forward, true, |byte| {
|
|
23
24
|
matches!(byte, b' ' | b'\t')
|
|
24
25
|
});
|
|
26
|
+
position = move_pos_str(text, position, forward, continuations, "\\\n");
|
|
25
27
|
position = move_pos(text, position, forward, newlines, |byte| byte == b'\n');
|
|
28
|
+
// `/\s/` upstream, which is Ruby's and takes the vertical tab that Rust's
|
|
29
|
+
// `is_ascii_whitespace` leaves out.
|
|
26
30
|
move_pos(text, position, forward, whitespace, |byte| {
|
|
27
|
-
byte
|
|
31
|
+
matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
|
|
28
32
|
})
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/// `move_pos_str`: the same walk over a fixed run of characters rather than a class.
|
|
36
|
+
///
|
|
37
|
+
/// The one caller wants `\` and a line break, which a class cannot express -- a backslash only ends
|
|
38
|
+
/// the line when the break follows it, and eating it on its own would join two statements.
|
|
39
|
+
fn move_pos_str(
|
|
40
|
+
text: &str,
|
|
41
|
+
mut position: usize,
|
|
42
|
+
forward: bool,
|
|
43
|
+
enabled: bool,
|
|
44
|
+
needle: &str,
|
|
45
|
+
) -> usize {
|
|
46
|
+
if !enabled {
|
|
47
|
+
return position;
|
|
48
|
+
}
|
|
49
|
+
let width = needle.len();
|
|
50
|
+
loop {
|
|
51
|
+
let found = match forward {
|
|
52
|
+
true => text.get(position..position + width) == Some(needle),
|
|
53
|
+
false => position >= width && text.get(position - width..position) == Some(needle),
|
|
54
|
+
};
|
|
55
|
+
if !found {
|
|
56
|
+
return position;
|
|
57
|
+
}
|
|
58
|
+
position = if forward {
|
|
59
|
+
position + width
|
|
60
|
+
} else {
|
|
61
|
+
position - width
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
fn move_pos(
|
|
32
67
|
text: &str,
|
|
33
68
|
mut position: usize,
|
|
@@ -173,6 +208,16 @@ pub(crate) struct Verification {
|
|
|
173
208
|
/// without changing the string it builds. Folding every concatenation to a canonical form is
|
|
174
209
|
/// what lets the two compare equal.
|
|
175
210
|
pub(crate) fold_string_concatenation: bool,
|
|
211
|
+
/// `foo()` and `foo` are one and the same node upstream: the parser builds nothing for an empty
|
|
212
|
+
/// argument list, and a receiverless call carrying only its name is spelled as a bare name.
|
|
213
|
+
/// The grammar here keeps the two apart -- `call` with an `argument_list` that has no children
|
|
214
|
+
/// on one side, a lone `identifier` on the other -- so a correction that drops nothing but a
|
|
215
|
+
/// pair of empty parentheses never compares equal without this.
|
|
216
|
+
///
|
|
217
|
+
/// Upstream's parser tells `(send nil :foo)` from `(lvar :foo)` by remembering what it has seen
|
|
218
|
+
/// assigned, which a tree already built cannot. The caller that turns this on has to keep the
|
|
219
|
+
/// calls whose name a local variable holds out on its own.
|
|
220
|
+
pub(crate) fold_empty_call_parentheses: bool,
|
|
176
221
|
}
|
|
177
222
|
|
|
178
223
|
/// `ReparsedEquivalence#verified_by_reparse`: the items whose corrections leave a tree equal to the
|
|
@@ -321,12 +366,18 @@ fn normalized(node: Node<'_>, text: &str, verification: Verification) -> Sexp {
|
|
|
321
366
|
return literal;
|
|
322
367
|
}
|
|
323
368
|
let mut children: Vec<Sexp> = Vec::new();
|
|
369
|
+
let mut dropped_parentheses = false;
|
|
324
370
|
let mut cursor = node.walk();
|
|
325
371
|
for child in node.named_children(&mut cursor) {
|
|
326
372
|
// Comments are invisible to the tree upstream compares.
|
|
327
373
|
if child.kind_str() == "comment" {
|
|
328
374
|
continue;
|
|
329
375
|
}
|
|
376
|
+
// So is the empty argument list of `foo()`, which upstream's parser never builds.
|
|
377
|
+
if verification.fold_empty_call_parentheses && empty_argument_list(child) {
|
|
378
|
+
dropped_parentheses = true;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
330
381
|
let normalized = normalized(child, text, verification);
|
|
331
382
|
// `splice_nested_sequences`: `x; (a; b)` and `x; a; b` are the same statement list.
|
|
332
383
|
match SEQUENCES.contains(&node.kind_str()) && normalized.label == BEGIN {
|
|
@@ -346,6 +397,9 @@ fn normalized(node: Node<'_>, text: &str, verification: Verification) -> Sexp {
|
|
|
346
397
|
children,
|
|
347
398
|
};
|
|
348
399
|
}
|
|
400
|
+
if dropped_parentheses && let Some(bare) = written_without_parentheses(node, &mut children) {
|
|
401
|
+
return bare;
|
|
402
|
+
}
|
|
349
403
|
if !verification.fold_string_concatenation {
|
|
350
404
|
let label = label_of(node, text, children.is_empty());
|
|
351
405
|
return rotate_same_operator(Sexp { label, children });
|
|
@@ -358,6 +412,39 @@ fn normalized(node: Node<'_>, text: &str, verification: Verification) -> Sexp {
|
|
|
358
412
|
fold_string_concatenation(rotate_same_operator(Sexp { label, children }))
|
|
359
413
|
}
|
|
360
414
|
|
|
415
|
+
/// Whether the node is the `()` of `foo()`, which upstream's parser leaves no node behind for.
|
|
416
|
+
fn empty_argument_list(node: Node<'_>) -> bool {
|
|
417
|
+
node.kind_str() == "argument_list" && node.named_child_count() == 0
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/// The label a bare `yield` carries, which is a leaf holding nothing but the keyword.
|
|
421
|
+
const BARE_YIELD: &str = "yield yield";
|
|
422
|
+
|
|
423
|
+
/// What a call whose empty parentheses were just dropped reads as, when dropping them leaves the
|
|
424
|
+
/// spelling the source would have used without them.
|
|
425
|
+
///
|
|
426
|
+
/// `foo()` and `foo` reach upstream as one node, and so do `yield()` and `yield`. Here the first of
|
|
427
|
+
/// each pair is a parent with an `argument_list` under it and the second is written with no node in
|
|
428
|
+
/// between at all -- a lone `identifier`, a childless `yield` -- so the two only compare equal once
|
|
429
|
+
/// the first is put in the shape of the second. A receiver or a block means something is still
|
|
430
|
+
/// written around the name and the spellings stay apart.
|
|
431
|
+
fn written_without_parentheses(node: Node<'_>, children: &mut Vec<Sexp>) -> Option<Sexp> {
|
|
432
|
+
match node.kind_str() {
|
|
433
|
+
"yield" if children.is_empty() => Some(Sexp {
|
|
434
|
+
label: BARE_YIELD.to_owned(),
|
|
435
|
+
children: Vec::new(),
|
|
436
|
+
}),
|
|
437
|
+
"call"
|
|
438
|
+
if children.len() == 1
|
|
439
|
+
&& node.field("receiver").is_none()
|
|
440
|
+
&& node.field("block").is_none() =>
|
|
441
|
+
{
|
|
442
|
+
children.pop()
|
|
443
|
+
}
|
|
444
|
+
_ => None,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
361
448
|
/// The kinds whose contents upstream's parser reads as whitespace-separated words, one node each.
|
|
362
449
|
const WORD_ARRAYS: &[&str] = &["string_array", "symbol_array"];
|
|
363
450
|
|
|
@@ -386,7 +473,7 @@ fn word_array(node: Node<'_>, text: &str) -> Option<Sexp> {
|
|
|
386
473
|
}
|
|
387
474
|
continue;
|
|
388
475
|
}
|
|
389
|
-
if character
|
|
476
|
+
if separates_words(character) {
|
|
390
477
|
if !word.is_empty() {
|
|
391
478
|
words.push(word_leaf(std::mem::take(&mut word)));
|
|
392
479
|
}
|
|
@@ -403,6 +490,15 @@ fn word_array(node: Node<'_>, text: &str) -> Option<Sexp> {
|
|
|
403
490
|
})
|
|
404
491
|
}
|
|
405
492
|
|
|
493
|
+
/// Whether the character ends a word of a `%w` or `%i` literal.
|
|
494
|
+
///
|
|
495
|
+
/// Ruby's lexer asks `ISSPACE`, which is the six ASCII spacing characters and nothing else -- a
|
|
496
|
+
/// no-break space is part of the word. `char::is_ascii_whitespace` is the same set minus the
|
|
497
|
+
/// vertical tab, so spelling the six out is what matches (measured: `%w[a\vb]` is two words).
|
|
498
|
+
fn separates_words(character: char) -> bool {
|
|
499
|
+
matches!(character, ' ' | '\t' | '\n' | '\r' | '\u{b}' | '\u{c}')
|
|
500
|
+
}
|
|
501
|
+
|
|
406
502
|
fn word_leaf(text: String) -> Sexp {
|
|
407
503
|
Sexp {
|
|
408
504
|
label: format!("word {text}"),
|