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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +199 -501
  3. data/README.md +94 -1
  4. data/bin/yard-lint +68 -32
  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 +42 -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 +3 -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/type_syntax/parser.rb +13 -2
  48. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +11 -1
  49. data/lib/yard/lint/version.rb +1 -1
  50. data/lib/yard/lint.rb +3 -57
  51. metadata +21 -1
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ # Rule based data validation, copied from karafka-core
6
+ module Contractable
7
+ # Base contract for all the contracts that check data format
8
+ #
9
+ # Copied from karafka-core (`Karafka::Core::Contractable`), without its `Configurable`
10
+ # dependency: `error_messages` is a plain class-level accessor.
11
+ #
12
+ # @note This contract does NOT support rules inheritance
13
+ class Contract
14
+ # Constant representing a miss during dig
15
+ # We use it as a result value not to return an array with found object and a state to
16
+ # prevent additional array allocation
17
+ DIG_MISS = Object.new
18
+
19
+ # Empty array for scope default to avoid allocating a new Array on each
20
+ # `#call` / `#validate!` invocation. Safe because scope is never mutated - it is only
21
+ # used in `scope + rule.path` which creates a new Array.
22
+ EMPTY_ARRAY = [].freeze
23
+
24
+ private_constant :DIG_MISS, :EMPTY_ARRAY
25
+
26
+ # Class level API definitions
27
+ class << self
28
+ # @return [Array<Rule>] all the validation rules defined for a given contract
29
+ attr_reader :rules
30
+
31
+ attr_writer :error_messages
32
+
33
+ # @return [Hash{String => String}] error messages for error keys that rules return
34
+ # instead of custom message Strings
35
+ def error_messages
36
+ @error_messages || {}
37
+ end
38
+
39
+ # Allows for definition of a scope/namespace for nested validations
40
+ #
41
+ # @param path [Symbol] path in the hash for nesting
42
+ #
43
+ # @example
44
+ # nested(:key) do
45
+ # required(:inside) { |inside| inside.is_a?(String) }
46
+ # end
47
+ def nested(path, &)
48
+ init_accu
49
+ @nested << path
50
+ begin
51
+ instance_eval(&)
52
+ ensure
53
+ # Always pop, even if the block raises. Otherwise a rescued exception during
54
+ # contract definition would leave `path` on @nested and prefix it onto every rule
55
+ # defined afterwards.
56
+ @nested.pop
57
+ end
58
+ end
59
+
60
+ # Defines a rule for a required field (required means, that will automatically create an
61
+ # error if missing)
62
+ #
63
+ # @param keys [Array<Symbol>] single or full path
64
+ # @param block [Proc] validation rule
65
+ def required(*keys, &block)
66
+ init_accu
67
+ @rules << Rule.new(@nested + keys, :required, block).freeze
68
+ end
69
+
70
+ # @param keys [Array<Symbol>] single or full path
71
+ # @param block [Proc] validation rule
72
+ def optional(*keys, &block)
73
+ init_accu
74
+ @rules << Rule.new(@nested + keys, :optional, block).freeze
75
+ end
76
+
77
+ # Defines a virtual rule that validates the whole data rather than a single key. Unlike
78
+ # `required`/`optional`, the block receives the full data and returns its own errors.
79
+ #
80
+ # @param block [Proc] validation rule, called with `(data, errors, contract)`. It must
81
+ # return either a non-Array (`true`/`nil`/`false`) for "no errors", or an `Array` of
82
+ # `[path, message]` error pairs (where `path` is itself an `Array` of symbols).
83
+ #
84
+ # @note The returned error `Array` is owned by the contract: `#call` prepends the current
85
+ # scope onto each pair in place and collects them, so a rule must return a freshly
86
+ # built `Array` on every call. Returning a memoized, shared or frozen `Array` is not
87
+ # supported -- in-place scoping would accumulate the prefix across validations (or
88
+ # raise `FrozenError`). Build the result in the block (e.g. `[[%i[id], :invalid]]`)
89
+ # rather than returning a constant.
90
+ def virtual(&block)
91
+ init_accu
92
+ @rules << Rule.new([], :virtual, block).freeze
93
+ end
94
+
95
+ private
96
+
97
+ # Initializes nestings and rules building accumulator
98
+ def init_accu
99
+ @nested ||= []
100
+ @rules ||= []
101
+ end
102
+ end
103
+
104
+ # Runs the validation
105
+ #
106
+ # The per-rule handling is inlined instead of dispatching to per-type methods because
107
+ # this runs per rule per validation. Required and optional rules share the whole flow
108
+ # except the missing-key handling. `DIG_MISS` is compared via `#equal?` so we never dispatch `#==` to the
109
+ # validated (user-provided) values.
110
+ #
111
+ # @param data [Hash] hash with data we want to validate
112
+ # @param scope [Array<String>] scope of this contract (if any) or empty array if no parent
113
+ # scope is needed if contract starts from root
114
+ # @return [Result] validation result
115
+ def call(data, scope: EMPTY_ARRAY)
116
+ errors = []
117
+
118
+ # A non-Hash root has no keys to dig; resolve it once here so #dig stays on its lean
119
+ # fast path and is only invoked with a Hash. Non-Hash data makes every required rule
120
+ # report missing, consistent with the non-Hash intermediate handling inside #dig.
121
+ data_is_hash = data.is_a?(Hash)
122
+
123
+ self.class.rules.each do |rule|
124
+ if rule.type == :virtual
125
+ result = rule.validator.call(data, errors, self)
126
+
127
+ # A virtual rule signals "no errors" with any non-Array result (true, but also a
128
+ # falsy `nil`/`false` returned by e.g. `condition && [[...], :err]`). Only an Array
129
+ # of error pairs is iterated.
130
+ next unless result.is_a?(Array)
131
+
132
+ # Apply the scope prefix in place on the rule's returned pairs and collect them
133
+ # directly. Per the `virtual` contract the rule hands back a freshly built Array
134
+ # each call (see `.virtual`), so mutating it here is safe and avoids allocating a
135
+ # new pair per error.
136
+ result.each do |sub_result|
137
+ sub_result[0] = scope + sub_result[0]
138
+ end
139
+
140
+ errors.push(*result)
141
+ else
142
+ for_checking = data_is_hash ? dig(data, rule.path) : DIG_MISS
143
+
144
+ if DIG_MISS.equal?(for_checking)
145
+ errors << [scope + rule.path, :missing] if rule.type == :required
146
+ else
147
+ result = rule.validator.call(for_checking, data, errors, self)
148
+
149
+ next if result == true
150
+
151
+ errors << [scope + rule.path, result || :format]
152
+ end
153
+ end
154
+ end
155
+
156
+ return Result.success if errors.empty?
157
+
158
+ Result.new(errors, self)
159
+ end
160
+
161
+ # Runs the validation like `#call`, but raises instead of returning a failed `Result`
162
+ #
163
+ # @param data [Hash] data for validation
164
+ # @param error_class [Class] error class that should be used when validation fails
165
+ # @param scope [Array<String>] scope of this contract (if any) or empty array if no parent
166
+ # scope is needed if contract starts from root
167
+ # @return [Boolean] true
168
+ # @raise [StandardError] any error provided in the error_class that inherits from the
169
+ # standard error
170
+ def validate!(data, error_class, scope: EMPTY_ARRAY)
171
+ result = call(data, scope: scope)
172
+
173
+ return true if result.success?
174
+
175
+ raise error_class, result.errors
176
+ end
177
+
178
+ private
179
+
180
+ # Tries to dig for a given key in a hash and returns it with indication whether or not it
181
+ # was possible to find it (dig returns nil and we don't know if it wasn't the digged key
182
+ # value)
183
+ #
184
+ # Uses `Hash#fetch` with the `DIG_MISS` sentinel as the default, which resolves presence
185
+ # and value in a single hash lookup instead of a `key?` check followed by `[]`.
186
+ # `fetch` with a default ignores `default_proc`.
187
+ #
188
+ # @param data [Hash]
189
+ # @param keys [Array<Symbol>]
190
+ # @return [DIG_MISS, Object] found element or DIG_MISS indicating that not found
191
+ def dig(data, keys)
192
+ case keys.length
193
+ when 1
194
+ data.fetch(keys[0], DIG_MISS)
195
+ when 2
196
+ mid = data.fetch(keys[0], DIG_MISS)
197
+
198
+ return DIG_MISS if DIG_MISS.equal?(mid)
199
+ return DIG_MISS unless mid.is_a?(Hash)
200
+
201
+ mid.fetch(keys[1], DIG_MISS)
202
+ else
203
+ current = data
204
+
205
+ keys.each do |nesting|
206
+ return DIG_MISS unless current.is_a?(Hash)
207
+
208
+ current = current.fetch(nesting, DIG_MISS)
209
+
210
+ return DIG_MISS if DIG_MISS.equal?(current)
211
+ end
212
+
213
+ current
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Contractable
6
+ # Representation of a validation result with resolved error messages
7
+ class Result
8
+ attr_reader :errors
9
+
10
+ # Empty hash that we use when no errors to save on allocations
11
+ EMPTY_HASH = {}.freeze
12
+
13
+ private_constant :EMPTY_HASH
14
+
15
+ class << self
16
+ # @return [Result] cached frozen success result to avoid allocating a new Result on
17
+ # every successful contract validation. Safe because successful results are
18
+ # identical regardless of contract (they all have @errors = EMPTY_HASH).
19
+ def success
20
+ @success ||= new([], nil).freeze
21
+ end
22
+ end
23
+
24
+ # Builds a result object and remaps (if needed) error keys to proper error messages
25
+ #
26
+ # @param errors [Array<Array>] array with sub-arrays with paths and error keys
27
+ # @param contract [Object] contract that generated the error
28
+ def initialize(errors, contract)
29
+ # Short track to skip object allocation for the happy path
30
+ if errors.empty?
31
+ @errors = EMPTY_HASH
32
+ return
33
+ end
34
+
35
+ hashed = {}
36
+
37
+ errors.each do |error|
38
+ scope = error.first.map(&:to_s).join('.').to_sym
39
+
40
+ # This will allow for usage of custom messages instead of yaml keys if needed
41
+ hashed[scope] = if error.last.is_a?(String)
42
+ error.last
43
+ else
44
+ build_message(contract, scope, error.last)
45
+ end
46
+ end
47
+
48
+ @errors = hashed
49
+ end
50
+
51
+ # @return [Boolean] true if no errors
52
+ def success?
53
+ errors.empty?
54
+ end
55
+
56
+ private
57
+
58
+ # Builds message based on the error messages and scope
59
+ #
60
+ # @param contract [Object] contract for which we build the result
61
+ # @param scope [Symbol] path to the key that has an error
62
+ # @param error_key [Symbol] error key for yaml errors lookup
63
+ # @return [String] error message
64
+ def build_message(contract, scope, error_key)
65
+ messages = contract.class.error_messages
66
+
67
+ # Split scope into parts for progressive checking
68
+ scope_parts = scope.to_s.split('.')
69
+
70
+ # Try full scope first, then progressively remove from beginning
71
+ # This allows us to have full path scoped errors but can also be used as a fallback,
72
+ # when scopes are dynamic. For example 'consumer_group_name.topic_name.name_format'
73
+ (0..scope_parts.length).each do |i|
74
+ current_scope_parts = scope_parts[i..]
75
+
76
+ key = if current_scope_parts.empty?
77
+ error_key.to_s
78
+ else
79
+ "#{current_scope_parts.join('.')}_#{error_key}"
80
+ end
81
+
82
+ return messages[key] if messages.key?(key)
83
+ end
84
+
85
+ # If nothing found, raise the original error
86
+ messages.fetch(error_key.to_s) do
87
+ messages.fetch("#{scope}_#{error_key}")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Contractable
6
+ # Representation of a single validation rule
7
+ Rule = Struct.new(:path, :type, :validator)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ # Contracts that validate the configuration structure and values
6
+ module Contracts
7
+ # Base for the configuration contracts with the helpers that build their error messages
8
+ class Base < Contractable::Contract
9
+ # Valid boolean values
10
+ BOOLEAN_VALUES = [true, false].freeze
11
+
12
+ class << self
13
+ private
14
+
15
+ # Builds an error message out of its lines
16
+ # @param lines [Array<String, nil>] message lines, nil lines are skipped
17
+ # @return [String] multi-line error message
18
+ def message(*lines)
19
+ lines.compact.join("\n")
20
+ end
21
+
22
+ # Converts errors of a nested contract into virtual rule errors of the parent contract
23
+ # @param result [Contractable::Result] nested contract validation result
24
+ # @return [Array<Array>] error pairs with the nested error scope as their path
25
+ def nested_errors(result)
26
+ result.errors.map { |scope, error| [[scope], error] }
27
+ end
28
+
29
+ # Suggest similar validator names using did_you_mean
30
+ # @param invalid_name [String] invalid validator name
31
+ # @return [String] suggestion line
32
+ def suggest_validator_name(invalid_name)
33
+ checker = DidYouMean::SpellChecker.new(dictionary: ConfigLoader::ALL_VALIDATORS)
34
+ suggestions = checker.correct(invalid_name)
35
+
36
+ if suggestions.any?
37
+ " Did you mean: #{suggestions.first}?"
38
+ else
39
+ ' See https://github.com/mensfeld/yard-lint/wiki/Validators ' \
40
+ 'for all available validators'
41
+ end
42
+ end
43
+
44
+ # Suggest similar severity values
45
+ # @param invalid_severity [String] invalid severity value
46
+ # @return [String, nil] suggestion line or nil when there is no similar value
47
+ def suggest_similar_severity(invalid_severity)
48
+ checker = DidYouMean::SpellChecker.new(dictionary: Lint::Config::VALID_SEVERITIES)
49
+ suggestions = checker.correct(invalid_severity)
50
+
51
+ " Did you mean: #{suggestions.first}?" if suggestions.any?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Contracts
6
+ # Validates a category-level configuration (e.g. `Documentation: { Severity: ... }`)
7
+ class Category < Base
8
+ # Keys that are valid in a category-level configuration
9
+ KEYS = %w[Enabled Exclude Severity].freeze
10
+
11
+ # @return [String] category name
12
+ attr_reader :name
13
+
14
+ class << self
15
+ private
16
+
17
+ # Suggest similar category configuration keys
18
+ # @param invalid_key [String] invalid category key
19
+ # @return [String, nil] suggestion line or nil when there is no similar key
20
+ def suggest_category_key(invalid_key)
21
+ checker = DidYouMean::SpellChecker.new(dictionary: KEYS)
22
+ suggestions = checker.correct(invalid_key)
23
+
24
+ " Did you mean: #{suggestions.first}?" if suggestions.any?
25
+ end
26
+ end
27
+
28
+ # @param name [String] category name used in error messages
29
+ def initialize(name)
30
+ super()
31
+ @name = name
32
+ end
33
+
34
+ optional('Enabled') do |enabled, _data, _errors, contract|
35
+ BOOLEAN_VALUES.include?(enabled) ||
36
+ message(
37
+ "Invalid Enabled value for category #{contract.name}: '#{enabled}'",
38
+ ' Must be true or false'
39
+ )
40
+ end
41
+
42
+ optional('Exclude') do |exclude, _data, _errors, contract|
43
+ !exclude || exclude.is_a?(Array) ||
44
+ "Invalid Exclude for category #{contract.name}: must be an array, got #{exclude.class}"
45
+ end
46
+
47
+ optional('Severity') do |severity, _data, _errors, contract|
48
+ !severity || Lint::Config::VALID_SEVERITIES.include?(severity.to_s) ||
49
+ message(
50
+ "Invalid Severity for category #{contract.name}: '#{severity}'",
51
+ " Valid values: #{Lint::Config::VALID_SEVERITIES.join(', ')}",
52
+ suggest_similar_severity(severity.to_s)
53
+ )
54
+ end
55
+
56
+ virtual do |data, _errors, contract|
57
+ (data.keys - KEYS).map do |key|
58
+ [
59
+ [key],
60
+ message(
61
+ "Unknown configuration key for category #{contract.name}: '#{key}'",
62
+ " Valid keys: #{KEYS.join(', ')}",
63
+ suggest_category_key(key.to_s)
64
+ )
65
+ ]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Contracts
6
+ # Validates the whole configuration: root keys, AllValidators, categories and validators
7
+ class Config < Base
8
+ # Valid category names (from ConfigUpdater::CATEGORY_ORDER)
9
+ VALID_CATEGORIES = %w[Documentation Tags Warnings Semantic].freeze
10
+
11
+ # Keys that are valid at the root level but are not validators
12
+ SPECIAL_KEYS = %w[
13
+ AllValidators
14
+ inherit_from
15
+ inherit_gem
16
+ ].freeze
17
+
18
+ # Validator names must contain a '/' (category/name format)
19
+ virtual do |data|
20
+ data.keys.filter_map do |key|
21
+ next if SPECIAL_KEYS.include?(key)
22
+ next if VALID_CATEGORIES.include?(key)
23
+ next if key.include?('/')
24
+
25
+ [[key], message("Unknown validator: '#{key}'", suggest_validator_name(key))]
26
+ end
27
+ end
28
+
29
+ optional('AllValidators') do |all_validators|
30
+ !all_validators || all_validators.is_a?(Hash) ||
31
+ "Invalid AllValidators: must be a Hash, got #{all_validators.class}"
32
+ end
33
+
34
+ # Only specific known settings are validated, unknown keys pass through
35
+ # This allows users to put custom data in AllValidators
36
+ nested('AllValidators') do
37
+ optional('FailOnSeverity') do |fail_on|
38
+ !fail_on || Lint::Config::VALID_SEVERITIES.include?(fail_on.to_s) ||
39
+ message("Invalid FailOnSeverity: '#{fail_on}'", " Valid values: #{Lint::Config::VALID_SEVERITIES.join(', ')}")
40
+ end
41
+
42
+ optional('MinCoverage') do |min_cov|
43
+ !min_cov || (min_cov.is_a?(Numeric) && !(min_cov < 0 || min_cov > 100)) ||
44
+ message("Invalid MinCoverage: '#{min_cov}'", ' Must be a number between 0 and 100')
45
+ end
46
+
47
+ optional('Exclude') do |exclude|
48
+ !exclude || exclude.is_a?(Array) ||
49
+ "Invalid Exclude in AllValidators: must be an array, got #{exclude.class}"
50
+ end
51
+
52
+ optional('YardOptions') do |yard_options|
53
+ !yard_options || yard_options.is_a?(Array) ||
54
+ "Invalid YardOptions in AllValidators: must be an array, got #{yard_options.class}"
55
+ end
56
+ end
57
+
58
+ virtual do |data|
59
+ data.flat_map do |key, value|
60
+ next [] unless VALID_CATEGORIES.include?(key)
61
+ next [] unless value.is_a?(Hash)
62
+
63
+ nested_errors(Category.new(key).call(value, scope: [key]))
64
+ end
65
+ end
66
+
67
+ virtual do |data|
68
+ data.flat_map do |key, value|
69
+ next [] unless key.include?('/')
70
+
71
+ unless value.is_a?(Hash)
72
+ next [[[key], "Invalid configuration for validator '#{key}': expected a Hash, got #{value.class}"]]
73
+ end
74
+
75
+ nested_errors(Validator.new(key).call(value, scope: [key]))
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Contracts
6
+ # Validates a single validator configuration (e.g. `Tags/Order: { Enabled: ... }`)
7
+ class Validator < Base
8
+ # Base config keys that are valid for all validators
9
+ BASE_KEYS = %w[Enabled Severity Exclude YardOptions].freeze
10
+
11
+ # @return [String] validator name
12
+ attr_reader :name
13
+
14
+ # @param name [String] validator name used in error messages
15
+ def initialize(name)
16
+ super()
17
+ @name = name
18
+ end
19
+
20
+ virtual do |_data, _errors, contract|
21
+ next if ConfigLoader::ALL_VALIDATORS.include?(contract.name)
22
+
23
+ [[[], message("Unknown validator: '#{contract.name}'", suggest_validator_name(contract.name))]]
24
+ end
25
+
26
+ optional('Enabled') do |enabled, _data, _errors, contract|
27
+ !enabled || BOOLEAN_VALUES.include?(enabled) ||
28
+ message("Invalid Enabled value for #{contract.name}: '#{enabled}'", ' Must be true or false')
29
+ end
30
+
31
+ optional('Severity') do |severity, _data, _errors, contract|
32
+ !severity || Lint::Config::VALID_SEVERITIES.include?(severity.to_s) ||
33
+ message(
34
+ "Invalid Severity for #{contract.name}: '#{severity}'",
35
+ " Valid values: #{Lint::Config::VALID_SEVERITIES.join(', ')}",
36
+ suggest_similar_severity(severity.to_s)
37
+ )
38
+ end
39
+
40
+ optional('Exclude') do |exclude, _data, _errors, contract|
41
+ !exclude || exclude.is_a?(Array) ||
42
+ "Invalid Exclude for #{contract.name}: must be an array, got #{exclude.class}"
43
+ end
44
+
45
+ optional('YardOptions') do |yard_options, _data, _errors, contract|
46
+ !yard_options || yard_options.is_a?(Array) ||
47
+ "Invalid YardOptions for #{contract.name}: must be an array, got #{yard_options.class}"
48
+ end
49
+
50
+ # Unknown validator-specific keys (an unknown validator is already reported)
51
+ virtual do |data, _errors, contract|
52
+ next unless ConfigLoader::ALL_VALIDATORS.include?(contract.name)
53
+
54
+ validator_config_class = ConfigLoader.validator_config(contract.name)
55
+ next unless validator_config_class
56
+
57
+ valid_keys = (validator_config_class.defaults.keys + Lint::Config::METADATA_KEYS + BASE_KEYS).uniq.sort
58
+
59
+ (data.keys - valid_keys).map do |key|
60
+ [
61
+ [key],
62
+ message(
63
+ "Unknown configuration key for #{contract.name}: '#{key}'",
64
+ " Valid keys: #{valid_keys.join(', ')}"
65
+ )
66
+ ]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -22,6 +22,9 @@ module Yard
22
22
  # Raised when .yard-lint-todo.yml already exists without force flag
23
23
  class TodoFileExistsError < BaseError; end
24
24
 
25
+ # Raised when .yard-lint-todo.yml is required but does not exist
26
+ class TodoFileNotFoundError < BaseError; end
27
+
25
28
  # Raised when configuration is invalid
26
29
  class InvalidConfigError < BaseError; end
27
30
  end
@@ -112,7 +112,7 @@ module Yard
112
112
  next false unless obj.file
113
113
 
114
114
  file_excludes.any? do |pattern|
115
- File.fnmatch(pattern, obj.file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
115
+ File.fnmatch(pattern, obj.file, Config::EXCLUDE_FLAGS)
116
116
  end
117
117
  end
118
118
  end
@@ -24,11 +24,34 @@ module Yard
24
24
  new(name).call
25
25
  end
26
26
 
27
+ # Structured description of a validator for programmatic consumers such as
28
+ # the SARIF formatter's rule metadata (as opposed to #call's terminal text).
29
+ # @param name [String] validator name (e.g. 'Tags/TypeSyntax')
30
+ # @return [Hash{Symbol => String}, nil] { short:, full: }, or nil when the
31
+ # name is unknown or the validator has no docstring
32
+ def self.describe(name)
33
+ new(name).describe
34
+ end
35
+
27
36
  # @param name [String] validator name (e.g. 'Tags/TypeSyntax')
28
37
  def initialize(name)
29
38
  @name = name
30
39
  end
31
40
 
41
+ # @return [Hash{Symbol => String}, nil] { short:, full: } description of the
42
+ # validator, or nil when unknown or undocumented
43
+ def describe
44
+ return nil unless ConfigLoader::ALL_VALIDATORS.include?(@name)
45
+
46
+ doc = documentation
47
+ return nil unless doc
48
+
49
+ full = prose_only(doc[:description])
50
+ return nil if full.empty?
51
+
52
+ { short: first_sentence(full), full: full }
53
+ end
54
+
32
55
  # @return [String] formatted, terminal-ready explanation
33
56
  # @raise [ArgumentError] if name is not a known validator
34
57
  def call
@@ -114,6 +137,25 @@ module Yard
114
137
  end
115
138
  end
116
139
 
140
+ # The human description of a validator: its docstring prose with the leading
141
+ # "<Name> validator" title line and any trailing "## ..." sections (e.g.
142
+ # Configuration) removed, leaving just the explanation.
143
+ # @param description [String] the raw docstring text
144
+ # @return [String] the trimmed prose (may be empty)
145
+ def prose_only(description)
146
+ lines = description.to_s.lines.take_while { |line| !line.start_with?('## ') }
147
+ text = lines.join
148
+ text = text.sub(/\A.*validator[ \t]*\n+/i, '') if text.match?(/\A.*validator[ \t]*\n/i)
149
+ text.strip
150
+ end
151
+
152
+ # @param text [String] prose text
153
+ # @return [String] the first sentence, for a concise one-line summary
154
+ def first_sentence(text)
155
+ flat = text.tr("\n", ' ').squeeze(' ').strip
156
+ flat[/.*?[.!?](?=\s|\z)/] || flat
157
+ end
158
+
117
159
  # @return [String] the fully-qualified code object path (e.g.
118
160
  # 'Yard::Lint::Validators::Tags::TypeSyntax')
119
161
  def object_path