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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +201 -498
- data/README.md +103 -1
- data/bin/yard-lint +100 -40
- 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 +205 -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 +63 -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/tag_group_separator/validator.rb +8 -0
- data/lib/yard/lint/validators/tags/tag_separator/validator.rb +8 -0
- 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 +23 -2
|
@@ -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
|
|
@@ -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
|