sonicop 26.8.109 → 26.8.110
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/config/loader.rs +11 -1
- data/src/engine.rs +158 -2
- data/src/rules/layout/empty_line_after_guard_clause.rs +33 -13
- data/src/rules/layout/trailing_whitespace.rs +19 -8
- data/src/rules/lint/literal_as_condition.rs +20 -20
- data/src/rules/support.rs +32 -8
- 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: a00a7d0dc817fde927c8f9a5eb24db975f85aa2f4dda36ae1f7de548aa6557f6
|
|
4
|
+
data.tar.gz: 2c09e86e186f680c8f31cf5fd0e979fb974bc23ad173fa14e4f100f98dc95ce3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 786c6eb994b2d5020f9f035623ec0f54ab95ad3f0359f747efd0bf97fd1bd4cca00d21b7a652b483861aef2af80dabb18dea4185bf9320a214307c4ea8780a8a
|
|
7
|
+
data.tar.gz: 8c0b93e14e6e7a56f497cc55aad95ce730c681515f9e2f8d67efcfe3c8ae44d98dc8f5ab1bf9c25b60f6de0791b0ed077bc3df0b4a679d3c07416a731426c66d
|
data/Cargo.lock
CHANGED
data/Cargo.toml
CHANGED
data/lib/sonicop/version.rb
CHANGED
data/src/config/loader.rs
CHANGED
|
@@ -11,7 +11,17 @@ pub(super) fn find_config(start: &Path) -> Option<PathBuf> {
|
|
|
11
11
|
if candidate.is_file() {
|
|
12
12
|
return fs::canonicalize(candidate).ok();
|
|
13
13
|
}
|
|
14
|
-
if
|
|
14
|
+
// `break if dir == stop_dir || dir == FileFinder.root_level`
|
|
15
|
+
//
|
|
16
|
+
// **`stop_dir` が無いことは「止まらない」であって「止まる」ではない。**
|
|
17
|
+
// 上流の `find_project_dotfile` は `find_file_upwards(DOTFILE, target_dir, project_root)`
|
|
18
|
+
// で、`project_root` が nil のときは何とも一致しないので**ファイルシステムの根まで
|
|
19
|
+
// 昇る**。ここを `is_none_or` で書いていたため、Gemfile の無い木では最初の 1 段で
|
|
20
|
+
// 止まり、**`.rubocop.yml` がリポジトリ直下・コードが `lib/` という標準の配置で
|
|
21
|
+
// 設定が 1 つも効かなかった。**
|
|
22
|
+
//
|
|
23
|
+
// `ancestors()` は根で終わるので、`root_level` の側は自然に満たされる。
|
|
24
|
+
if project_root.as_deref() == Some(directory) {
|
|
15
25
|
break;
|
|
16
26
|
}
|
|
17
27
|
}
|
data/src/engine.rs
CHANGED
|
@@ -1255,6 +1255,64 @@ fn anchor_range(offense: &Offense, source: &str) -> (usize, usize) {
|
|
|
1255
1255
|
}
|
|
1256
1256
|
}
|
|
1257
1257
|
|
|
1258
|
+
/// Writes the corrector of every cop of one pass, and what the merge did with it, to stderr under
|
|
1259
|
+
/// `SONICOP_TRACE_CORRECTORS`.
|
|
1260
|
+
///
|
|
1261
|
+
/// The output is the same shape upstream's correctors can be dumped in -- one line per scheduled
|
|
1262
|
+
/// edit, `line:column-line:column` then the replacement -- so that the two can be read side by side.
|
|
1263
|
+
/// Comparing text alone cannot say *why* a pass landed where it did: a cop whose corrections are
|
|
1264
|
+
/// discarded whole still reports every offense as corrected, so the report is identical either way.
|
|
1265
|
+
mod trace {
|
|
1266
|
+
pub(super) fn enabled() -> bool {
|
|
1267
|
+
static ENABLED: std::sync::LazyLock<bool> =
|
|
1268
|
+
std::sync::LazyLock::new(|| std::env::var_os("SONICOP_TRACE_CORRECTORS").is_some());
|
|
1269
|
+
*ENABLED
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/// The 1-based line and column of an offset, counted the way an offense is reported.
|
|
1273
|
+
///
|
|
1274
|
+
/// Slices with `get` rather than `[..]`: the offsets this walks are the ones under suspicion
|
|
1275
|
+
/// whenever the trace is worth reading, and an offset landing inside a multi-byte character
|
|
1276
|
+
/// would panic exactly when it is being investigated. A malformed edit should print oddly, not
|
|
1277
|
+
/// take the run down.
|
|
1278
|
+
fn position(source: &str, offset: usize) -> (usize, usize) {
|
|
1279
|
+
let mut offset = offset.min(source.len());
|
|
1280
|
+
while offset > 0 && !source.is_char_boundary(offset) {
|
|
1281
|
+
offset -= 1;
|
|
1282
|
+
}
|
|
1283
|
+
let head = &source[..offset];
|
|
1284
|
+
let line = head.bytes().filter(|byte| *byte == b'\n').count() + 1;
|
|
1285
|
+
let start = head.rfind('\n').map_or(0, |index| index + 1);
|
|
1286
|
+
(line, head[start..].chars().count())
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
pub(super) fn span(source: &str, start: usize, end: usize) -> String {
|
|
1290
|
+
let (first_line, first_column) = position(source, start);
|
|
1291
|
+
let (last_line, last_column) = position(source, end);
|
|
1292
|
+
format!("{first_line}:{first_column}-{last_line}:{last_column}")
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/// Names a cop whose own edits cannot stand together, under `SONICOP_TRACE_OVERLAP`.
|
|
1296
|
+
///
|
|
1297
|
+
/// The cop-side guard only covers the four cops that reparse their own correction; this end sees
|
|
1298
|
+
/// every cop, because every offense's edits pass through the correction tree. See
|
|
1299
|
+
/// [`crate::rules::support::report_overlap`] for what the stages mean.
|
|
1300
|
+
pub(super) fn overlap(report: &super::FileReport, index: usize, stage: &str) {
|
|
1301
|
+
if !crate::rules::support::overlap_trace_enabled() {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
let offense = &report.offenses[index];
|
|
1305
|
+
let (line, column) = report.source.line_column(offense.start);
|
|
1306
|
+
crate::rules::support::report_overlap(
|
|
1307
|
+
offense.cop_name,
|
|
1308
|
+
&report.path.display().to_string(),
|
|
1309
|
+
line,
|
|
1310
|
+
column,
|
|
1311
|
+
stage,
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1258
1316
|
/// Which cops a correction pass takes edits from.
|
|
1259
1317
|
///
|
|
1260
1318
|
/// RuboCop runs the cop that reads directives on a team of its own, after the inspection loop has
|
|
@@ -1320,6 +1378,9 @@ pub fn corrected_text(
|
|
|
1320
1378
|
// `Team#each_corrector`'s skip set. See [`autocorrect_incompatible_with`].
|
|
1321
1379
|
let mut skips: HashSet<&'static str> = HashSet::new();
|
|
1322
1380
|
let mut rest = candidates.as_slice();
|
|
1381
|
+
if trace::enabled() {
|
|
1382
|
+
eprintln!("=== cop ごとの corrector (マージ順)");
|
|
1383
|
+
}
|
|
1323
1384
|
while let Some(&first) = rest.first() {
|
|
1324
1385
|
let cop_name = report.offenses[first].cop_name;
|
|
1325
1386
|
let taken = rest
|
|
@@ -1330,12 +1391,24 @@ pub fn corrected_text(
|
|
|
1330
1391
|
rest = remainder;
|
|
1331
1392
|
|
|
1332
1393
|
let skipped = skips.contains(cop_name);
|
|
1394
|
+
if trace::enabled() {
|
|
1395
|
+
eprintln!(" {cop_name}{}", if skipped { " (skip 済み)" } else { "" });
|
|
1396
|
+
}
|
|
1333
1397
|
|
|
1334
1398
|
// The cop's own corrector. An offense that cannot be placed in it is the cop error RuboCop
|
|
1335
1399
|
// reports and steps over, so it costs that offense alone.
|
|
1336
1400
|
let mut cop = Action::root();
|
|
1337
1401
|
let mut placed = Vec::new();
|
|
1338
1402
|
for &index in group {
|
|
1403
|
+
if trace::enabled() {
|
|
1404
|
+
for edit in &report.offenses[index].corrections {
|
|
1405
|
+
eprintln!(
|
|
1406
|
+
" {:16} {:?}",
|
|
1407
|
+
trace::span(source, edit.start, edit.end),
|
|
1408
|
+
edit.replacement
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1339
1412
|
// `combine` rather than `combine_children`: it is the entry point that drops an edit
|
|
1340
1413
|
// asking for nothing at all, the way `Corrector#replace` and friends do.
|
|
1341
1414
|
let anchor = anchor_range(&report.offenses[index], source);
|
|
@@ -1345,11 +1418,23 @@ pub fn corrected_text(
|
|
|
1345
1418
|
.try_fold(Action::root(), |tree, edit| {
|
|
1346
1419
|
tree.combine(&Action::from_edit(edit, anchor))
|
|
1347
1420
|
});
|
|
1348
|
-
let Ok(offense) = offense else {
|
|
1421
|
+
let Ok(offense) = offense else {
|
|
1422
|
+
if trace::enabled() {
|
|
1423
|
+
eprintln!(" ★ この offense の中で衝突");
|
|
1424
|
+
}
|
|
1425
|
+
// The guard that names a corrector written twice over. Reaching it from here covers
|
|
1426
|
+
// every cop; the cop-side path only sees the four that reparse their own correction.
|
|
1427
|
+
trace::overlap(report, index, "offense-tree");
|
|
1428
|
+
continue;
|
|
1429
|
+
};
|
|
1349
1430
|
if offense.children.is_empty() {
|
|
1350
1431
|
continue;
|
|
1351
1432
|
}
|
|
1352
1433
|
let Ok(merged) = cop.clone().combine_children(&offense.children) else {
|
|
1434
|
+
if trace::enabled() {
|
|
1435
|
+
eprintln!(" ★ cop の corrector に入らなかった (この offense だけ捨てた)");
|
|
1436
|
+
}
|
|
1437
|
+
trace::overlap(report, index, "cop-tree");
|
|
1353
1438
|
continue;
|
|
1354
1439
|
};
|
|
1355
1440
|
cop = merged;
|
|
@@ -1378,8 +1463,16 @@ pub fn corrected_text(
|
|
|
1378
1463
|
run = merged;
|
|
1379
1464
|
applied += placed.len();
|
|
1380
1465
|
trace_outcome("apply", cop_name);
|
|
1466
|
+
if trace::enabled() {
|
|
1467
|
+
eprintln!(" 取り込み");
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
Err(_) => {
|
|
1471
|
+
trace_outcome("clash", cop_name);
|
|
1472
|
+
if trace::enabled() {
|
|
1473
|
+
eprintln!(" ★ 丸ごと捨てた");
|
|
1474
|
+
}
|
|
1381
1475
|
}
|
|
1382
|
-
Err(_) => trace_outcome("clash", cop_name),
|
|
1383
1476
|
}
|
|
1384
1477
|
}
|
|
1385
1478
|
|
|
@@ -2073,6 +2166,69 @@ mod tests {
|
|
|
2073
2166
|
);
|
|
2074
2167
|
}
|
|
2075
2168
|
|
|
2169
|
+
/// The range a cop hands `insert_after` -- not the offset the text lands at -- decides whether
|
|
2170
|
+
/// another cop's replacement of the same construct swallows the insertion or wraps around it.
|
|
2171
|
+
///
|
|
2172
|
+
/// `Layout/EmptyLineAfterGuardClause` is the pair that measures it: it reports the `end` keyword
|
|
2173
|
+
/// but inserts after `range_by_whole_lines(node.source_range)`, and `Style/IfUnlessModifier`
|
|
2174
|
+
/// replaces exactly that conditional. On the whole lines the insertion is the *parent* of the
|
|
2175
|
+
/// replacement and both land; on the keyword it is a *child* of it, which is the
|
|
2176
|
+
/// `swallowed_insertions` clobbering -- and that costs `Style/IfUnlessModifier` every correction
|
|
2177
|
+
/// it asked for in the file, so a different cop's form wins the node in a later pass. Measured on
|
|
2178
|
+
/// rails' `activerecord/.../schema_definitions.rb`.
|
|
2179
|
+
#[test]
|
|
2180
|
+
fn the_range_an_insertion_hangs_off_decides_who_survives() {
|
|
2181
|
+
let source = "if a\n raise\nend\nb\n";
|
|
2182
|
+
// `end` is 13..16; the whole lines of the conditional are 0..16; the blank line lands at 16.
|
|
2183
|
+
let insertion = |anchor: std::ops::Range<usize>| {
|
|
2184
|
+
Offense::new(
|
|
2185
|
+
"Layout/EmptyLineAfterGuardClause",
|
|
2186
|
+
Severity::Convention,
|
|
2187
|
+
"test",
|
|
2188
|
+
13,
|
|
2189
|
+
16,
|
|
2190
|
+
)
|
|
2191
|
+
.corrected_by(Edit {
|
|
2192
|
+
start: 16,
|
|
2193
|
+
end: 16,
|
|
2194
|
+
replacement: "\n".to_owned(),
|
|
2195
|
+
safe: true,
|
|
2196
|
+
})
|
|
2197
|
+
.corrections_anchored_at(anchor)
|
|
2198
|
+
};
|
|
2199
|
+
let fold = || {
|
|
2200
|
+
Offense::new(
|
|
2201
|
+
"Style/IfUnlessModifier",
|
|
2202
|
+
Severity::Convention,
|
|
2203
|
+
"test",
|
|
2204
|
+
0,
|
|
2205
|
+
16,
|
|
2206
|
+
)
|
|
2207
|
+
.corrected_by(Edit {
|
|
2208
|
+
start: 0,
|
|
2209
|
+
end: 16,
|
|
2210
|
+
replacement: "raise if a".to_owned(),
|
|
2211
|
+
safe: true,
|
|
2212
|
+
})
|
|
2213
|
+
};
|
|
2214
|
+
let run = |offenses: Vec<Offense>| {
|
|
2215
|
+
let mut report = FileReport {
|
|
2216
|
+
path: "test.rb".into(),
|
|
2217
|
+
source: SourceFile::new("test.rb", source.to_owned()),
|
|
2218
|
+
offenses,
|
|
2219
|
+
};
|
|
2220
|
+
corrected_text(&mut report, CorrectMode::All, Correcting::Everything).0
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
assert_eq!(run(vec![insertion(0..16), fold()]), "raise if a\n\nb\n");
|
|
2224
|
+
// The negative control: anchored on the keyword the fold is swallowed and dropped whole, so
|
|
2225
|
+
// the conditional keeps its written form and only the blank line lands.
|
|
2226
|
+
assert_eq!(
|
|
2227
|
+
run(vec![insertion(13..16), fold()]),
|
|
2228
|
+
"if a\n raise\nend\n\nb\n"
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2076
2232
|
#[test]
|
|
2077
2233
|
fn a_clobbering_cop_loses_every_correction_it_asked_for() {
|
|
2078
2234
|
// The cop's second edit collides with what is already scheduled, so the run drops the cop
|
|
@@ -74,10 +74,13 @@ fn inspect<'tree>(
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
let terminator = terminator_range(context, body);
|
|
77
|
+
// `range_by_whole_lines(node.loc.heredoc_body)`: the body's own lines, not the statement's.
|
|
78
|
+
let (edit, anchor) = insertion(context, body.byte_range());
|
|
77
79
|
offenses.push(
|
|
78
80
|
context
|
|
79
81
|
.offense(MESSAGE, terminator.clone())
|
|
80
|
-
.corrected_by(
|
|
82
|
+
.corrected_by(edit)
|
|
83
|
+
.corrections_anchored_at(anchor),
|
|
81
84
|
);
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
@@ -86,27 +89,44 @@ fn inspect<'tree>(
|
|
|
86
89
|
return;
|
|
87
90
|
}
|
|
88
91
|
let range = end_keyword(node).map_or_else(|| node.byte_range(), |keyword| keyword.byte_range());
|
|
92
|
+
let (edit, anchor) = insertion(context, node.byte_range());
|
|
89
93
|
offenses.push(
|
|
90
94
|
context
|
|
91
95
|
.offense(MESSAGE, range)
|
|
92
|
-
.corrected_by(
|
|
96
|
+
.corrected_by(edit)
|
|
97
|
+
.corrections_anchored_at(anchor),
|
|
93
98
|
);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/// `corrector.insert_after(range_by_whole_lines(...), "\n")`, stepping over a directive comment on
|
|
97
102
|
/// the line that follows.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
///
|
|
104
|
+
/// The second half of the pair is the range upstream hands `insert_after`, which is **not** the
|
|
105
|
+
/// `end` keyword the offense is reported on: it is the whole lines of the conditional. Which of the
|
|
106
|
+
/// two is recorded decides where the insertion sits in the correction tree, and the two fall on
|
|
107
|
+
/// opposite sides of `Style/IfUnlessModifier`'s replacement of the same conditional -- the
|
|
108
|
+
/// whole-lines range contains it, so the two merge, while the `end` keyword is contained by it and
|
|
109
|
+
/// reads as an insertion the replacement swallows. That swallowing is the one RuboCop raises on, and
|
|
110
|
+
/// it costs the clobbering cop every correction it asked for in the file. Measured on
|
|
111
|
+
/// `rails/activerecord/.../schema_definitions.rb`, where recording the keyword sent
|
|
112
|
+
/// `Style/IfUnlessModifier` away and left `Style/GuardClause`'s form in its place.
|
|
113
|
+
fn insertion(context: &RuleContext<'_>, range: Range<usize>) -> (Edit, Range<usize>) {
|
|
114
|
+
let first_line = context.source.line_column(range.start).0;
|
|
115
|
+
let last_line = context.source.line_column(range.end).0;
|
|
116
|
+
let mut anchor = context.source.line_range(first_line).start..line_end(context, last_line);
|
|
117
|
+
if let Some(comment) = allowed_directive_comment(context, last_line + 1) {
|
|
118
|
+
anchor = comment;
|
|
109
119
|
}
|
|
120
|
+
let offset = anchor.end;
|
|
121
|
+
(
|
|
122
|
+
Edit {
|
|
123
|
+
start: offset,
|
|
124
|
+
end: offset,
|
|
125
|
+
replacement: "\n".to_owned(),
|
|
126
|
+
safe: true,
|
|
127
|
+
},
|
|
128
|
+
anchor,
|
|
129
|
+
)
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
fn line_end(context: &RuleContext<'_>, line: usize) -> usize {
|
|
@@ -9,18 +9,29 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
9
9
|
let heredocs = heredocs(context);
|
|
10
10
|
let text = context.source.text();
|
|
11
11
|
|
|
12
|
+
// `/[[:blank:]]\z/` and `sub(/[[:blank:]]+\z/, '')`: **the widest of Ruby's three whitespace
|
|
13
|
+
// sets, and the only one that reaches past ASCII.** A line ending in a no-break space or an
|
|
14
|
+
// ideographic space carries trailing whitespace upstream, and reading the run as `[' ', '\t']`
|
|
15
|
+
// left those lines unreported. (A line that *begins* with one parses as an identifier, so the
|
|
16
|
+
// judgement has to be made on the line's text rather than on a node.)
|
|
17
|
+
//
|
|
18
|
+
// **On a source that declares itself binary the set collapses back to ASCII.** `[[:blank:]]`
|
|
19
|
+
// names Unicode's `Zs`, and an `ASCII-8BIT` string holds no Unicode characters for it to
|
|
20
|
+
// name -- Ruby matches only tab and space there. The decoder maps each byte of such a file to
|
|
21
|
+
// one `char` so that columns count bytes, which hands this cop U+00A0 for byte `0xA0`: the
|
|
22
|
+
// tail of `à`, `Р` or `ภ` written in a comment. Reading that as a no-break space reported 35
|
|
23
|
+
// lines of `ruby/ruby`'s `test/ruby/test_transcode.rb` that upstream leaves alone, and the
|
|
24
|
+
// correction **deleted the byte**, leaving a lone `0xC3` where a character had been. The file
|
|
25
|
+
// still parsed, so nothing downstream noticed.
|
|
26
|
+
let blank: fn(char) -> bool = match crate::engine::declared_literal_encoding(text) {
|
|
27
|
+
crate::engine::LiteralEncoding::Binary => |character| matches!(character, ' ' | '\t'),
|
|
28
|
+
_ => crate::rules::support::is_ruby_blank,
|
|
29
|
+
};
|
|
12
30
|
for line_number in 1..=context.source.line_count() {
|
|
13
31
|
let range = context.source.line_range(line_number);
|
|
14
32
|
let line = &text[range.clone()];
|
|
15
33
|
let content_end = line.trim_end_matches(['\r', '\n']).len();
|
|
16
|
-
|
|
17
|
-
// sets, and the only one that reaches past ASCII.** A line ending in a no-break space or an
|
|
18
|
-
// ideographic space carries trailing whitespace upstream, and reading the run as `[' ', '\t']`
|
|
19
|
-
// left those lines unreported. (A line that *begins* with one parses as an identifier, so the
|
|
20
|
-
// judgement has to be made on the line's text rather than on a node.)
|
|
21
|
-
let trimmed_end = line[..content_end]
|
|
22
|
-
.trim_end_matches(crate::rules::support::is_ruby_blank)
|
|
23
|
-
.len();
|
|
34
|
+
let trimmed_end = line[..content_end].trim_end_matches(blank).len();
|
|
24
35
|
if trimmed_end == content_end {
|
|
25
36
|
continue;
|
|
26
37
|
}
|
|
@@ -4,6 +4,7 @@ use tree_sitter::Node;
|
|
|
4
4
|
|
|
5
5
|
use crate::diagnostic::{Edit, Offense};
|
|
6
6
|
use crate::rules::RuleContext;
|
|
7
|
+
use crate::rules::send_node;
|
|
7
8
|
use crate::rules::send_node::named_children;
|
|
8
9
|
|
|
9
10
|
use super::literals::{is_basic_literal, is_falsey_literal, is_literal, is_truthy_literal};
|
|
@@ -85,6 +86,10 @@ fn literal(node: Node<'_>, context: &RuleContext<'_>) -> bool {
|
|
|
85
86
|
const HANDLED: &[&str] = &[
|
|
86
87
|
"binary",
|
|
87
88
|
"unary",
|
|
89
|
+
// `x.!` is the same `(send _ :!)` upstream reaches through `on_send`, but the grammar writes
|
|
90
|
+
// it as a `call`. Without it here the negation entry is never reached for that spelling, and
|
|
91
|
+
// `if 1.!` / `while 1.!` / `until 1.!` / `1.! ? a : b` / `s if 1.!` all go unreported.
|
|
92
|
+
"call",
|
|
88
93
|
"if",
|
|
89
94
|
"elsif",
|
|
90
95
|
"unless",
|
|
@@ -109,7 +114,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
|
109
114
|
for node in context.nodes_of_any(HANDLED) {
|
|
110
115
|
match node.kind_str() {
|
|
111
116
|
"binary" => check_operator_keyword(node, context, offenses),
|
|
112
|
-
"unary" => check_negation(node, context, offenses),
|
|
117
|
+
"unary" | "call" => check_negation(node, context, offenses),
|
|
113
118
|
"while" | "while_modifier" => check_loop(node, true, context, offenses),
|
|
114
119
|
"until" | "until_modifier" => check_loop(node, false, context, offenses),
|
|
115
120
|
"case" => check_case(node, context, offenses),
|
|
@@ -168,35 +173,30 @@ fn check_operator_keyword(node: Node<'_>, context: &RuleContext<'_>, offenses: &
|
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/// `on_send` with `RESTRICT_ON_SEND = [:!]`: what a negation is applied to is a condition too.
|
|
176
|
+
///
|
|
177
|
+
/// **The entry uses `negation_method?`, not `prefix_bang?`.** That is why `if not 1` is reported
|
|
178
|
+
/// here while `check_node` -- the recursive half -- leaves it alone: the two halves of this cop ask
|
|
179
|
+
/// different questions, and answering both with one predicate loses a form either way.
|
|
171
180
|
fn check_negation(node: Node<'_>, context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
172
|
-
|
|
173
|
-
.field("operator")
|
|
174
|
-
.is_none_or(|operator| !matches!(context.source.node_text(operator), "!" | "not"))
|
|
175
|
-
{
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
let Some(operand) = node.field("operand") else {
|
|
181
|
+
let Some(found) = send_node::negation(node, context) else {
|
|
179
182
|
return;
|
|
180
183
|
};
|
|
181
|
-
if literal(operand, context) {
|
|
182
|
-
offenses.push(report(operand.byte_range(), context));
|
|
184
|
+
if literal(found.operand, context) {
|
|
185
|
+
offenses.push(report(found.operand.byte_range(), context));
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
185
|
-
check_node(operand, context, offenses);
|
|
188
|
+
check_node(found.operand, context, offenses);
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
/// `check_node`: the shapes whose operands are conditions in their own right.
|
|
189
192
|
fn check_node(node: Node<'_>, context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
|
|
193
|
+
// **`prefix_bang?` here, `negation_method?` at the entry.** `not` is excluded on this side, so
|
|
194
|
+
// widening both to the same predicate reports an operand upstream never looks at.
|
|
195
|
+
if let Some(found) = send_node::bang(node, context) {
|
|
196
|
+
handle_node(found.operand, context, offenses);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
190
199
|
match node.kind_str() {
|
|
191
|
-
"unary"
|
|
192
|
-
if node
|
|
193
|
-
.field("operator")
|
|
194
|
-
.is_some_and(|operator| context.source.node_text(operator) == "!") =>
|
|
195
|
-
{
|
|
196
|
-
if let Some(operand) = node.field("operand") {
|
|
197
|
-
handle_node(operand, context, offenses);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
200
|
"binary"
|
|
201
201
|
if node
|
|
202
202
|
.field("operator")
|
data/src/rules/support.rs
CHANGED
|
@@ -217,11 +217,12 @@ pub(crate) fn correction_parses(context: &RuleContext<'_>, edits: &[Edit]) -> bo
|
|
|
217
217
|
/// and the offense it loses never reaches the output to be noticed.
|
|
218
218
|
///
|
|
219
219
|
/// Set `SONICOP_TRACE_OVERLAP=1` to list them.
|
|
220
|
+
///
|
|
221
|
+
/// This path only sees the cops that verify their own correction by reparsing it -- four of the 609.
|
|
222
|
+
/// The engine reports the same thing for every cop from the other end, where an offense's edits are
|
|
223
|
+
/// folded into the correction tree ([`report_overlap`]); the `stage` field says which end named it.
|
|
220
224
|
fn trace_overlapping_edits(context: &RuleContext<'_>, edits: &[Edit], at: usize) {
|
|
221
|
-
|
|
222
|
-
static ENABLED: std::sync::LazyLock<bool> =
|
|
223
|
-
std::sync::LazyLock::new(|| std::env::var_os("SONICOP_TRACE_OVERLAP").is_some());
|
|
224
|
-
if !*ENABLED {
|
|
225
|
+
if !overlap_trace_enabled() {
|
|
225
226
|
return;
|
|
226
227
|
}
|
|
227
228
|
let mut ordered: Vec<&Edit> = edits.iter().collect();
|
|
@@ -230,12 +231,12 @@ fn trace_overlapping_edits(context: &RuleContext<'_>, edits: &[Edit], at: usize)
|
|
|
230
231
|
for edit in ordered {
|
|
231
232
|
if edit.start < cursor {
|
|
232
233
|
let (line, column) = context.source.line_column(at);
|
|
233
|
-
|
|
234
|
-
"[overlap]\t{}\t{}:{}:{}",
|
|
234
|
+
report_overlap(
|
|
235
235
|
context.rule.name,
|
|
236
|
-
context.source.path().display(),
|
|
236
|
+
&context.source.path().display().to_string(),
|
|
237
237
|
line,
|
|
238
|
-
column
|
|
238
|
+
column,
|
|
239
|
+
"reparse-gate",
|
|
239
240
|
);
|
|
240
241
|
return;
|
|
241
242
|
}
|
|
@@ -243,6 +244,29 @@ fn trace_overlapping_edits(context: &RuleContext<'_>, edits: &[Edit], at: usize)
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/// Read once. Every offense of every cop on these paths would otherwise pay for the lookup.
|
|
248
|
+
pub(crate) fn overlap_trace_enabled() -> bool {
|
|
249
|
+
static ENABLED: std::sync::LazyLock<bool> =
|
|
250
|
+
std::sync::LazyLock::new(|| std::env::var_os("SONICOP_TRACE_OVERLAP").is_some());
|
|
251
|
+
*ENABLED
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// One line naming a cop whose own edits cannot stand together.
|
|
255
|
+
///
|
|
256
|
+
/// `stage` says which end noticed, because the two do not cover the same thing:
|
|
257
|
+
///
|
|
258
|
+
/// * `reparse-gate` -- [`apply_edits`] refused the set while the cop was checking that its own
|
|
259
|
+
/// correction still parses. Only the four cops behind that gate reach it.
|
|
260
|
+
/// * `offense-tree` -- the engine could not fold one offense's edits into a correction tree. Every
|
|
261
|
+
/// cop reaches this one, and it is strictly the worse condition: the tree accepts overlaps that
|
|
262
|
+
/// `apply_edits` refuses (two insertions at one offset merge), so a failure here is a corrector
|
|
263
|
+
/// written twice over rather than a policy call.
|
|
264
|
+
/// * `cop-tree` -- two offenses of the same cop collide. RuboCop reports that as the cop error it
|
|
265
|
+
/// steps over, so it is not by itself a defect, but a cop appearing here often is worth reading.
|
|
266
|
+
pub(crate) fn report_overlap(cop_name: &str, path: &str, line: usize, column: usize, stage: &str) {
|
|
267
|
+
eprintln!("[overlap]\t{cop_name}\t{path}:{line}:{column}\t{stage}");
|
|
268
|
+
}
|
|
269
|
+
|
|
246
270
|
/// The source with every edit applied, or `None` when two of them overlap.
|
|
247
271
|
///
|
|
248
272
|
/// Sorting by span puts an insertion at a span's start before the span itself and one at its end
|