yard-lint 1.12.0 → 1.13.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 +4 -4
- data/CHANGELOG.md +199 -501
- data/README.md +94 -1
- data/bin/yard-lint +68 -32
- data/lib/yard/lint/config.rb +81 -5
- data/lib/yard/lint/config_validator.rb +7 -191
- data/lib/yard/lint/contractable/contract.rb +219 -0
- data/lib/yard/lint/contractable/result.rb +93 -0
- data/lib/yard/lint/contractable/rule.rb +10 -0
- data/lib/yard/lint/contracts/base.rb +57 -0
- data/lib/yard/lint/contracts/category.rb +71 -0
- data/lib/yard/lint/contracts/config.rb +81 -0
- data/lib/yard/lint/contracts/validator.rb +72 -0
- data/lib/yard/lint/errors.rb +3 -0
- data/lib/yard/lint/executor/in_process_registry.rb +1 -1
- data/lib/yard/lint/explainer.rb +42 -0
- data/lib/yard/lint/formatters/sarif.rb +218 -0
- data/lib/yard/lint/git.rb +5 -3
- data/lib/yard/lint/result_builder.rb +7 -6
- data/lib/yard/lint/runner.rb +2 -21
- data/lib/yard/lint/templates/default_config.yml +10 -0
- data/lib/yard/lint/templates/strict_config.yml +10 -0
- data/lib/yard/lint/todo_generator.rb +143 -12
- data/lib/yard/lint/validators/base.rb +3 -0
- data/lib/yard/lint/validators/documentation/reference_typo/config.rb +20 -0
- data/lib/yard/lint/validators/documentation/reference_typo/messages_builder.rb +24 -0
- data/lib/yard/lint/validators/documentation/reference_typo/parser.rb +44 -0
- data/lib/yard/lint/validators/documentation/reference_typo/result.rb +27 -0
- data/lib/yard/lint/validators/documentation/reference_typo/validator.rb +214 -0
- data/lib/yard/lint/validators/documentation/reference_typo.rb +39 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +13 -12
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +84 -5
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +16 -3
- data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +3 -2
- data/lib/yard/lint/validators/tags/example_style/validator.rb +4 -4
- data/lib/yard/lint/validators/tags/example_syntax/validator.rb +4 -2
- data/lib/yard/lint/validators/tags/invalid_types/messages_builder.rb +15 -4
- data/lib/yard/lint/validators/tags/invalid_types/parser.rb +18 -7
- data/lib/yard/lint/validators/tags/invalid_types/validator.rb +22 -12
- data/lib/yard/lint/validators/tags/overload_consistency/config.rb +20 -0
- data/lib/yard/lint/validators/tags/overload_consistency/messages_builder.rb +32 -0
- data/lib/yard/lint/validators/tags/overload_consistency/parser.rb +49 -0
- data/lib/yard/lint/validators/tags/overload_consistency/result.rb +27 -0
- data/lib/yard/lint/validators/tags/overload_consistency/validator.rb +71 -0
- data/lib/yard/lint/validators/tags/overload_consistency.rb +53 -0
- data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +3 -3
- data/lib/yard/lint/validators/tags/type_syntax/parser.rb +13 -2
- data/lib/yard/lint/validators/tags/type_syntax/validator.rb +11 -1
- data/lib/yard/lint/version.rb +1 -1
- data/lib/yard/lint.rb +3 -57
- metadata +21 -1
data/README.md
CHANGED
|
@@ -114,8 +114,14 @@ yard-lint --auto-gen-config
|
|
|
114
114
|
yard-lint lib/
|
|
115
115
|
|
|
116
116
|
# Fix violations incrementally by removing exclusions from .yard-lint-todo.yml
|
|
117
|
+
|
|
118
|
+
# Fail (exit 1) when .yard-lint-todo.yml has entries that no longer match any offense
|
|
119
|
+
# (use the PATH of your normal run; a PATH only limits which entries are reported)
|
|
120
|
+
yard-lint --check-todo lib/
|
|
117
121
|
```
|
|
118
122
|
|
|
123
|
+
`.yard-lint-todo.yml` is written and read next to the loaded `.yard-lint.yml` (or in the current directory without one), and its entries are relative to that directory, so the baseline works from any directory you run yard-lint in.
|
|
124
|
+
|
|
119
125
|
**Learn more:** [Incremental Adoption Guide](https://github.com/mensfeld/yard-lint/wiki/Incremental-Adoption)
|
|
120
126
|
|
|
121
127
|
### Check Documentation Coverage
|
|
@@ -208,6 +214,26 @@ Then `:make` lints the current file and `:cnext` / `:cprev` jump between offense
|
|
|
208
214
|
|
|
209
215
|
Then `M-x compile` and `M-g n` / `M-g p` navigate between offenses.
|
|
210
216
|
|
|
217
|
+
### SARIF Output (GitHub Code Scanning)
|
|
218
|
+
|
|
219
|
+
Use `--format sarif` to emit [SARIF 2.1.0](https://sarifweb.azurewebsites.net/), the format GitHub code scanning and most result aggregators ingest. Uploading it makes yard-lint offenses appear as annotations in the PR "Files changed" view and the repository Security tab, instead of being buried in CI logs:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
yard-lint --format sarif lib/ > yard-lint.sarif
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Each rule carries its description (from the validator's own YARD docs), and severities map to SARIF levels (`error` → error, `warning` → warning, `convention` → note). Artifact paths are emitted repository-relative so GitHub maps them onto your files. In GitHub Actions, upload the file with `github/codeql-action/upload-sarif`:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
- run: bundle exec yard-lint --format sarif lib/ > yard-lint.sarif
|
|
229
|
+
continue-on-error: true
|
|
230
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
231
|
+
with:
|
|
232
|
+
sarif_file: yard-lint.sarif
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
(Run yard-lint from the repository root so paths resolve correctly; `continue-on-error` lets the upload step run even when offenses are found.)
|
|
236
|
+
|
|
211
237
|
## Configuration Basics
|
|
212
238
|
|
|
213
239
|
Create a `.yard-lint.yml` file in your project root:
|
|
@@ -322,10 +348,35 @@ Documentation/UnderfilledLines:
|
|
|
322
348
|
|
|
323
349
|
**Key features:**
|
|
324
350
|
- Per-validator control (enable/disable, severity, exclusions)
|
|
351
|
+
- Category-level defaults (enable/disable, severity, exclusions) for a whole department
|
|
325
352
|
- Configuration inheritance with `inherit_from` and `inherit_gem`
|
|
326
353
|
- Automatic configuration validation with helpful error messages
|
|
327
354
|
- Per-validator YARD options and file exclusions
|
|
328
355
|
|
|
356
|
+
**Category-level defaults:** set `Enabled` or `Severity` on a category (`Documentation`, `Tags`, `Warnings`, `Semantic`) to apply it to every validator in that department. A per-validator setting always wins over the category, which in turn wins over the validator's built-in default:
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
# Make every Tags/* offense an error...
|
|
360
|
+
Tags:
|
|
361
|
+
Severity: error
|
|
362
|
+
|
|
363
|
+
# ...except Tags/Order, which stays a convention
|
|
364
|
+
Tags/Order:
|
|
365
|
+
Severity: convention
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
A category-level `Exclude` is combined with each validator's own `Exclude` (it is not replaced by it), so these files are skipped by every `Documentation/*` validator, including one that sets its own `Exclude`:
|
|
369
|
+
|
|
370
|
+
```yaml
|
|
371
|
+
Documentation:
|
|
372
|
+
Exclude:
|
|
373
|
+
- 'lib/generated/**/*'
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
A category accepts only the keys `Enabled` (`true` or `false`), `Severity` and `Exclude`; an unknown key or a non-boolean `Enabled` fails configuration validation.
|
|
377
|
+
|
|
378
|
+
`Exclude` patterns at every level are relative to the directory of the loaded `.yard-lint.yml` (or the current directory without one), whatever PATH or directory you run yard-lint from. A file outside that directory is matched as `../...` (with `--config config/yard-lint.yml`, write `../lib/generated/**/*`); absolute patterns also work.
|
|
379
|
+
|
|
329
380
|
**Learn more:** [Complete Configuration Guide](https://github.com/mensfeld/yard-lint/wiki/Configuration)
|
|
330
381
|
|
|
331
382
|
## Catching Orphaned Documentation Comments
|
|
@@ -434,6 +485,47 @@ The validator reports one offense per wasteful paragraph and is deliberately con
|
|
|
434
485
|
|
|
435
486
|
Unlike `LineLength` (which measures an objective property), "this line should have been longer" is a stylistic judgement. Projects that use **semantic line breaks** (one sentence or clause per line, see [sembr.org](https://sembr.org)) are never flagged - any paragraph whose non-final lines all end at a sentence boundary (`.?!:;`) is treated as intentional. Leave the validator off, or add `,` to `SentenceEndChars`, if that style is used heavily.
|
|
436
487
|
|
|
488
|
+
## Catching Typos in References
|
|
489
|
+
|
|
490
|
+
The `Documentation/ReferenceTypo` validator (enabled by default, severity `warning`) checks `{Foo#bar}` links in docstrings and tag text, and `@see` targets. When a reference does not resolve and a close match exists in its namespace, it reports the reference and suggests the closest name:
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
# Bad - reports: Reference 'Parser#prase' does not resolve (did you mean 'Parser#parse'?)
|
|
494
|
+
# Delegates to {Parser#prase}
|
|
495
|
+
def call; end
|
|
496
|
+
|
|
497
|
+
# Good
|
|
498
|
+
# Delegates to {Parser#parse}
|
|
499
|
+
def call; end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
To keep false positives low, it skips references into namespaces YARD does not know (stdlib, other gems), methods that a loaded Ruby ancestor may define (e.g. `{#to_s}`), names with no close match, and code spans and code blocks. A bare constant such as `{Kafka}` is only compared with constants of the enclosing namespaces, never with top-level ones, so a constant of a gem YARD did not parse is not reported as a typo of a similar project name.
|
|
503
|
+
|
|
504
|
+
## Checking @overload Tags
|
|
505
|
+
|
|
506
|
+
YARD checks `@param` names only against the method's own signature, never the tags inside an `@overload`. The `Tags/OverloadConsistency` validator (enabled by default, severity `warning`) checks each overload against its own signature and reports:
|
|
507
|
+
|
|
508
|
+
- a `@param` that names no parameter of that overload
|
|
509
|
+
- a parameter with no `@param`, when the overload documents at least one other parameter (splat and block parameters are not required; a `@param` on the method docstring counts for every overload)
|
|
510
|
+
- an overload with no `@return`, when another overload of the method has one and the method docstring has none
|
|
511
|
+
|
|
512
|
+
```ruby
|
|
513
|
+
# Bad - reports: Overload 'fetch(key)' documents @param 'name', which is not in its signature
|
|
514
|
+
# Overload 'fetch(key)' has no @param for 'key'
|
|
515
|
+
# @overload fetch(key)
|
|
516
|
+
# @param name [String] the key
|
|
517
|
+
# @return [Object]
|
|
518
|
+
def fetch(*args); end
|
|
519
|
+
|
|
520
|
+
# Good
|
|
521
|
+
# @overload fetch(key)
|
|
522
|
+
# @param key [String] the key
|
|
523
|
+
# @return [Object]
|
|
524
|
+
def fetch(*args); end
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Overloads that forward arguments (`...`) or take anonymous `*`, `**` or `&` parameters are not checked for unknown names.
|
|
528
|
+
|
|
437
529
|
## Handling Non-Standard Types
|
|
438
530
|
|
|
439
531
|
By default `Tags/InvalidTypes` accepts all built-in Ruby classes, constants, and a set of YARD pseudo-types (`nil`, `true`, `false`, `self`, `void`, `Boolean`, `undefined`, `unspecified`, `unknown`). If your project uses additional type names that are not real Ruby classes - project-specific aliases, LSP extensions, or informal conventions - you can declare them via `ExtraTypes` so yard-lint does not report them as `InvalidTagType` offenses.
|
|
@@ -530,7 +622,7 @@ Configuration:
|
|
|
530
622
|
-c, --config FILE Path to config file (default: .yard-lint.yml)
|
|
531
623
|
|
|
532
624
|
Output:
|
|
533
|
-
-f, --format FORMAT Output format (text, json, quickfix)
|
|
625
|
+
-f, --format FORMAT Output format (text, json, quickfix, sarif)
|
|
534
626
|
-q, --quiet Quiet mode (only show summary)
|
|
535
627
|
--stats Show documentation coverage statistics
|
|
536
628
|
--[no-]progress Show progress indicator (default: auto-detect TTY)
|
|
@@ -555,6 +647,7 @@ Configuration Generation:
|
|
|
555
647
|
--force Force overwrite when using --init
|
|
556
648
|
--auto-gen-config Generate .yard-lint-todo.yml to silence existing violations
|
|
557
649
|
--regenerate-todo Regenerate .yard-lint-todo.yml (overwrites existing)
|
|
650
|
+
--check-todo Report .yard-lint-todo.yml entries that no longer match any offense
|
|
558
651
|
--exclude-limit N Min files before grouping into pattern (default: 15)
|
|
559
652
|
|
|
560
653
|
Information:
|
data/bin/yard-lint
CHANGED
|
@@ -18,7 +18,7 @@ begin
|
|
|
18
18
|
config_file = file
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
opts.on('-f', '--format FORMAT', 'Output format (text, json, quickfix)') do |format|
|
|
21
|
+
opts.on('-f', '--format FORMAT', 'Output format (text, json, quickfix, sarif)') do |format|
|
|
22
22
|
options[:format] = format
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -99,6 +99,10 @@ begin
|
|
|
99
99
|
options[:force] = true
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
opts.on('--check-todo', 'Report .yard-lint-todo.yml entries that no longer match any offense') do
|
|
103
|
+
options[:check_todo] = true
|
|
104
|
+
end
|
|
105
|
+
|
|
102
106
|
opts.on('--exclude-limit COUNT', Integer, 'Min files before grouping into pattern (default: 15)') do |count|
|
|
103
107
|
options[:exclude_limit] = count
|
|
104
108
|
end
|
|
@@ -122,12 +126,14 @@ begin
|
|
|
122
126
|
puts ' yard-lint --changed # Lint only uncommitted files'
|
|
123
127
|
puts ' yard-lint --format json lib/ # Output in JSON format'
|
|
124
128
|
puts ' yard-lint --format quickfix lib/ # Output in quickfix format (Vim/Emacs)'
|
|
129
|
+
puts ' yard-lint --format sarif lib/ # Output SARIF 2.1.0 (GitHub code scanning)'
|
|
125
130
|
puts ' yard-lint --init # Generate default config'
|
|
126
131
|
puts ' yard-lint --init --strict # Generate strict config'
|
|
127
132
|
puts ' yard-lint --update # Update config with new validators'
|
|
128
133
|
puts ' yard-lint --auto-gen-config # Generate todo file for existing violations'
|
|
129
134
|
puts ' yard-lint --regenerate-todo # Regenerate todo file'
|
|
130
135
|
puts ' yard-lint --auto-gen-config --exclude-limit 10 # Custom grouping threshold'
|
|
136
|
+
puts ' yard-lint --check-todo # Fail if todo file has unused entries'
|
|
131
137
|
puts ' cat lib/foo.rb | yard-lint --stdin lib/foo.rb # Lint source piped from stdin'
|
|
132
138
|
exit
|
|
133
139
|
end
|
|
@@ -154,6 +160,35 @@ report_unknown_validators = lambda do |names|
|
|
|
154
160
|
puts "\nRun `yard-lint --explain VALIDATOR` to see what a validator checks."
|
|
155
161
|
end
|
|
156
162
|
|
|
163
|
+
# Apply --only filter if provided, exiting on unknown validator names
|
|
164
|
+
apply_only = lambda do |config|
|
|
165
|
+
next unless options[:only]
|
|
166
|
+
|
|
167
|
+
unknown = options[:only] - Yard::Lint::ConfigLoader::ALL_VALIDATORS
|
|
168
|
+
if unknown.any?
|
|
169
|
+
report_unknown_validators.call(unknown)
|
|
170
|
+
exit 1
|
|
171
|
+
end
|
|
172
|
+
config.only_validators = options[:only]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Load the config (-c file or auto-discovered), exiting with a configuration error
|
|
176
|
+
# instead of a backtrace when it is missing, invalid or inherits in a cycle.
|
|
177
|
+
# Shared by the normal run, --auto-gen-config and --check-todo.
|
|
178
|
+
load_config = lambda do
|
|
179
|
+
if config_file
|
|
180
|
+
Yard::Lint::Config.from_file(config_file)
|
|
181
|
+
else
|
|
182
|
+
Yard::Lint::Config.load || Yard::Lint::Config.new
|
|
183
|
+
end
|
|
184
|
+
rescue Yard::Lint::Errors::ConfigFileNotFoundError, Yard::Lint::Errors::CircularDependencyError => e
|
|
185
|
+
puts "Configuration Error: #{e.message}"
|
|
186
|
+
exit 1
|
|
187
|
+
rescue Yard::Lint::Errors::InvalidConfigError => e
|
|
188
|
+
puts "Configuration Error:\n#{e.message}"
|
|
189
|
+
exit 1
|
|
190
|
+
end
|
|
191
|
+
|
|
157
192
|
# Handle --explain flag (early exit; needs no config or path)
|
|
158
193
|
if options[:explain]
|
|
159
194
|
name = options[:explain]
|
|
@@ -221,12 +256,7 @@ end
|
|
|
221
256
|
# Handle --auto-gen-config flag
|
|
222
257
|
if options[:auto_gen_config]
|
|
223
258
|
begin
|
|
224
|
-
|
|
225
|
-
config = if config_file
|
|
226
|
-
Yard::Lint::Config.from_file(config_file)
|
|
227
|
-
else
|
|
228
|
-
Yard::Lint::Config.load || Yard::Lint::Config.new
|
|
229
|
-
end
|
|
259
|
+
config = load_config.call
|
|
230
260
|
|
|
231
261
|
# Get path argument
|
|
232
262
|
path = ARGV[0] || '.'
|
|
@@ -254,6 +284,26 @@ if options[:auto_gen_config]
|
|
|
254
284
|
end
|
|
255
285
|
end
|
|
256
286
|
|
|
287
|
+
# Handle --check-todo flag
|
|
288
|
+
if options[:check_todo]
|
|
289
|
+
begin
|
|
290
|
+
config = load_config.call
|
|
291
|
+
apply_only.call(config)
|
|
292
|
+
|
|
293
|
+
result = Yard::Lint::TodoGenerator.check(path: ARGV[0] || '.', config: config)
|
|
294
|
+
|
|
295
|
+
puts result[:message]
|
|
296
|
+
exit result[:unused_entries].empty? ? 0 : 1
|
|
297
|
+
rescue Yard::Lint::Errors::TodoFileNotFoundError, Yard::Lint::Errors::FileNotFoundError,
|
|
298
|
+
Yard::Lint::Git::Error => e
|
|
299
|
+
puts "Error: #{e.message}"
|
|
300
|
+
exit 1
|
|
301
|
+
rescue Yard::Lint::Errors::InvalidConfigError => e
|
|
302
|
+
puts "Configuration Error:\n#{e.message}"
|
|
303
|
+
exit 1
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
257
307
|
# Get path argument (defaults to current directory)
|
|
258
308
|
path = ARGV[0] || '.'
|
|
259
309
|
|
|
@@ -271,43 +321,23 @@ end
|
|
|
271
321
|
YARD::Registry.clear
|
|
272
322
|
|
|
273
323
|
# Load config and apply CLI overrides
|
|
274
|
-
|
|
275
|
-
config = if config_file
|
|
276
|
-
Yard::Lint::Config.from_file(config_file)
|
|
277
|
-
else
|
|
278
|
-
Yard::Lint::Config.load || Yard::Lint::Config.new
|
|
279
|
-
end
|
|
280
|
-
rescue Yard::Lint::Errors::ConfigFileNotFoundError => e
|
|
281
|
-
puts "Configuration Error: #{e.message}"
|
|
282
|
-
exit 1
|
|
283
|
-
rescue Yard::Lint::Errors::InvalidConfigError => e
|
|
284
|
-
puts "Configuration Error:\n#{e.message}"
|
|
285
|
-
exit 1
|
|
286
|
-
end
|
|
324
|
+
config = load_config.call
|
|
287
325
|
|
|
288
326
|
# Apply CLI min_coverage override if provided
|
|
289
327
|
config.min_coverage = options[:min_coverage] if options[:min_coverage]
|
|
290
328
|
|
|
291
|
-
|
|
292
|
-
if options[:only]
|
|
293
|
-
unknown = options[:only] - Yard::Lint::ConfigLoader::ALL_VALIDATORS
|
|
294
|
-
if unknown.any?
|
|
295
|
-
report_unknown_validators.call(unknown)
|
|
296
|
-
exit 1
|
|
297
|
-
end
|
|
298
|
-
config.only_validators = options[:only]
|
|
299
|
-
end
|
|
329
|
+
apply_only.call(config)
|
|
300
330
|
|
|
301
331
|
# Validate the output format before doing any (potentially long) work, rather
|
|
302
332
|
# than running the whole lint and only then rejecting an unknown format.
|
|
303
|
-
valid_formats = %w[text json quickfix]
|
|
333
|
+
valid_formats = %w[text json quickfix sarif]
|
|
304
334
|
if options[:format] && !valid_formats.include?(options[:format])
|
|
305
335
|
puts "Error: Unknown format '#{options[:format]}'. Valid formats: #{valid_formats.join(', ')}"
|
|
306
336
|
exit 1
|
|
307
337
|
end
|
|
308
338
|
|
|
309
|
-
# Suppress progress for
|
|
310
|
-
options[:progress] = false if options[:format]
|
|
339
|
+
# Suppress progress for machine-readable formats to avoid contaminating output
|
|
340
|
+
options[:progress] = false if %w[quickfix sarif].include?(options[:format]) && options[:progress].nil?
|
|
311
341
|
|
|
312
342
|
# Run the linter
|
|
313
343
|
begin
|
|
@@ -356,6 +386,12 @@ when 'quickfix'
|
|
|
356
386
|
puts "#{offense[:location]}:#{offense[:location_line]}: #{char}: #{offense[:validator]}: #{message}"
|
|
357
387
|
end
|
|
358
388
|
exit result.exit_code
|
|
389
|
+
when 'sarif'
|
|
390
|
+
# Base paths on the repository root so GitHub can map offense URIs onto files
|
|
391
|
+
# even when yard-lint is run from a subdirectory.
|
|
392
|
+
sarif = Yard::Lint::Formatters::Sarif.new(result.offenses, base_dir: Yard::Lint::Git.repository_root)
|
|
393
|
+
puts sarif.generate
|
|
394
|
+
exit result.exit_code
|
|
359
395
|
when 'text', nil
|
|
360
396
|
# Calculate coverage stats if requested or configured
|
|
361
397
|
coverage = result.documentation_coverage if options[:stats] || options[:min_coverage] || config.min_coverage
|
data/lib/yard/lint/config.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Yard
|
|
|
6
6
|
class Config
|
|
7
7
|
attr_reader :raw_config, :validators
|
|
8
8
|
attr_accessor :only_validators
|
|
9
|
+
attr_writer :base_dir
|
|
9
10
|
|
|
10
11
|
# Default YAML config file name
|
|
11
12
|
DEFAULT_CONFIG_FILE = '.yard-lint.yml'
|
|
@@ -16,6 +17,9 @@ module Yard
|
|
|
16
17
|
# Metadata keys to skip when merging validator configs
|
|
17
18
|
METADATA_KEYS = %w[Description StyleGuide VersionAdded VersionChanged].freeze
|
|
18
19
|
|
|
20
|
+
# File.fnmatch flags used for Exclude patterns
|
|
21
|
+
EXCLUDE_FLAGS = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
22
|
+
|
|
19
23
|
# @param raw_config [Hash] raw configuration hash (new hierarchical format)
|
|
20
24
|
def initialize(raw_config = {})
|
|
21
25
|
@raw_config = raw_config
|
|
@@ -42,7 +46,7 @@ module Yard
|
|
|
42
46
|
# Load with inheritance support
|
|
43
47
|
merged_yaml = ConfigLoader.load(path)
|
|
44
48
|
|
|
45
|
-
new(merged_yaml)
|
|
49
|
+
new(merged_yaml) { |config| config.base_dir = File.dirname(File.expand_path(path)) }
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
# Search for and load config file from current directory upwards
|
|
@@ -173,29 +177,101 @@ module Yard
|
|
|
173
177
|
end
|
|
174
178
|
|
|
175
179
|
# Get validator severity
|
|
180
|
+
#
|
|
181
|
+
# Resolution order (highest priority first):
|
|
182
|
+
# 1. an explicit per-validator Severity in the user's config
|
|
183
|
+
# 2. a category-level Severity (e.g. `Documentation: { Severity: convention }`),
|
|
184
|
+
# matching how category-level Enabled cascades in #validator_enabled?
|
|
185
|
+
# 3. the validator's built-in default Severity
|
|
186
|
+
# 4. the global 'warning' default
|
|
187
|
+
#
|
|
176
188
|
# @param validator_name [String] full validator name
|
|
177
189
|
# @return [String] severity level for this validator
|
|
178
190
|
def validator_severity(validator_name)
|
|
191
|
+
# An explicit per-validator Severity in the user's config wins.
|
|
192
|
+
raw_validator = @raw_config[validator_name]
|
|
193
|
+
if raw_validator.is_a?(Hash) && raw_validator['Severity']
|
|
194
|
+
return raw_validator['Severity'].to_s
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Otherwise honor a category-level Severity, which previously validated
|
|
198
|
+
# but was ignored (the category-level Enabled key already cascades).
|
|
199
|
+
category = validator_name.split('/').first
|
|
200
|
+
raw_category = @raw_config[category]
|
|
201
|
+
if raw_category.is_a?(Hash) && raw_category['Severity']
|
|
202
|
+
return raw_category['Severity'].to_s
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Fall back to the validator's built-in default, then the global default.
|
|
179
206
|
validator_config = validators[validator_name] || {}
|
|
180
207
|
validator_config['Severity'] || 'warning'
|
|
181
208
|
end
|
|
182
209
|
|
|
183
210
|
# Get validator-specific exclude patterns
|
|
211
|
+
#
|
|
212
|
+
# A category-level Exclude (e.g. `Documentation: { Exclude: [...] }`) is
|
|
213
|
+
# unioned with the per-validator one rather than replaced by it, so a
|
|
214
|
+
# validator that sets its own Exclude still skips its category's files.
|
|
215
|
+
#
|
|
184
216
|
# @param validator_name [String] full validator name
|
|
185
|
-
# @return [Array<String>] exclusion patterns for this validator
|
|
217
|
+
# @return [Array<String>] exclusion patterns for this validator (category + per-validator)
|
|
186
218
|
def validator_exclude(validator_name)
|
|
187
219
|
validator_config = validators[validator_name] || {}
|
|
188
|
-
|
|
220
|
+
raw_category = @raw_config[validator_name.split('/').first]
|
|
221
|
+
category_exclude = raw_category.is_a?(Hash) ? Array(raw_category['Exclude']) : []
|
|
222
|
+
|
|
223
|
+
(category_exclude + (validator_config['Exclude'] || [])).uniq
|
|
189
224
|
end
|
|
190
225
|
|
|
191
|
-
# Combined global and per-validator exclusions
|
|
226
|
+
# Combined global, category and per-validator exclusions
|
|
192
227
|
# Returns all exclusion patterns that apply to this validator
|
|
193
228
|
# @param validator_name [String] full validator name
|
|
194
|
-
# @return [Array<String>] combined exclusion patterns (global + per-validator)
|
|
229
|
+
# @return [Array<String>] combined exclusion patterns (global + category + per-validator)
|
|
195
230
|
def validator_all_excludes(validator_name)
|
|
196
231
|
exclude + validator_exclude(validator_name)
|
|
197
232
|
end
|
|
198
233
|
|
|
234
|
+
# Directory that Exclude patterns are relative to
|
|
235
|
+
# @return [String] directory of the loaded config file, or the current directory without one
|
|
236
|
+
def base_dir
|
|
237
|
+
@base_dir || Dir.pwd
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Whether a file matches any of the given Exclude patterns
|
|
241
|
+
# A pattern matches the path relative to {#base_dir} or the absolute path
|
|
242
|
+
# @param file [String] absolute file path, or one relative to the current directory
|
|
243
|
+
# @param patterns [Array<String>] glob patterns
|
|
244
|
+
# @return [Boolean] true if any pattern matches
|
|
245
|
+
def excluded_path?(file, patterns)
|
|
246
|
+
absolute = File.expand_path(file)
|
|
247
|
+
relative = relative_path(absolute)
|
|
248
|
+
|
|
249
|
+
patterns.any? do |pattern|
|
|
250
|
+
File.fnmatch(pattern, relative, EXCLUDE_FLAGS) || File.fnmatch(pattern, absolute, EXCLUDE_FLAGS)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Path of a file relative to {#base_dir}
|
|
255
|
+
# Symlinks are resolved when the plain paths do not nest (/var is /private/var on macOS)
|
|
256
|
+
# @param file [String] absolute file path, or one relative to the current directory
|
|
257
|
+
# @return [String] relative path (with ../ for a file outside {#base_dir}), or the absolute
|
|
258
|
+
# path when there is no relative one (another drive)
|
|
259
|
+
def relative_path(file)
|
|
260
|
+
absolute = File.expand_path(file)
|
|
261
|
+
base = File.expand_path(base_dir)
|
|
262
|
+
return absolute.delete_prefix("#{base}/") if absolute.start_with?("#{base}/")
|
|
263
|
+
|
|
264
|
+
from, to = begin
|
|
265
|
+
[File.realpath(absolute), File.realpath(base)]
|
|
266
|
+
rescue SystemCallError
|
|
267
|
+
[absolute, base]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
Pathname.new(from).relative_path_from(Pathname.new(to)).to_s
|
|
271
|
+
rescue ArgumentError
|
|
272
|
+
absolute
|
|
273
|
+
end
|
|
274
|
+
|
|
199
275
|
# Get validator-specific configuration value
|
|
200
276
|
# @param validator_name [String] full validator name
|
|
201
277
|
# @param key [String] configuration key
|
|
@@ -4,23 +4,9 @@ module Yard
|
|
|
4
4
|
module Lint
|
|
5
5
|
# Validates configuration structure and values to catch typos and invalid settings
|
|
6
6
|
class ConfigValidator
|
|
7
|
-
# Valid category names (from ConfigUpdater::CATEGORY_ORDER)
|
|
8
|
-
VALID_CATEGORIES = %w[Documentation Tags Warnings Semantic].freeze
|
|
9
|
-
|
|
10
|
-
# Keys that are valid at the root level but are not validators
|
|
11
|
-
SPECIAL_KEYS = %w[
|
|
12
|
-
AllValidators
|
|
13
|
-
inherit_from
|
|
14
|
-
inherit_gem
|
|
15
|
-
].freeze
|
|
16
|
-
|
|
17
|
-
# Valid boolean values
|
|
18
|
-
BOOLEAN_VALUES = [true, false].freeze
|
|
19
|
-
|
|
20
7
|
# @param raw_config [Hash] raw configuration hash to validate
|
|
21
8
|
def initialize(raw_config)
|
|
22
9
|
@raw_config = raw_config
|
|
23
|
-
@errors = []
|
|
24
10
|
end
|
|
25
11
|
|
|
26
12
|
# Validate configuration and raise error if invalid
|
|
@@ -36,191 +22,21 @@ module Yard
|
|
|
36
22
|
# @return [void]
|
|
37
23
|
# @raise [Errors::InvalidConfigError] if configuration is invalid
|
|
38
24
|
def validate!
|
|
39
|
-
|
|
40
|
-
validate_global_settings!
|
|
41
|
-
validate_validators!
|
|
25
|
+
result = Contracts::Config.new.call(@raw_config)
|
|
42
26
|
|
|
43
|
-
return if
|
|
27
|
+
return if result.success?
|
|
44
28
|
|
|
45
|
-
raise Errors::InvalidConfigError, build_error_message
|
|
29
|
+
raise Errors::InvalidConfigError, build_error_message(result.errors.values)
|
|
46
30
|
end
|
|
47
31
|
|
|
48
32
|
private
|
|
49
33
|
|
|
50
|
-
# Validate root-level keys in configuration
|
|
51
|
-
def validate_root_keys!
|
|
52
|
-
@raw_config.each_key do |key|
|
|
53
|
-
# Skip special keys
|
|
54
|
-
next if SPECIAL_KEYS.include?(key)
|
|
55
|
-
|
|
56
|
-
# Skip category-level configs (e.g., 'Documentation', 'Tags')
|
|
57
|
-
next if VALID_CATEGORIES.include?(key)
|
|
58
|
-
|
|
59
|
-
# Validator names must contain a '/' (category/name format)
|
|
60
|
-
next if key.include?('/')
|
|
61
|
-
|
|
62
|
-
# If we get here, it's an unknown validator (missing category prefix)
|
|
63
|
-
@errors << "Unknown validator: '#{key}'"
|
|
64
|
-
suggest_validator_name(key)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Validate AllValidators section
|
|
69
|
-
def validate_global_settings!
|
|
70
|
-
all_validators = @raw_config['AllValidators']
|
|
71
|
-
return unless all_validators
|
|
72
|
-
|
|
73
|
-
unless all_validators.is_a?(Hash)
|
|
74
|
-
@errors << "Invalid AllValidators: must be a Hash, got #{all_validators.class}"
|
|
75
|
-
return
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Only validate specific known settings, allow unknown keys to pass through
|
|
79
|
-
# This allows users to put custom data in AllValidators
|
|
80
|
-
|
|
81
|
-
# Validate FailOnSeverity value
|
|
82
|
-
fail_on = all_validators['FailOnSeverity']
|
|
83
|
-
if fail_on && !Config::VALID_SEVERITIES.include?(fail_on.to_s)
|
|
84
|
-
@errors << "Invalid FailOnSeverity: '#{fail_on}'"
|
|
85
|
-
@errors << " Valid values: #{Config::VALID_SEVERITIES.join(', ')}"
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Validate MinCoverage value
|
|
89
|
-
min_cov = all_validators['MinCoverage']
|
|
90
|
-
if min_cov && (!min_cov.is_a?(Numeric) || min_cov < 0 || min_cov > 100)
|
|
91
|
-
@errors << "Invalid MinCoverage: '#{min_cov}'"
|
|
92
|
-
@errors << ' Must be a number between 0 and 100'
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Validate Exclude is an array
|
|
96
|
-
exclude = all_validators['Exclude']
|
|
97
|
-
if exclude && !exclude.is_a?(Array)
|
|
98
|
-
@errors << "Invalid Exclude in AllValidators: must be an array, got #{exclude.class}"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Validate YardOptions is an array
|
|
102
|
-
yard_options = all_validators['YardOptions']
|
|
103
|
-
if yard_options && !yard_options.is_a?(Array)
|
|
104
|
-
@errors << "Invalid YardOptions in AllValidators: must be an array, got #{yard_options.class}"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Validate validator-specific configurations
|
|
109
|
-
def validate_validators!
|
|
110
|
-
@raw_config.each do |key, value|
|
|
111
|
-
# Only process validator keys (containing '/')
|
|
112
|
-
next unless key.include?('/')
|
|
113
|
-
|
|
114
|
-
unless value.is_a?(Hash)
|
|
115
|
-
@errors << "Invalid configuration for validator '#{key}': expected a Hash, got #{value.class}"
|
|
116
|
-
next
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
validate_validator_exists!(key)
|
|
120
|
-
validate_validator_config!(key, value)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Check if validator name exists
|
|
125
|
-
# @param validator_name [String] validator name to check
|
|
126
|
-
# @return [void]
|
|
127
|
-
def validate_validator_exists!(validator_name)
|
|
128
|
-
return if ConfigLoader::ALL_VALIDATORS.include?(validator_name)
|
|
129
|
-
|
|
130
|
-
@errors << "Unknown validator: '#{validator_name}'"
|
|
131
|
-
suggest_validator_name(validator_name)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Validate individual validator configuration
|
|
135
|
-
# @param validator_name [String] validator name
|
|
136
|
-
# @param config [Hash] validator configuration hash
|
|
137
|
-
# @return [void]
|
|
138
|
-
def validate_validator_config!(validator_name, config)
|
|
139
|
-
# Validate Enabled value
|
|
140
|
-
enabled = config['Enabled']
|
|
141
|
-
if enabled && !BOOLEAN_VALUES.include?(enabled)
|
|
142
|
-
@errors << "Invalid Enabled value for #{validator_name}: '#{enabled}'"
|
|
143
|
-
@errors << ' Must be true or false'
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Validate Severity value
|
|
147
|
-
severity = config['Severity']
|
|
148
|
-
if severity && !Config::VALID_SEVERITIES.include?(severity.to_s)
|
|
149
|
-
@errors << "Invalid Severity for #{validator_name}: '#{severity}'"
|
|
150
|
-
@errors << " Valid values: #{Config::VALID_SEVERITIES.join(', ')}"
|
|
151
|
-
suggest_similar_severity(severity.to_s)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Validate Exclude is an array
|
|
155
|
-
exclude = config['Exclude']
|
|
156
|
-
if exclude && !exclude.is_a?(Array)
|
|
157
|
-
@errors << "Invalid Exclude for #{validator_name}: must be an array, got #{exclude.class}"
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Validate YardOptions is an array
|
|
161
|
-
yard_options = config['YardOptions']
|
|
162
|
-
if yard_options && !yard_options.is_a?(Array)
|
|
163
|
-
@errors << "Invalid YardOptions for #{validator_name}: must be an array, got #{yard_options.class}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Check for unknown validator-specific keys
|
|
167
|
-
validate_validator_specific_keys!(validator_name, config)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Validate validator-specific configuration keys
|
|
171
|
-
# @param validator_name [String] validator name
|
|
172
|
-
# @param config [Hash] validator configuration hash
|
|
173
|
-
# @return [void]
|
|
174
|
-
def validate_validator_specific_keys!(validator_name, config)
|
|
175
|
-
# Skip if validator doesn't exist (already reported)
|
|
176
|
-
return unless ConfigLoader::ALL_VALIDATORS.include?(validator_name)
|
|
177
|
-
|
|
178
|
-
validator_config_class = ConfigLoader.validator_config(validator_name)
|
|
179
|
-
return unless validator_config_class
|
|
180
|
-
|
|
181
|
-
# Base config keys that are valid for all validators
|
|
182
|
-
base_keys = %w[Enabled Severity Exclude YardOptions]
|
|
183
|
-
valid_keys = validator_config_class.defaults.keys + Config::METADATA_KEYS + base_keys
|
|
184
|
-
|
|
185
|
-
config.each_key do |key|
|
|
186
|
-
next if valid_keys.include?(key)
|
|
187
|
-
|
|
188
|
-
@errors << "Unknown configuration key for #{validator_name}: '#{key}'"
|
|
189
|
-
@errors << " Valid keys: #{valid_keys.uniq.sort.join(', ')}"
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Suggest similar validator names using did_you_mean
|
|
194
|
-
# @param invalid_name [String] invalid validator name
|
|
195
|
-
# @return [void]
|
|
196
|
-
def suggest_validator_name(invalid_name)
|
|
197
|
-
checker = DidYouMean::SpellChecker.new(dictionary: ConfigLoader::ALL_VALIDATORS)
|
|
198
|
-
suggestions = checker.correct(invalid_name)
|
|
199
|
-
|
|
200
|
-
if suggestions.any?
|
|
201
|
-
@errors << " Did you mean: #{suggestions.first}?"
|
|
202
|
-
else
|
|
203
|
-
@errors << ' See https://github.com/mensfeld/yard-lint/wiki/Validators ' \
|
|
204
|
-
'for all available validators'
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Suggest similar severity values
|
|
209
|
-
# @param invalid_severity [String] invalid severity value
|
|
210
|
-
# @return [void]
|
|
211
|
-
def suggest_similar_severity(invalid_severity)
|
|
212
|
-
checker = DidYouMean::SpellChecker.new(dictionary: Config::VALID_SEVERITIES)
|
|
213
|
-
suggestions = checker.correct(invalid_severity)
|
|
214
|
-
|
|
215
|
-
if suggestions.any?
|
|
216
|
-
@errors << " Did you mean: #{suggestions.first}?"
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
34
|
# Build comprehensive error message
|
|
221
|
-
|
|
35
|
+
# @param messages [Array<String>] error messages, each one or more lines
|
|
36
|
+
# @return [String] error message with one indented line per error line
|
|
37
|
+
def build_error_message(messages)
|
|
222
38
|
header = 'Invalid configuration detected:'
|
|
223
|
-
errors_text =
|
|
39
|
+
errors_text = messages.flat_map { |message| message.split("\n") }.map { |e| " #{e}" }.join("\n")
|
|
224
40
|
|
|
225
41
|
"#{header}\n#{errors_text}"
|
|
226
42
|
end
|