yard-lint 0.2.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 +7 -0
- data/.coditsu/ci.yml +3 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +454 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/bin/yard-lint +109 -0
- data/lib/yard/lint/command_cache.rb +77 -0
- data/lib/yard/lint/config.rb +255 -0
- data/lib/yard/lint/config_loader.rb +198 -0
- data/lib/yard/lint/errors.rb +17 -0
- data/lib/yard/lint/formatters/progress.rb +50 -0
- data/lib/yard/lint/parsers/base.rb +23 -0
- data/lib/yard/lint/parsers/one_line_base.rb +35 -0
- data/lib/yard/lint/parsers/two_line_base.rb +45 -0
- data/lib/yard/lint/result_builder.rb +130 -0
- data/lib/yard/lint/results/aggregate.rb +86 -0
- data/lib/yard/lint/results/base.rb +156 -0
- data/lib/yard/lint/runner.rb +125 -0
- data/lib/yard/lint/validators/base.rb +120 -0
- data/lib/yard/lint/validators/config.rb +30 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/config.rb +20 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +43 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/result.rb +26 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +48 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +13 -0
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/config.rb +20 -0
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/messages_builder.rb +24 -0
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +45 -0
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/result.rb +25 -0
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +55 -0
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +13 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects/config.rb +21 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects/messages_builder.rb +23 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +39 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects/result.rb +25 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +39 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects.rb +14 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/config.rb +24 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/messages_builder.rb +25 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/parser.rb +45 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +42 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +65 -0
- data/lib/yard/lint/validators/semantic/abstract_methods.rb +13 -0
- data/lib/yard/lint/validators/tags/api_tags/config.rb +21 -0
- data/lib/yard/lint/validators/tags/api_tags/messages_builder.rb +29 -0
- data/lib/yard/lint/validators/tags/api_tags/parser.rb +50 -0
- data/lib/yard/lint/validators/tags/api_tags/result.rb +42 -0
- data/lib/yard/lint/validators/tags/api_tags/validator.rb +69 -0
- data/lib/yard/lint/validators/tags/api_tags.rb +13 -0
- data/lib/yard/lint/validators/tags/invalid_types/config.rb +22 -0
- data/lib/yard/lint/validators/tags/invalid_types/messages_builder.rb +24 -0
- data/lib/yard/lint/validators/tags/invalid_types/parser.rb +16 -0
- data/lib/yard/lint/validators/tags/invalid_types/result.rb +25 -0
- data/lib/yard/lint/validators/tags/invalid_types/validator.rb +106 -0
- data/lib/yard/lint/validators/tags/invalid_types.rb +13 -0
- data/lib/yard/lint/validators/tags/option_tags/config.rb +21 -0
- data/lib/yard/lint/validators/tags/option_tags/messages_builder.rb +24 -0
- data/lib/yard/lint/validators/tags/option_tags/parser.rb +45 -0
- data/lib/yard/lint/validators/tags/option_tags/result.rb +42 -0
- data/lib/yard/lint/validators/tags/option_tags/validator.rb +61 -0
- data/lib/yard/lint/validators/tags/option_tags.rb +13 -0
- data/lib/yard/lint/validators/tags/order/config.rb +33 -0
- data/lib/yard/lint/validators/tags/order/messages_builder.rb +30 -0
- data/lib/yard/lint/validators/tags/order/parser.rb +66 -0
- data/lib/yard/lint/validators/tags/order/result.rb +26 -0
- data/lib/yard/lint/validators/tags/order/validator.rb +89 -0
- data/lib/yard/lint/validators/tags/order.rb +13 -0
- data/lib/yard/lint/validators/warnings/duplicated_parameter_name/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/duplicated_parameter_name/parser.rb +22 -0
- data/lib/yard/lint/validators/warnings/duplicated_parameter_name/result.rb +25 -0
- data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +33 -0
- data/lib/yard/lint/validators/warnings/duplicated_parameter_name.rb +14 -0
- data/lib/yard/lint/validators/warnings/invalid_directive_format/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/invalid_directive_format/parser.rb +22 -0
- data/lib/yard/lint/validators/warnings/invalid_directive_format/result.rb +25 -0
- data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +33 -0
- data/lib/yard/lint/validators/warnings/invalid_directive_format.rb +14 -0
- data/lib/yard/lint/validators/warnings/invalid_tag_format/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb +22 -0
- data/lib/yard/lint/validators/warnings/invalid_tag_format/result.rb +25 -0
- data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +33 -0
- data/lib/yard/lint/validators/warnings/invalid_tag_format.rb +14 -0
- data/lib/yard/lint/validators/warnings/unknown_directive/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/unknown_directive/parser.rb +22 -0
- data/lib/yard/lint/validators/warnings/unknown_directive/result.rb +25 -0
- data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +33 -0
- data/lib/yard/lint/validators/warnings/unknown_directive.rb +14 -0
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/parser.rb +22 -0
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/result.rb +25 -0
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +33 -0
- data/lib/yard/lint/validators/warnings/unknown_parameter_name.rb +14 -0
- data/lib/yard/lint/validators/warnings/unknown_tag/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +24 -0
- data/lib/yard/lint/validators/warnings/unknown_tag/result.rb +25 -0
- data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +33 -0
- data/lib/yard/lint/validators/warnings/unknown_tag.rb +14 -0
- data/lib/yard/lint/version.rb +8 -0
- data/lib/yard/lint.rb +76 -0
- data/lib/yard-lint.rb +11 -0
- data/renovate.json +22 -0
- metadata +178 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
# Cache for YARD command executions to avoid running identical commands multiple times
|
|
6
|
+
# This provides a transparent optimization layer - validators don't need to know about it
|
|
7
|
+
class CommandCache
|
|
8
|
+
def initialize
|
|
9
|
+
@cache = {}
|
|
10
|
+
@hits = 0
|
|
11
|
+
@misses = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Execute a command through the cache
|
|
15
|
+
# If the command has been executed before, return cached result
|
|
16
|
+
# Otherwise execute and cache the result
|
|
17
|
+
# @param command_string [String] the shell command to execute
|
|
18
|
+
# @return [Hash] hash with stdout, stderr, exit_code keys
|
|
19
|
+
# @note Returns a deep clone to prevent validators from modifying cached data
|
|
20
|
+
def execute(command_string)
|
|
21
|
+
cache_key = generate_cache_key(command_string)
|
|
22
|
+
|
|
23
|
+
if @cache.key?(cache_key)
|
|
24
|
+
@hits += 1
|
|
25
|
+
deep_clone(@cache[cache_key])
|
|
26
|
+
else
|
|
27
|
+
@misses += 1
|
|
28
|
+
result = execute_command(command_string)
|
|
29
|
+
@cache[cache_key] = deep_clone(result)
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get cache statistics
|
|
35
|
+
# @return [Hash] hash with hits, misses, and total executions
|
|
36
|
+
def stats
|
|
37
|
+
{
|
|
38
|
+
hits: @hits,
|
|
39
|
+
misses: @misses,
|
|
40
|
+
total: @hits + @misses,
|
|
41
|
+
saved_executions: @hits
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Generate a cache key for the command
|
|
48
|
+
# Normalizes the command to handle whitespace differences
|
|
49
|
+
# @param command_string [String] the command to generate key for
|
|
50
|
+
# @return [String] SHA256 hash of normalized command
|
|
51
|
+
def generate_cache_key(command_string)
|
|
52
|
+
# Normalize whitespace: collapse multiple spaces/newlines into single spaces
|
|
53
|
+
normalized = command_string.strip.gsub(/\s+/, ' ')
|
|
54
|
+
Digest::SHA256.hexdigest(normalized)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Actually execute the command
|
|
58
|
+
# @param command_string [String] the command to execute
|
|
59
|
+
# @return [Hash] hash with stdout, stderr, exit_code keys
|
|
60
|
+
def execute_command(command_string)
|
|
61
|
+
stdout, stderr, status = Open3.capture3(command_string)
|
|
62
|
+
{
|
|
63
|
+
stdout: stdout,
|
|
64
|
+
stderr: stderr,
|
|
65
|
+
exit_code: status.exitstatus
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Deep clone a hash to prevent modifications to cached data
|
|
70
|
+
# @param hash [Hash] the hash to clone
|
|
71
|
+
# @return [Hash] deep cloned hash
|
|
72
|
+
def deep_clone(hash)
|
|
73
|
+
Marshal.load(Marshal.dump(hash))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# YARD Lint - comprehensive linter for YARD documentation
|
|
4
|
+
module Yard
|
|
5
|
+
module Lint
|
|
6
|
+
# Configuration object for YARD Lint
|
|
7
|
+
class Config
|
|
8
|
+
attr_reader :raw_config, :validators
|
|
9
|
+
|
|
10
|
+
# Default YAML config file name
|
|
11
|
+
DEFAULT_CONFIG_FILE = '.yard-lint.yml'
|
|
12
|
+
|
|
13
|
+
# Valid severity levels for fail_on_severity
|
|
14
|
+
VALID_SEVERITIES = %w[error warning convention never].freeze
|
|
15
|
+
|
|
16
|
+
# Metadata keys to skip when merging validator configs
|
|
17
|
+
METADATA_KEYS = %w[Description StyleGuide VersionAdded VersionChanged].freeze
|
|
18
|
+
|
|
19
|
+
# @param raw_config [Hash] raw configuration hash (new hierarchical format)
|
|
20
|
+
def initialize(raw_config = {})
|
|
21
|
+
@raw_config = raw_config
|
|
22
|
+
@validators = build_validators_config
|
|
23
|
+
|
|
24
|
+
yield self if block_given?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Load configuration from a YAML file
|
|
29
|
+
# @param path [String] path to YAML config file
|
|
30
|
+
# @return [Yard::Lint::Config] configuration object
|
|
31
|
+
# @raise [Yard::Lint::Errors::ConfigFileNotFoundError] if config file doesn't exist
|
|
32
|
+
def from_file(path)
|
|
33
|
+
unless File.exist?(path)
|
|
34
|
+
raise Errors::ConfigFileNotFoundError, "Config file not found: #{path}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Load with inheritance support
|
|
38
|
+
merged_yaml = ConfigLoader.load(path)
|
|
39
|
+
|
|
40
|
+
new(merged_yaml)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Search for and load config file from current directory upwards
|
|
44
|
+
# @param start_path [String] directory to start searching from (default: current dir)
|
|
45
|
+
# @return [Yard::Lint::Config, nil] config if found, nil otherwise
|
|
46
|
+
def load(start_path: Dir.pwd)
|
|
47
|
+
config_path = find_config_file(start_path)
|
|
48
|
+
config_path ? from_file(config_path) : nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Find config file by searching upwards from start_path
|
|
52
|
+
# @param start_path [String] directory to start searching from
|
|
53
|
+
# @return [String, nil] path to config file if found
|
|
54
|
+
def find_config_file(start_path)
|
|
55
|
+
current = File.expand_path(start_path)
|
|
56
|
+
root = File.expand_path('/')
|
|
57
|
+
|
|
58
|
+
loop do
|
|
59
|
+
config_path = File.join(current, DEFAULT_CONFIG_FILE)
|
|
60
|
+
return config_path if File.exist?(config_path)
|
|
61
|
+
|
|
62
|
+
break if current == root
|
|
63
|
+
|
|
64
|
+
current = File.dirname(current)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# YARD command-line options
|
|
72
|
+
# @return [Array<String>] YARD options
|
|
73
|
+
def options
|
|
74
|
+
all_validators['YardOptions'] || []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get YARD options for a specific validator
|
|
78
|
+
# Falls back to global options if validator doesn't specify its own
|
|
79
|
+
# @param validator_name [String] full validator name
|
|
80
|
+
# @return [Array<String>] YARD options for this validator
|
|
81
|
+
def validator_yard_options(validator_name)
|
|
82
|
+
validator_config = validators[validator_name] || {}
|
|
83
|
+
validator_config['YardOptions'] || options
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Global file exclusion patterns
|
|
87
|
+
# @return [Array<String>] exclusion patterns
|
|
88
|
+
def exclude
|
|
89
|
+
all_validators['Exclude'] || ['\.git', 'vendor/**/*', 'node_modules/**/*']
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Minimum severity level to fail on
|
|
93
|
+
# @return [String] severity level (error, warning, convention, never)
|
|
94
|
+
def fail_on_severity
|
|
95
|
+
all_validators['FailOnSeverity'] || 'warning'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if a validator is enabled
|
|
99
|
+
# @param validator_name [String] full validator name (e.g., 'Tags/Order')
|
|
100
|
+
# @return [Boolean] true if validator is enabled
|
|
101
|
+
def validator_enabled?(validator_name)
|
|
102
|
+
validator_config = validators[validator_name] || {}
|
|
103
|
+
validator_config['Enabled'] != false # Default to true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get validator severity
|
|
107
|
+
# @param validator_name [String] full validator name
|
|
108
|
+
# @return [String] severity level for this validator
|
|
109
|
+
def validator_severity(validator_name)
|
|
110
|
+
validator_config = validators[validator_name] || {}
|
|
111
|
+
validator_config['Severity'] || 'warning'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get validator-specific exclude patterns
|
|
115
|
+
# @param validator_name [String] full validator name
|
|
116
|
+
# @return [Array<String>] exclusion patterns for this validator
|
|
117
|
+
def validator_exclude(validator_name)
|
|
118
|
+
validator_config = validators[validator_name] || {}
|
|
119
|
+
validator_config['Exclude'] || []
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Combined global and per-validator exclusions
|
|
123
|
+
# Returns all exclusion patterns that apply to this validator
|
|
124
|
+
# @param validator_name [String] full validator name
|
|
125
|
+
# @return [Array<String>] combined exclusion patterns (global + per-validator)
|
|
126
|
+
def validator_all_excludes(validator_name)
|
|
127
|
+
exclude + validator_exclude(validator_name)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get validator-specific configuration value
|
|
131
|
+
# @param validator_name [String] full validator name
|
|
132
|
+
# @param key [String] configuration key
|
|
133
|
+
# @return [Object, nil] configuration value
|
|
134
|
+
def validator_config(validator_name, key)
|
|
135
|
+
validators.dig(validator_name, key)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Setter methods for programmatic configuration
|
|
139
|
+
|
|
140
|
+
# Set YARD options
|
|
141
|
+
# @param value [Array<String>] YARD options
|
|
142
|
+
def options=(value)
|
|
143
|
+
@raw_config['AllValidators'] ||= {}
|
|
144
|
+
@raw_config['AllValidators']['YardOptions'] = value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Set global exclude patterns
|
|
148
|
+
# @param value [Array<String>] exclusion patterns
|
|
149
|
+
def exclude=(value)
|
|
150
|
+
@raw_config['AllValidators'] ||= {}
|
|
151
|
+
@raw_config['AllValidators']['Exclude'] = value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Set fail on severity level
|
|
155
|
+
# @param value [String] severity level
|
|
156
|
+
def fail_on_severity=(value)
|
|
157
|
+
@raw_config['AllValidators'] ||= {}
|
|
158
|
+
@raw_config['AllValidators']['FailOnSeverity'] = value
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Allow hash-like access for convenience
|
|
162
|
+
# @param key [Symbol, String] attribute name to access
|
|
163
|
+
# @return [Object, nil] attribute value or nil if not found
|
|
164
|
+
def [](key)
|
|
165
|
+
respond_to?(key) ? send(key) : nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# Generic helper to set validator configuration
|
|
171
|
+
# @param validator_name [String] full validator name (e.g., 'Tags/Order')
|
|
172
|
+
# @param key [String] configuration key
|
|
173
|
+
# @param value [Object] configuration value
|
|
174
|
+
def set_validator_config(validator_name, key, value)
|
|
175
|
+
@raw_config[validator_name] ||= {}
|
|
176
|
+
@raw_config[validator_name][key] = value
|
|
177
|
+
@validators = build_validators_config
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Generic helper to get validator configuration with default fallback
|
|
181
|
+
# @param validator_name [String] full validator name
|
|
182
|
+
# @param key [String] configuration key
|
|
183
|
+
# @return [Object, nil] configuration value or default
|
|
184
|
+
def get_validator_config_with_default(validator_name, key)
|
|
185
|
+
validator_config(validator_name, key) || begin
|
|
186
|
+
validator_cfg = ConfigLoader.validator_config(validator_name)
|
|
187
|
+
validator_cfg&.defaults&.dig(key)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Get AllValidators section
|
|
192
|
+
# @return [Hash] AllValidators configuration
|
|
193
|
+
def all_validators
|
|
194
|
+
@raw_config['AllValidators'] || {}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Build validators configuration from raw config
|
|
198
|
+
# @return [Hash] validators configuration
|
|
199
|
+
def build_validators_config
|
|
200
|
+
config = {}
|
|
201
|
+
|
|
202
|
+
# Start with defaults for all validators
|
|
203
|
+
ConfigLoader::ALL_VALIDATORS.each do |validator_name|
|
|
204
|
+
config[validator_name] = build_default_validator_config(validator_name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Apply validator-specific overrides
|
|
208
|
+
@raw_config.each do |key, value|
|
|
209
|
+
next unless key.include?('/') # Validator-specific config
|
|
210
|
+
next unless ConfigLoader::ALL_VALIDATORS.include?(key)
|
|
211
|
+
|
|
212
|
+
config[key] = merge_validator_config(config[key], value) if value.is_a?(Hash)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
config
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Build default configuration for a validator
|
|
219
|
+
# @param validator_name [String] full validator name
|
|
220
|
+
# @return [Hash] default configuration
|
|
221
|
+
def build_default_validator_config(validator_name)
|
|
222
|
+
# Get defaults from validator config
|
|
223
|
+
validator_cfg = ConfigLoader.validator_config(validator_name)
|
|
224
|
+
defaults = validator_cfg&.defaults || {}
|
|
225
|
+
base = ConfigLoader::DEFAULT_VALIDATOR_CONFIG.dup
|
|
226
|
+
|
|
227
|
+
# Merge validator-specific defaults with base config
|
|
228
|
+
base.merge(defaults)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Merge validator configuration
|
|
232
|
+
# @param base [Hash] base configuration
|
|
233
|
+
# @param override [Hash] overriding configuration
|
|
234
|
+
# @return [Hash] merged configuration
|
|
235
|
+
def merge_validator_config(base, override)
|
|
236
|
+
result = base.dup
|
|
237
|
+
|
|
238
|
+
override.each do |key, value|
|
|
239
|
+
# Skip metadata keys
|
|
240
|
+
next if METADATA_KEYS.include?(key)
|
|
241
|
+
|
|
242
|
+
result[key] = if value.is_a?(Array) && result[key].is_a?(Array)
|
|
243
|
+
value # Array replacement
|
|
244
|
+
elsif value.is_a?(Hash) && result[key].is_a?(Hash)
|
|
245
|
+
result[key].merge(value)
|
|
246
|
+
else
|
|
247
|
+
value
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
result
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
# Handles loading and merging of configuration files with inheritance support
|
|
6
|
+
class ConfigLoader
|
|
7
|
+
# Inheritance keys to skip when merging configs
|
|
8
|
+
INHERITANCE_KEYS = %w[inherit_from inherit_gem].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Get the validator namespace module for a given validator name
|
|
12
|
+
# @param validator_name [String] validator name (e.g., 'Tags/Order')
|
|
13
|
+
# @return [Module, nil] validator namespace module or nil if doesn't exist
|
|
14
|
+
def validator_module(validator_name)
|
|
15
|
+
category, name = validator_name.split('/')
|
|
16
|
+
module_path = "Validators::#{category}::#{name}"
|
|
17
|
+
|
|
18
|
+
module_path.split('::').reduce(Yard::Lint) do |mod, const_name|
|
|
19
|
+
return nil unless mod.const_defined?(const_name)
|
|
20
|
+
|
|
21
|
+
mod.const_get(const_name)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get the validator config for a given validator name
|
|
26
|
+
# Dynamically resolves the config class based on the validator name
|
|
27
|
+
# @param validator_name [String] validator name (e.g., 'Tags/Order')
|
|
28
|
+
# @return [Class, nil] validator config class or nil if doesn't exist
|
|
29
|
+
def validator_config(validator_name)
|
|
30
|
+
namespace = validator_module(validator_name)
|
|
31
|
+
return nil unless namespace
|
|
32
|
+
|
|
33
|
+
# Return the Config class from within the validator namespace
|
|
34
|
+
namespace.const_defined?(:Config) ? namespace.const_get(:Config) : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Auto-discover validators from the codebase
|
|
38
|
+
# Scans the validators directory and loads all validator modules that have
|
|
39
|
+
# an .id method and .defaults method (indicating they're valid validators)
|
|
40
|
+
# @return [Hash<String, Array<String>>] hash of category names to validator names
|
|
41
|
+
def discover_validators
|
|
42
|
+
categories = Hash.new { |h, k| h[k] = [] }
|
|
43
|
+
|
|
44
|
+
validators_path = File.join(__dir__, 'validators')
|
|
45
|
+
|
|
46
|
+
# Find all validator module files (e.g., validators/tags/order.rb)
|
|
47
|
+
Dir.glob(File.join(validators_path, '*', '*.rb')).each do |file_path|
|
|
48
|
+
# Require the validator module file to ensure it's loaded
|
|
49
|
+
require file_path
|
|
50
|
+
|
|
51
|
+
# Extract category and validator name from path
|
|
52
|
+
# e.g., .../validators/tags/order.rb -> ['tags', 'order']
|
|
53
|
+
relative_path = file_path.sub("#{validators_path}/", '')
|
|
54
|
+
parts = relative_path.sub('.rb', '').split('/')
|
|
55
|
+
category_dir = parts[0]
|
|
56
|
+
validator_dir = parts[1]
|
|
57
|
+
|
|
58
|
+
# Convert to proper casing:
|
|
59
|
+
# 'tags' -> 'Tags', 'undocumented_objects' -> 'UndocumentedObjects'
|
|
60
|
+
category = category_dir.split('_').map(&:capitalize).join
|
|
61
|
+
validator = validator_dir.split('_').map(&:capitalize).join
|
|
62
|
+
|
|
63
|
+
# Construct the validator name
|
|
64
|
+
validator_name = "#{category}/#{validator}"
|
|
65
|
+
|
|
66
|
+
# Verify it's a valid validator by checking if it has a Config class
|
|
67
|
+
cfg = validator_config(validator_name)
|
|
68
|
+
# Every validator must have a Config with id and defaults
|
|
69
|
+
categories[category] << validator_name if cfg && cfg.id && cfg.defaults
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Sort for consistent ordering
|
|
73
|
+
categories.transform_values(&:sort).sort.to_h
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Load configuration from file with inheritance support
|
|
77
|
+
# @param path [String] path to configuration file
|
|
78
|
+
# @return [Hash] merged configuration hash
|
|
79
|
+
def load(path)
|
|
80
|
+
new(path).load
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# All validator names (auto-discovered from codebase structure)
|
|
85
|
+
ALL_VALIDATORS = discover_validators.values.flatten.freeze
|
|
86
|
+
|
|
87
|
+
# Default configuration for each validator
|
|
88
|
+
DEFAULT_VALIDATOR_CONFIG = {
|
|
89
|
+
'Enabled' => true,
|
|
90
|
+
'Severity' => nil, # Will use validator's default or department fallback
|
|
91
|
+
'Exclude' => []
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
# @param path [String] path to configuration file
|
|
95
|
+
def initialize(path)
|
|
96
|
+
@path = path
|
|
97
|
+
@loaded_files = []
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Load and merge configuration with inheritance
|
|
101
|
+
# @return [Hash] final merged configuration
|
|
102
|
+
def load
|
|
103
|
+
load_file(@path)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Load a single configuration file and handle inheritance
|
|
109
|
+
# @param path [String] path to configuration file
|
|
110
|
+
# @return [Hash] configuration hash with inheritance resolved
|
|
111
|
+
# @raise [Yard::Lint::Errors::CircularDependencyError] if circular dependency detected
|
|
112
|
+
def load_file(path)
|
|
113
|
+
# Prevent circular dependencies
|
|
114
|
+
if @loaded_files.include?(path)
|
|
115
|
+
raise Errors::CircularDependencyError, "Circular dependency detected: #{path}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@loaded_files << path
|
|
119
|
+
|
|
120
|
+
yaml = YAML.load_file(path) || {}
|
|
121
|
+
|
|
122
|
+
# Handle inheritance
|
|
123
|
+
base_config = load_inherited_configs(yaml, File.dirname(path))
|
|
124
|
+
|
|
125
|
+
# Merge current config over inherited config
|
|
126
|
+
merge_configs(base_config, yaml)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Load all inherited configurations
|
|
130
|
+
# @param yaml [Hash] current configuration hash
|
|
131
|
+
# @param base_dir [String] directory containing the config file
|
|
132
|
+
# @return [Hash] merged inherited configuration
|
|
133
|
+
def load_inherited_configs(yaml, base_dir)
|
|
134
|
+
config = {}
|
|
135
|
+
|
|
136
|
+
# Load inherit_from (local files)
|
|
137
|
+
if yaml['inherit_from']
|
|
138
|
+
inherit_from = Array(yaml['inherit_from'])
|
|
139
|
+
inherit_from.each do |file|
|
|
140
|
+
inherited_path = File.expand_path(file, base_dir)
|
|
141
|
+
if File.exist?(inherited_path)
|
|
142
|
+
inherited = load_file(inherited_path)
|
|
143
|
+
config = merge_configs(config, inherited)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Load inherit_gem (gem-based configs)
|
|
149
|
+
yaml['inherit_gem']&.each do |gem_name, gem_file|
|
|
150
|
+
inherited = load_gem_config(gem_name, gem_file)
|
|
151
|
+
config = merge_configs(config, inherited) if inherited
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
config
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Load configuration from a gem
|
|
158
|
+
# @param gem_name [String] name of the gem
|
|
159
|
+
# @param gem_file [String] relative path within the gem
|
|
160
|
+
# @return [Hash, nil] configuration hash or nil if not found
|
|
161
|
+
def load_gem_config(gem_name, gem_file)
|
|
162
|
+
gem_spec = Gem::Specification.find_by_name(gem_name)
|
|
163
|
+
config_path = File.join(gem_spec.gem_dir, gem_file)
|
|
164
|
+
|
|
165
|
+
return nil unless File.exist?(config_path)
|
|
166
|
+
|
|
167
|
+
load_file(config_path)
|
|
168
|
+
rescue Gem::MissingSpecError
|
|
169
|
+
warn "Warning: Gem '#{gem_name}' not found for configuration inheritance"
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Merge two configuration hashes
|
|
174
|
+
# @param base [Hash] base configuration
|
|
175
|
+
# @param override [Hash] overriding configuration
|
|
176
|
+
# @return [Hash] merged configuration
|
|
177
|
+
def merge_configs(base, override)
|
|
178
|
+
result = base.dup
|
|
179
|
+
|
|
180
|
+
override.each do |key, value|
|
|
181
|
+
# Skip inheritance keys in merged result
|
|
182
|
+
next if INHERITANCE_KEYS.include?(key)
|
|
183
|
+
|
|
184
|
+
result[key] = if value.is_a?(Hash) && result[key].is_a?(Hash)
|
|
185
|
+
merge_configs(result[key], value)
|
|
186
|
+
elsif value.is_a?(Array) && result[key].is_a?(Array)
|
|
187
|
+
# For arrays, override completely (RuboCop behavior)
|
|
188
|
+
value
|
|
189
|
+
else
|
|
190
|
+
value
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
# Namespace for all yard-lint errors
|
|
6
|
+
module Errors
|
|
7
|
+
# Base error class for all yard-lint errors
|
|
8
|
+
class BaseError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Raised when a configuration file is not found
|
|
11
|
+
class ConfigFileNotFoundError < BaseError; end
|
|
12
|
+
|
|
13
|
+
# Raised when a circular dependency is detected in configuration inheritance
|
|
14
|
+
class CircularDependencyError < BaseError; end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
# Output formatters for displaying linting progress and results
|
|
6
|
+
module Formatters
|
|
7
|
+
# Simple progress formatter that shows which validator is running
|
|
8
|
+
# Similar to RuboCop's progress display
|
|
9
|
+
class Progress
|
|
10
|
+
# Initialize progress formatter
|
|
11
|
+
# @param output [IO] output stream (default: $stdout)
|
|
12
|
+
def initialize(output = $stdout)
|
|
13
|
+
@output = output
|
|
14
|
+
@total = 0
|
|
15
|
+
@current = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Start progress display
|
|
19
|
+
# @param total [Integer] total number of validators
|
|
20
|
+
def start(total)
|
|
21
|
+
@total = total
|
|
22
|
+
@current = 0
|
|
23
|
+
@output.print "Inspecting with #{total} validators\n"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Update progress with current validator
|
|
27
|
+
# @param current [Integer] current validator number
|
|
28
|
+
# @param validator_name [String] name of the validator
|
|
29
|
+
def update(current, validator_name)
|
|
30
|
+
@current = current
|
|
31
|
+
# Clear line and show progress
|
|
32
|
+
@output.print "\r\e[K" # Clear line
|
|
33
|
+
@output.print format(
|
|
34
|
+
'[%<current>d/%<total>d] %<name>s',
|
|
35
|
+
current: current,
|
|
36
|
+
total: @total,
|
|
37
|
+
name: validator_name
|
|
38
|
+
)
|
|
39
|
+
@output.flush
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Finish progress display
|
|
43
|
+
def finish
|
|
44
|
+
@output.print "\r\e[K" # Clear the progress line
|
|
45
|
+
@output.flush
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
# Parsers for extracting offense details from YARD command output
|
|
6
|
+
module Parsers
|
|
7
|
+
# Base class used for all the subparsers of a yard parser
|
|
8
|
+
class Base
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :regexps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param string [String] string from which we want to extract informations
|
|
14
|
+
# @param regexp_name [Symbol] name of a regexp used to extract a given information
|
|
15
|
+
# @return [Array<String>] array with extracted details or empty array if there's
|
|
16
|
+
# nothing worth extracting
|
|
17
|
+
def match(string, regexp_name)
|
|
18
|
+
string.match(self.class.regexps[regexp_name])&.captures || []
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Parsers
|
|
6
|
+
# Base class for all one line warnings parsers
|
|
7
|
+
class OneLineBase < Base
|
|
8
|
+
# @param yard_stats [String] raw yard stats results string
|
|
9
|
+
# @return [Array<Hash>] array with all warnings informations from yard stats analysis
|
|
10
|
+
def call(yard_stats)
|
|
11
|
+
# Not all the lines from the yard_stats output are valuable, that's why we filter
|
|
12
|
+
# them out, preprocess and leave only those against which we should match
|
|
13
|
+
rows = classify(yard_stats.split("\n"))
|
|
14
|
+
|
|
15
|
+
rows.map do |warning|
|
|
16
|
+
{
|
|
17
|
+
name: self.class.to_s.split('::').last,
|
|
18
|
+
message: match(warning, :message).last,
|
|
19
|
+
location: match(warning, :location).last,
|
|
20
|
+
line: match(warning, :line).last.to_i
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @param rows [Array<String>] array with lines of output from yard stats
|
|
28
|
+
# @return [Array<String>] Array with rows that match the pattern
|
|
29
|
+
def classify(rows)
|
|
30
|
+
rows.grep(self.class.regexps[:general])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|