yard-lint 1.2.2 → 1.3.0.rc1

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +174 -1
  3. data/README.md +118 -3
  4. data/Rakefile +20 -0
  5. data/bin/yard-lint +80 -37
  6. data/lib/yard/lint/config.rb +5 -0
  7. data/lib/yard/lint/config_generator.rb +8 -179
  8. data/lib/yard/lint/config_updater.rb +222 -0
  9. data/lib/yard/lint/errors.rb +6 -0
  10. data/lib/yard/lint/executor/in_process_registry.rb +130 -0
  11. data/lib/yard/lint/executor/query_executor.rb +109 -0
  12. data/lib/yard/lint/executor/result_collector.rb +55 -0
  13. data/lib/yard/lint/executor/warning_dispatcher.rb +79 -0
  14. data/lib/yard/lint/results/base.rb +2 -1
  15. data/lib/yard/lint/runner.rb +88 -35
  16. data/lib/yard/lint/stats_calculator.rb +1 -1
  17. data/lib/yard/lint/templates/default_config.yml +279 -0
  18. data/lib/yard/lint/templates/strict_config.yml +283 -0
  19. data/lib/yard/lint/validators/base.rb +52 -118
  20. data/lib/yard/lint/validators/documentation/blank_line_before_definition/config.rb +25 -0
  21. data/lib/yard/lint/validators/documentation/blank_line_before_definition/messages_builder.rb +39 -0
  22. data/lib/yard/lint/validators/documentation/blank_line_before_definition/parser.rb +59 -0
  23. data/lib/yard/lint/validators/documentation/blank_line_before_definition/result.rb +61 -0
  24. data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +94 -0
  25. data/lib/yard/lint/validators/documentation/blank_line_before_definition.rb +63 -0
  26. data/lib/yard/lint/validators/documentation/empty_comment_line/config.rb +24 -0
  27. data/lib/yard/lint/validators/documentation/empty_comment_line/messages_builder.rb +34 -0
  28. data/lib/yard/lint/validators/documentation/empty_comment_line/parser.rb +60 -0
  29. data/lib/yard/lint/validators/documentation/empty_comment_line/result.rb +25 -0
  30. data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +109 -0
  31. data/lib/yard/lint/validators/documentation/empty_comment_line.rb +58 -0
  32. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +36 -21
  33. data/lib/yard/lint/validators/documentation/markdown_syntax.rb +0 -1
  34. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +19 -29
  35. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +0 -1
  36. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +18 -34
  37. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +0 -1
  38. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +2 -2
  39. data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +17 -25
  40. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +4 -5
  41. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -21
  42. data/lib/yard/lint/validators/documentation/undocumented_options.rb +0 -1
  43. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +2 -2
  44. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +31 -43
  45. data/lib/yard/lint/validators/semantic/abstract_methods.rb +0 -1
  46. data/lib/yard/lint/validators/tags/api_tags/validator.rb +24 -39
  47. data/lib/yard/lint/validators/tags/api_tags.rb +0 -1
  48. data/lib/yard/lint/validators/tags/collection_type/parser.rb +1 -1
  49. data/lib/yard/lint/validators/tags/collection_type/validator.rb +37 -66
  50. data/lib/yard/lint/validators/tags/collection_type.rb +0 -1
  51. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +51 -64
  52. data/lib/yard/lint/validators/tags/example_syntax.rb +0 -1
  53. data/lib/yard/lint/validators/tags/informal_notation/config.rb +40 -0
  54. data/lib/yard/lint/validators/tags/informal_notation/messages_builder.rb +35 -0
  55. data/lib/yard/lint/validators/tags/informal_notation/parser.rb +55 -0
  56. data/lib/yard/lint/validators/tags/informal_notation/result.rb +26 -0
  57. data/lib/yard/lint/validators/tags/informal_notation/validator.rb +133 -0
  58. data/lib/yard/lint/validators/tags/informal_notation.rb +45 -0
  59. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +57 -70
  60. data/lib/yard/lint/validators/tags/invalid_types.rb +0 -1
  61. data/lib/yard/lint/validators/tags/meaningless_tag/parser.rb +1 -1
  62. data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +22 -54
  63. data/lib/yard/lint/validators/tags/meaningless_tag.rb +0 -1
  64. data/lib/yard/lint/validators/tags/non_ascii_type/config.rb +21 -0
  65. data/lib/yard/lint/validators/tags/non_ascii_type/messages_builder.rb +29 -0
  66. data/lib/yard/lint/validators/tags/non_ascii_type/parser.rb +59 -0
  67. data/lib/yard/lint/validators/tags/non_ascii_type/result.rb +25 -0
  68. data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +50 -0
  69. data/lib/yard/lint/validators/tags/non_ascii_type.rb +39 -0
  70. data/lib/yard/lint/validators/tags/option_tags/result.rb +2 -2
  71. data/lib/yard/lint/validators/tags/option_tags/validator.rb +25 -40
  72. data/lib/yard/lint/validators/tags/option_tags.rb +0 -1
  73. data/lib/yard/lint/validators/tags/order/validator.rb +28 -55
  74. data/lib/yard/lint/validators/tags/order.rb +0 -1
  75. data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +15 -1
  76. data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +5 -0
  77. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +134 -100
  78. data/lib/yard/lint/validators/tags/redundant_param_description.rb +0 -1
  79. data/lib/yard/lint/validators/tags/tag_group_separator/config.rb +29 -0
  80. data/lib/yard/lint/validators/tags/tag_group_separator/messages_builder.rb +49 -0
  81. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +67 -0
  82. data/lib/yard/lint/validators/tags/tag_group_separator/result.rb +28 -0
  83. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +117 -0
  84. data/lib/yard/lint/validators/tags/tag_group_separator.rb +49 -0
  85. data/lib/yard/lint/validators/tags/tag_type_position/parser.rb +1 -1
  86. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +53 -84
  87. data/lib/yard/lint/validators/tags/tag_type_position.rb +4 -5
  88. data/lib/yard/lint/validators/tags/type_syntax/parser.rb +8 -3
  89. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +29 -59
  90. data/lib/yard/lint/validators/tags/type_syntax.rb +0 -1
  91. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +1 -18
  92. data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +1 -18
  93. data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +1 -18
  94. data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +1 -18
  95. data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +243 -0
  96. data/lib/yard/lint/validators/warnings/unknown_parameter_name/result.rb +4 -3
  97. data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +1 -18
  98. data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +144 -0
  99. data/lib/yard/lint/validators/warnings/unknown_tag/result.rb +4 -3
  100. data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +1 -18
  101. data/lib/yard/lint/validators/warnings/unknown_tag.rb +10 -0
  102. data/lib/yard/lint/version.rb +1 -1
  103. data/lib/yard/lint.rb +81 -13
  104. data/renovate.json +1 -8
  105. metadata +40 -6
  106. data/bin/console +0 -11
  107. data/bin/setup +0 -8
  108. data/lib/yard/lint/command_cache.rb +0 -93
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'did_you_mean'
4
+ require 'shellwords'
5
+
6
+ module Yard
7
+ module Lint
8
+ module Validators
9
+ module Warnings
10
+ module UnknownParameterName
11
+ # Builds enhanced messages with "did you mean" suggestions
12
+ class MessagesBuilder
13
+ class << self
14
+ # Build message with suggestion for unknown parameter
15
+ # @param offense [Hash] offense data with :message, :location (file), :line keys
16
+ # @return [String] formatted message with suggestion if available
17
+ def call(offense)
18
+ message = offense[:message] || 'UnknownParameterName detected'
19
+
20
+ # Extract the unknown parameter name from the message
21
+ # Format: "@param tag has unknown parameter name: param_name"
22
+ match = message.match(/@param tag has unknown parameter name: (\w+)/)
23
+ return message unless match
24
+
25
+ unknown_param = match[1]
26
+
27
+ # Get actual parameters for the method at this location
28
+ # Note: offense[:location] contains the file path
29
+ file = offense[:location]
30
+ line = offense[:line]
31
+ actual_params = fetch_actual_parameters(file, line)
32
+ return message if actual_params.empty?
33
+
34
+ # Find best suggestion using did_you_mean
35
+ suggestion = find_suggestion(unknown_param, actual_params)
36
+
37
+ if suggestion
38
+ "#{message} (did you mean '#{suggestion}'?)"
39
+ else
40
+ message
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Fetch actual method parameters from YARD at the given location
47
+ # @param file [String] file path
48
+ # @param line [Integer, String] line number
49
+ # @return [Array<String>] array of actual parameter names
50
+ def fetch_actual_parameters(file, line)
51
+ return [] unless file && line
52
+
53
+ line_num = line.to_i
54
+
55
+ # First, try to parse directly from the Ruby source file
56
+ # This is faster and doesn't require YARD to be fully loaded
57
+ params = parse_parameters_from_source(file, line_num)
58
+ return params unless params.empty?
59
+
60
+ # Fallback: Query YARD list for the method
61
+ # This requires YARD to parse the file first
62
+ fetch_parameters_via_yard(file, line_num)
63
+ rescue StandardError => e
64
+ # If anything goes wrong, just return empty array (no suggestion)
65
+ warn "Failed to fetch parameters: #{e.message}" if ENV['DEBUG']
66
+ []
67
+ end
68
+
69
+ # Parse method parameters directly from Ruby source file
70
+ # @param file [String] file path
71
+ # @param line [Integer] line number (approximate location of method)
72
+ # @return [Array<String>] array of parameter names
73
+ def parse_parameters_from_source(file, line)
74
+ return [] unless File.exist?(file)
75
+
76
+ # Calculate the search range (line numbers are 1-indexed)
77
+ start_line = [(line - 15), 1].max
78
+ end_line = line + 5
79
+
80
+ # Only read the lines in the relevant range to avoid loading the whole file
81
+ lines = []
82
+ current_line_num = 1
83
+ File.foreach(file) do |source_line|
84
+ lines << source_line if current_line_num.between?(start_line, end_line)
85
+ break if current_line_num > end_line
86
+
87
+ current_line_num += 1
88
+ end
89
+
90
+ # Search for method definition in the collected lines
91
+ in_multiline_def = false
92
+ param_lines = []
93
+
94
+ lines.each do |source_line|
95
+ # Match single-line method definitions: def method_name(param1, param2)
96
+ if source_line =~ /^\s*def\s+\w+\s*\((.*?)\)/
97
+ params_str = ::Regexp.last_match(1)
98
+ return extract_parameter_names(params_str)
99
+ # Match start of multi-line method definition: def method_name(
100
+ elsif source_line =~ /^\s*def\s+\w+\s*\((.*)$/
101
+ in_multiline_def = true
102
+ param_lines << ::Regexp.last_match(1)
103
+ next
104
+ elsif in_multiline_def
105
+ param_lines << source_line.strip
106
+ # Check if this line closes the parameter list
107
+ if source_line.include?(')')
108
+ # Join all lines and extract params
109
+ params_str = param_lines.join(' ')
110
+ # Remove trailing ')' and anything after it
111
+ params_str = params_str[/\A(.*?)\)/, 1] || params_str
112
+ return extract_parameter_names(params_str)
113
+ end
114
+ elsif source_line.match?(/^\s*def\s+\w+\s*$/)
115
+ # Method with no parameters
116
+ return []
117
+ end
118
+ end
119
+
120
+ []
121
+ rescue StandardError => e
122
+ warn "Failed to parse source: #{e.message}" if ENV['DEBUG']
123
+ []
124
+ end
125
+
126
+ # Extract parameter names from a parameter string
127
+ # Handles various parameter formats: regular, default values, splat, keyword, block
128
+ # @param params_str [String] parameter string from method signature
129
+ # @return [Array<String>] array of parameter names
130
+ def extract_parameter_names(params_str)
131
+ return [] if params_str.nil? || params_str.strip.empty?
132
+
133
+ params_str.split(',').map do |param|
134
+ # Remove default values: "name = 'default'" => "name"
135
+ param = param.split('=').first
136
+ # Remove type annotations: "name:" => "name"
137
+ param = param.delete(':')
138
+ # Remove splat and block symbols: "*args", "**kwargs", "&block"
139
+ param = param.delete('*&')
140
+ # Strip whitespace
141
+ param.strip
142
+ end.reject(&:empty?)
143
+ end
144
+
145
+ # Fetch parameters via YARD list command (fallback method)
146
+ # @param file [String] file path
147
+ # @param line [Integer] line number
148
+ # @return [Array<String>] array of parameter names
149
+ def fetch_parameters_via_yard(file, line)
150
+ # Query YARD for the method at this location
151
+ # Use Shellwords.escape to prevent command injection
152
+ escaped_file = Shellwords.escape(file)
153
+ query = "'type == :method && file == \"#{escaped_file}\" && line >= #{line - 15} && line <= #{line + 5}'"
154
+ cmd = "yard list --query #{query} 2>/dev/null"
155
+
156
+ output = `#{cmd}`.strip
157
+ return [] if output.empty?
158
+
159
+ # YARD list doesn't show parameters, we'd need to parse the source
160
+ # So this fallback is just for validation - use source parsing instead
161
+ []
162
+ rescue StandardError => e
163
+ warn "Failed to query YARD: #{e.message}" if ENV['DEBUG']
164
+ []
165
+ end
166
+
167
+ # Find the best suggestion using DidYouMean spell checker
168
+ # @param unknown_param [String] the unknown parameter name
169
+ # @param actual_params [Array<String>] array of actual parameter names
170
+ # @return [String, nil] suggested parameter name or nil
171
+ def find_suggestion(unknown_param, actual_params)
172
+ return nil if actual_params.empty?
173
+
174
+ # Use DidYouMean::SpellChecker for smart suggestions
175
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: actual_params)
176
+ suggestions = spell_checker.correct(unknown_param)
177
+
178
+ # If DidYouMean found suggestions, return the best one
179
+ return suggestions.first unless suggestions.empty?
180
+
181
+ # Otherwise, fallback to Levenshtein distance
182
+ find_suggestion_fallback(unknown_param, actual_params)
183
+ rescue StandardError => e
184
+ # Fallback to simple Levenshtein distance if DidYouMean fails
185
+ warn "DidYouMean failed: #{e.message}, using fallback" if ENV['DEBUG']
186
+ find_suggestion_fallback(unknown_param, actual_params)
187
+ end
188
+
189
+ # Fallback suggestion finder using simple Levenshtein distance
190
+ # @param unknown_param [String] the unknown parameter name
191
+ # @param actual_params [Array<String>] array of actual parameter names
192
+ # @return [String, nil] suggested parameter name or nil
193
+ def find_suggestion_fallback(unknown_param, actual_params)
194
+ # Calculate Levenshtein distance for each parameter
195
+ distances = actual_params.map do |param|
196
+ [param, levenshtein_distance(unknown_param, param)]
197
+ end
198
+
199
+ # Sort by distance and get the closest match
200
+ best_match = distances.min_by { |_param, distance| distance }
201
+
202
+ # Only suggest if the distance is reasonable (less than half the length)
203
+ return nil unless best_match
204
+
205
+ param, distance = best_match
206
+ max_distance = [unknown_param.length, param.length].max / 2
207
+
208
+ distance <= max_distance ? param : nil
209
+ end
210
+
211
+ # Calculate Levenshtein distance between two strings
212
+ # @param str1 [String] first string
213
+ # @param str2 [String] second string
214
+ # @return [Integer] Levenshtein distance
215
+ def levenshtein_distance(str1, str2)
216
+ return str2.length if str1.empty?
217
+ return str1.length if str2.empty?
218
+
219
+ matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
220
+
221
+ (0..str1.length).each { |i| matrix[i][0] = i }
222
+ (0..str2.length).each { |j| matrix[0][j] = j }
223
+
224
+ (1..str1.length).each do |i|
225
+ (1..str2.length).each do |j|
226
+ cost = str1[i - 1] == str2[j - 1] ? 0 : 1
227
+ matrix[i][j] = [
228
+ matrix[i - 1][j] + 1, # deletion
229
+ matrix[i][j - 1] + 1, # insertion
230
+ matrix[i - 1][j - 1] + cost # substitution
231
+ ].min
232
+ end
233
+ end
234
+
235
+ matrix[str1.length][str2.length]
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -12,10 +12,11 @@ module Yard
12
12
  self.offense_name = 'UnknownParameterName'
13
13
 
14
14
  # Build human-readable message for UnknownParameterName offense
15
- # @param offense [Hash] offense data with :message key
16
- # @return [String] formatted message
15
+ # Uses MessagesBuilder to add "did you mean" suggestions
16
+ # @param offense [Hash] offense data with :message, :location, :line keys
17
+ # @return [String] formatted message with suggestion if available
17
18
  def build_message(offense)
18
- offense[:message] || 'UnknownParameterName detected'
19
+ MessagesBuilder.call(offense)
19
20
  end
20
21
  end
21
22
  end
@@ -5,25 +5,8 @@ module Yard
5
5
  module Validators
6
6
  module Warnings
7
7
  module UnknownParameterName
8
- # Runs YARD stats command to check for unknownparametername
8
+ # Validator for detecting unknown parameter name warnings from YARD
9
9
  class Validator < Base
10
- private
11
-
12
- # Runs YARD stats command with proper settings on a given dir and files
13
- # @param dir [String] dir where we should generate the temp docs
14
- # @param file_list_path [String] path to temp file containing file paths (one per line)
15
- # @return [Hash] shell command execution hash results
16
- def yard_cmd(dir, file_list_path)
17
- cmd = <<~CMD
18
- cat #{Shellwords.escape(file_list_path)} | xargs yard stats \
19
- #{shell_arguments} \
20
- --compact \
21
- -b #{Shellwords.escape(dir)}
22
- CMD
23
- cmd = cmd.tr("\n", ' ')
24
-
25
- shell(cmd)
26
- end
27
10
  end
28
11
  end
29
12
  end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Warnings
7
+ module UnknownTag
8
+ # Builds enhanced messages with "did you mean" suggestions for unknown tags
9
+ class MessagesBuilder
10
+ class << self
11
+ # Dynamically fetch list of valid YARD meta-data tags from YARD::Tags::Library
12
+ # This ensures we're always in sync with the installed YARD version
13
+ # @return [Array<String>] array of tag names (without @ prefix)
14
+ def known_tags
15
+ @known_tags ||= begin
16
+ lib = ::YARD::Tags::Library.instance
17
+ lib.methods
18
+ .grep(/_tag$/)
19
+ .map { |m| m.to_s.sub(/_tag$/, '') }
20
+ .sort
21
+ .freeze
22
+ end
23
+ end
24
+
25
+ # Dynamically fetch list of valid YARD directives from YARD::Tags::Library
26
+ # This ensures we're always in sync with the installed YARD version
27
+ # @return [Array<String>] array of directive names (without @! prefix)
28
+ def known_directives
29
+ @known_directives ||= begin
30
+ lib = ::YARD::Tags::Library.instance
31
+ lib.methods
32
+ .grep(/_directive$/)
33
+ .map { |m| m.to_s.sub(/_directive$/, '') }
34
+ .sort
35
+ .freeze
36
+ end
37
+ end
38
+
39
+ # Combined list of all known tags and directives
40
+ # @return [Array<String>] array of all valid tag and directive names
41
+ def all_known_tags
42
+ @all_known_tags ||= (known_tags + known_directives).freeze
43
+ end
44
+ # Build message with suggestion for unknown tag
45
+ # @param offense [Hash] offense data with :message, :location (file), :line keys
46
+ # @return [String] formatted message with suggestion if available
47
+ def call(offense)
48
+ message = offense[:message] || 'Unknown tag detected'
49
+
50
+ # Extract the unknown tag name from the message
51
+ # Format: "Unknown tag @tagname in file..."
52
+ match = message.match(/Unknown tag @(\w+)/)
53
+ return message unless match
54
+
55
+ unknown_tag = match[1]
56
+
57
+ # Find best suggestion using did_you_mean
58
+ suggestion = find_suggestion(unknown_tag)
59
+
60
+ if suggestion
61
+ # Replace just the descriptive part before "in file"
62
+ message.sub(/Unknown tag @\w+/, "Unknown tag @#{unknown_tag} (did you mean '@#{suggestion}'?)")
63
+ else
64
+ message
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Find the best suggestion using DidYouMean spell checker
71
+ # @param unknown_tag [String] the unknown tag name (without @ prefix)
72
+ # @return [String, nil] suggested tag name or nil
73
+ def find_suggestion(unknown_tag)
74
+ return nil if unknown_tag.nil? || unknown_tag.empty?
75
+
76
+ # Use DidYouMean::SpellChecker for smart suggestions
77
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: all_known_tags)
78
+ suggestions = spell_checker.correct(unknown_tag)
79
+
80
+ # If DidYouMean found suggestions, return the best one
81
+ return suggestions.first unless suggestions.empty?
82
+
83
+ # Otherwise, fallback to Levenshtein distance
84
+ find_suggestion_fallback(unknown_tag)
85
+ rescue StandardError => e
86
+ # Fallback to simple Levenshtein distance if DidYouMean fails
87
+ warn "DidYouMean failed: #{e.message}, using fallback" if ENV['DEBUG']
88
+ find_suggestion_fallback(unknown_tag)
89
+ end
90
+
91
+ # Fallback suggestion finder using simple Levenshtein distance
92
+ # @param unknown_tag [String] the unknown tag name
93
+ # @return [String, nil] suggested tag name or nil
94
+ def find_suggestion_fallback(unknown_tag)
95
+ # Calculate Levenshtein distance for each tag
96
+ distances = all_known_tags.map do |tag|
97
+ [tag, levenshtein_distance(unknown_tag, tag)]
98
+ end
99
+
100
+ # Sort by distance and get the closest match
101
+ best_match = distances.min_by { |_tag, distance| distance }
102
+
103
+ # Only suggest if the distance is reasonable (less than half the length)
104
+ return nil unless best_match
105
+
106
+ tag, distance = best_match
107
+ max_distance = [unknown_tag.length, tag.length].max / 2
108
+
109
+ distance <= max_distance ? tag : nil
110
+ end
111
+
112
+ # Calculate Levenshtein distance between two strings
113
+ # @param str1 [String] first string
114
+ # @param str2 [String] second string
115
+ # @return [Integer] Levenshtein distance
116
+ def levenshtein_distance(str1, str2)
117
+ return str2.length if str1.empty?
118
+ return str1.length if str2.empty?
119
+
120
+ matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
121
+
122
+ (0..str1.length).each { |i| matrix[i][0] = i }
123
+ (0..str2.length).each { |j| matrix[0][j] = j }
124
+
125
+ (1..str1.length).each do |i|
126
+ (1..str2.length).each do |j|
127
+ cost = str1[i - 1] == str2[j - 1] ? 0 : 1
128
+ matrix[i][j] = [
129
+ matrix[i - 1][j] + 1, # deletion
130
+ matrix[i][j - 1] + 1, # insertion
131
+ matrix[i - 1][j - 1] + cost # substitution
132
+ ].min
133
+ end
134
+ end
135
+
136
+ matrix[str1.length][str2.length]
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -12,10 +12,11 @@ module Yard
12
12
  self.offense_name = 'UnknownTag'
13
13
 
14
14
  # Build human-readable message for unknown tag offense
15
- # @param offense [Hash] offense data with :message key
16
- # @return [String] formatted message
15
+ # Uses MessagesBuilder to add "did you mean" suggestions
16
+ # @param offense [Hash] offense data with :message, :location, :line keys
17
+ # @return [String] formatted message with suggestion if available
17
18
  def build_message(offense)
18
- offense[:message] || 'Unknown tag detected'
19
+ MessagesBuilder.call(offense)
19
20
  end
20
21
  end
21
22
  end
@@ -5,25 +5,8 @@ module Yard
5
5
  module Validators
6
6
  module Warnings
7
7
  module UnknownTag
8
- # Runs YARD stats command to check for unknown YARD tags
8
+ # Validator for detecting unknown YARD tag warnings
9
9
  class Validator < Base
10
- private
11
-
12
- # Runs YARD stats command with proper settings on a given dir and files
13
- # @param dir [String] dir where we should generate the temp docs
14
- # @param file_list_path [String] path to temp file containing file paths (one per line)
15
- # @return [Hash] shell command execution hash results
16
- def yard_cmd(dir, file_list_path)
17
- cmd = <<~CMD
18
- cat #{Shellwords.escape(file_list_path)} | xargs yard stats \
19
- #{shell_arguments} \
20
- --compact \
21
- -b #{Shellwords.escape(dir)}
22
- CMD
23
- cmd = cmd.tr("\n", ' ')
24
-
25
- shell(cmd)
26
- end
27
10
  end
28
11
  end
29
12
  end
@@ -12,18 +12,28 @@ module Yard
12
12
  # doesn't recognize, which could indicate typos or unsupported tags.
13
13
  # This validator is enabled by default.
14
14
  #
15
+ # Provides intelligent "did you mean" suggestions for common typos using
16
+ # Ruby's did_you_mean gem with Levenshtein distance fallback.
17
+ #
15
18
  # @example Bad - Misspelled or non-existent tags
16
19
  # # @param name [String] typo in param tag
17
20
  # # @returns [String] should be @return not @returns
21
+ # # @raises [Error] should be @raise not @raises
18
22
  # def process(name)
19
23
  # end
20
24
  #
21
25
  # @example Good - Standard YARD tags
22
26
  # # @param name [String] the name
23
27
  # # @return [String] the result
28
+ # # @raise [Error] the error
24
29
  # def process(name)
25
30
  # end
26
31
  #
32
+ # **Output with suggestions:**
33
+ #
34
+ # lib/foo.rb:10: [error] Unknown tag @returns (did you mean '@return'?)
35
+ # lib/foo.rb:11: [error] Unknown tag @raises (did you mean '@raise'?)
36
+ #
27
37
  # ## Configuration
28
38
  #
29
39
  # To disable this validator:
@@ -3,6 +3,6 @@
3
3
  module Yard
4
4
  module Lint
5
5
  # @return [String] version of the YARD Lint gem
6
- VERSION = '1.2.2'
6
+ VERSION = '1.3.0.rc1'
7
7
  end
8
8
  end
data/lib/yard/lint.rb CHANGED
@@ -6,6 +6,9 @@ require 'open3'
6
6
  require 'tempfile'
7
7
  require 'tmpdir'
8
8
  require 'digest'
9
+ require 'did_you_mean'
10
+ require 'yard'
11
+ require 'set'
9
12
 
10
13
  module Yard
11
14
  # YARD Lint module providing linting functionality for YARD documentation
@@ -59,6 +62,9 @@ module Yard
59
62
  # @param config [Yard::Lint::Config] configuration object
60
63
  # @return [Array<String>] array of absolute file paths
61
64
  def get_diff_files(diff, path, config)
65
+ # Determine the base directory for relative path calculations
66
+ base_dir = determine_base_dir(path)
67
+
62
68
  # Get changed files from git based on mode
63
69
  git_files = case diff[:mode]
64
70
  when :ref
@@ -72,11 +78,7 @@ module Yard
72
78
  end
73
79
 
74
80
  # Apply exclusion patterns
75
- git_files.reject do |file|
76
- config.exclude.any? do |pattern|
77
- File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
78
- end
79
- end
81
+ git_files.reject { |file| excluded_file?(file, config.exclude, base_dir) }
80
82
  end
81
83
 
82
84
  # Expand path/glob patterns into an array of files
@@ -84,28 +86,94 @@ module Yard
84
86
  # @param config [Yard::Lint::Config] configuration object
85
87
  # @return [Array<String>] array of absolute file paths
86
88
  def expand_path(path, config)
89
+ # Determine the base directory for relative path calculations
90
+ base_dir = determine_base_dir(path)
91
+
92
+ files = discover_ruby_files(path)
93
+
94
+ # Convert to absolute paths for YARD
95
+ files = files.map { |file| File.expand_path(file) }
96
+
97
+ # Filter out excluded files
98
+ files.reject { |file| excluded_file?(file, config.exclude, base_dir) }
99
+ end
100
+
101
+ # Discover Ruby files from path/glob patterns
102
+ # @param path [String, Array<String>] path or array of paths
103
+ # @return [Array<String>] array of discovered Ruby file paths
104
+ # @raise [Errors::FileNotFoundError] if a specified path does not exist
105
+ def discover_ruby_files(path)
87
106
  files = Array(path).flat_map do |p|
88
107
  if p.include?('*')
89
108
  Dir.glob(p)
90
109
  elsif File.directory?(p)
91
110
  Dir.glob(File.join(p, '**/*.rb'))
92
111
  else
112
+ validate_path_exists!(p)
93
113
  p
94
114
  end
95
115
  end
96
116
 
97
- files = files.select { |f| File.file?(f) && f.end_with?('.rb') }
117
+ files.select { |file| File.file?(file) && file.end_with?('.rb') }
118
+ end
98
119
 
99
- # Convert to absolute paths for YARD
100
- files = files.map { |f| File.expand_path(f) }
120
+ # Validate that a path exists
121
+ # @param path [String] file or directory path to check for existence
122
+ # @raise [Errors::FileNotFoundError] if path does not exist
123
+ def validate_path_exists!(path)
124
+ return if File.exist?(path)
101
125
 
102
- # Filter out excluded files
103
- files.reject do |file|
104
- config.exclude.any? do |pattern|
105
- File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
106
- end
126
+ raise Errors::FileNotFoundError, "No such file or directory: #{path}"
127
+ end
128
+
129
+ # Determine base directory for relative path calculations
130
+ # @param path [String, Array<String>] path or array of paths
131
+ # @return [String] absolute base directory path
132
+ def determine_base_dir(path)
133
+ first_path = Array(path).first
134
+ return Dir.pwd unless first_path
135
+
136
+ absolute_path = File.expand_path(first_path)
137
+ File.directory?(absolute_path) ? absolute_path : File.dirname(absolute_path)
138
+ end
139
+
140
+ # Check if a file matches any exclusion pattern
141
+ # Patterns are matched against both absolute and relative paths
142
+ # @param file [String] absolute file path
143
+ # @param patterns [Array<String>] exclusion patterns
144
+ # @param base_dir [String] base directory for relative path calculation
145
+ # @return [Boolean] true if file should be excluded
146
+ def excluded_file?(file, patterns, base_dir)
147
+ relative_path = relative_path_from(file, base_dir)
148
+
149
+ patterns.any? do |pattern|
150
+ match_path?(pattern, file, relative_path)
107
151
  end
108
152
  end
153
+
154
+ # Calculate relative path from base directory
155
+ # @param file [String] absolute file path
156
+ # @param base_dir [String] base directory
157
+ # @return [String] relative path
158
+ def relative_path_from(file, base_dir)
159
+ if file.start_with?("#{base_dir}/")
160
+ file.sub("#{base_dir}/", '')
161
+ else
162
+ file
163
+ end
164
+ end
165
+
166
+ # Check if a pattern matches a file path
167
+ # Tries matching against both relative and absolute paths
168
+ # @param pattern [String] glob pattern
169
+ # @param absolute_path [String] absolute file path
170
+ # @param relative_path [String] relative file path
171
+ # @return [Boolean] true if pattern matches
172
+ def match_path?(pattern, absolute_path, relative_path)
173
+ flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
174
+
175
+ File.fnmatch(pattern, relative_path, flags) || File.fnmatch(pattern, absolute_path, flags)
176
+ end
109
177
  end
110
178
  end
111
179
  end
data/renovate.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "extends": [
4
4
  "config:recommended"
5
5
  ],
6
+ "minimumReleaseAge": "7 days",
6
7
  "github-actions": {
7
8
  "enabled": true,
8
9
  "pinDigests": true
@@ -10,13 +11,5 @@
10
11
  "includePaths": [
11
12
  "Gemfile",
12
13
  "yard-lint.gemspec"
13
- ],
14
- "packageRules": [
15
- {
16
- "matchManagers": [
17
- "github-actions"
18
- ],
19
- "minimumReleaseAge": "7 days"
20
- }
21
14
  ]
22
15
  }