sonicop 26.8.101 → 26.8.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.
- checksums.yaml +4 -4
- data/CONFORMANCE.md +112 -86
- data/Cargo.lock +12 -9
- data/Cargo.toml +3 -3
- data/README.ja.md +37 -2
- data/README.md +61 -7
- data/lib/sonicop/version.rb +1 -1
- data/src/config/mod.rs +39 -0
- data/src/config/paths.rs +67 -10
- data/src/diagnostic.rs +26 -8
- data/src/directives.rs +59 -7
- data/src/engine.rs +263 -17
- data/src/lib.rs +1 -0
- data/src/magic_comment.rs +222 -0
- data/src/rules/layout/empty_line_after_magic_comment.rs +40 -23
- data/src/rules/layout/end_of_line.rs +19 -11
- data/src/rules/layout/line_length.rs +910 -106
- data/src/rules/layout/space_after_comma.rs +29 -11
- data/src/rules/layout/space_around_operators.rs +880 -69
- data/src/rules/layout/space_inside_parens.rs +10 -4
- data/src/rules/layout/trailing_empty_lines.rs +27 -15
- data/src/rules/layout/trailing_whitespace.rs +117 -13
- data/src/rules/lint/duplicate_methods.rs +766 -40
- data/src/rules/lint/mod.rs +3 -0
- data/src/rules/lint/syntax.rs +742 -89
- data/src/rules/lint/unused_block_argument.rs +117 -60
- data/src/rules/lint/useless_assignment.rs +533 -78
- data/src/rules/lint/variable_force.rs +1680 -0
- data/src/rules/metrics/block_length.rs +57 -19
- data/src/rules/metrics/class_length.rs +43 -15
- data/src/rules/metrics/method_length.rs +61 -2
- data/src/rules/metrics/module_length.rs +48 -8
- data/src/rules/metrics/parameter_lists.rs +162 -13
- data/src/rules/metrics/support.rs +206 -39
- data/src/rules/mod.rs +24 -1
- data/src/rules/naming/ascii_identifiers.rs +45 -9
- data/src/rules/naming/constant_name.rs +251 -18
- data/src/rules/naming/method_name.rs +340 -44
- data/src/rules/naming/support.rs +370 -9
- data/src/rules/naming/variable_name.rs +39 -35
- data/src/rules/security/eval.rs +5 -0
- data/src/rules/style/frozen_string_literal_comment.rs +164 -41
- data/src/rules/style/hash_syntax.rs +93 -24
- data/src/rules/style/numeric_literals.rs +157 -21
- data/src/rules/style/redundant_return.rs +163 -17
- data/src/rules/style/semicolon.rs +245 -33
- data/src/rules/style/string_literals.rs +502 -29
- data/src/rules/support.rs +57 -7
- data/src/source.rs +18 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea2dfe101378c37802dfe7e5cf3a4e3c3595942c75c91c9f25ade094ab11e632
|
|
4
|
+
data.tar.gz: 01e5d99dcd5989fee69f66403712918e792d0c268ea3b82096ec260dcfd3f176
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 726107889fe04bf60cf2f056c8ca7d34c5c825650426120281e9afa88d2c819d38d7342d99cdc45dd0b81a942011e5d130f6745fd7ee6cc6c5249663526d159d
|
|
7
|
+
data.tar.gz: 51b76434f05a2b35cecc62eda0e3636592f7058ff123ab936872f577082502301a5c609828fc5d0659abd94f3d67291aa3dbc71ddd6ffc0be2fc344a13e0bf51
|
data/CONFORMANCE.md
CHANGED
|
@@ -1,109 +1,135 @@
|
|
|
1
1
|
# RuboCop conformance
|
|
2
2
|
|
|
3
|
-
Snapshot date: 2026-08-
|
|
4
|
-
Reference: RuboCop 1.89.0
|
|
5
|
-
|
|
6
|
-
Configuration: RuboCop 1.89.0 built-in defaults (`--force-default-config`)
|
|
3
|
+
Snapshot date: 2026-08-12
|
|
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
|
|
7
6
|
|
|
8
7
|
The bundled `config/default.yml` records the upstream version it was vendored from in its header.
|
|
9
8
|
Re-fetch it with `scripts/sync_default_yml.sh <rubocop-version>`.
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
10
|
+
## What is measured
|
|
11
|
+
|
|
12
|
+
Five Ruby projects, 18,244 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.
|
|
15
|
+
|
|
16
|
+
| Corpus | Commit | Target files | Reference offenses |
|
|
17
|
+
|---|---|---:|---:|
|
|
18
|
+
| rubocop/rubocop | `e82df38` | 1,765 | 4,142 |
|
|
19
|
+
| rails/rails | `729d2e9` | 3,550 | 117,541 |
|
|
20
|
+
| ruby/ruby | `52975b7` | 7,465 | 603,331 |
|
|
21
|
+
| Homebrew/brew | `5d49126` | 2,175 | 38,742 |
|
|
22
|
+
| mastodon/mastodon | `e5db3aa` | 3,289 | 7,610 |
|
|
23
|
+
|
|
24
|
+
The **target file lists match exactly** on all five — not just the counts but the paths. That is
|
|
25
|
+
worth stating separately because file discovery is where a port silently diverges first: RuboCop
|
|
26
|
+
never reads `.gitignore`, applies a shebang test only to extensionless files, descends through
|
|
27
|
+
directory symlinks, and treats a hidden path by its *first* component rather than by any dot in it.
|
|
28
|
+
|
|
29
|
+
Only the cops Sonicop implements are compared; the reference process is restricted to them with
|
|
30
|
+
`--only`. RuboCop's extension plugins are not installed, so this measures the range that can be
|
|
31
|
+
checked without them.
|
|
33
32
|
|
|
34
33
|
```bash
|
|
35
|
-
cops
|
|
34
|
+
cops=$(sonicop --show-cops --force-default-config \
|
|
35
|
+
| grep -B3 'Implemented: true' | grep '^[A-Z].*:$' | tr -d ':' | paste -sd, -)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
--cop "${cops}" \
|
|
40
|
-
-- /path/to/rubocop
|
|
37
|
+
rubocop --force-default-config --cache false --only "$cops" -f json
|
|
38
|
+
sonicop --force-default-config --format json
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
accepts the modern syntax in all 1,759 files, while Sonicop resolves Ruby 2.6 from the corpus
|
|
45
|
-
gemspec and applies a post-parse syntax feature gate. Its four fatal offenses, including the legacy
|
|
46
|
-
parser recovery diagnostics, exactly match RuboCop.
|
|
41
|
+
## Results
|
|
47
42
|
|
|
48
|
-
|
|
43
|
+
| Corpus | Excess | Missing | Field differences |
|
|
44
|
+
|---|---:|---:|---|
|
|
45
|
+
| rubocop/rubocop | 0 | 0 | none |
|
|
46
|
+
| rails/rails | 0 | 0 | none |
|
|
47
|
+
| mastodon/mastodon | 0 | 0 | none |
|
|
48
|
+
| Homebrew/brew | 253 | 996 | none |
|
|
49
|
+
| ruby/ruby | 89 | 346 | 1 |
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
Homebrew's remaining difference is entirely `Lint/Syntax`; every other cop matches exactly. Most of
|
|
52
|
+
ruby/ruby's is too — 117 of the 346 missing and 70 of the 89 excess are `Lint/Syntax` itself. The
|
|
53
|
+
rest follows from it: a file the two disagree about is inspected by one tool and skipped by the
|
|
54
|
+
other, so every offense in it lands on one side of the ledger. Both are explained under
|
|
55
|
+
*Known divergences*.
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
Autocorrect is compared the same way, byte for byte over the whole tree: run `-a` (and separately
|
|
58
|
+
`-A`) with both tools from a clean checkout and diff the results. On all four corpora measured this way —
|
|
59
|
+
rubocop/rubocop, rails/rails, Homebrew/brew and mastodon/mastodon — the corrected trees are
|
|
60
|
+
**identical**.
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
| `Naming/AsciiIdentifiers` | 0 | Rails contains exactly two occurrences; both diverge on message casing |
|
|
62
|
+
Run the comparison on a copy of the corpus. Autocorrect rewrites the tree in place, so a run that
|
|
63
|
+
shares a checkout with anything else — another comparison, a lint measurement — has both tools
|
|
64
|
+
reading different files and reports differences that are not there.
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
corpus does not. Treat a clean run on any single corpus as evidence about that corpus.
|
|
66
|
+
## Reading a measurement
|
|
65
67
|
|
|
66
|
-
Two
|
|
68
|
+
Two traps make a run look better or worse than it is, and both have produced wrong conclusions here.
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- RuboCop emits **only** `Lint/Syntax` for a file it considers fatally unparseable and suppresses
|
|
71
|
-
every other cop for that file, while Sonicop keeps inspecting it. Whenever the two disagree
|
|
72
|
-
about syntax validity — most easily triggered by a `TargetRubyVersion` the source does not
|
|
73
|
-
satisfy — the false-positive count is dominated by that single behaviour rather than by cop
|
|
74
|
-
logic. Check the reference output for `"severity": "fatal"` before reading a large false-positive
|
|
75
|
-
count as a cop problem.
|
|
70
|
+
**Counts cancel out.** Comparing offense totals hides an excess and a shortfall of the same size.
|
|
71
|
+
Compare the *set* of locations, then compare fields at the locations both tools produced.
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
an
|
|
73
|
+
**RuboCop can stop early and still emit valid JSON.** On ruby/ruby its parser raises
|
|
74
|
+
`invalid byte sequence in UTF-8` on `test/ruby/test_regexp.rb` and the whole run unwinds; the JSON
|
|
75
|
+
formatter still writes a well-formed document from an `ensure` block. The result reports 6,924 of
|
|
76
|
+
7,465 files inspected, and the 541 files after that one in sort order were never looked at — so
|
|
77
|
+
every offense Sonicop found in them counts as "excess". Check `summary.inspected_file_count`
|
|
78
|
+
against `summary.target_file_count` on every run. To get a complete reference for that corpus, split
|
|
79
|
+
the file list into chunks and re-run each chunk past the file that killed it.
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## Known divergences
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
rubocop-minitest 0.40.0, rubocop-packaging 0.6.0, rubocop-performance 1.26.1,
|
|
86
|
-
rubocop-rails 2.36.0, and rubocop-md 2.0.4.
|
|
83
|
+
`tests/conformance/known_divergences.yml` is the machine-checked record: a divergence that is listed
|
|
84
|
+
but no longer reproduces fails the test, and so does one that appears without being listed.
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
hidden paths, and the inherited `guides/.rubocop.yml` configuration.
|
|
86
|
+
### Error recovery after a syntax error
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
RuboCop
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
88
|
+
RuboCop parses with `parser`, an LALR parser that recovers from an error and keeps going, emitting
|
|
89
|
+
further diagnostics from the recovered state. Sonicop parses with tree-sitter, which does not model
|
|
90
|
+
that recovery, so the follow-on diagnostics cannot be reproduced. On Homebrew this accounts for the
|
|
91
|
+
996 missing `Lint/Syntax` offenses: `class definition in method body`, `dynamic constant assignment`,
|
|
92
|
+
`cannot assign to a keyword`, and repeated `unexpected token` inside one multi-line hash.
|
|
93
|
+
|
|
94
|
+
The first diagnostic in each file does match, which is what decides whether the file is inspected at
|
|
95
|
+
all: RuboCop runs no cop other than `Lint/Syntax` on a file that does not parse, and Sonicop does the
|
|
96
|
+
same.
|
|
97
|
+
|
|
98
|
+
### Grammar gaps
|
|
99
|
+
|
|
100
|
+
Where tree-sitter rejects code that Ruby accepts, Sonicop reports a syntax error and — following the
|
|
101
|
+
rule above — reports nothing else for that file. One such gap costs every offense in the file at
|
|
102
|
+
once, which makes them worth hunting: fixing eight lexer rules in the grammar fork took ruby/ruby
|
|
103
|
+
from 24 affected files to 5, and the offenses Sonicop was missing there from 2,457 to 346.
|
|
104
|
+
|
|
105
|
+
What remains are constructs whose ambiguity Ruby resolves with information a grammar does not have.
|
|
106
|
+
`$a?0:1` is the clearest: whether `?0` is a character literal or the start of a ternary depends on
|
|
107
|
+
whether the token before it completed an expression, which in turn depends on knowing that `a` is a
|
|
108
|
+
local variable rather than a method call.
|
|
109
|
+
|
|
110
|
+
### Encoding — the one deliberate difference
|
|
111
|
+
|
|
112
|
+
RuboCop's autocorrect ends in a plain `File.write`, so a corrected Shift_JIS file is written back as
|
|
113
|
+
UTF-8 while its magic comment still claims Shift_JIS. The result no longer loads: read it back as
|
|
114
|
+
cp932 and you get mojibake, not the source you had.
|
|
115
|
+
|
|
116
|
+
**Sonicop writes the correction back in the encoding the file declares**, and refuses to write at all
|
|
117
|
+
when the correction holds a character that encoding cannot represent, leaving the file untouched
|
|
118
|
+
rather than substituting something else. Drop-in compatibility reaches a long way, but not as far as
|
|
119
|
+
reproducing data loss on purpose. This is the only place Sonicop knowingly differs, and it only
|
|
120
|
+
shows up on files that declare a non-UTF-8 encoding — 8 of the 18,244 files measured here.
|
|
121
|
+
|
|
122
|
+
A source declaring `ASCII-8BIT` or `binary` needs no such treatment: Ruby measures it one byte at a
|
|
123
|
+
time, so Sonicop reads it that way too and the bytes go back out unchanged.
|
|
124
|
+
|
|
125
|
+
## Limits
|
|
126
|
+
|
|
127
|
+
A clean run is a property of the corpora, not a general claim. `known_divergences.yml` carries the
|
|
128
|
+
current list of what these corpora never exercise; the main ones are non-default configuration
|
|
129
|
+
values, extension plugins, and Windows line endings. Cops that never fire contribute nothing to a
|
|
130
|
+
match count, and their silence is indistinguishable from agreement.
|
|
131
|
+
|
|
132
|
+
Closing that gap properly means porting RuboCop's own spec suite, which exercises each cop against
|
|
133
|
+
inputs written to break it. Until that lands, this document records what was measured.
|
|
134
|
+
|
|
135
|
+
Timings live in the README's Performance section.
|
data/Cargo.lock
CHANGED
|
@@ -243,6 +243,15 @@ version = "1.17.0"
|
|
|
243
243
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
244
244
|
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
|
245
245
|
|
|
246
|
+
[[package]]
|
|
247
|
+
name = "encoding_rs"
|
|
248
|
+
version = "0.8.35"
|
|
249
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
|
251
|
+
dependencies = [
|
|
252
|
+
"cfg-if",
|
|
253
|
+
]
|
|
254
|
+
|
|
246
255
|
[[package]]
|
|
247
256
|
name = "equivalent"
|
|
248
257
|
version = "1.0.2"
|
|
@@ -733,11 +742,12 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
|
733
742
|
|
|
734
743
|
[[package]]
|
|
735
744
|
name = "sonicop"
|
|
736
|
-
version = "26.8.
|
|
745
|
+
version = "26.8.102"
|
|
737
746
|
dependencies = [
|
|
738
747
|
"anyhow",
|
|
739
748
|
"assert_cmd",
|
|
740
749
|
"clap",
|
|
750
|
+
"encoding_rs",
|
|
741
751
|
"globset",
|
|
742
752
|
"ignore",
|
|
743
753
|
"rayon",
|
|
@@ -749,7 +759,6 @@ dependencies = [
|
|
|
749
759
|
"tempfile",
|
|
750
760
|
"tree-sitter",
|
|
751
761
|
"tree-sitter-ruby",
|
|
752
|
-
"unicode-width",
|
|
753
762
|
"ureq",
|
|
754
763
|
]
|
|
755
764
|
|
|
@@ -839,7 +848,7 @@ checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
|
|
|
839
848
|
[[package]]
|
|
840
849
|
name = "tree-sitter-ruby"
|
|
841
850
|
version = "26.8.100"
|
|
842
|
-
source = "git+https://github.com/owayo/tree-sitter-ruby.git?rev=
|
|
851
|
+
source = "git+https://github.com/owayo/tree-sitter-ruby.git?rev=6408d8b7adff44f0b346517b56071460efd323b6#6408d8b7adff44f0b346517b56071460efd323b6"
|
|
843
852
|
dependencies = [
|
|
844
853
|
"cc",
|
|
845
854
|
"tree-sitter-language",
|
|
@@ -851,12 +860,6 @@ version = "1.0.24"
|
|
|
851
860
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
852
861
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
853
862
|
|
|
854
|
-
[[package]]
|
|
855
|
-
name = "unicode-width"
|
|
856
|
-
version = "0.2.2"
|
|
857
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
858
|
-
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
|
859
|
-
|
|
860
863
|
[[package]]
|
|
861
864
|
name = "unsafe-libyaml"
|
|
862
865
|
version = "0.2.11"
|
data/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "sonicop"
|
|
3
|
-
version = "26.8.
|
|
3
|
+
version = "26.8.102"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.85"
|
|
6
6
|
description = "A fast, native RuboCop-compatible Ruby linter and formatter"
|
|
@@ -17,6 +17,7 @@ path = "src/main.rs"
|
|
|
17
17
|
[dependencies]
|
|
18
18
|
anyhow = "1.0.104"
|
|
19
19
|
clap = { version = "4.6.4", features = ["derive", "wrap_help"] }
|
|
20
|
+
encoding_rs = "0.8.35"
|
|
20
21
|
globset = "0.4.19"
|
|
21
22
|
ignore = "0.4.31"
|
|
22
23
|
rayon = "1.12.0"
|
|
@@ -27,8 +28,7 @@ serde_yaml_ng = "0.10"
|
|
|
27
28
|
shell-words = "1.1.1"
|
|
28
29
|
tempfile = "3.27.0"
|
|
29
30
|
tree-sitter = "0.26.11"
|
|
30
|
-
tree-sitter-ruby = { git = "https://github.com/owayo/tree-sitter-ruby.git", rev = "
|
|
31
|
-
unicode-width = "0.2.2"
|
|
31
|
+
tree-sitter-ruby = { git = "https://github.com/owayo/tree-sitter-ruby.git", rev = "6408d8b7adff44f0b346517b56071460efd323b6" }
|
|
32
32
|
ureq = { version = "3.4", default-features = false, features = ["native-tls"] }
|
|
33
33
|
|
|
34
34
|
[dev-dependencies]
|
data/README.ja.md
CHANGED
|
@@ -111,8 +111,43 @@ Ruby プラグイン実行、キャッシュ再利用、カスタム Cop、実
|
|
|
111
111
|
本家の全 1,759 ファイルを本家既定設定で検査した現在のスナップショットは、参照 4,052 件中
|
|
112
112
|
4,052 件すべての位置が一致(**recall 100%**)し、**誤検出とメタデータ差もともに 0 件**です。コマンド、比較範囲、測定結果は
|
|
113
113
|
[CONFORMANCE.md](CONFORMANCE.md) にまとめています。Rails では Ruby 対象 3,453 ファイルと
|
|
114
|
-
ルート/`guides/` の有効 Cop 一覧が完全一致し、全体の誤検出は 0 件です。
|
|
115
|
-
|
|
114
|
+
ルート/`guides/` の有効 Cop 一覧が完全一致し、全体の誤検出は 0 件です。
|
|
115
|
+
|
|
116
|
+
### 性能
|
|
117
|
+
|
|
118
|
+
Rails 8.2.0.alpha のソースツリーで測定しました。両者とも同梱の既定設定
|
|
119
|
+
(`--force-default-config`)で走らせているため Rails の `.rubocop.yml` は読まず、
|
|
120
|
+
**対象ファイルは 3,550 件で完全に一致**しています。
|
|
121
|
+
|
|
122
|
+
| 実行条件 | RuboCop 1.89.0 | Sonicop 26.8.101 | 比 |
|
|
123
|
+
|---|---:|---:|---:|
|
|
124
|
+
| 同一の 28 Cop・並列 | 10.44 秒 | **2.78 秒** | 3.8 倍 |
|
|
125
|
+
| 同一の 28 Cop・単一プロセス | 33.82 秒 | **10.17 秒** | 3.3 倍 |
|
|
126
|
+
| 各ツールが既定で有効にする全 Cop | 22.52 秒 *(394 Cop・並列)* | **2.78 秒** *(28 Cop・並列)* | — |
|
|
127
|
+
|
|
128
|
+
最後の行は素で叩いたときの実測ですが、**対等な比較ではありません**。Sonicop は RuboCop の
|
|
129
|
+
609 Cop のうち 28 個しか実装しておらず、答えている問いがそもそも小さいためです。
|
|
130
|
+
上 2 行は RuboCop を同じ 28 Cop に絞ったもので、エンジンの速度差はこちらが実態を表します。
|
|
131
|
+
この 28 Cop での検出数は 117,541 件中 243 件(0.2%)しか違わないため、
|
|
132
|
+
仕事を省いて速いわけではありません。
|
|
133
|
+
|
|
134
|
+
再現時に注意が必要な点が 2 つあります。RuboCop は **`--cache false` と併用すると
|
|
135
|
+
`--parallel` を黙って無効化します**。そのためここでの並列実行はキャッシュを有効にしたうえで
|
|
136
|
+
実行ごとにキャッシュディレクトリを消しており、`--cache false --parallel` で計測すると
|
|
137
|
+
単一プロセスを測ることになり差が過大に出ます。また RuboCop の既定は単一プロセス、
|
|
138
|
+
Sonicop は `--no-parallel` を渡さない限り並列です。
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# RuboCop(並列・キャッシュは毎回空・Sonicop の実装済み Cop に限定)
|
|
142
|
+
rubocop --force-default-config --cache true --cache-root "$(mktemp -d)" \
|
|
143
|
+
--no-color --parallel --only "$COPS" -f quiet
|
|
144
|
+
|
|
145
|
+
# Sonicop
|
|
146
|
+
sonicop --force-default-config --format quiet
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
測定機は Apple M2(8 コア)、Ruby 4.0.6(YJIT 利用可)、RubyGems 導入の RuboCop 1.89.0。
|
|
150
|
+
各値は暖機後 2〜3 回の最速値です。
|
|
116
151
|
|
|
117
152
|
## 開発
|
|
118
153
|
|
data/README.md
CHANGED
|
@@ -116,13 +116,67 @@ reuse, custom Ruby cops, or cops outside the implemented set.
|
|
|
116
116
|
|
|
117
117
|
### Conformance
|
|
118
118
|
|
|
119
|
-
The implemented cops are verified against RuboCop 1.89
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
The implemented cops are verified against RuboCop 1.89.0 over five Ruby projects — RuboCop itself,
|
|
120
|
+
Rails, Ruby, Homebrew and Mastodon — totalling 18,244 files, with the upstream default
|
|
121
|
+
configuration on both sides. Every offense is compared by cop, path, line, column, last line, last
|
|
122
|
+
column, length, message, severity and correctability.
|
|
123
|
+
|
|
124
|
+
Three of the five match **exactly**: RuboCop's own tree (4,142 offenses), Rails (117,541) and
|
|
125
|
+
Mastodon (7,610), with no excess, no shortfall and no metadata differences. The target file lists
|
|
126
|
+
match exactly on all five. What remains is concentrated in `Lint/Syntax`, where RuboCop's LALR
|
|
127
|
+
parser recovers from an error and emits diagnostics a tree-sitter parse cannot reconstruct.
|
|
128
|
+
Autocorrect is byte-identical on every corpus it was run against.
|
|
129
|
+
|
|
130
|
+
See [CONFORMANCE.md](CONFORMANCE.md) for the commands, the per-corpus numbers, and the two ways a
|
|
131
|
+
measurement of this kind can mislead you.
|
|
132
|
+
|
|
133
|
+
### Performance
|
|
134
|
+
|
|
135
|
+
Measured over all five conformance corpora. Both tools were given their own bundled default
|
|
136
|
+
configuration (`--force-default-config`), so neither reads the project's `.rubocop.yml`, and on every
|
|
137
|
+
corpus the two resolve **the same file list**.
|
|
138
|
+
|
|
139
|
+
RuboCop is restricted to the 28 cops Sonicop implements, which is the only like-for-like comparison
|
|
140
|
+
available. Times are the fastest of two warmed runs.
|
|
141
|
+
|
|
142
|
+
| Corpus | Files | RuboCop parallel | Sonicop parallel | RuboCop single | Sonicop single |
|
|
143
|
+
|---|---:|---:|---:|---:|---:|
|
|
144
|
+
| rubocop/rubocop | 1,765 | 5.30 s | **1.41 s** | 20.88 s | **6.22 s** |
|
|
145
|
+
| mastodon/mastodon | 3,289 | 3.77 s | **1.54 s** | 11.18 s | **5.67 s** |
|
|
146
|
+
| Homebrew/brew | 2,175 | 8.52 s | **1.47 s** | 19.26 s | **5.20 s** |
|
|
147
|
+
| rails/rails | 3,550 | 10.62 s | **5.07 s** | 32.89 s | **15.58 s** |
|
|
148
|
+
| ruby/ruby | 7,465 | 27.31 s | **11.19 s** | 96.19 s | **34.85 s** |
|
|
149
|
+
|
|
150
|
+
The gap is 2.1x to 5.8x in parallel and 2.0x to 3.7x single-process, so no single corpus summarizes
|
|
151
|
+
it. The single-process column is the steadier of the two — it measures the engines rather than how
|
|
152
|
+
well each one's parallelism happens to fit the tree it was pointed at.
|
|
153
|
+
|
|
154
|
+
Out of the box the two commands do different amounts of work — RuboCop enables 394 cops by default
|
|
155
|
+
against Sonicop's 28 — so that comparison is **not** like-for-like. For the record, RuboCop's default
|
|
156
|
+
parallel run takes 10.46 s on its own tree, 9.91 s on Mastodon, 10.83 s on Homebrew, 20.73 s on
|
|
157
|
+
Rails and 75.05 s on Ruby.
|
|
158
|
+
|
|
159
|
+
Over the 28 shared cops the two agree on **every offense** on RuboCop's own tree, on Rails and on
|
|
160
|
+
Mastodon, so the speed is not bought by skipping work.
|
|
161
|
+
|
|
162
|
+
Two details matter for reproducing this. RuboCop **silently turns `--parallel` off when combined
|
|
163
|
+
with `--cache false`**, so its parallel runs here use a cache directory that is deleted before each
|
|
164
|
+
run rather than disabled; timing it with `--cache false --parallel` measures a single process and
|
|
165
|
+
overstates the difference. RuboCop's default is a single process, while Sonicop is parallel unless
|
|
166
|
+
`--no-parallel` is passed.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# RuboCop, parallel, cold cache, restricted to the cops Sonicop implements
|
|
170
|
+
rubocop --force-default-config --cache true --cache-root "$(mktemp -d)" \
|
|
171
|
+
--no-color --parallel --only "$COPS" -f quiet
|
|
172
|
+
|
|
173
|
+
# Sonicop
|
|
174
|
+
sonicop --force-default-config --format quiet
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Machine: Apple M2 (8 cores), Ruby 4.0.6 with YJIT available, RubyGems-installed RuboCop 1.89.0.
|
|
178
|
+
Measure on an otherwise idle machine: anything else competing for cores inflates both sides
|
|
179
|
+
unevenly, and a contended run here reported RuboCop's Rails time as 21 s rather than 10.6 s.
|
|
126
180
|
|
|
127
181
|
## Development
|
|
128
182
|
|
data/lib/sonicop/version.rb
CHANGED
data/src/config/mod.rs
CHANGED
|
@@ -31,6 +31,9 @@ pub struct Config {
|
|
|
31
31
|
raw: Value,
|
|
32
32
|
user: Value,
|
|
33
33
|
project_root: PathBuf,
|
|
34
|
+
/// Where the run was launched from. RuboCop shortens paths it prints inside offense messages
|
|
35
|
+
/// against `Dir.pwd`, not against the project root, so the two cannot be collapsed.
|
|
36
|
+
cwd: PathBuf,
|
|
34
37
|
config_path: Option<PathBuf>,
|
|
35
38
|
target_ruby: ResolvedTargetRuby,
|
|
36
39
|
known_cops: HashSet<String>,
|
|
@@ -108,6 +111,7 @@ impl Config {
|
|
|
108
111
|
raw,
|
|
109
112
|
user,
|
|
110
113
|
project_root,
|
|
114
|
+
cwd: cwd.to_path_buf(),
|
|
111
115
|
config_path,
|
|
112
116
|
target_ruby,
|
|
113
117
|
known_cops,
|
|
@@ -121,6 +125,10 @@ impl Config {
|
|
|
121
125
|
&self.project_root
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
pub fn cwd(&self) -> &Path {
|
|
129
|
+
&self.cwd
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
pub fn config_path(&self) -> Option<&Path> {
|
|
125
133
|
self.config_path.as_deref()
|
|
126
134
|
}
|
|
@@ -205,9 +213,40 @@ impl Config {
|
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
pub fn path_included(&self, path: &Path) -> bool {
|
|
216
|
+
// RuboCop shortcuts a path whose *first* component is hidden: no `Include` pattern can reach
|
|
217
|
+
// it unless one of them mentions a dot. Only the leading component counts, so a dot
|
|
218
|
+
// directory nested under a visible one -- `docs/.mdl_style.rb` -- is still matched normally.
|
|
219
|
+
if self.top_level_hidden(path) && !self.possibly_include_hidden() {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
208
222
|
self.includes.is_empty() || self.includes.matches_includes(path, &self.project_root)
|
|
209
223
|
}
|
|
210
224
|
|
|
225
|
+
fn top_level_hidden(&self, path: &Path) -> bool {
|
|
226
|
+
paths::project_relative(path, &self.project_root).is_some_and(|relative| {
|
|
227
|
+
relative
|
|
228
|
+
.components()
|
|
229
|
+
.next()
|
|
230
|
+
.and_then(|component| component.as_os_str().to_str())
|
|
231
|
+
.is_some_and(|first| first.starts_with('.') && first != "..")
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Whether a *directory* is one the configuration excludes wholesale, which is how RuboCop
|
|
236
|
+
/// prunes `.git` and `node_modules` before descending rather than filtering their contents one
|
|
237
|
+
/// file at a time.
|
|
238
|
+
pub fn directory_excluded(&self, path: &Path) -> bool {
|
|
239
|
+
let patterns: Vec<String> = self.all_cops_value("Exclude").unwrap_or_default();
|
|
240
|
+
let Some(relative) = paths::project_relative(path, &self.project_root) else {
|
|
241
|
+
return false;
|
|
242
|
+
};
|
|
243
|
+
patterns.iter().any(|pattern| {
|
|
244
|
+
pattern
|
|
245
|
+
.strip_suffix("/**/*")
|
|
246
|
+
.is_some_and(|directory| relative == Path::new(directory))
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
211
250
|
pub fn path_excluded(&self, path: &Path) -> bool {
|
|
212
251
|
self.excluded_by("AllCops", path)
|
|
213
252
|
}
|