sonicop 26.9.100 → 26.9.102

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CONFORMANCE.md +45 -333
  3. data/Cargo.lock +42 -7
  4. data/Cargo.toml +5 -5
  5. data/README.ja.md +128 -233
  6. data/README.md +120 -238
  7. data/lib/sonicop/version.rb +1 -1
  8. data/src/config/paths.rs +10 -10
  9. data/src/diagnostic.rs +21 -6
  10. data/src/engine.rs +61 -36
  11. data/src/rules/layout/comment_indentation.rs +4 -4
  12. data/src/rules/layout/condition_position.rs +9 -9
  13. data/src/rules/layout/else_alignment.rs +4 -4
  14. data/src/rules/layout/empty_line_after_guard_clause.rs +4 -5
  15. data/src/rules/layout/empty_lines_around_body.rs +7 -1
  16. data/src/rules/layout/end_of_line.rs +46 -4
  17. data/src/rules/layout/hash_alignment.rs +4 -4
  18. data/src/rules/layout/indentation_width.rs +10 -10
  19. data/src/rules/layout/line_length.rs +14 -14
  20. data/src/rules/layout/multiline_block_layout.rs +22 -26
  21. data/src/rules/layout/rescue_ensure_alignment.rs +9 -10
  22. data/src/rules/layout/space_around_block_parameters.rs +8 -8
  23. data/src/rules/layout/space_around_keyword.rs +4 -4
  24. data/src/rules/layout/support.rs +30 -21
  25. data/src/rules/lint/ambiguity.rs +5 -4
  26. data/src/rules/lint/ambiguous_block_association.rs +2 -2
  27. data/src/rules/lint/debugger.rs +2 -1
  28. data/src/rules/lint/deprecated_open_ssl_constant.rs +3 -1
  29. data/src/rules/lint/duplicate_methods.rs +4 -0
  30. data/src/rules/lint/interpolation_check.rs +13 -46
  31. data/src/rules/lint/return_in_void_context.rs +4 -6
  32. data/src/rules/lint/variable_force.rs +20 -17
  33. data/src/rules/metrics/abc_size.rs +1 -1
  34. data/src/rules/metrics/complexity.rs +21 -15
  35. data/src/rules/metrics/locals.rs +6 -3
  36. data/src/rules/mod.rs +15 -7
  37. data/src/rules/style/case_like_if.rs +56 -24
  38. data/src/rules/style/date_time.rs +4 -4
  39. data/src/rules/style/guard_clause.rs +42 -8
  40. data/src/rules/style/hash_each_methods.rs +16 -17
  41. data/src/rules/style/hash_syntax.rs +2 -2
  42. data/src/rules/style/identical_conditional_branches.rs +5 -6
  43. data/src/rules/style/line_length_help.rs +5 -4
  44. data/src/rules/style/multiline_block_chain.rs +11 -2
  45. data/src/rules/style/redundant_begin.rs +4 -4
  46. data/src/rules/style/redundant_each.rs +11 -11
  47. data/src/rules/style/redundant_interpolation.rs +9 -11
  48. data/src/rules/style/redundant_parentheses.rs +17 -15
  49. data/src/rules/style/redundant_regexp_argument.rs +1 -1
  50. data/src/rules/style/safe_navigation.rs +15 -15
  51. data/src/rules/style/special_global_vars.rs +4 -4
  52. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a163c5bd6a8d400159bd4ba0f973dffb08ec9a588554d11ba4d1240962b7f4ed
4
- data.tar.gz: 23e90583b4aafc8fbe66ecee0202d0fedddf45ed75d90e6060a7d5fb77345343
3
+ metadata.gz: 3ee92dd7275a249c8db40072683d08b20ffa8817cd429baf8b9c60f38b36aafe
4
+ data.tar.gz: 1f68e0627a4a12853fc7a34309a16fe1788c41fa251c57120a75e1728a1d84f6
5
5
  SHA512:
6
- metadata.gz: 0cb9a7320c8bbbe4b3fa3d773486cf66460bc5f29eb43161226dd195566210ec7cab52757c4543a01a6d9f127e0508bcb3ccb9c1ddcd95dbc6908678a202115a
7
- data.tar.gz: faa62e145f9d96b10db86cf60a9a565a3c6033b8a4b28c994ecb76d99a1a4e8e79ed30775a85e1b512988117901f7b9ddb3ff19e9a8e9d116e34125a0eed03f2
6
+ metadata.gz: 022f75103a2e2f5815884412de648e658b32b3d7811bd714bf1a9625472b2e83b33d00f5d5d9f326abea9d8fe662bf7e92555fde904166a952892821cce11bc4
7
+ data.tar.gz: 4bf9bb1d6e191d578ba9cf9e6a034219d1e3dc784932987d8b0cf269c42cbebb8a222637711b5704ad0d9c344566ee3207fda5ecf5ad12ae45cfbd60172db36a
data/CONFORMANCE.md CHANGED
@@ -1,357 +1,69 @@
1
1
  # RuboCop conformance
2
2
 
3
- Snapshot date: per row — see the `Measured` column of *Results*
4
- Reference: RuboCop 1.89.0 on Ruby 4.0.6
5
- Configuration: RuboCop 1.89.0 built-in defaults (`--force-default-config`) on both sides
3
+ Measured: 2026-09-27. The behavioral reference is **RuboCop 1.89.0 on Ruby 4.0.6**; Sonicop's vendored `config/default.yml` comes from that release. Both tools used `--force-default-config`. The current upstream gem may have more cops; a newer release is used only for the separate speed comparison in the README.
6
4
 
7
- The bundled `config/default.yml` records the upstream version it was vendored from in its header.
8
- Re-fetch it with `scripts/sync_default_yml.sh <rubocop-version>`.
5
+ ## Scope and method
9
6
 
10
- ## What is measured
7
+ Sonicop implements **609 / 609** RuboCop 1.89.0 cops, with the same names and no extras: 394 enabled, 159 pending, and 56 disabled by default. These are registry counts. A default run exercises only the enabled group. The separate [cop conformance table](README.md#cop-conformance) records which cops actually fired on upstream spec cases; a cop that stayed silent is never counted as an exact match.
11
8
 
12
- Five Ruby projects, 18,251 target files between them, are linted by both tools and compared offense
13
- by offense. An offense is keyed by cop name, path, line and column; at each shared key the last
14
- line, last column, length, message, severity and correctability are compared as well.
9
+ The five public corpora below have **18,251** target files in total. RuboCop and Sonicop resolve identical target *path sets* on all five. Each corpus is pinned to a commit; numbers from another revision are not interchangeable. For each common offense position `(cop, path, line, column)`, we also compare end line, end column, length, message, severity and correctability. Equal total offense counts alone do not establish a match.
15
10
 
16
- | Corpus | Commit | Target files | Reference offenses |
17
- |---|---|---:|---:|
18
- | rubocop/rubocop | `f009b33` | 1,765 | 5,766 |
19
- | rails/rails | `62b5458` | 3,551 | 167,760 |
20
- | ruby/ruby | `3349f41` | 7,466 | 761,578 |
21
- | Homebrew/brew | `38ee325` | 2,179 | 49,920 |
22
- | mastodon/mastodon | `fad3685` | 3,290 | 15,286 |
11
+ | Corpus | Revision | Target files | RuboCop offenses | Sonicop offenses |
12
+ |---|---|---:|---:|---:|
13
+ | rubocop/rubocop | `f009b33` | 1,765 | 5,766 | 5,766 |
14
+ | rails/rails | `62b5458` | 3,551 | 167,760 | 167,760 |
15
+ | ruby/ruby | `3349f41` | 7,466 | 761,578 | 761,188 |
16
+ | Homebrew/brew | `38ee325` | 2,179 | 49,920 | 49,326 |
17
+ | mastodon/mastodon | `fad3685` | 3,290 | 15,286 | 15,286 |
23
18
 
24
- The commits are pinned because the numbers move with them: check the corpora out at these before
25
- comparing, or a difference in the corpus will read as a difference in the port.
19
+ Ruby's reference was assembled in chunks. RuboCop raises an invalid UTF-8 error on `test/ruby/test_regexp.rb`, and its JSON formatter cannot serialize an offense from `test/ruby/test_file_exhaustive.rb`. Those **two files are excluded from the Ruby offense comparison**, which covers 7,464 files; both tools still discover the same 7,466 target paths. A single RuboCop invocation can emit well-formed but incomplete JSON after a crash. Every comparison checks that its inspected count reached its target count or records the excluded files explicitly.
26
20
 
27
- ruby/ruby's reference had to be assembled from chunked runs, and two files are excluded from it.
28
- `test/ruby/test_regexp.rb` cannot be inspected at all — RuboCop's lexer raises `invalid byte
29
- sequence in UTF-8` on it — and `test/ruby/test_file_exhaustive.rb` holds an offense whose text is
30
- not valid UTF-8, which stops the JSON formatter from emitting a document. The comparison for that
31
- corpus therefore covers 7,464 of its 7,466 files.
21
+ ## Detection results
32
22
 
33
- The **target file lists match exactly** on all five — not just the counts but the paths, compared as
34
- sets: rubocop/rubocop (1,765), rails/rails (3,551), mastodon/mastodon (3,290), Homebrew/brew (2,179)
35
- and ruby/ruby (7,466), with nothing on either side of any of the five. Counts alone would not settle
36
- it — a set that loses one file and gains another keeps its count — so the check compares the sorted
37
- lists and reports what only one side holds. That is
38
- worth stating separately because file discovery is where a port silently diverges first: RuboCop
39
- never reads `.gitignore`, applies a shebang test only to extensionless files, descends through
40
- directory symlinks, and treats a hidden path by its *first* component rather than by any dot in it.
23
+ | Corpus | Sonicop-only positions | RuboCop-only positions | Shared-position field differences | Interpretation |
24
+ |---|---:|---:|---:|---|
25
+ | rubocop/rubocop | 0 | 0 | 0 | Exact |
26
+ | rails/rails | 0 | 0 | 0 | Exact |
27
+ | ruby/ruby | 168 | 558 | 4 | Parser recovery, grammar and remaining cop differences |
28
+ | Homebrew/brew | 330 | 924 | 0 | All positions differ in `Lint/Syntax` |
29
+ | mastodon/mastodon | 0 | 0 | 0 | Exact |
41
30
 
42
- Sonicop implements **all 609 of RuboCop's cops**, matched name for name, with nothing extra on either
43
- side. That includes the 159 RuboCop ships as `Enabled: pending` and the 56 it ships as
44
- `Enabled: false`, neither of which a default run reaches — checking those means naming them with
45
- `--only` or switching them on in a configuration, exactly as with RuboCop.
31
+ Homebrew has the same **569 syntax-error files** on both sides. The different `Lint/Syntax` positions are produced after error recovery; they do not represent different accepted-file sets there. Ruby has both parser and cop residue. Its four shared-position differences are two `correctable` flags, one range length, and one syntax message. These differences remain open and are not described as exact matches.
46
32
 
47
- Because the two cop sets are identical, neither side is restricted with `--only`; the plain run is
48
- already like for like. RuboCop's extension plugins are not installed, so this measures the range
49
- that can be checked without them.
33
+ A separate CRLF probe used the pinned `rubocop/rubocop` revision `f009b33`, converted 1,748 Ruby files in a copy, and inspected the same 1,765 target paths on both sides. The **7,514 offense records** matched in full, including range, message, severity and correctability; neither side had an extra record.
50
34
 
51
- ```bash
52
- rubocop --force-default-config --cache false -f json
53
- sonicop --force-default-config --format json
54
- ```
55
-
56
- ## Results
57
-
58
- Three kinds of difference are counted separately because they mean different things. The set of
59
- files with a fatal `Lint/Syntax` offense says whether the parsers disagree about accepting a file.
60
- `Lint/Syntax` positions inside a file both already rejected measure diagnostic recovery, not file
61
- acceptance. A difference in any other cop says the port reads the same tree and draws a different
62
- conclusion. Only the last category is a defect in a cop.
63
-
64
- | Corpus | Excess | Missing | of which `Lint/Syntax` | Other cops | Field differences | Measured |
65
- |---|---:|---:|---|---:|---|---|
66
- | rubocop/rubocop | 0 | 0 | — | 0 | correctable ×1 | 2026-08-17 |
67
- | rails/rails | 0 | 0 | — | 0 | none | 2026-08-17 |
68
- | mastodon/mastodon | 0 | 0 | — | 0 | none | 2026-08-17 |
69
- | Homebrew/brew | 263 | 997 | **all of them** | **0** | none | 2026-08-18 |
70
- | ruby/ruby | 142 | 585 | 117 missing, 44 excess | 92 | 5 | 2026-08-16 |
71
-
72
- The last column is per row on purpose. A single date at the top of the file would say the five were
73
- measured together, and they were not: the first four are re-measured on every release, ruby/ruby is
74
- not — it is the one corpus whose reference cannot be produced in a single run (see above), so it is
75
- re-measured deliberately rather than routinely. Its row is the older of the two and should be read
76
- as such.
77
-
78
- **No cop other than `Lint/Syntax` differs on four of the five corpora**, Homebrew included: across
79
- its 2,179 files and 49,920 offenses the two agree on every position, message, severity and
80
- correctability that is not a syntax diagnostic.
81
-
82
- Homebrew's 1,260 differences — 997 missing and 263 excess — share one cause, and it is not a parser
83
- bug on either side. Homebrew is a Ruby 4.0
84
- codebase — `Library/Homebrew/.ruby-version` says `4.0.6` — but that file sits below the directory the
85
- run starts from, and `TargetRubyVersion` is only inferred from the working directory's ancestors. Both
86
- tools therefore fall back to the default of 2.7 (RuboCop says so itself: `Using Ruby 2.7 parser`) and
87
- both call `dry_run:,` — a hash value omission, valid since 3.1 — a syntax error. **Run the same corpus
88
- at 3.1 and both report zero `Lint/Syntax`.** What is left is not disagreement about which files parse:
89
- the file sets are identical, 569 on each side, with no file unique to either parser. The two parsers
90
- recover differently after encountering invalid syntax, which puts diagnostic positions on both sides
91
- of the ledger rather than only on RuboCop's.
92
-
93
- All 263 Sonicop-only positions were classified mechanically, not inferred from their cop name. They
94
- occur in 135 of those shared syntax-error files, and every one follows a `Lint/Syntax` position that
95
- both tools reported in the same file. Their messages are 113 end-of-input (`$end`), 86 right
96
- parentheses (`tRPAREN`), 53 commas (`tCOMMA`), seven `end` keywords (`kEND`), two equals signs
97
- (`tEQL`), one `when` (`kWHEN`) and one identifier (`tIDENTIFIER`). Thus the former “excess” is not a
98
- set of valid files rejected only by Sonicop; it is the other direction of the same diagnostic-recovery
99
- divergence as the 997 RuboCop-only positions. Suppressing every diagnostic after Sonicop's first one
100
- would make the excess zero, but would also discard 1,998 positions that already match and increase the
101
- missing side from 997 to 2,995. That is not a conformance fix. See *Known divergences*.
102
-
103
- Much of ruby/ruby's difference is the same shape,
104
- with 117 of the 585 missing and 44 of the 142 excess being `Lint/Syntax` itself,
105
- and the rest follows from it: a file the two disagree about is inspected by one tool and skipped by
106
- the other, so every offense in it lands on one side of the ledger. Counting that through, 635 of the
107
- 727 differences sit in files one tool or the other calls a syntax error, and they are spread thinly
108
- across the Layout department rather than sitting in one cop. Of the 92 that do not, 77 are the
109
- `Style/RedundantParentheses` case under *Differences that should not be closed*, which leaves 15 —
110
- ten of them in TRICK entries or encoding fixtures. Both are explained under *Known divergences*.
111
-
112
- ruby/ruby also carries five differences at positions both tools reported: two `correctable` flags and
113
- one `last_column`/`length` pair, all on indentation cops inside files the two parse differently, plus
114
- one `Lint/Syntax` message naming a different token (`tLCURLY` where RuboCop says `tLAMBEG`).
115
-
116
- Autocorrect is compared the same way, byte for byte over the whole tree: run `-A` with both tools
117
- from a clean checkout and diff the results.
118
-
119
- | Corpus | Corrected tree |
120
- |---|---|
121
- | rubocop/rubocop | **identical** |
122
- | mastodon/mastodon | **identical** |
123
- | Homebrew/brew | 2 files differ |
124
- | rails/rails | 25 files differ |
125
- | ruby/ruby | not measurable |
126
-
127
- RuboCop's own tree and Mastodon are the hard line: a change that breaks byte equality on either is a
128
- regression to be fixed, not a new known divergence to be recorded.
129
-
130
- The rails/rails residue is one shape, and Homebrew contributes one more of it: the body of a
131
- `begin`/`rescue`/`end` ends up indented two columns off. It appears only in a full `-A` run — the
132
- first-pass detection matches exactly, and reducing the case to a single file reproduces nothing —
133
- because what differs is which correction pass a nested indentation fix lands in. Homebrew's other
134
- file pairs a `disable`/`enable` around `Lint/EmptyBlock`, a cop RuboCop ships as pending; the
135
- upstream run leaves both comments alone and Sonicop removes them.
136
-
137
- ruby/ruby cannot be measured this way at all: RuboCop's own run does not finish on it (see *Reading a
138
- measurement*), so there is no complete reference tree to diff against.
139
-
140
- Run the comparison on a copy of the corpus. Autocorrect rewrites the tree in place, so a run that
141
- shares a checkout with anything else — another comparison, a lint measurement — has both tools
142
- reading different files and reports differences that are not there.
143
-
144
- ## Reading a measurement
145
-
146
- Two traps make a run look better or worse than it is, and both have produced wrong conclusions here.
147
-
148
- **Counts cancel out.** Comparing offense totals hides an excess and a shortfall of the same size.
149
- Compare the *set* of locations, then compare fields at the locations both tools produced.
150
-
151
- **RuboCop can stop early.** On ruby/ruby its lexer raises `invalid byte sequence in UTF-8` on
152
- `test/ruby/test_regexp.rb` and the whole run unwinds. A single-invocation run reports 6,909 of 7,466
153
- files inspected, and the 557 files after that one in sort order were never looked at — so every
154
- offense Sonicop found in them counts as "excess". Check `summary.inspected_file_count` against
155
- `summary.target_file_count` on every run. To get a complete reference for that corpus, split the file
156
- list into chunks and re-run each chunk past the file that killed it.
157
-
158
- Read those two counts with `-f offenses`, not `-f json`: with the full cop set the JSON formatter
159
- never gets to write them (see below), and the run looks like a crash rather than a truncation.
160
-
161
- **And sometimes it emits nothing at all.** `test/ruby/test_file_exhaustive.rb` holds an offense whose
162
- text is not valid UTF-8, and `JSONFormatter#finished` raises `source sequence is illegal/malformed
163
- utf-8` while serializing — before a single byte reaches stdout. The failure is not per-file but
164
- per-document: one such file discards the JSON for the entire run, summary included, which is why the
165
- counts above have to come from another formatter.
166
-
167
- Two things about this are easy to get backwards. **What matters is the encoding of the offense text,
168
- not of the source.** `test/ruby/enc/test_euc_jp.rb` and `test/ruby/enc/test_shift_jis.rb` do hold
169
- bytes that are not valid UTF-8, yet both serialize fine on their own — their magic comments make
170
- RuboCop read them in their declared encoding — while `test_file_exhaustive.rb` is valid UTF-8 as a
171
- file. **And which files are affected depends on the cop set, not on the corpus.** Running the full
172
- 609 excludes two files from ruby/ruby; running `--only Lint/LiteralAsCondition,Layout/IndentationWidth`
173
- excludes one, because neither of those two cops produces an offense whose text is ill-formed. A
174
- recorded exclusion count is part of a measurement's conditions, not a property of the corpus.
175
-
176
- A chunked reference run has to tell this apart from the case above: retrying one file at a time is
177
- right when a file killed the parser, and useless when the run never had a chance to start. Two other
178
- stdout notices break JSON the same way — `--parallel` being ignored under `--cache false`, and the
179
- plugin suggestions RuboCop prints after a run — so a reference reader should skip to the first `{`
180
- rather than trust byte zero.
181
-
182
- ## Known divergences
183
-
184
- `tests/conformance/known_divergences.yml` is the machine-checked record: a divergence that is listed
185
- but no longer reproduces fails the test, and so does one that appears without being listed.
186
-
187
- ### Error recovery after a syntax error
35
+ The default configuration is the required baseline. Native project configurations were also attempted. Their plugin requirements stop RuboCop before inspection on four corpora, so no native-config agreement is claimed for them. For the Ruby corpus, a nested native-config run over `spec/ruby` inspected 4,435 files and matched offense by offense after a directory-discovery fix. Directory target discovery uses the configuration of the starting directory; the cop configuration is resolved for each inspected file.
188
36
 
189
- RuboCop parses with `parser`, an LALR parser that recovers from an error and keeps going, emitting
190
- further diagnostics from the recovered state. Sonicop parses with tree-sitter and reaches different
191
- recovery states. On Homebrew this produces 997 RuboCop-only and 263 Sonicop-only `Lint/Syntax`
192
- positions: examples on the RuboCop side include `class definition in method body`, `dynamic constant
193
- assignment`, `cannot assign to a keyword`, and repeated `unexpected token` inside one multi-line hash.
37
+ ## Autocorrect and safety
194
38
 
195
- What the divergence does not change is **which** files are held to be unparseable, and that is what
196
- decides whether a file is inspected at all: RuboCop runs no cop other than `Lint/Syntax` on a file
197
- that does not parse, and Sonicop does the same. On Homebrew the two agree on the *set*, not merely on
198
- its size: 569 files, with the difference in either direction empty. This is why every cop other than
199
- `Lint/Syntax` matches exactly there despite the 1,260 offenses of difference.
39
+ `-a` and `-A` rewrite files, so each tool runs on its own copy of the same clean tree. Corrected trees are compared byte for byte, including non-`.rb` files. The known differences are pinned by file and output hash in `tests/conformance/known_divergences.yml`; a changed hash or an unregistered difference fails the gate.
200
40
 
201
- The agreement is not universal. On ruby/ruby the sets differ by six files: 56 agree, five are flagged
202
- only by Sonicop and one only by RuboCop. The five are TRICK contest entries — deliberately obfuscated
203
- programs, one of which is English prose that happens to parse as Ruby — where tree-sitter reports an
204
- error and `parser` does not. Those five hold 465 of that corpus's 773 differences.
41
+ | Corpus | `-a` corrected-tree differences | `-A` corrected-tree differences |
42
+ |---|---:|---:|
43
+ | rubocop/rubocop | 0 | 1 known safety difference |
44
+ | rails/rails | 4 known safety differences | 3 known safety differences |
45
+ | mastodon/mastodon | 3 known safety differences | 3 known safety differences |
46
+ | Homebrew/brew | 0 | 0 |
47
+ | ruby/ruby | Reference aborts | Reference aborts |
205
48
 
206
- Within Homebrew's 569 files the agreement is partial, as recovery cannot be reproduced: 499 report the
207
- same first diagnostic (position and message), and 222 report an identical list end to end. In all 569,
208
- Sonicop's earliest source-position diagnostic is also present in RuboCop's output. The 277 files in
209
- between are the divergence in its purest form — the two agree on at least one error position and part
210
- company as recovery proceeds. The 70
211
- files whose first diagnostic differs follow one shape — an endless method definition (`def to_s =
212
- to_str`, valid from Ruby 3.0, rejected by the default `TargetRubyVersion: 2.7`) leaves `parser`'s
213
- method context open, so the enclosing `class` emits `class definition in method body` when it is
214
- finally reduced. The diagnostic is reported at the `class` keyword, far above the line that actually
215
- failed, which puts it first in source order.
49
+ RuboCop's own autocorrect can produce invalid Ruby in cases that Sonicop's syntax guard leaves untouched. Other recorded exceptions prevent an assignment-order change that loses the old buffer and a guard-clause rewrite that turns a local variable read into a method call raising `NameError`. Sonicop also preserves the source encoding on write-back; RuboCop can write UTF-8 bytes under a non-UTF-8 magic comment. Each intentional incompatibility needs a minimal disk-level test and an entry in the divergence manifest. These safety exceptions are monitored differences, never omitted files.
216
50
 
217
- ### What is left on ruby/ruby
51
+ For Ruby, both `-a` and `-A` exit unsuccessfully in RuboCop 1.89.0 on the source that also blocks its lint run. There is no complete corrected reference tree, so full-corpus autocorrect equality is unmeasured rather than assumed.
218
52
 
219
- ruby/ruby is the only corpus with a residue that is not one shape, so it is worth taking apart. Of
220
- its 727 differences:
53
+ On the four public corpora with complete corrected trees, a second Sonicop `-A` pass changed **0 files**. Syntax checks of files changed by either tool found **0 candidate-only invalid Ruby files**; RuboCop alone produced invalid Ruby in 1 file of its own tree, 2 Rails files and 3 Mastodon files. A partial Sonicop `-a` run over Ruby was stopped during a large conversion table; among the 206 changed Ruby files that were valid before correction, **0 became invalid**. This partial result does not stand in for a complete Ruby autocorrect comparison.
221
54
 
222
- | | Count | |
223
- |---|---:|---|
224
- | in files one tool calls a syntax error | 635 | the recovery difference above |
225
- | `Style/RedundantParentheses` in one file | 77 | RuboCop's defect, see below |
226
- | everything else | 15 | |
227
-
228
- The last 15 are the honest remainder. Ten of them sit in TRICK entries or encoding fixtures —
229
- `Layout/SpaceInsideParens` five times in one obfuscated program, `Layout/TrailingWhitespace` and
230
- `Layout/LeadingCommentSpace` on a UTF-16 fixture that holds no BOM. Three are the `"…"%[…]` lexing
231
- gap described under *Grammar gaps*. That leaves two — `Lint/NestedMethodDefinition` on
232
- `def (obj.bar = Object.new).baz` and `Style/MixinUsage` on a top-level `include RbConfig` — which
233
- have not been investigated.
234
-
235
- Naming that number is the point. It was 138 before the cop fixes that landed with the 609th cop, and
236
- every step down came from taking one shape at a time rather than from a general improvement.
237
-
238
- ### Grammar gaps
239
-
240
- Where tree-sitter rejects code that Ruby accepts, Sonicop reports a syntax error and — following the
241
- rule above — reports nothing else for that file. One such gap costs every offense in the file at
242
- once, which makes them worth hunting: fixing eight lexer rules in the grammar fork took ruby/ruby
243
- from 24 affected files to 5, and cut the offenses Sonicop was missing there by more than sevenfold.
244
- What is left moves with how many cops are implemented — one unparseable file costs every offense
245
- every cop would have reported in it — so the shortfall grows as coverage does.
246
-
247
- What remains are constructs whose ambiguity Ruby resolves with information a grammar does not have.
248
- `$a?0:1` is the clearest: whether `?0` is a character literal or the start of a ternary depends on
249
- whether the token before it completed an expression, which in turn depends on knowing that `a` is a
250
- local variable rather than a method call.
251
-
252
- A gap does not have to reject the file to cost offenses. `"%3d %s"%[l+1, line]` parses, but not as
253
- Ruby reads it: a `%` written against a string literal can only be the operator, while tree-sitter
254
- takes `%[…]` for a percent-literal string and yields two adjacent strings instead of a binary
255
- operation. Nothing downstream can recover it — the operator node the cop would report on is not in
256
- the tree — so `Layout/SpaceAroundOperators` misses both the `%` and the `+` nested inside the
257
- brackets. Three offenses on ruby/ruby come from this, all on one line of `libexec/erb`.
258
-
259
- ### Encoding — the one deliberate difference
260
-
261
- RuboCop's autocorrect ends in a plain `File.write`, so a corrected Shift_JIS file is written back as
262
- UTF-8 while its magic comment still claims Shift_JIS. The result no longer loads: read it back as
263
- cp932 and you get mojibake, not the source you had.
264
-
265
- **Sonicop writes the correction back in the encoding the file declares**, and refuses to write at all
266
- when the correction holds a character that encoding cannot represent, leaving the file untouched
267
- rather than substituting something else. Drop-in compatibility reaches a long way, but not as far as
268
- reproducing data loss on purpose. This is the only place Sonicop knowingly differs, and it only
269
- shows up on files that declare a non-UTF-8 encoding — 8 of the 18,251 files measured here.
270
-
271
- A source declaring `ASCII-8BIT` or `binary` needs no such treatment: Ruby measures it one byte at a
272
- time, so Sonicop reads it that way too and the bytes go back out unchanged.
273
-
274
- ## Differences that should not be closed
275
-
276
- Everything above records a place Sonicop has not reached. This section records the opposite: a
277
- difference where RuboCop is the one that is wrong, and matching it would mean discarding a correct
278
- report. The two are worth separating, because a table that mixes them reads as though every
279
- difference counts against the port.
280
-
281
- On ruby/ruby's `test/-ext-/bignum/test_pack.rb`, `Style/RedundantParentheses` finds 3 offenses in
282
- RuboCop and 79 in Sonicop. The 76 are real: they are `(-n)`, `(-n+1)` and `(+n-1)` written as method
283
- arguments, the same shape RuboCop reports elsewhere in the same file and in a file of its own.
284
-
285
- RuboCop drops them because of an unrelated line further down. Truncating the file shows it exactly:
286
-
287
- ```
288
- first 82 lines + a closing end → RuboCop reports 8
289
- first 83 lines + a closing end → RuboCop reports 0 ← the 8 already reported are withdrawn
290
- line 83 with \xFF changed to \x00 → RuboCop reports 9
291
- ```
292
-
293
- Line 83 is `assert_equal([-1, "\xFF"], Bug::Bignum.test_pack((-0x0FF), 1, 1, 0, BIG_ENDIAN))`, in a
294
- file whose first line is `# coding: ASCII-8BIT`. Reading that far makes RuboCop abandon
295
- `Style/RedundantParentheses` for the whole file, silently:
296
-
297
- - it is specific to that cop — `Style/StringLiterals` still reports its 10 offenses in the same file
298
- - it is not the cop-error path — `--raise-cop-error` says nothing
299
- - the file is inspected — the run reports `inspected: 1` and simply finds no offense of that cop
300
- - it is not a parse failure — neither tool emits `Lint/Syntax`, and `ruby -c` accepts the file
301
- - a synthesised minimum does not reproduce it; the surrounding file is part of the trigger
302
-
303
- Sonicop reports all 79. Bringing that down to 3 would mean suppressing correct output to match a
304
- defect, so the difference stands as it is.
305
-
306
- ## Limits
307
-
308
- A clean run is a property of the corpora, not a general claim. `known_divergences.yml` carries the
309
- current list of what these corpora never exercise; the main ones are extension plugins and Windows
310
- line endings. Cops that never fire contribute nothing to a match count, and their silence is
311
- indistinguishable from agreement.
312
-
313
- Non-default configuration values used to be the largest of those gaps, and are now measured. Every
314
- one of the 111 cops carrying an `Enforced*` setting was switched to a non-default value at once —
315
- `Layout/SpaceInsideParens` to `space`, `Style/HashSyntax` to `hash_rockets` with
316
- `EnforcedShorthandSyntax: always`, and so on — and rubocop/rubocop re-run against RuboCop under the
317
- same file. Of 622,317 reference offenses, **99.99% match**, and 84 of the 96 cops that fired match
318
- exactly.
319
-
320
- | Cop | Offenses differing | |
321
- |---|---:|---|
322
- | `Layout/HashAlignment` | 35 | **RuboCop's own bug**, see below |
323
- | `Style/BlockDelimiters` | 17 | |
324
- | `Style/ConditionalAssignment` | 12 | |
325
- | `Style/MixinGrouping` | 8 | |
326
- | `Layout/SpaceInsideStringInterpolation` | 6 | |
327
- | `Style/NumericPredicate` | 5 | |
328
- | `Style/PercentQLiterals` | 4 | |
329
- | `Naming/VariableName` | 3 | |
330
- | `Lint/SymbolConversion` | 3 | |
331
- | `Layout/SpaceInsideParens` | 1 | |
332
-
333
- `Layout/HashAlignment`'s 35 are not a defect here. Under `EnforcedHashRocketStyle: separator`
334
- RuboCop raises `Parser::ClobberingError` on `spec/rubocop/config_loader_spec.rb` — three times, at
335
- `1111:35`, `1130:38` and `1217:35` — and reports nothing for the hashes it crashed on. Sonicop
336
- inspects them and reports. Every one of the 35 sits in that file between lines 1114 and 1234.
337
- Reproduce with:
55
+ ## How to reproduce
338
56
 
339
57
  ```bash
340
- rubocop -c <variants.yml> --only Layout/HashAlignment -d spec/rubocop/config_loader_spec.rb
341
- ```
342
-
343
- One value per cop is not the whole configuration space — a cop with four supported styles is
344
- measured at two of them — so this is a floor, not a ceiling. It does settle the shape of the old
345
- limit: a cop is no longer allowed to be half absent and still count as matching.
58
+ # 固定した各コーパスで、先に対象パスの集合を比較する。
59
+ rubocop --force-default-config --cache false -L
60
+ sonicop --force-default-config -L
346
61
 
347
- The 215 cops RuboCop ships switched off are a limit of a different kind. They are implemented, but a
348
- default run never reaches them, so the corpus numbers above say nothing about them. What stands
349
- behind those is a separate measurement: the same corpora linted with all 609 cops switched on, on
350
- both sides, and the residue classified. That is weaker evidence than it sounds — a cop that never
351
- fires on these corpora contributes nothing either way — and the residue is not yet zero. The figures
352
- for that run are tracked in `known_divergences.yml` rather than here, because they are still moving.
62
+ # 検査数を確認してから、JSON の位置と全項目を比較する。
63
+ rubocop --force-default-config --cache false -f json . > rubocop.json
64
+ sonicop --force-default-config --cache false -f json . > sonicop.json
65
+ ```
353
66
 
354
- Closing that gap properly means porting RuboCop's own spec suite, which exercises each cop against
355
- inputs written to break it. Until that lands, this document records what was measured.
67
+ Use the `migrate-rubocop` measurement scripts for the actual comparison. Ruby needs their chunked `rcfull.py` reference. `fullfmt.sh` checks a clean source and compares copies, including both safe and unsafe correction. RuboCop 1.89.0 is the oracle for these conformance results even when a newer RuboCop is installed for timing.
356
68
 
357
- Timings live in the README's Performance section.
69
+ The real-code corpora cannot cover every branch: some cops are disabled by default, and some syntax shapes do not occur. The upstream spec fixture suite supplies deliberately constructed cases; README's *Exercised* column keeps coverage separate from correctness. The goal remains **Cops = Exercised = Exact match** in every department. The last complete spec sweep (2026-08-28) exercised 609 and matched 579 exactly, so that goal is still open. The table must be regenerated by `scripts/conformance_table.rb` from measured JSON, never edited by hand.
data/Cargo.lock CHANGED
@@ -102,9 +102,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
102
102
 
103
103
  [[package]]
104
104
  name = "bitflags"
105
- version = "2.13.1"
105
+ version = "2.13.2"
106
106
  source = "registry+https://github.com/rust-lang/crates.io-index"
107
- checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
107
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
108
108
 
109
109
  [[package]]
110
110
  name = "blake3"
@@ -221,6 +221,12 @@ version = "0.8.7"
221
221
  source = "registry+https://github.com/rust-lang/crates.io-index"
222
222
  checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
223
223
 
224
+ [[package]]
225
+ name = "core_detect"
226
+ version = "1.0.0"
227
+ source = "registry+https://github.com/rust-lang/crates.io-index"
228
+ checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
229
+
224
230
  [[package]]
225
231
  name = "cpufeatures"
226
232
  version = "0.3.1"
@@ -279,11 +285,16 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
279
285
 
280
286
  [[package]]
281
287
  name = "encoding_rs"
282
- version = "0.8.35"
288
+ version = "0.8.42"
283
289
  source = "registry+https://github.com/rust-lang/crates.io-index"
284
- checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
290
+ checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
285
291
  dependencies = [
286
292
  "cfg-if",
293
+ "core_detect",
294
+ "multiversion_no_op",
295
+ "rustversion",
296
+ "scopeguard",
297
+ "simdutf8",
287
298
  ]
288
299
 
289
300
  [[package]]
@@ -453,6 +464,12 @@ version = "2.8.3"
453
464
  source = "registry+https://github.com/rust-lang/crates.io-index"
454
465
  checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
455
466
 
467
+ [[package]]
468
+ name = "multiversion_no_op"
469
+ version = "1.0.0"
470
+ source = "registry+https://github.com/rust-lang/crates.io-index"
471
+ checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
472
+
456
473
  [[package]]
457
474
  name = "native-tls"
458
475
  version = "0.2.18"
@@ -668,6 +685,12 @@ dependencies = [
668
685
  "zeroize",
669
686
  ]
670
687
 
688
+ [[package]]
689
+ name = "rustversion"
690
+ version = "1.0.23"
691
+ source = "registry+https://github.com/rust-lang/crates.io-index"
692
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
693
+
671
694
  [[package]]
672
695
  name = "ryu"
673
696
  version = "1.0.23"
@@ -692,6 +715,12 @@ dependencies = [
692
715
  "windows-sys",
693
716
  ]
694
717
 
718
+ [[package]]
719
+ name = "scopeguard"
720
+ version = "1.2.0"
721
+ source = "registry+https://github.com/rust-lang/crates.io-index"
722
+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
723
+
695
724
  [[package]]
696
725
  name = "security-framework"
697
726
  version = "3.7.0"
@@ -784,9 +813,15 @@ version = "2.0.1"
784
813
  source = "registry+https://github.com/rust-lang/crates.io-index"
785
814
  checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
786
815
 
816
+ [[package]]
817
+ name = "simdutf8"
818
+ version = "0.1.5"
819
+ source = "registry+https://github.com/rust-lang/crates.io-index"
820
+ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
821
+
787
822
  [[package]]
788
823
  name = "sonicop"
789
- version = "26.9.100"
824
+ version = "26.9.102"
790
825
  dependencies = [
791
826
  "anyhow",
792
827
  "assert_cmd",
@@ -892,8 +927,8 @@ checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
892
927
 
893
928
  [[package]]
894
929
  name = "tree-sitter-ruby"
895
- version = "26.8.103"
896
- source = "git+https://github.com/owayo/tree-sitter-ruby.git?tag=v26.8.103#e3b414485ce2b0a154334f02576ec79c27154326"
930
+ version = "26.9.100"
931
+ source = "git+https://github.com/owayo/tree-sitter-ruby.git?tag=v26.9.100#710bc56bda9d8548dc5e8714a6418f2deb292e05"
897
932
  dependencies = [
898
933
  "cc",
899
934
  "tree-sitter-language",
data/Cargo.toml CHANGED
@@ -1,8 +1,8 @@
1
1
  [package]
2
2
  name = "sonicop"
3
- version = "26.9.100"
3
+ version = "26.9.102"
4
4
  edition = "2024"
5
- rust-version = "1.85"
5
+ rust-version = "1.98"
6
6
  description = "A fast, native RuboCop-compatible Ruby linter and formatter"
7
7
  license = "MIT"
8
8
  repository = "https://github.com/owayo/sonicop"
@@ -18,7 +18,7 @@ path = "src/main.rs"
18
18
  anyhow = "1.0.104"
19
19
  blake3 = "1.8.7"
20
20
  clap = { version = "4.6.6", features = ["derive", "wrap_help"] }
21
- encoding_rs = "0.8.35"
21
+ encoding_rs = "0.8.42"
22
22
  fs2 = "0.4.3"
23
23
  globset = "0.4.20"
24
24
  ignore = "0.4.33"
@@ -30,7 +30,7 @@ serde_yaml_ng = "0.10"
30
30
  shell-words = "1.1.1"
31
31
  tempfile = "3.27.0"
32
32
  tree-sitter = "0.27.0"
33
- tree-sitter-ruby = { git = "https://github.com/owayo/tree-sitter-ruby.git", tag = "v26.8.103" }
33
+ tree-sitter-ruby = { git = "https://github.com/owayo/tree-sitter-ruby.git", tag = "v26.9.100" }
34
34
  ureq = { version = "3.4.1", default-features = false, features = ["native-tls"] }
35
35
 
36
36
  [dev-dependencies]
@@ -40,7 +40,7 @@ assert_cmd = "2.2.2"
40
40
  blake3 = "1.8.7"
41
41
 
42
42
  [profile.release]
43
- lto = "thin"
43
+ lto = true
44
44
  codegen-units = 1
45
45
  strip = true
46
46