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.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.coditsu/ci.yml +3 -0
  3. data/CHANGELOG.md +28 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +454 -0
  6. data/bin/console +11 -0
  7. data/bin/setup +8 -0
  8. data/bin/yard-lint +109 -0
  9. data/lib/yard/lint/command_cache.rb +77 -0
  10. data/lib/yard/lint/config.rb +255 -0
  11. data/lib/yard/lint/config_loader.rb +198 -0
  12. data/lib/yard/lint/errors.rb +17 -0
  13. data/lib/yard/lint/formatters/progress.rb +50 -0
  14. data/lib/yard/lint/parsers/base.rb +23 -0
  15. data/lib/yard/lint/parsers/one_line_base.rb +35 -0
  16. data/lib/yard/lint/parsers/two_line_base.rb +45 -0
  17. data/lib/yard/lint/result_builder.rb +130 -0
  18. data/lib/yard/lint/results/aggregate.rb +86 -0
  19. data/lib/yard/lint/results/base.rb +156 -0
  20. data/lib/yard/lint/runner.rb +125 -0
  21. data/lib/yard/lint/validators/base.rb +120 -0
  22. data/lib/yard/lint/validators/config.rb +30 -0
  23. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/config.rb +20 -0
  24. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +43 -0
  25. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/result.rb +26 -0
  26. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +48 -0
  27. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +13 -0
  28. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/config.rb +20 -0
  29. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/messages_builder.rb +24 -0
  30. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +45 -0
  31. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/result.rb +25 -0
  32. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +55 -0
  33. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +13 -0
  34. data/lib/yard/lint/validators/documentation/undocumented_objects/config.rb +21 -0
  35. data/lib/yard/lint/validators/documentation/undocumented_objects/messages_builder.rb +23 -0
  36. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +39 -0
  37. data/lib/yard/lint/validators/documentation/undocumented_objects/result.rb +25 -0
  38. data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +39 -0
  39. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +14 -0
  40. data/lib/yard/lint/validators/semantic/abstract_methods/config.rb +24 -0
  41. data/lib/yard/lint/validators/semantic/abstract_methods/messages_builder.rb +25 -0
  42. data/lib/yard/lint/validators/semantic/abstract_methods/parser.rb +45 -0
  43. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +42 -0
  44. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +65 -0
  45. data/lib/yard/lint/validators/semantic/abstract_methods.rb +13 -0
  46. data/lib/yard/lint/validators/tags/api_tags/config.rb +21 -0
  47. data/lib/yard/lint/validators/tags/api_tags/messages_builder.rb +29 -0
  48. data/lib/yard/lint/validators/tags/api_tags/parser.rb +50 -0
  49. data/lib/yard/lint/validators/tags/api_tags/result.rb +42 -0
  50. data/lib/yard/lint/validators/tags/api_tags/validator.rb +69 -0
  51. data/lib/yard/lint/validators/tags/api_tags.rb +13 -0
  52. data/lib/yard/lint/validators/tags/invalid_types/config.rb +22 -0
  53. data/lib/yard/lint/validators/tags/invalid_types/messages_builder.rb +24 -0
  54. data/lib/yard/lint/validators/tags/invalid_types/parser.rb +16 -0
  55. data/lib/yard/lint/validators/tags/invalid_types/result.rb +25 -0
  56. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +106 -0
  57. data/lib/yard/lint/validators/tags/invalid_types.rb +13 -0
  58. data/lib/yard/lint/validators/tags/option_tags/config.rb +21 -0
  59. data/lib/yard/lint/validators/tags/option_tags/messages_builder.rb +24 -0
  60. data/lib/yard/lint/validators/tags/option_tags/parser.rb +45 -0
  61. data/lib/yard/lint/validators/tags/option_tags/result.rb +42 -0
  62. data/lib/yard/lint/validators/tags/option_tags/validator.rb +61 -0
  63. data/lib/yard/lint/validators/tags/option_tags.rb +13 -0
  64. data/lib/yard/lint/validators/tags/order/config.rb +33 -0
  65. data/lib/yard/lint/validators/tags/order/messages_builder.rb +30 -0
  66. data/lib/yard/lint/validators/tags/order/parser.rb +66 -0
  67. data/lib/yard/lint/validators/tags/order/result.rb +26 -0
  68. data/lib/yard/lint/validators/tags/order/validator.rb +89 -0
  69. data/lib/yard/lint/validators/tags/order.rb +13 -0
  70. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/config.rb +22 -0
  71. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/parser.rb +22 -0
  72. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/result.rb +25 -0
  73. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +33 -0
  74. data/lib/yard/lint/validators/warnings/duplicated_parameter_name.rb +14 -0
  75. data/lib/yard/lint/validators/warnings/invalid_directive_format/config.rb +22 -0
  76. data/lib/yard/lint/validators/warnings/invalid_directive_format/parser.rb +22 -0
  77. data/lib/yard/lint/validators/warnings/invalid_directive_format/result.rb +25 -0
  78. data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +33 -0
  79. data/lib/yard/lint/validators/warnings/invalid_directive_format.rb +14 -0
  80. data/lib/yard/lint/validators/warnings/invalid_tag_format/config.rb +22 -0
  81. data/lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb +22 -0
  82. data/lib/yard/lint/validators/warnings/invalid_tag_format/result.rb +25 -0
  83. data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +33 -0
  84. data/lib/yard/lint/validators/warnings/invalid_tag_format.rb +14 -0
  85. data/lib/yard/lint/validators/warnings/unknown_directive/config.rb +22 -0
  86. data/lib/yard/lint/validators/warnings/unknown_directive/parser.rb +22 -0
  87. data/lib/yard/lint/validators/warnings/unknown_directive/result.rb +25 -0
  88. data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +33 -0
  89. data/lib/yard/lint/validators/warnings/unknown_directive.rb +14 -0
  90. data/lib/yard/lint/validators/warnings/unknown_parameter_name/config.rb +22 -0
  91. data/lib/yard/lint/validators/warnings/unknown_parameter_name/parser.rb +22 -0
  92. data/lib/yard/lint/validators/warnings/unknown_parameter_name/result.rb +25 -0
  93. data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +33 -0
  94. data/lib/yard/lint/validators/warnings/unknown_parameter_name.rb +14 -0
  95. data/lib/yard/lint/validators/warnings/unknown_tag/config.rb +22 -0
  96. data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +24 -0
  97. data/lib/yard/lint/validators/warnings/unknown_tag/result.rb +25 -0
  98. data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +33 -0
  99. data/lib/yard/lint/validators/warnings/unknown_tag.rb +14 -0
  100. data/lib/yard/lint/version.rb +8 -0
  101. data/lib/yard/lint.rb +76 -0
  102. data/lib/yard-lint.rb +11 -0
  103. data/renovate.json +22 -0
  104. 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