yard-lint 1.11.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +201 -498
  3. data/README.md +103 -1
  4. data/bin/yard-lint +100 -40
  5. data/lib/yard/lint/config.rb +81 -5
  6. data/lib/yard/lint/config_validator.rb +7 -191
  7. data/lib/yard/lint/contractable/contract.rb +219 -0
  8. data/lib/yard/lint/contractable/result.rb +93 -0
  9. data/lib/yard/lint/contractable/rule.rb +10 -0
  10. data/lib/yard/lint/contracts/base.rb +57 -0
  11. data/lib/yard/lint/contracts/category.rb +71 -0
  12. data/lib/yard/lint/contracts/config.rb +81 -0
  13. data/lib/yard/lint/contracts/validator.rb +72 -0
  14. data/lib/yard/lint/errors.rb +3 -0
  15. data/lib/yard/lint/executor/in_process_registry.rb +1 -1
  16. data/lib/yard/lint/explainer.rb +205 -0
  17. data/lib/yard/lint/formatters/sarif.rb +218 -0
  18. data/lib/yard/lint/git.rb +5 -3
  19. data/lib/yard/lint/result_builder.rb +7 -6
  20. data/lib/yard/lint/runner.rb +2 -21
  21. data/lib/yard/lint/templates/default_config.yml +10 -0
  22. data/lib/yard/lint/templates/strict_config.yml +10 -0
  23. data/lib/yard/lint/todo_generator.rb +143 -12
  24. data/lib/yard/lint/validators/base.rb +63 -0
  25. data/lib/yard/lint/validators/documentation/reference_typo/config.rb +20 -0
  26. data/lib/yard/lint/validators/documentation/reference_typo/messages_builder.rb +24 -0
  27. data/lib/yard/lint/validators/documentation/reference_typo/parser.rb +44 -0
  28. data/lib/yard/lint/validators/documentation/reference_typo/result.rb +27 -0
  29. data/lib/yard/lint/validators/documentation/reference_typo/validator.rb +214 -0
  30. data/lib/yard/lint/validators/documentation/reference_typo.rb +39 -0
  31. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +13 -12
  32. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +84 -5
  33. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +16 -3
  34. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +3 -2
  35. data/lib/yard/lint/validators/tags/example_style/validator.rb +4 -4
  36. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +4 -2
  37. data/lib/yard/lint/validators/tags/invalid_types/messages_builder.rb +15 -4
  38. data/lib/yard/lint/validators/tags/invalid_types/parser.rb +18 -7
  39. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +22 -12
  40. data/lib/yard/lint/validators/tags/overload_consistency/config.rb +20 -0
  41. data/lib/yard/lint/validators/tags/overload_consistency/messages_builder.rb +32 -0
  42. data/lib/yard/lint/validators/tags/overload_consistency/parser.rb +49 -0
  43. data/lib/yard/lint/validators/tags/overload_consistency/result.rb +27 -0
  44. data/lib/yard/lint/validators/tags/overload_consistency/validator.rb +71 -0
  45. data/lib/yard/lint/validators/tags/overload_consistency.rb +53 -0
  46. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +3 -3
  47. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +8 -0
  48. data/lib/yard/lint/validators/tags/tag_separator/validator.rb +8 -0
  49. data/lib/yard/lint/validators/tags/type_syntax/parser.rb +13 -2
  50. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +11 -1
  51. data/lib/yard/lint/version.rb +1 -1
  52. data/lib/yard/lint.rb +3 -57
  53. metadata +23 -2
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
@@ -143,6 +149,14 @@ yard-lint lib/ --only Tags/Order,Documentation/UndocumentedObjects
143
149
 
144
150
  **Learn more:** [Advanced Usage Guide](https://github.com/mensfeld/yard-lint/wiki/Advanced-Usage)
145
151
 
152
+ ### Explain a Validator
153
+
154
+ Print what a validator checks, its default severity, configuration options, and Bad/Good examples directly in the terminal - no need to open the wiki. The explanation is sourced from the validator's own YARD documentation.
155
+
156
+ ```bash
157
+ yard-lint --explain Tags/TypeSyntax
158
+ ```
159
+
146
160
  ### Lint from stdin (LSP / Editor Integration)
147
161
 
148
162
  Pass source bytes directly without reading from disk. The `path` argument is still required - it governs config resolution, exclusion matching, and offense location reporting.
@@ -200,6 +214,26 @@ Then `:make` lints the current file and `:cnext` / `:cprev` jump between offense
200
214
 
201
215
  Then `M-x compile` and `M-g n` / `M-g p` navigate between offenses.
202
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
+
203
237
  ## Configuration Basics
204
238
 
205
239
  Create a `.yard-lint.yml` file in your project root:
@@ -314,10 +348,35 @@ Documentation/UnderfilledLines:
314
348
 
315
349
  **Key features:**
316
350
  - Per-validator control (enable/disable, severity, exclusions)
351
+ - Category-level defaults (enable/disable, severity, exclusions) for a whole department
317
352
  - Configuration inheritance with `inherit_from` and `inherit_gem`
318
353
  - Automatic configuration validation with helpful error messages
319
354
  - Per-validator YARD options and file exclusions
320
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
+
321
380
  **Learn more:** [Complete Configuration Guide](https://github.com/mensfeld/yard-lint/wiki/Configuration)
322
381
 
323
382
  ## Catching Orphaned Documentation Comments
@@ -426,6 +485,47 @@ The validator reports one offense per wasteful paragraph and is deliberately con
426
485
 
427
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.
428
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
+
429
529
  ## Handling Non-Standard Types
430
530
 
431
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.
@@ -522,7 +622,7 @@ Configuration:
522
622
  -c, --config FILE Path to config file (default: .yard-lint.yml)
523
623
 
524
624
  Output:
525
- -f, --format FORMAT Output format (text, json, quickfix)
625
+ -f, --format FORMAT Output format (text, json, quickfix, sarif)
526
626
  -q, --quiet Quiet mode (only show summary)
527
627
  --stats Show documentation coverage statistics
528
628
  --[no-]progress Show progress indicator (default: auto-detect TTY)
@@ -538,6 +638,7 @@ Diff Mode:
538
638
 
539
639
  Validators:
540
640
  --only VALIDATORS Run only specified validators (comma-separated)
641
+ --explain VALIDATOR Explain what a validator checks and how to configure it
541
642
 
542
643
  Configuration Generation:
543
644
  --init Generate .yard-lint.yml config file
@@ -546,6 +647,7 @@ Configuration Generation:
546
647
  --force Force overwrite when using --init
547
648
  --auto-gen-config Generate .yard-lint-todo.yml to silence existing violations
548
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
549
651
  --exclude-limit N Min files before grouping into pattern (default: 15)
550
652
 
551
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
 
@@ -64,6 +64,10 @@ begin
64
64
  options[:only] = validators.split(',').map(&:strip)
65
65
  end
66
66
 
67
+ opts.on('--explain VALIDATOR', 'Explain what a validator checks and how to configure it') do |validator|
68
+ options[:explain] = validator
69
+ end
70
+
67
71
  opts.separator ''
68
72
  opts.separator 'Other options:'
69
73
 
@@ -95,6 +99,10 @@ begin
95
99
  options[:force] = true
96
100
  end
97
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
+
98
106
  opts.on('--exclude-limit COUNT', Integer, 'Min files before grouping into pattern (default: 15)') do |count|
99
107
  options[:exclude_limit] = count
100
108
  end
@@ -112,17 +120,20 @@ begin
112
120
  puts ' yard-lint lib/ # Lint all files in lib/'
113
121
  puts ' yard-lint --only Tags/TypeSyntax lib/ # Run only one validator on lib/'
114
122
  puts ' yard-lint --only Tags/Order,Tags/TypeSyntax # Run specific validators'
123
+ puts ' yard-lint --explain Tags/TypeSyntax # Explain what a validator checks'
115
124
  puts ' yard-lint --diff main # Lint files changed since main branch'
116
125
  puts ' yard-lint --staged # Lint only staged files'
117
126
  puts ' yard-lint --changed # Lint only uncommitted files'
118
127
  puts ' yard-lint --format json lib/ # Output in JSON format'
119
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)'
120
130
  puts ' yard-lint --init # Generate default config'
121
131
  puts ' yard-lint --init --strict # Generate strict config'
122
132
  puts ' yard-lint --update # Update config with new validators'
123
133
  puts ' yard-lint --auto-gen-config # Generate todo file for existing violations'
124
134
  puts ' yard-lint --regenerate-todo # Regenerate todo file'
125
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'
126
137
  puts ' cat lib/foo.rb | yard-lint --stdin lib/foo.rb # Lint source piped from stdin'
127
138
  exit
128
139
  end
@@ -133,6 +144,62 @@ rescue OptionParser::ParseError => e
133
144
  exit 1
134
145
  end
135
146
 
147
+ # Report one or more unknown validator names with "did you mean" suggestions,
148
+ # the full list of available validators, and a pointer to --explain. Shared by
149
+ # --only and --explain so the two stay consistent.
150
+ report_unknown_validators = lambda do |names|
151
+ checker = DidYouMean::SpellChecker.new(dictionary: Yard::Lint::ConfigLoader::ALL_VALIDATORS)
152
+ messages = names.map do |name|
153
+ suggestions = checker.correct(name)
154
+ suggestions.any? ? " #{name} (did you mean: #{suggestions.first}?)" : " #{name}"
155
+ end
156
+ puts 'Error: Unknown validator(s):'
157
+ puts messages.join("\n")
158
+ puts "\nAvailable validators:"
159
+ Yard::Lint::ConfigLoader::ALL_VALIDATORS.each { |v| puts " #{v}" }
160
+ puts "\nRun `yard-lint --explain VALIDATOR` to see what a validator checks."
161
+ end
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
+
192
+ # Handle --explain flag (early exit; needs no config or path)
193
+ if options[:explain]
194
+ name = options[:explain]
195
+ unless Yard::Lint::ConfigLoader::ALL_VALIDATORS.include?(name)
196
+ report_unknown_validators.call([name])
197
+ exit 1
198
+ end
199
+ puts Yard::Lint::Explainer.call(name)
200
+ exit 0
201
+ end
202
+
136
203
  # Disambiguate `--diff PATH`. `--diff [REF]` takes an optional argument, so
137
204
  # `yard-lint --diff lib/` makes OptionParser consume `lib/` as the REF, leaving
138
205
  # no PATH (the run then tries `git diff lib/...HEAD` and fails). When the --diff
@@ -189,12 +256,7 @@ end
189
256
  # Handle --auto-gen-config flag
190
257
  if options[:auto_gen_config]
191
258
  begin
192
- # Load config
193
- config = if config_file
194
- Yard::Lint::Config.from_file(config_file)
195
- else
196
- Yard::Lint::Config.load || Yard::Lint::Config.new
197
- end
259
+ config = load_config.call
198
260
 
199
261
  # Get path argument
200
262
  path = ARGV[0] || '.'
@@ -222,6 +284,26 @@ if options[:auto_gen_config]
222
284
  end
223
285
  end
224
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
+
225
307
  # Get path argument (defaults to current directory)
226
308
  path = ARGV[0] || '.'
227
309
 
@@ -239,51 +321,23 @@ end
239
321
  YARD::Registry.clear
240
322
 
241
323
  # Load config and apply CLI overrides
242
- begin
243
- config = if config_file
244
- Yard::Lint::Config.from_file(config_file)
245
- else
246
- Yard::Lint::Config.load || Yard::Lint::Config.new
247
- end
248
- rescue Yard::Lint::Errors::ConfigFileNotFoundError => e
249
- puts "Configuration Error: #{e.message}"
250
- exit 1
251
- rescue Yard::Lint::Errors::InvalidConfigError => e
252
- puts "Configuration Error:\n#{e.message}"
253
- exit 1
254
- end
324
+ config = load_config.call
255
325
 
256
326
  # Apply CLI min_coverage override if provided
257
327
  config.min_coverage = options[:min_coverage] if options[:min_coverage]
258
328
 
259
- # Apply --only filter if provided
260
- if options[:only]
261
- unknown = options[:only] - Yard::Lint::ConfigLoader::ALL_VALIDATORS
262
- if unknown.any?
263
- checker = DidYouMean::SpellChecker.new(dictionary: Yard::Lint::ConfigLoader::ALL_VALIDATORS)
264
- messages = unknown.map do |name|
265
- suggestions = checker.correct(name)
266
- suggestions.any? ? " #{name} (did you mean: #{suggestions.first}?)" : " #{name}"
267
- end
268
- puts "Error: Unknown validator(s):"
269
- puts messages.join("\n")
270
- puts "\nAvailable validators:"
271
- Yard::Lint::ConfigLoader::ALL_VALIDATORS.each { |v| puts " #{v}" }
272
- exit 1
273
- end
274
- config.only_validators = options[:only]
275
- end
329
+ apply_only.call(config)
276
330
 
277
331
  # Validate the output format before doing any (potentially long) work, rather
278
332
  # than running the whole lint and only then rejecting an unknown format.
279
- valid_formats = %w[text json quickfix]
333
+ valid_formats = %w[text json quickfix sarif]
280
334
  if options[:format] && !valid_formats.include?(options[:format])
281
335
  puts "Error: Unknown format '#{options[:format]}'. Valid formats: #{valid_formats.join(', ')}"
282
336
  exit 1
283
337
  end
284
338
 
285
- # Suppress progress for quickfix to avoid contaminating machine-readable output
286
- options[:progress] = false if options[:format] == 'quickfix' && options[:progress].nil?
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?
287
341
 
288
342
  # Run the linter
289
343
  begin
@@ -332,6 +386,12 @@ when 'quickfix'
332
386
  puts "#{offense[:location]}:#{offense[:location_line]}: #{char}: #{offense[:validator]}: #{message}"
333
387
  end
334
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
335
395
  when 'text', nil
336
396
  # Calculate coverage stats if requested or configured
337
397
  coverage = result.documentation_coverage if options[:stats] || options[:min_coverage] || config.min_coverage
@@ -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
- validator_config['Exclude'] || []
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