rubocop-spellcheck 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ed77adacb8aa94379378b10cebf66ffd8a710761d873c2eb845cebb15610c13
4
- data.tar.gz: 949d777b38250c6f8427ec9509d5ea02bc181ef972515dec0e07e0d3ef974075
3
+ metadata.gz: 1632104e9d052f6d9f121d814c60b072fd5af231437efc0cb4e424c910fd5cf6
4
+ data.tar.gz: 4ed0b83c791c4ea57a8d5f6dd8ed2e3b37fdb67604b1c2f0376e4c3fe9dc4135
5
5
  SHA512:
6
- metadata.gz: d54af9fc4f4faab80ff0f2d4476a2cd2dd923a22809d0e41d105137ef3d2d87e0df8e403be2d0c0a9d784f42d142b93c3a4b0499234987cfe791ede01e2501d8
7
- data.tar.gz: 98c3511624a7244ce86a354a1842f04532bfb6cbd81b62f5783b7873b3221c60652bdee77d6a68eb82a5dea351afe63070dec44ad204b6d3c414dac8f859f40a
6
+ metadata.gz: 1cf73548e344df7cfbdece8a4ac0b82d983f68b5facf2cd820515ab64aa923bb197f377bd22ad1a805039fdbe89ed68659bf81b3b6cc161eefbffbb80ac1707f
7
+ data.tar.gz: b397a5491ea97e5040ed71288442d940558acb8a42d4089626210ed213dab5c18c473a3ddaefbb0b629ca980e4dbc53c6c7177dc5bdbfdd95aa73062558745cf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.2.0 (2026/10/21)
4
+
5
+ * [#28](https://github.com/dblock/rubocop-spellcheck/pull/28): Exclude `require`, `require_relative`, `load`, and `autoload` paths and `gem` names and version requirements from spelling checks in Ruby files and Markdown Ruby fences - [@dblock](https://github.com/dblock).
6
+ * [#29](https://github.com/dblock/rubocop-spellcheck/pull/29): Display generated vocabulary paths relative to the working directory, matching RuboCop output - [@dblock](https://github.com/dblock).
7
+ * [#27](https://github.com/dblock/rubocop-spellcheck/pull/27): Correct lowercase and title-case typos when the first suggestion is one edit away, even when multiple suggestions exist; require dictionary-accepted replacements to prevent correction loops and retain uppercase, mixed-case, and technical-token guards - [@dblock](https://github.com/dblock).
8
+ * [#26](https://github.com/dblock/rubocop-spellcheck/pull/26): Restrict autocorrection to unique, single-edit lowercase typos outside dotted and identifier-like tokens; keep other suggestions report-only - [@dblock](https://github.com/dblock).
9
+ * [#25](https://github.com/dblock/rubocop-spellcheck/pull/25): Skip spelling suggestions during vocabulary generation to speed up `--auto-gen-config` - [@dblock](https://github.com/dblock).
10
+ * [#24](https://github.com/dblock/rubocop-spellcheck/pull/24): Fix spelling checks crashing on `__FILE__` in Ruby files and Markdown Ruby fences - [@dblock](https://github.com/dblock).
11
+ * [#22](https://github.com/dblock/rubocop-spellcheck/pull/22): Check strings and opt-in identifiers in explicitly tagged Ruby Markdown fences; keep strings enabled and identifiers disabled by default in all Ruby checks - [@dblock](https://github.com/dblock).
12
+ * [#21](https://github.com/dblock/rubocop-spellcheck/pull/21): Check Ruby strings and identifiers by default, with independent opt-outs, source-mapped string corrections, and report-only identifier diagnostics - [@dblock](https://github.com/dblock).
13
+ * [#20](https://github.com/dblock/rubocop-spellcheck/pull/20): Generate project vocabulary and dictionary configuration with `rubocop --auto-gen-config` instead of suppressing spelling checks - [@dblock](https://github.com/dblock).
14
+ * [#19](https://github.com/dblock/rubocop-spellcheck/pull/19): Add parser-backed AsciiDoc spelling checks and autocorrection - [@dblock](https://github.com/dblock).
15
+ * [#18](https://github.com/dblock/rubocop-spellcheck/pull/18): Check RDoc files and parse RDoc markup in Ruby comments, excluding code examples and link destinations - [@dblock](https://github.com/dblock).
16
+ * [#17](https://github.com/dblock/rubocop-spellcheck/pull/17): Select front matter values with dotted inclusion and exclusion paths - [@dblock](https://github.com/dblock).
17
+ * [#16](https://github.com/dblock/rubocop-spellcheck/pull/16): Support Markdown HTML comment directives for spelling suppression - [@dblock](https://github.com/dblock).
18
+ * [#15](https://github.com/dblock/rubocop-spellcheck/pull/15): Add spelling autocorrection for words with exactly one dictionary suggestion - [@dblock](https://github.com/dblock).
19
+ * [#14](https://github.com/dblock/rubocop-spellcheck/pull/14): Reuse loaded dictionaries and word verdicts across file investigations instead of rebuilding them for every file - [@dblock](https://github.com/dblock).
3
20
  ### 0.1.0 (2026/10/20)
4
21
 
5
22
  * [#12](https://github.com/dblock/rubocop-spellcheck/pull/12): Preserve Markdown parsing and use original prose buffers - [@dblock](https://github.com/dblock).
data/README.md CHANGED
@@ -5,15 +5,27 @@
5
5
  [![Package](https://github.com/dblock/rubocop-spellcheck/actions/workflows/package.yml/badge.svg)](https://github.com/dblock/rubocop-spellcheck/actions/workflows/package.yml)
6
6
  [![Gem Version](https://badge.fury.io/rb/rubocop-spellcheck.svg)](https://rubygems.org/gems/rubocop-spellcheck)
7
7
 
8
- Spellcheck Ruby comments, Markdown prose, and plain text with [RuboCop](https://rubocop.org/) and [Kotoshu](https://github.com/kotoshu/kotoshu).
8
+ Spellcheck Ruby comments, strings, identifiers, and documentation with [RuboCop](https://rubocop.org/) and [Kotoshu](https://github.com/kotoshu/kotoshu).
9
9
 
10
10
  ## Table of Contents
11
11
 
12
12
  - [Installation](#installation)
13
13
  - [Usage](#usage)
14
+ - [Other languages](#other-languages)
15
+ - [Supported files](#supported-files)
16
+ - [Ruby comments](#ruby-comments)
17
+ - [Ruby strings and identifiers](#ruby-strings-and-identifiers)
18
+ - [RDoc](#rdoc)
19
+ - [Markdown](#markdown)
20
+ - [YAML front matter](#yaml-front-matter)
21
+ - [AsciiDoc](#asciidoc)
22
+ - [Plain text](#plain-text)
23
+ - [Autocorrection](#autocorrection)
14
24
  - [Configuration](#configuration)
25
+ - [Front matter paths](#front-matter-paths)
15
26
  - [Dictionary files](#dictionary-files)
16
27
  - [Adding project vocabulary](#adding-project-vocabulary)
28
+ - [Generating project vocabulary](#generating-project-vocabulary)
17
29
  - [Replacing dictionaries and adding languages](#replacing-dictionaries-and-adding-languages)
18
30
  - [Exclusions and suppression](#exclusions-and-suppression)
19
31
  - [Scope and limitations](#scope-and-limitations)
@@ -46,7 +58,42 @@ bundle install
46
58
  bundle exec rubocop
47
59
  ```
48
60
 
49
- By default, the plugin uses the bundled American English dictionary. No dictionary setup, external cache, or network access is needed.
61
+ No dictionary setup or network access is needed.
62
+
63
+ ## Usage
64
+
65
+ `Spellcheck/Spelling` is enabled by default and checks Ruby comments, strings, Markdown, RDoc, AsciiDoc, and plain text. Identifier checking is opt-in:
66
+
67
+ ```sh
68
+ bundle exec rubocop
69
+ bundle exec rubocop --only Spellcheck/Spelling
70
+ ```
71
+
72
+ For example, given `example.rb`:
73
+
74
+ ```ruby
75
+ # The red brown fox jumsp over the lazy dog.
76
+ ```
77
+
78
+ Running `bundle exec rubocop --only Spellcheck/Spelling example.rb` reports:
79
+
80
+ ```text
81
+ example.rb:1:21: C: Spellcheck/Spelling: Misspelled word: jumsp. Did you mean jumps, jump, jumpy?
82
+ # The red brown fox jumsp over the lazy dog.
83
+ ^^^^^
84
+ ```
85
+
86
+ Each offense highlights the misspelled word and includes up to three suggestions when available.
87
+
88
+ Fix typos, then generate an initial `.spellcheck.txt` for remaining project-specific words:
89
+
90
+ ```sh
91
+ bundle exec rubocop --auto-gen-config --only Spellcheck/Spelling
92
+ ```
93
+
94
+ The command adds both the bundled English dictionary and `.spellcheck.txt` to your configuration when no dictionaries are configured. Review the generated vocabulary: remaining typos are accepted too. See [Generating project vocabulary](#generating-project-vocabulary) for regeneration behavior.
95
+
96
+ ## Other languages
50
97
 
51
98
  For another language, download a Hunspell dictionary and its license into your project. For example, download the [latest German resources](https://github.com/kotoshu/dictionaries/tree/v1/de/spelling):
52
99
 
@@ -66,49 +113,42 @@ Spellcheck:
66
113
  - dictionaries/de/de.dic
67
114
  ```
68
115
 
69
- The matching `dictionaries/de/de.aff` is loaded alongside it. Review the downloaded license and preserve its notices when committing or redistributing dictionary files. Commit these files or provision the same files in CI for reproducible results. You can also point to Hunspell files supplied by your operating system. The plugin does not use Kotoshu's resource cache, registration, or setup command; linting never downloads anything.
116
+ The matching `.aff` file is required. Commit both files and their license, or provision them in CI. Review each dictionary's license before redistributing it; linting never downloads dictionaries.
70
117
 
71
- ## Usage
118
+ ## Supported files
72
119
 
73
- `Spellcheck/Spelling` is enabled by default:
120
+ ### Ruby comments
74
121
 
75
- ```ruby
76
- # bad
77
- # The red brown fox jumsp over the lazy dog.
122
+ Ruby comments, including trailing and block comments, are checked. Shebangs, magic comments, RuboCop/Standard directives, and Sorbet `typed:` declarations are skipped. URLs and email addresses are ignored.
78
123
 
79
- # good
80
- # The red brown fox jumps over the lazy dog.
81
- ```
124
+ Comments are parsed as RDoc, excluding indented examples and inline code.
82
125
 
83
- ```sh
84
- bundle exec rubocop
85
- bundle exec rubocop --only Spellcheck/Spelling
86
- ```
126
+ ### Ruby strings and identifiers
87
127
 
88
- For example, given `example.rb`:
128
+ Strings include ordinary and percent literals, string arrays, heredocs, and literal text inside interpolation. Contents are checked literally, not as markup. Paths passed to `require`, `require_relative`, `load`, and `autoload`, plus `gem` names and version requirements, are excluded. Symbols, regular expression text, and shell-command text are also excluded; Ruby expressions inside interpolation are still checked. Ruby code is never evaluated.
89
129
 
90
- ```ruby
91
- # The red brown fox jumsp over the lazy dog.
92
- ```
130
+ Identifier checking is disabled by default. When enabled, every occurrence is checked, including method calls, definitions, constants, arguments, and variables. Names such as `hello_world`, `helloWorld`, and `HTTPClient` are split at underscores, numbers, and case boundaries. Add intentional API names and abbreviations to your project vocabulary.
93
131
 
94
- Running `bundle exec rubocop --only Spellcheck/Spelling example.rb` reports:
132
+ Enable identifiers or disable strings independently:
95
133
 
96
- ```text
97
- Inspecting 1 file
98
- C
134
+ ```yaml
135
+ Spellcheck/Spelling:
136
+ Ruby:
137
+ Strings: false
138
+ Identifiers: true
139
+ ```
99
140
 
100
- Offenses:
141
+ Standard Ruby suppression directives apply to all three scopes. Identifier errors are report-only: renaming one occurrence could break references or external APIs. Strings support `rubocop -A`, but changing a string can change program behavior; review these edits carefully.
101
142
 
102
- example.rb:1:21: C: Spellcheck/Spelling: Misspelled word: jumsp. Did you mean jumps, jump, jumpy?
103
- # The red brown fox jumsp over the lazy dog.
104
- ^^^^^
143
+ Explicitly tagged `ruby` Markdown fences use the same string and identifier settings, including inside lists and blockquotes. Untagged or indented code, other languages, comments inside fences, and invalid Ruby snippets remain excluded.
105
144
 
106
- 1 file inspected, 1 offense detected
107
- ```
145
+ ### RDoc
146
+
147
+ `.rdoc` files and Ruby comments use RDoc parsing. Headings, paragraphs, lists, and link labels are checked; code, link destinations, and documentation directives are skipped. Directives such as `:include:` are never executed.
108
148
 
109
- Each offense highlights the misspelled word and includes up to three suggestions when available. Spelling is ambiguous, so there is no autocorrection.
149
+ ### Markdown
110
150
 
111
- The same cop discovers `.md` and `.markdown` files and checks their prose:
151
+ The cop discovers `.md` and `.markdown` files and checks their prose:
112
152
 
113
153
  ```markdown
114
154
  # The red brown fox jumsp over the lazy dog.
@@ -116,9 +156,11 @@ The same cop discovers `.md` and `.markdown` files and checks their prose:
116
156
  Check [the spelling](https://example.com) but not `code_identifiers`.
117
157
  ```
118
158
 
119
- Markdown headings, paragraphs, lists, blockquotes, table cells, link labels, image descriptions, and footnote text are checked. Code blocks and inline code, link destinations and reference identifiers, and HTML tags/comments and HTML blocks are skipped. URLs and email addresses are ignored in both formats. HTML entities and escaped punctuation are decoded with offenses mapped back to the original source. No `rubocop-md` plugin is required; Ruby code inside Markdown blocks is not inspected.
159
+ Prose, headings, lists, tables, link labels, image descriptions, and footnotes are checked. Code other than explicitly tagged `ruby` fences, link destinations, HTML, Liquid expressions, URLs, and email addresses are skipped. No `rubocop-md` plugin is required.
160
+
161
+ ### YAML front matter
120
162
 
121
- YAML front matter checks string values recursively, including values inside mappings and arrays. Keys and strongly typed values such as numbers, booleans, dates, timestamps, and nulls are skipped. Quoted values are strings even when their contents resemble another type. Anchored strings are checked at their definition, not repeated through aliases; explicit string tags are supported, while other tags are ignored. YAML is parsed without constructing application objects. Plain and quoted values and multiline literal/folded values retain their original diagnostic locations, including quoted escape sequences. Invalid YAML produces an explicit error. Strings such as layout names and slugs are checked too; add intentional vocabulary to your dictionary. To disable front matter checks:
163
+ String values are checked recursively, including multiline values and arrays. Keys, numbers, booleans, dates, and nulls are skipped. Layout names and slugs are strings too; use [front matter paths](#front-matter-paths) to select prose fields. Invalid YAML produces an error. To disable these checks:
122
164
 
123
165
  ```yaml
124
166
  Spellcheck/Spelling:
@@ -127,14 +169,51 @@ Spellcheck/Spelling:
127
169
  Enabled: false
128
170
  ```
129
171
 
130
- Before the first release, `Spellcheck/Comments` was renamed to `Spellcheck/Spelling`. Update any existing cop-specific configuration, `--only` arguments, or Ruby suppression directives to use the new name.
172
+ ### AsciiDoc
173
+
174
+ `.adoc` and `.asciidoc` files use [Asciidoctor](https://asciidoctor.org/) to check prose, headings, lists, tables, and link labels. Code, comments, attributes, link destinations, and macros are skipped. Includes and extensions are disabled; linting does not render documents or access external resources.
175
+
176
+ ### Plain text
131
177
 
132
- Liquid output expressions and tags are ignored, including expressions inside link destinations. English words retain accented letters and internal apostrophes; surrounding quotation marks and numeric identifiers such as commit hashes and ordinals are not checked.
178
+ `.txt` files are checked literally, excluding URLs and email addresses. Configured dictionaries and bundled resources are excluded automatically. Use normal RuboCop exclusions for other files you do not want checked.
133
179
 
134
- Plain `.txt` files are also discovered by default. Their entire contents are checked literally, including text that looks like Markdown code or headings; URLs and email addresses are still ignored. Configured dictionary files (including symbolic links to them) and this gem's bundled `data/` resources are excluded from spelling checks, so dictionary words and license/provenance files are not treated as prose. Hunspell `.dic` and `.aff` files are not included by default. Other text files, including project license files, can be excluded with normal `Spellcheck/Spelling` configuration.
180
+ ## Autocorrection
181
+
182
+ Dictionary suggestions may not match your intended meaning, so spelling corrections require `rubocop -A`; `rubocop -a` leaves spelling unchanged. To apply corrections, use:
183
+
184
+ ```sh
185
+ bundle exec rubocop -A --only Spellcheck/Spelling
186
+ ```
187
+
188
+ Lowercase and title-case words whose first suggestion is at a distance of one edit are corrected: one insertion, deletion, substitution, or adjacent transposition. The replacement must pass the configured dictionaries. Other suggestions may be equally close; the first suggestion wins, preserving lowercase or title case. ALL CAPS, mixed-case words, and components of dotted or identifier-like tokens remain report-only. Corrections do not introduce multiple words or formatting characters. Title-case corrections can change people's names; review the resulting diff.
189
+
190
+ Corrections preserve surrounding markup and string delimiters; escaped letters may become literal letters. Unsafe replacements such as a heredoc terminator or a typed YAML value are withheld. Identifiers are never renamed.
135
191
 
136
192
  ## Configuration
137
193
 
194
+ ### Front matter paths
195
+
196
+ By default, all front matter string values are checked. Use dotted paths under `Markdown.FrontMatter` to select which values participate:
197
+
198
+ ```yaml
199
+ Spellcheck/Spelling:
200
+ Markdown:
201
+ FrontMatter:
202
+ Enabled: true
203
+ Include:
204
+ - title
205
+ - description
206
+ - metadata
207
+ - authors.description
208
+ Exclude:
209
+ - metadata.slug
210
+ - metadata.internal
211
+ ```
212
+
213
+ Paths are case-sensitive, with dots separating nested keys. Selecting a parent includes its descendants; `authors.description` selects descriptions in every author mapping without array indices. Wildcards and literal dots in key names are not supported.
214
+
215
+ `Include: null` checks all paths; `Include: []` checks none. `Exclude` always takes precedence. These settings do not affect Markdown body prose.
216
+
138
217
  ### Dictionary files
139
218
 
140
219
  `DictionaryFiles` maps language codes to lists of dictionary paths. When omitted or `null`, it selects packaged English, equivalent to:
@@ -146,13 +225,13 @@ Spellcheck:
146
225
  - gem:rubocop-spellcheck/data/en/en.dic
147
226
  ```
148
227
 
149
- An explicit map replaces the default selection completely; no English dictionary is implicitly retained. Each language key determines Kotoshu's tokenization for its dictionaries. A word passes if any configured dictionary accepts it. This supports multilingual comments, but a typo that is valid in another active language will pass. There is no language detection or separate `Language` setting.
228
+ An explicit map replaces the defaults: retain the English path if you still want English. A word passes if any configured dictionary accepts it; there is no automatic language detection.
150
229
 
151
- Paths beginning with `gem:<gem-name>/` resolve relative to that installed gem's root; for example, `gem:rubocop-spellcheck/data/en/en.dic`. Other paths are relative to the effective RuboCop configuration directory (or working directory without a configuration file). No ERB is needed. Missing gems, files, empty language lists, and unsupported file extensions produce explicit errors; there is no fallback.
230
+ `gem:<gem-name>/` paths resolve inside the installed gem. Other paths are relative to the effective RuboCop configuration directory. Missing dictionaries and invalid settings produce errors rather than falling back to defaults.
152
231
 
153
232
  Hunspell `.dic` files load their matching `.aff` file in the same directory with the same basename. Plain `.txt` files contain literal words, one per line, matched case-insensitively without automatic inflection. Blank lines and lines starting with `#` are ignored; phrases are rejected.
154
233
 
155
- Settings can also be overridden for `Spellcheck/Spelling` using normal RuboCop department/cop configuration. RuboCop merges explicitly inherited hashes by its normal configuration rules; within an explicit map, arrays replace inherited arrays unless `inherit_mode` says otherwise. Kotoshu's personal dictionary is not consulted. Changes to any selected dictionary or affix file invalidate cached results.
234
+ Normal RuboCop inheritance and cop-specific overrides apply. Dictionary changes invalidate cached results; Kotoshu's personal dictionary is not used.
156
235
 
157
236
  ### Adding project vocabulary
158
237
 
@@ -172,9 +251,27 @@ Kotoshu
172
251
  RuboCop
173
252
  ```
174
253
 
175
- In `.spellcheck.txt`, blank lines and `#` comments are ignored. Words match case-insensitively, with no automatic inflection: list plural or other forms explicitly when needed. Unlike this literal word list, the bundled English dictionary uses Hunspell's `.dic` word list and `.aff` rules to recognize forms such as plurals and suffixes. Both formats can be combined in `DictionaryFiles`.
254
+ Words match case-insensitively; list plural and other forms explicitly. Blank lines and `#` comments are ignored. To use only your own word list, omit the gem path.
255
+
256
+ ### Generating project vocabulary
176
257
 
177
- There is no separate `AllowedWords`, `DictionaryPath`, or additional-files option. To use only your own word list, omit the gem path.
258
+ Use the RuboCop configuration generator to build an initial vocabulary:
259
+
260
+ ```sh
261
+ bundle exec rubocop --auto-gen-config
262
+ ```
263
+
264
+ Spelling offenses create or update `.spellcheck.txt`. To generate vocabulary without configuring other cops:
265
+
266
+ ```sh
267
+ bundle exec rubocop --auto-gen-config --only Spellcheck/Spelling
268
+ ```
269
+
270
+ Every reported misspelling becomes accepted vocabulary, including genuine typos and words with correction suggestions. **Review the generated list**, remove mistakes, and fix them in the source. Generation does not correct source files.
271
+
272
+ Rerunning rebuilds `.spellcheck.txt` from current violations without accepting its existing words during checking. Unneeded entries are removed, including manually added ones. Comments and capitalization of retained words are preserved; new words are lowercase, sorted, and added without duplicates. The command adds the vocabulary to `DictionaryFiles` while retaining existing dictionaries, or bundled English when using defaults.
273
+
274
+ Vocabulary is written beside the effective configuration, including nested configurations. Exclusions and suppression still apply. **Run against the full intended file set:** words needed only in omitted files will be removed. Clean runs clear obsolete entries but do not create a new vocabulary file.
178
275
 
179
276
  ### Replacing dictionaries and adding languages
180
277
 
@@ -207,7 +304,17 @@ Spellcheck/Spelling:
207
304
  # rubocop:enable Spellcheck/Spelling
208
305
  ```
209
306
 
210
- Ruby inline directives apply to comments; Markdown HTML comments are ignored, not interpreted as suppression directives. To check only one format, use standard file inclusion:
307
+ Ruby directives apply to comments, strings, and identifiers, including within tagged Ruby fences. In Markdown, use HTML comments:
308
+
309
+ ```markdown
310
+ <!-- rubocop:disable Spellcheck/Spelling -->
311
+ Intentional project vocabulary.
312
+ <!-- rubocop:enable Spellcheck/Spelling -->
313
+ ```
314
+
315
+ A trailing `disable` or any `todo` applies only to its own line. Directives accept `all`, cop lists, and reasons after ` -- `. HTML-comment directives inside code, Liquid, or front matter are ignored; Ruby directives apply within their own fence. Suppression prevents diagnostics and autocorrection.
316
+
317
+ To check only one format, use standard file inclusion:
211
318
 
212
319
  ```yaml
213
320
  Spellcheck/Spelling:
@@ -217,9 +324,9 @@ Spellcheck/Spelling:
217
324
 
218
325
  ## Scope and limitations
219
326
 
220
- The first release checks Ruby comments, including trailing comments, Markdown prose, and plain `.txt` files. Ruby checks skip shebangs, magic comments, RuboCop/Standard directives, and Sorbet `typed:` declarations. It does not inspect Ruby strings, identifiers, symbols, or regular expressions.
327
+ English words retain accented letters and internal apostrophes; surrounding quotation marks and numeric identifiers such as commit hashes and ordinals are not checked.
221
328
 
222
- Other comment content, including embedded code and documentation tags, is treated as text. Use a project dictionary or standard suppression for intentional non-prose. Word boundaries and spelling follow Kotoshu's selected language; this extension does not add language detection or grammar checking.
329
+ Ruby comments use RDoc markup, not YARD or other documentation syntaxes. Other documentation tags are treated as text. Use a project dictionary or standard suppression for intentional non-prose. Word boundaries and spelling follow Kotoshu's selected language; this extension does not add language detection or grammar checking.
223
330
 
224
331
  Kotoshu ships precompiled gems for macOS, Linux, and Windows. Building native dependencies from source on other platforms may require a Rust toolchain.
225
332
 
@@ -233,4 +340,4 @@ Copyright (c) 2026 Daniel (dB.) Doubrovkine. MIT License; see [LICENSE.txt](LICE
233
340
 
234
341
  Kotoshu is a separate BSD-2-Clause dependency. The bundled SCOWL-derived American English dictionary carries its own copyright and redistribution terms; see [its full license](data/en/LICENSE) and [pinned provenance](data/en/README.md). Dictionary files and notices are included in the packaged gem.
235
342
 
236
- As of October 20, 2026, `kotoshu/dictionaries` describes its repository as MIT-licensed, but individual dictionaries retain their own licenses. The English package declares [`MIT AND BSD`](https://github.com/kotoshu/dictionaries/blob/v1/en/spelling/package.json); its [full notice](data/en/LICENSE) includes SCOWL's permissive redistribution grant, Ispell's BSD-style terms, and constituent word-list notices. The German dictionary used in the download example is [`GPL-2.0 OR GPL-3.0`](https://github.com/kotoshu/dictionaries/blob/v1/de/spelling/package.json), confirmed by its [license notice](https://github.com/kotoshu/dictionaries/blob/v1/de/spelling/license). German is not bundled with this gem. Other languages vary: inspect each dictionary's own license before using or redistributing it; the repository's MIT label does not relicense those files.
343
+ Other dictionaries have their own licenses regardless of the dictionary repository's license. The German example is [`GPL-2.0 OR GPL-3.0`](https://github.com/kotoshu/dictionaries/blob/v1/de/spelling/license) and is not bundled. Review each dictionary's notices before redistributing it.
data/config/default.yml CHANGED
@@ -7,11 +7,21 @@ AllCops:
7
7
  - "**/*.md"
8
8
  - "**/*.markdown"
9
9
  - "**/*.txt"
10
+ - "**/*.rdoc"
11
+ - "**/*.adoc"
12
+ - "**/*.asciidoc"
10
13
 
11
14
  Spellcheck/Spelling:
12
- Description: Check spelling in Ruby comments, Markdown prose, and plain text.
15
+ Description: Check spelling in Ruby comments, strings, identifiers, and documentation.
13
16
  Enabled: true
17
+ SafeAutoCorrect: false
18
+ Ruby:
19
+ Strings: true
20
+ Identifiers: false
14
21
  Markdown:
15
22
  FrontMatter:
16
23
  Enabled: true
24
+ Include:
25
+ Exclude: []
17
26
  VersionAdded: "0.1"
27
+ VersionChanged: "0.2"
@@ -3,25 +3,24 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Spellcheck
6
- # Checks Ruby comments, Markdown prose, and plain text with shared dictionaries.
6
+ # Checks Ruby code and documentation with shared spelling dictionaries.
7
7
  class Spelling < Base
8
+ extend AutoCorrector
9
+
8
10
  BUNDLED_DATA = File.expand_path('../../../../data', __dir__).freeze
9
- DIRECTIVE = /
10
- \A\#\s*(?:!|(?:rubocop|standard|coding|encoding|frozen_string_literal|
11
- warn_indent|shareable_constant_value|typed)\s*:)|\A\#.*-\*-\s*(?:coding|encoding):
12
- /x
13
11
  ADDRESS = %r{(?:https?://|www\.)[^\s<>]+|[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}}
14
12
 
13
+ def self.support_multiple_source?
14
+ true
15
+ end
16
+
15
17
  def on_new_investigation
16
18
  super
17
19
  if RuboCop::Spellcheck::Markdown.file?(processed_source.path)
18
20
  check_markdown
19
21
  else
20
- processed_source.comments.each do |comment|
21
- next if DIRECTIVE.match?(comment.text)
22
-
23
- check_comment(comment)
24
- end
22
+ RuboCop::Spellcheck::RubyCode.new(processed_source, config: cop_config.fetch('Ruby', {}))
23
+ .each_span { |span| check_markdown_span(span) }
25
24
  end
26
25
  end
27
26
 
@@ -30,6 +29,8 @@ module RuboCop
30
29
  check_text
31
30
  elsif RuboCop::Spellcheck::Markdown.file?(processed_source.path)
32
31
  check_markdown
32
+ elsif (extractor = prose_extractor)
33
+ extractor.new(prose_buffer).each_span { |span| check_markdown_span(span) }
33
34
  else
34
35
  super
35
36
  end
@@ -47,23 +48,40 @@ module RuboCop
47
48
  checker.checksum
48
49
  end
49
50
 
51
+ def current_corrector
52
+ return super unless RuboCop::Spellcheck::ProseSource.file?(processed_source.path)
53
+
54
+ # Prose deliberately bypasses Ruby parsing, but its buffer can still be rewritten.
55
+ @current_corrector ||= Corrector.new(processed_source)
56
+ end
57
+
50
58
  private
51
59
 
52
- def front_matter_enabled?
53
- cop_config.fetch('Markdown', {}).fetch('FrontMatter', {}).fetch('Enabled', true)
60
+ def prose_extractor
61
+ [RuboCop::Spellcheck::RDoc, RuboCop::Spellcheck::AsciiDoc].find do |extractor|
62
+ extractor.file?(processed_source.path)
63
+ end
54
64
  end
55
65
 
56
66
  def check_markdown
57
- if front_matter_enabled?
58
- RuboCop::Spellcheck::FrontMatter.new(prose_buffer).each_span do |text, positions|
59
- check_markdown_span(text, positions)
60
- end
67
+ check_front_matter
68
+ markdown = RuboCop::Spellcheck::Markdown.new(prose_buffer)
69
+ markdown.each_span do |text, positions|
70
+ check_markdown_span(RuboCop::Spellcheck::Span.new(text, positions))
61
71
  end
62
- RuboCop::Spellcheck::Markdown.new(prose_buffer).each_span do |text, positions|
63
- check_markdown_span(text, positions)
72
+ markdown.each_ruby_span(processed_source.ruby_version, config: cop_config.fetch('Ruby', {})) do |span|
73
+ check_markdown_span(span)
64
74
  end
65
75
  end
66
76
 
77
+ def check_front_matter
78
+ front_matter = cop_config.fetch('Markdown', {}).fetch('FrontMatter', {})
79
+ return unless front_matter.fetch('Enabled', true)
80
+
81
+ RuboCop::Spellcheck::FrontMatter.new(prose_buffer, config: front_matter)
82
+ .each_span { |span| check_markdown_span(span) }
83
+ end
84
+
67
85
  def check_text
68
86
  checker.misspellings(mask_addresses(processed_source.buffer.source)).each do |word, position|
69
87
  next unless word.match?(/\p{L}/)
@@ -73,32 +91,31 @@ module RuboCop
73
91
  end
74
92
 
75
93
  def checker
76
- @checker ||= RuboCop::Spellcheck::Checker.new(
94
+ @checker ||= spelling_checker.new(
77
95
  dictionary_files: cop_config['DictionaryFiles'],
78
96
  base_directory: config.base_dir_for_path_parameters
79
97
  )
80
98
  end
81
99
 
82
- def check_comment(comment)
83
- checker.misspellings(prose(comment.text)).each do |word, position|
84
- first = comment.source_range.begin_pos + position
85
- report_word(word, first, first + word.length)
86
- end
100
+ def spelling_checker
101
+ @options[:auto_gen_config] ? RuboCop::Spellcheck::Vocabulary::Checker : RuboCop::Spellcheck::Checker
87
102
  end
88
103
 
89
- def check_markdown_span(text, positions)
90
- text = text.sub(/\A['’]s\b/, ' ')
91
- checker.misspellings(mask_addresses(text)).each do |word, position|
92
- next unless word.match?(/\p{L}/)
93
-
94
- report_word(word, positions.fetch(position).first, positions.fetch(position + word.length - 1).last)
104
+ def check_markdown_span(span)
105
+ text = mask_addresses(span.text.sub(/\A['’]s\b/, ' '))
106
+ span.each_misspelling(checker, text) do |word, first, last, correction, whole|
107
+ report_word(word, first, last, correction: correction, correctable: whole)
95
108
  end
96
109
  end
97
110
 
98
- def report_word(word, first, last)
111
+ def report_word(word, first, last, correction: nil, correctable: true)
99
112
  range = Parser::Source::Range.new(prose_buffer, first, last)
100
113
  range = markdown_document.processed_range(range) if markdown_document
101
- add_offense(range, message: spelling_message(word))
114
+ correction ||= RuboCop::Spellcheck::Correction.new(word, checker.suggestions(word))
115
+ replacement = correction.replacement_in(prose_buffer.source, first, last, checker: checker)
116
+ add_offense(range, message: correction.message) do |corrector|
117
+ corrector.replace(range, replacement) if replacement && correctable
118
+ end
102
119
  end
103
120
 
104
121
  def markdown_document
@@ -109,22 +126,9 @@ module RuboCop
109
126
  markdown_document ? markdown_document.buffer : processed_source.buffer
110
127
  end
111
128
 
112
- def prose(text)
113
- # Preserve character offsets while removing non-prose addresses and block delimiters.
114
- mask_addresses(text).gsub(/^=(?:begin|end)\b[^\n]*/) { |marker| ' ' * marker.length }
115
- end
116
-
117
129
  def mask_addresses(text)
118
130
  text.gsub(ADDRESS) { |address| ' ' * address.length }
119
131
  end
120
-
121
- def spelling_message(word)
122
- suggestions = checker.suggestions(word)
123
- message = "Misspelled word: `#{word}`."
124
- return message if suggestions.empty?
125
-
126
- "#{message} Did you mean #{suggestions.map { |suggestion| "`#{suggestion}`" }.join(', ')}?"
127
- end
128
132
  end
129
133
  end
130
134
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'asciidoctor'
4
+
5
+ module RuboCop
6
+ module Spellcheck
7
+ # Extracts original prose selected by the Asciidoctor block parser in secure mode.
8
+ class AsciiDoc
9
+ PROSE = %i[paragraph admonition verse list_item table_cell].freeze
10
+ SKIP = %i[listing literal pass stem comment].freeze
11
+
12
+ def self.file?(path)
13
+ path && %w[.adoc .asciidoc].include?(File.extname(path).downcase)
14
+ end
15
+
16
+ def initialize(buffer)
17
+ @buffer = buffer
18
+ @lines = buffer.source.lines
19
+ end
20
+
21
+ def each_span(&)
22
+ return enum_for(__method__) unless block_given?
23
+
24
+ @columns = Hash.new(0)
25
+ @tables = {}
26
+ source = @buffer.source.gsub(/^include::.*$/) { |line| ' ' * line.length }
27
+ document = ::Asciidoctor.load(source, safe: :secure, sourcemap: true, extensions: false)
28
+ spans(document, &)
29
+ end
30
+
31
+ private
32
+
33
+ def spans(document, &block)
34
+ document.find_by.each do |node|
35
+ next if excluded?(node) || !node.source_location
36
+
37
+ if node.context == :table_cell
38
+ table_spans(node, &block)
39
+ else
40
+ fragments(node).each { |text, line| yield mapped_span(text, line) }
41
+ end
42
+ end
43
+ end
44
+
45
+ def table_spans(node, &)
46
+ table = node.parent.parent
47
+ span = (@tables[table] ||= AsciiDocTable.new(@buffer, table)).span(node)
48
+ return if node.style == :literal
49
+ return spans(node.inner_document, &) if node.style == :asciidoc
50
+
51
+ yield span
52
+ end
53
+
54
+ def excluded?(node)
55
+ return false if node.context == :document
56
+ return true if SKIP.include?(node.context)
57
+
58
+ node.parent && excluded?(node.parent)
59
+ end
60
+
61
+ def fragments(node)
62
+ line = node.source_location.lineno
63
+ return [] if node.context == :document
64
+ return heading(line) if node.context == :section
65
+
66
+ titles(node, line) + prose(node, line)
67
+ end
68
+
69
+ def heading(line)
70
+ source = @lines.fetch(line - 1)
71
+ title = source[/\A={1,6} (.*)/, 1]
72
+ title ||= source.chomp if @lines[line]&.match?(/\A[=\-~^+]{2,}\s*\z/)
73
+ title ? [[title, line]] : []
74
+ end
75
+
76
+ def titles(node, line)
77
+ return [] unless node.title?
78
+
79
+ index = line - 2
80
+ index -= 1 while index >= 0 && @lines[index].start_with?('[')
81
+ text = @lines[index] if index >= 0
82
+ text&.start_with?('.') ? [[text.chomp.delete_prefix('.'), index + 1]] : []
83
+ end
84
+
85
+ def prose(node, line)
86
+ return [] unless PROSE.include?(node.context)
87
+
88
+ if node.context == :list_item
89
+ node.subs.clear
90
+ text = node.text
91
+ else
92
+ text = node.source
93
+ end
94
+ text ? [[text, line]] : []
95
+ end
96
+
97
+ def mapped_span(text, line)
98
+ positions = text.lines.flat_map do |part|
99
+ line += 1 while @lines.fetch(line - 1).start_with?('//')
100
+ mapped = line_positions(part, line)
101
+ line += 1
102
+ mapped
103
+ end
104
+ AsciiDocInline.new(text, positions).span
105
+ end
106
+
107
+ def line_positions(part, line)
108
+ column = column_for(part, line)
109
+ first = @buffer.line_range(line).begin_pos + column
110
+ @columns[line] = column + part.chomp.length
111
+ part.length.times.map { |index| [first + index, first + index + 1] }
112
+ end
113
+
114
+ def column_for(part, line)
115
+ @lines.fetch(line - 1).index(part.chomp, @columns[line]) ||
116
+ raise(RuboCop::ValidationError, "Cannot map AsciiDoc prose in #{@buffer.name}:#{line}: #{part.inspect}")
117
+ end
118
+ end
119
+ end
120
+ end