yard-lint 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -1
- data/README.md +98 -4
- data/Rakefile +20 -0
- data/bin/yard-lint +71 -38
- data/lib/yard/lint/config.rb +5 -0
- 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/ext/irb_notifier_shim.rb +19 -6
- data/lib/yard/lint/results/base.rb +2 -1
- data/lib/yard/lint/runner.rb +50 -38
- data/lib/yard/lint/templates/default_config.yml +105 -0
- data/lib/yard/lint/templates/strict_config.yml +105 -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/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/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/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/validator.rb +53 -84
- data/lib/yard/lint/validators/tags/tag_type_position.rb +0 -1
- data/lib/yard/lint/validators/tags/type_syntax/parser.rb +7 -2
- 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/lib/yard-lint.rb +1 -1
- data/renovate.json +1 -8
- metadata +38 -2
- data/lib/yard/lint/command_cache.rb +0 -93
|
@@ -9,100 +9,69 @@ module Yard
|
|
|
9
9
|
# YARD standard (type_after_name): @param name [String] description
|
|
10
10
|
# Alternative (type_first): @param name [String] description
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# @note @return tags are not checked as they don't have parameter names
|
|
13
13
|
class Validator < Base
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# @
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Skip comment-only lines without tags
|
|
65
|
-
next unless line.include?("@")
|
|
66
|
-
|
|
67
|
-
checked_tags.each do |tag_name|
|
|
68
|
-
if enforced_style == "type_first"
|
|
69
|
-
# Detect: @tag_name word [Type] (violation when type_first is enforced)
|
|
70
|
-
pattern = /@\#{tag_name}\\s+(\\w+)\\s+\\[([^\\]]+)\\]/
|
|
71
|
-
if line =~ pattern
|
|
72
|
-
param_name = $1
|
|
73
|
-
type_info = $2
|
|
74
|
-
puts object.file + ":" + (line_num + 1).to_s + ": " + object.title
|
|
75
|
-
puts tag_name + "|" + param_name + "|" + type_info + "|type_after_name"
|
|
76
|
-
end
|
|
77
|
-
else
|
|
78
|
-
# Detect: @tag_name [Type] word (violation when type_after_name is enforced)
|
|
79
|
-
pattern = /@\#{tag_name}\\s+\\[([^\\]]+)\\]\\s+(\\w+)/
|
|
80
|
-
if line =~ pattern
|
|
81
|
-
type_info = $1
|
|
82
|
-
param_name = $2
|
|
83
|
-
puts object.file + ":" + (line_num + 1).to_s + ": " + object.title
|
|
84
|
-
puts tag_name + "|" + param_name + "|" + type_info + "|type_first"
|
|
85
|
-
end
|
|
14
|
+
# Enable in-process execution
|
|
15
|
+
in_process visibility: :public
|
|
16
|
+
|
|
17
|
+
# Execute query for a single object during in-process execution.
|
|
18
|
+
# Checks type annotation position in @param and @option tags.
|
|
19
|
+
# @param object [YARD::CodeObjects::Base] the code object to query
|
|
20
|
+
# @param collector [Executor::ResultCollector] collector for output
|
|
21
|
+
# @return [void]
|
|
22
|
+
def in_process_query(object, collector)
|
|
23
|
+
return unless object.file && File.exist?(object.file)
|
|
24
|
+
|
|
25
|
+
checked_tags = config_or_default('CheckedTags')
|
|
26
|
+
style = enforced_style
|
|
27
|
+
|
|
28
|
+
source_lines = File.readlines(object.file)
|
|
29
|
+
start_line = [object.line - 50, 0].max
|
|
30
|
+
end_line = [object.line, source_lines.length - 1].min
|
|
31
|
+
|
|
32
|
+
# Look for comments before the object definition
|
|
33
|
+
(start_line...(end_line - 1)).reverse_each do |line_num|
|
|
34
|
+
line = source_lines[line_num].to_s.strip
|
|
35
|
+
|
|
36
|
+
# Skip empty lines
|
|
37
|
+
next if line.empty?
|
|
38
|
+
|
|
39
|
+
# Stop if we hit code (non-comment line)
|
|
40
|
+
break unless line.start_with?('#')
|
|
41
|
+
|
|
42
|
+
# Skip comment-only lines without tags
|
|
43
|
+
next unless line.include?('@')
|
|
44
|
+
|
|
45
|
+
checked_tags.each do |tag_name|
|
|
46
|
+
if style == 'type_first'
|
|
47
|
+
# Detect: @tag_name word [Type] (violation when type_first is enforced)
|
|
48
|
+
pattern = /@#{tag_name}\s+(\w+)\s+\[([^\]]+)\]/
|
|
49
|
+
if line =~ pattern
|
|
50
|
+
param_name = ::Regexp.last_match(1)
|
|
51
|
+
type_info = ::Regexp.last_match(2)
|
|
52
|
+
collector.puts "#{object.file}:#{line_num + 1}: #{object.title}"
|
|
53
|
+
collector.puts "#{tag_name}|#{param_name}|#{type_info}|type_after_name"
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
# Detect: @tag_name [Type] word (violation when type_after_name is enforced)
|
|
57
|
+
pattern = /@#{tag_name}\s+\[([^\]]+)\]\s+(\w+)/
|
|
58
|
+
if line =~ pattern
|
|
59
|
+
type_info = ::Regexp.last_match(1)
|
|
60
|
+
param_name = ::Regexp.last_match(2)
|
|
61
|
+
collector.puts "#{object.file}:#{line_num + 1}: #{object.title}"
|
|
62
|
+
collector.puts "#{tag_name}|#{param_name}|#{type_info}|type_first"
|
|
86
63
|
end
|
|
87
64
|
end
|
|
88
65
|
end
|
|
89
|
-
|
|
90
|
-
false
|
|
91
|
-
'
|
|
92
|
-
QUERY
|
|
66
|
+
end
|
|
93
67
|
end
|
|
94
68
|
|
|
69
|
+
private
|
|
70
|
+
|
|
95
71
|
# @return [String] the enforced style ('type_after_name' (standard) or 'type_first')
|
|
96
72
|
def enforced_style
|
|
97
73
|
config_or_default('EnforcedStyle')
|
|
98
74
|
end
|
|
99
|
-
|
|
100
|
-
# Array of tag names to check, formatted for YARD query
|
|
101
|
-
# @return [String] Ruby array literal string
|
|
102
|
-
def checked_tags_array
|
|
103
|
-
tags = config_or_default('CheckedTags')
|
|
104
|
-
"[#{tags.map { |t| "\"#{t}\"" }.join(',')}]"
|
|
105
|
-
end
|
|
106
75
|
end
|
|
107
76
|
end
|
|
108
77
|
end
|
|
@@ -16,9 +16,14 @@ module Yard
|
|
|
16
16
|
# @option _kwargs [Object] :unused this parameter accepts no options (reserved for future use)
|
|
17
17
|
# @return [Array<Hash>] array with violation details
|
|
18
18
|
def call(yard_output, **_kwargs)
|
|
19
|
-
return [] if yard_output.nil?
|
|
19
|
+
return [] if yard_output.nil?
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# Handle encoding issues from YARD output that may contain invalid UTF-8 sequences
|
|
22
|
+
# This can happen when YARD processes files with non-ASCII characters in type specs
|
|
23
|
+
sanitized = yard_output.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
|
24
|
+
return [] if sanitized.strip.empty?
|
|
25
|
+
|
|
26
|
+
lines = sanitized.split("\n").map(&:strip).reject(&:empty?)
|
|
22
27
|
violations = []
|
|
23
28
|
|
|
24
29
|
lines.each_slice(2) do |location_line, details_line|
|
|
@@ -7,66 +7,36 @@ module Yard
|
|
|
7
7
|
module TypeSyntax
|
|
8
8
|
# Runs YARD to validate type syntax using TypesExplainer::Parser
|
|
9
9
|
class Validator < Base
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<<~QUERY.strip
|
|
38
|
-
'
|
|
39
|
-
require "yard"
|
|
40
|
-
|
|
41
|
-
docstring
|
|
42
|
-
.tags
|
|
43
|
-
.select { |tag| #{validated_tags_array}.include?(tag.tag_name) }
|
|
44
|
-
.each do |tag|
|
|
45
|
-
next unless tag.types
|
|
46
|
-
|
|
47
|
-
tag.types.each do |type_str|
|
|
48
|
-
begin
|
|
49
|
-
YARD::Tags::TypesExplainer::Parser.parse(type_str)
|
|
50
|
-
rescue SyntaxError => e
|
|
51
|
-
puts object.file + ":" + object.line.to_s + ": " + object.title
|
|
52
|
-
puts tag.tag_name + "|" + type_str + "|" + e.message
|
|
53
|
-
break
|
|
54
|
-
end
|
|
55
|
-
end
|
|
10
|
+
# Enable in-process execution
|
|
11
|
+
in_process visibility: :public
|
|
12
|
+
|
|
13
|
+
# Execute query for a single object during in-process execution.
|
|
14
|
+
# Validates type syntax in tags using YARD's TypesExplainer::Parser.
|
|
15
|
+
# @param object [YARD::CodeObjects::Base] the code object to query
|
|
16
|
+
# @param collector [Executor::ResultCollector] collector for output
|
|
17
|
+
# @return [void]
|
|
18
|
+
def in_process_query(object, collector)
|
|
19
|
+
validated_tags = config.validator_config('Tags/TypeSyntax', 'ValidatedTags') ||
|
|
20
|
+
%w[param option return yieldreturn]
|
|
21
|
+
|
|
22
|
+
object.docstring.tags
|
|
23
|
+
.select { |tag| validated_tags.include?(tag.tag_name) }
|
|
24
|
+
.each do |tag|
|
|
25
|
+
next unless tag.types
|
|
26
|
+
|
|
27
|
+
tag.types.each do |type_str|
|
|
28
|
+
begin
|
|
29
|
+
YARD::Tags::TypesExplainer::Parser.parse(type_str)
|
|
30
|
+
rescue SyntaxError => e
|
|
31
|
+
# Sanitize error message to handle invalid UTF-8 sequences
|
|
32
|
+
# YARD's parser may generate malformed error messages for non-ASCII input
|
|
33
|
+
error_msg = e.message.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
34
|
+
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
35
|
+
collector.puts "#{tag.tag_name}|#{type_str}|#{error_msg}"
|
|
36
|
+
break
|
|
56
37
|
end
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
QUERY
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Array of tag names to validate, formatted for YARD query
|
|
64
|
-
# @return [String] Ruby array literal string
|
|
65
|
-
def validated_tags_array
|
|
66
|
-
tags = config.validator_config('Tags/TypeSyntax', 'ValidatedTags') || %w[
|
|
67
|
-
param option return yieldreturn
|
|
68
|
-
]
|
|
69
|
-
"[#{tags.map { |t| "\"#{t}\"" }.join(',')}]"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
70
40
|
end
|
|
71
41
|
end
|
|
72
42
|
end
|
|
@@ -5,25 +5,8 @@ module Yard
|
|
|
5
5
|
module Validators
|
|
6
6
|
module Warnings
|
|
7
7
|
module DuplicatedParameterName
|
|
8
|
-
#
|
|
8
|
+
# Validator for detecting duplicated 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
|
|
@@ -5,25 +5,8 @@ module Yard
|
|
|
5
5
|
module Validators
|
|
6
6
|
module Warnings
|
|
7
7
|
module InvalidDirectiveFormat
|
|
8
|
-
#
|
|
8
|
+
# Validator for detecting invalid directive format 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
|
|
@@ -5,25 +5,8 @@ module Yard
|
|
|
5
5
|
module Validators
|
|
6
6
|
module Warnings
|
|
7
7
|
module InvalidTagFormat
|
|
8
|
-
#
|
|
8
|
+
# Validator for detecting invalid tag format 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
|
|
@@ -5,25 +5,8 @@ module Yard
|
|
|
5
5
|
module Validators
|
|
6
6
|
module Warnings
|
|
7
7
|
module UnknownDirective
|
|
8
|
-
#
|
|
8
|
+
# Validator for detecting unknown directive 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,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
|