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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +174 -1
- data/README.md +118 -3
- data/Rakefile +20 -0
- data/bin/yard-lint +80 -37
- data/lib/yard/lint/config.rb +5 -0
- data/lib/yard/lint/config_generator.rb +8 -179
- data/lib/yard/lint/config_updater.rb +222 -0
- data/lib/yard/lint/errors.rb +6 -0
- data/lib/yard/lint/executor/in_process_registry.rb +130 -0
- data/lib/yard/lint/executor/query_executor.rb +109 -0
- data/lib/yard/lint/executor/result_collector.rb +55 -0
- data/lib/yard/lint/executor/warning_dispatcher.rb +79 -0
- data/lib/yard/lint/results/base.rb +2 -1
- data/lib/yard/lint/runner.rb +88 -35
- data/lib/yard/lint/stats_calculator.rb +1 -1
- data/lib/yard/lint/templates/default_config.yml +279 -0
- data/lib/yard/lint/templates/strict_config.yml +283 -0
- data/lib/yard/lint/validators/base.rb +52 -118
- data/lib/yard/lint/validators/documentation/blank_line_before_definition/config.rb +25 -0
- data/lib/yard/lint/validators/documentation/blank_line_before_definition/messages_builder.rb +39 -0
- data/lib/yard/lint/validators/documentation/blank_line_before_definition/parser.rb +59 -0
- data/lib/yard/lint/validators/documentation/blank_line_before_definition/result.rb +61 -0
- data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +94 -0
- data/lib/yard/lint/validators/documentation/blank_line_before_definition.rb +63 -0
- data/lib/yard/lint/validators/documentation/empty_comment_line/config.rb +24 -0
- data/lib/yard/lint/validators/documentation/empty_comment_line/messages_builder.rb +34 -0
- data/lib/yard/lint/validators/documentation/empty_comment_line/parser.rb +60 -0
- data/lib/yard/lint/validators/documentation/empty_comment_line/result.rb +25 -0
- data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +109 -0
- data/lib/yard/lint/validators/documentation/empty_comment_line.rb +58 -0
- data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +36 -21
- data/lib/yard/lint/validators/documentation/markdown_syntax.rb +0 -1
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +19 -29
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +0 -1
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +18 -34
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +0 -1
- data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +2 -2
- data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +17 -25
- data/lib/yard/lint/validators/documentation/undocumented_objects.rb +4 -5
- data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -21
- data/lib/yard/lint/validators/documentation/undocumented_options.rb +0 -1
- data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +2 -2
- data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +31 -43
- data/lib/yard/lint/validators/semantic/abstract_methods.rb +0 -1
- data/lib/yard/lint/validators/tags/api_tags/validator.rb +24 -39
- data/lib/yard/lint/validators/tags/api_tags.rb +0 -1
- data/lib/yard/lint/validators/tags/collection_type/parser.rb +1 -1
- data/lib/yard/lint/validators/tags/collection_type/validator.rb +37 -66
- data/lib/yard/lint/validators/tags/collection_type.rb +0 -1
- data/lib/yard/lint/validators/tags/example_syntax/validator.rb +51 -64
- data/lib/yard/lint/validators/tags/example_syntax.rb +0 -1
- data/lib/yard/lint/validators/tags/informal_notation/config.rb +40 -0
- data/lib/yard/lint/validators/tags/informal_notation/messages_builder.rb +35 -0
- data/lib/yard/lint/validators/tags/informal_notation/parser.rb +55 -0
- data/lib/yard/lint/validators/tags/informal_notation/result.rb +26 -0
- data/lib/yard/lint/validators/tags/informal_notation/validator.rb +133 -0
- data/lib/yard/lint/validators/tags/informal_notation.rb +45 -0
- data/lib/yard/lint/validators/tags/invalid_types/validator.rb +57 -70
- data/lib/yard/lint/validators/tags/invalid_types.rb +0 -1
- data/lib/yard/lint/validators/tags/meaningless_tag/parser.rb +1 -1
- data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +22 -54
- data/lib/yard/lint/validators/tags/meaningless_tag.rb +0 -1
- data/lib/yard/lint/validators/tags/non_ascii_type/config.rb +21 -0
- data/lib/yard/lint/validators/tags/non_ascii_type/messages_builder.rb +29 -0
- data/lib/yard/lint/validators/tags/non_ascii_type/parser.rb +59 -0
- data/lib/yard/lint/validators/tags/non_ascii_type/result.rb +25 -0
- data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +50 -0
- data/lib/yard/lint/validators/tags/non_ascii_type.rb +39 -0
- data/lib/yard/lint/validators/tags/option_tags/result.rb +2 -2
- data/lib/yard/lint/validators/tags/option_tags/validator.rb +25 -40
- data/lib/yard/lint/validators/tags/option_tags.rb +0 -1
- data/lib/yard/lint/validators/tags/order/validator.rb +28 -55
- data/lib/yard/lint/validators/tags/order.rb +0 -1
- data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +15 -1
- data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +5 -0
- data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +134 -100
- data/lib/yard/lint/validators/tags/redundant_param_description.rb +0 -1
- data/lib/yard/lint/validators/tags/tag_group_separator/config.rb +29 -0
- data/lib/yard/lint/validators/tags/tag_group_separator/messages_builder.rb +49 -0
- data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +67 -0
- data/lib/yard/lint/validators/tags/tag_group_separator/result.rb +28 -0
- data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +117 -0
- data/lib/yard/lint/validators/tags/tag_group_separator.rb +49 -0
- data/lib/yard/lint/validators/tags/tag_type_position/parser.rb +1 -1
- data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +53 -84
- data/lib/yard/lint/validators/tags/tag_type_position.rb +4 -5
- data/lib/yard/lint/validators/tags/type_syntax/parser.rb +8 -3
- data/lib/yard/lint/validators/tags/type_syntax/validator.rb +29 -59
- data/lib/yard/lint/validators/tags/type_syntax.rb +0 -1
- data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +1 -18
- data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +1 -18
- data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +1 -18
- data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +1 -18
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +243 -0
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/result.rb +4 -3
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +1 -18
- data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +144 -0
- data/lib/yard/lint/validators/warnings/unknown_tag/result.rb +4 -3
- data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +1 -18
- data/lib/yard/lint/validators/warnings/unknown_tag.rb +10 -0
- data/lib/yard/lint/version.rb +1 -1
- data/lib/yard/lint.rb +81 -13
- data/renovate.json +1 -8
- metadata +40 -6
- data/bin/console +0 -11
- data/bin/setup +0 -8
- 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
|
-
#
|
|
16
|
-
# @
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
16
|
-
# @
|
|
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
|
|
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
|
-
#
|
|
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:
|
data/lib/yard/lint/version.rb
CHANGED
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
|
|
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
|
|
117
|
+
files.select { |file| File.file?(file) && file.end_with?('.rb') }
|
|
118
|
+
end
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
}
|