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
|
@@ -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,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
|
data/lib/yard/lint/errors.rb
CHANGED
|
@@ -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
|
data/lib/yard/lint/explainer.rb
CHANGED
|
@@ -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
|