yard-lint 1.6.1 → 1.7.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 +69 -1
- data/README.md +4 -3
- data/bin/yard-lint +35 -4
- data/lib/yard/lint/config.rb +21 -4
- data/lib/yard/lint/config_loader.rb +31 -6
- data/lib/yard/lint/config_updater.rb +22 -1
- data/lib/yard/lint/config_validator.rb +2 -1
- data/lib/yard/lint/executor/in_process_registry.rb +58 -23
- data/lib/yard/lint/executor/warning_dispatcher.rb +1 -0
- data/lib/yard/lint/git.rb +44 -3
- data/lib/yard/lint/path_grouper.rb +4 -1
- data/lib/yard/lint/results/aggregate.rb +15 -7
- data/lib/yard/lint/stats_calculator.rb +8 -2
- data/lib/yard/lint/templates/default_config.yml +34 -1
- data/lib/yard/lint/templates/strict_config.yml +34 -1
- data/lib/yard/lint/todo_generator.rb +35 -14
- data/lib/yard/lint/validators/base.rb +39 -1
- data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +17 -2
- data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +5 -2
- data/lib/yard/lint/validators/documentation/line_length/validator.rb +1 -0
- data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +66 -9
- data/lib/yard/lint/validators/documentation/missing_return/validator.rb +3 -3
- data/lib/yard/lint/validators/documentation/orphaned_doc_comment/validator.rb +79 -11
- data/lib/yard/lint/validators/documentation/orphaned_doc_comment.rb +4 -4
- data/lib/yard/lint/validators/documentation/text_substitution/validator.rb +10 -2
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/config.rb +10 -1
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +18 -4
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +38 -4
- data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +6 -0
- data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +1 -0
- data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +43 -4
- data/lib/yard/lint/validators/tags/api_tags/validator.rb +11 -1
- data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +36 -5
- data/lib/yard/lint/validators/tags/collection_type/validator.rb +10 -6
- data/lib/yard/lint/validators/tags/example_style/validator.rb +1 -1
- data/lib/yard/lint/validators/tags/example_syntax/config.rb +6 -1
- data/lib/yard/lint/validators/tags/example_syntax/validator.rb +74 -3
- data/lib/yard/lint/validators/tags/forbidden_tags/validator.rb +7 -3
- data/lib/yard/lint/validators/tags/informal_notation/config.rb +6 -1
- data/lib/yard/lint/validators/tags/informal_notation/validator.rb +24 -3
- data/lib/yard/lint/validators/tags/invalid_types/config.rb +7 -1
- data/lib/yard/lint/validators/tags/invalid_types/parser.rb +22 -5
- data/lib/yard/lint/validators/tags/invalid_types/validator.rb +24 -10
- data/lib/yard/lint/validators/tags/missing_yield/validator.rb +44 -4
- data/lib/yard/lint/validators/tags/missing_yield.rb +8 -8
- data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +13 -1
- data/lib/yard/lint/validators/tags/option_tags/result.rb +1 -0
- data/lib/yard/lint/validators/tags/option_tags/validator.rb +30 -2
- data/lib/yard/lint/validators/tags/order/parser.rb +12 -5
- data/lib/yard/lint/validators/tags/order/validator.rb +7 -2
- data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +21 -8
- data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +12 -5
- data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +9 -7
- data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +8 -2
- data/lib/yard/lint/validators/tags/type_syntax/validator.rb +1 -1
- data/lib/yard/lint/validators/warnings/invalid_directive_format/parser.rb +2 -2
- data/lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb +2 -2
- data/lib/yard/lint/validators/warnings/syntax_error/config.rb +22 -0
- data/lib/yard/lint/validators/warnings/syntax_error/parser.rb +28 -0
- data/lib/yard/lint/validators/warnings/syntax_error/result.rb +27 -0
- data/lib/yard/lint/validators/warnings/syntax_error/validator.rb +15 -0
- data/lib/yard/lint/validators/warnings/syntax_error.rb +34 -0
- data/lib/yard/lint/validators/warnings/unknown_directive/parser.rb +2 -2
- data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +51 -46
- data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +20 -3
- data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +2 -2
- data/lib/yard/lint/version.rb +1 -1
- data/lib/yard/lint.rb +4 -1
- metadata +6 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ripper'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
3
6
|
module Yard
|
|
4
7
|
module Lint
|
|
5
8
|
module Validators
|
|
@@ -31,6 +34,8 @@ module Yard
|
|
|
31
34
|
(def |class |module |attr_reader|attr_writer|attr_accessor|attr_internal|attr\b|alias_method\b|alias\b|define_method\b)
|
|
32
35
|
|
|
|
33
36
|
\A\s*[A-Z][A-Za-z0-9_:]*\s*=
|
|
37
|
+
|
|
|
38
|
+
\A\s*\w+\s+def\b
|
|
34
39
|
/x.freeze
|
|
35
40
|
|
|
36
41
|
# Matches a DSL-style method call whose first argument is a symbol or string literal
|
|
@@ -38,7 +43,13 @@ module Yard
|
|
|
38
43
|
# YARD's DSL handler turns such a call into a documentable method object when the
|
|
39
44
|
# preceding comment carries an implicit-docstring tag, so a doc comment in front of
|
|
40
45
|
# one of these is NOT orphaned.
|
|
41
|
-
DSL_CALL_PATTERN =
|
|
46
|
+
DSL_CALL_PATTERN = %r{\A\s*(?:[A-Za-z_][\w:]*\.)?(?<method>[a-z_]\w*[!?]?)(?:\s+|\s*\(\s*)(?::\w|:["']|["'])}.freeze
|
|
47
|
+
# Matches a plain method call (optionally with a receiver), used when
|
|
48
|
+
# the comment carries a @method/@attribute tag that names the object
|
|
49
|
+
# YARD creates - then the call's argument shape does not matter.
|
|
50
|
+
METHOD_CALL_PATTERN = /\A\s*(?:[A-Za-z_][\w:]*\.)?[a-z_]\w*[!?]?(?:[\s(]|\z)/.freeze
|
|
51
|
+
# Matches a @method/@attribute tag that names a created object.
|
|
52
|
+
NAMED_OBJECT_TAG_PATTERN = /\A\s*#\s*@(?:method|attribute)\s+\S/.freeze
|
|
42
53
|
# Mirror of YARD::Handlers::Ruby::DSLHandlerMethods::IGNORE_METHODS - calls to these
|
|
43
54
|
# are skipped by YARD's DSL handler, so a preceding doc comment really is dropped.
|
|
44
55
|
# (The `attr*`/`alias*` entries are already covered by DEFINITION_PATTERN.)
|
|
@@ -74,19 +85,26 @@ module Yard
|
|
|
74
85
|
# @param collector [Executor::ResultCollector] collector for output lines
|
|
75
86
|
# @return [void]
|
|
76
87
|
def scan_file(file, collector)
|
|
77
|
-
|
|
88
|
+
source = File.read(file)
|
|
89
|
+
lines = source.lines.map(&:chomp)
|
|
90
|
+
# Line indices (0-based) that hold a real, full-line Ruby comment.
|
|
91
|
+
# Derived from the lexer so that '#'-leading lines inside heredocs
|
|
92
|
+
# and string literals are not mistaken for documentation comments.
|
|
93
|
+
comment_lines = full_line_comment_indices(source, lines)
|
|
78
94
|
i = 0
|
|
79
95
|
|
|
80
96
|
while i < lines.length
|
|
81
|
-
if comment_line?(lines[i])
|
|
97
|
+
if comment_line?(lines[i], i, comment_lines)
|
|
82
98
|
block_start = i
|
|
83
99
|
tags = []
|
|
84
100
|
|
|
85
101
|
has_directive = false
|
|
86
102
|
has_implicit_tag = false
|
|
87
|
-
|
|
103
|
+
has_named_object_tag = false
|
|
104
|
+
while i < lines.length && comment_line?(lines[i], i, comment_lines)
|
|
88
105
|
has_directive = true if directive_line?(lines[i])
|
|
89
106
|
has_implicit_tag = true if implicit_docstring_tag?(lines[i])
|
|
107
|
+
has_named_object_tag = true if lines[i].match?(NAMED_OBJECT_TAG_PATTERN)
|
|
90
108
|
tag = extract_yard_tag(lines[i])
|
|
91
109
|
tags << tag if tag
|
|
92
110
|
i += 1
|
|
@@ -99,7 +117,7 @@ module Yard
|
|
|
99
117
|
# Skip trailing blank lines after the comment block
|
|
100
118
|
i += 1 while i < lines.length && lines[i].strip.empty?
|
|
101
119
|
|
|
102
|
-
unless documentable?(lines[i], has_implicit_tag)
|
|
120
|
+
unless documentable?(lines[i], has_implicit_tag, has_named_object_tag)
|
|
103
121
|
collector.puts "#{file}:#{block_start + 1}: #{tags.uniq.join(',')}"
|
|
104
122
|
end
|
|
105
123
|
else
|
|
@@ -108,17 +126,60 @@ module Yard
|
|
|
108
126
|
end
|
|
109
127
|
end
|
|
110
128
|
|
|
129
|
+
# Lex the source and collect the 0-based indices of lines whose first
|
|
130
|
+
# non-whitespace content is a Ruby comment. Heredoc bodies and string
|
|
131
|
+
# literals lex as content tokens (not `:on_comment`), so their
|
|
132
|
+
# `#`-leading lines are excluded - fixing the false positives where
|
|
133
|
+
# tag-looking text inside a heredoc was treated as a doc comment.
|
|
134
|
+
# @param source [String] the full file source
|
|
135
|
+
# @param lines [Array<String>] the source split into chomped lines
|
|
136
|
+
# @return [Set<Integer>] 0-based indices of full-line comments
|
|
137
|
+
def full_line_comment_indices(source, lines)
|
|
138
|
+
indices = Set.new
|
|
139
|
+
::Ripper.lex(source).each do |(position, type, _token, _state)|
|
|
140
|
+
next unless type == :on_comment
|
|
141
|
+
|
|
142
|
+
line_no, column = position
|
|
143
|
+
index = line_no - 1
|
|
144
|
+
current = lines[index]
|
|
145
|
+
next unless current
|
|
146
|
+
|
|
147
|
+
# Only a full-line comment (nothing but whitespace before the '#').
|
|
148
|
+
indices << index if current[0...column].to_s.strip.empty?
|
|
149
|
+
end
|
|
150
|
+
indices
|
|
151
|
+
rescue StandardError
|
|
152
|
+
# If the source cannot be lexed, fall back to the previous regex
|
|
153
|
+
# behaviour so a single unparseable file does not lose detection.
|
|
154
|
+
fallback = Set.new
|
|
155
|
+
lines.each_with_index do |line, index|
|
|
156
|
+
fallback << index if line.strip.start_with?('#')
|
|
157
|
+
end
|
|
158
|
+
fallback
|
|
159
|
+
end
|
|
160
|
+
|
|
111
161
|
# @param line [String] a raw source line
|
|
162
|
+
# @param index [Integer] 0-based line index
|
|
163
|
+
# @param comment_lines [Set<Integer>] indices of real full-line comments
|
|
112
164
|
# @return [Boolean] true if the line is a Ruby comment (excluding magic comments)
|
|
113
|
-
def comment_line?(line)
|
|
114
|
-
|
|
115
|
-
|
|
165
|
+
def comment_line?(line, index, comment_lines)
|
|
166
|
+
return false unless comment_lines.include?(index)
|
|
167
|
+
|
|
168
|
+
!magic_comment?(line.strip)
|
|
116
169
|
end
|
|
117
170
|
|
|
118
171
|
# @param stripped_line [String] a comment line with leading/trailing whitespace removed
|
|
119
172
|
# @return [Boolean] true if the line is a Ruby magic comment (frozen_string_literal, encoding, etc.)
|
|
120
173
|
def magic_comment?(stripped_line)
|
|
121
|
-
|
|
174
|
+
# A real magic comment has a single-token value (e.g. `true`,
|
|
175
|
+
# `utf-8`), optionally followed by `;` (combined directives) or an
|
|
176
|
+
# emacs `-*-` wrapper. Requiring that avoids treating prose that
|
|
177
|
+
# merely starts with a magic-comment word - like
|
|
178
|
+
# `# encoding: UTF-8 is assumed for all inputs` - as a magic
|
|
179
|
+
# comment, which would split a documentation block.
|
|
180
|
+
stripped_line.match?(
|
|
181
|
+
/\A#\s*(?:-\*-\s*)?(frozen[_-]string[_-]literal|encoding|warn[_-]indent|shareable[_-]constant[_-]value)\s*:\s*\S+\s*(?:;|-\*-|\z)/i
|
|
182
|
+
)
|
|
122
183
|
end
|
|
123
184
|
|
|
124
185
|
# @param line [String] a raw source line
|
|
@@ -137,11 +198,18 @@ module Yard
|
|
|
137
198
|
# @param line [String, nil] the source line following the comment block, or nil at EOF
|
|
138
199
|
# @param has_implicit_tag [Boolean] whether the comment block carries a tag that makes
|
|
139
200
|
# YARD's DSL handler emit a method object (see IMPLICIT_DOCSTRING_TAG_PATTERN)
|
|
201
|
+
# @param has_named_object_tag [Boolean] whether the comment block carries a
|
|
202
|
+
# @method/@attribute tag naming the object YARD creates, so any following
|
|
203
|
+
# method call attaches the docstring regardless of its arguments
|
|
140
204
|
# @return [Boolean] true if YARD will attach the comment to a documentable construct
|
|
141
|
-
def documentable?(line, has_implicit_tag)
|
|
205
|
+
def documentable?(line, has_implicit_tag, has_named_object_tag = false)
|
|
142
206
|
return false if line.nil?
|
|
143
207
|
|
|
144
|
-
definition_line?(line) ||
|
|
208
|
+
definition_line?(line) ||
|
|
209
|
+
(has_implicit_tag && dsl_method_line?(line)) ||
|
|
210
|
+
# A @method/@attribute tag names the object YARD creates, so any
|
|
211
|
+
# following method call (regardless of its arguments) attaches it.
|
|
212
|
+
(has_named_object_tag && line.match?(METHOD_CALL_PATTERN))
|
|
145
213
|
end
|
|
146
214
|
|
|
147
215
|
# @param line [String] a raw source line
|
|
@@ -15,10 +15,6 @@ module Yard
|
|
|
15
15
|
# non-documentable statement (variable assignment, `require`, `include`, etc.)
|
|
16
16
|
# or sits at the end of a file.
|
|
17
17
|
#
|
|
18
|
-
# @note This validator is complementary to `Documentation/BlankLineBeforeDefinition`,
|
|
19
|
-
# which catches doc blocks separated from a `def` by blank lines.
|
|
20
|
-
# `OrphanedDocComment` catches doc blocks that lead to non-definition code entirely.
|
|
21
|
-
#
|
|
22
18
|
# @example Bad - comment before variable assignment
|
|
23
19
|
# # @param name [String] the name
|
|
24
20
|
# # @return [void]
|
|
@@ -39,6 +35,10 @@ module Yard
|
|
|
39
35
|
# def process(name)
|
|
40
36
|
# end
|
|
41
37
|
#
|
|
38
|
+
# @note This validator is complementary to `Documentation/BlankLineBeforeDefinition`,
|
|
39
|
+
# which catches doc blocks separated from a `def` by blank lines.
|
|
40
|
+
# `OrphanedDocComment` catches doc blocks that lead to non-definition code entirely.
|
|
41
|
+
#
|
|
42
42
|
# ## Configuration
|
|
43
43
|
#
|
|
44
44
|
# To disable this validator:
|
|
@@ -16,6 +16,7 @@ module Yard
|
|
|
16
16
|
def in_process_query(object, collector)
|
|
17
17
|
docstring_text = object.docstring.to_s
|
|
18
18
|
return if docstring_text.empty?
|
|
19
|
+
return if duplicate_docstring?(object)
|
|
19
20
|
|
|
20
21
|
substitutions = config_or_default('Substitutions')
|
|
21
22
|
return if substitutions.nil? || substitutions.empty?
|
|
@@ -24,7 +25,8 @@ module Yard
|
|
|
24
25
|
return if violations.empty?
|
|
25
26
|
|
|
26
27
|
violations.each do |violation|
|
|
27
|
-
|
|
28
|
+
line = docstring_line(object, violation[:line_offset])
|
|
29
|
+
collector.puts "#{object.file}:#{line}: #{object.title}"
|
|
28
30
|
collector.puts violation[:forbidden]
|
|
29
31
|
collector.puts violation[:replacement]
|
|
30
32
|
collector.puts "#{violation[:line_offset]}|#{violation[:line_text]}"
|
|
@@ -48,10 +50,16 @@ module Yard
|
|
|
48
50
|
end
|
|
49
51
|
next if in_code_block
|
|
50
52
|
|
|
53
|
+
# Match against the line with inline code spans (`...`) removed,
|
|
54
|
+
# so a forbidden string that only appears inside code is not
|
|
55
|
+
# flagged - it is literal code, not prose to be substituted. The
|
|
56
|
+
# reported line_text remains the original line.
|
|
57
|
+
scannable = line.gsub(/`[^`]*`/, '')
|
|
58
|
+
|
|
51
59
|
substitutions.each do |forbidden, replacement|
|
|
52
60
|
next if forbidden.nil? || forbidden.empty?
|
|
53
61
|
next if replacement.nil? || replacement.empty?
|
|
54
|
-
next unless
|
|
62
|
+
next unless scannable.include?(forbidden)
|
|
55
63
|
|
|
56
64
|
violations << {
|
|
57
65
|
forbidden: forbidden,
|
|
@@ -12,7 +12,16 @@ module Yard
|
|
|
12
12
|
'Enabled' => true,
|
|
13
13
|
'Severity' => 'warning',
|
|
14
14
|
'AllowedParentClasses' => [],
|
|
15
|
-
'AllowedMethods' => []
|
|
15
|
+
'AllowedMethods' => [],
|
|
16
|
+
# Match each parameter to a @param tag by name (default). Catches a
|
|
17
|
+
# misnamed @param (e.g. `@param wrong` for `def push(item)`) that a
|
|
18
|
+
# count-only check would accept. Set to false to fall back to the
|
|
19
|
+
# lenient count-only comparison.
|
|
20
|
+
'CheckParameterNames' => true,
|
|
21
|
+
# Opt-in: skip methods with no documentation at all and let
|
|
22
|
+
# Documentation/UndocumentedObjects report them, avoiding a second
|
|
23
|
+
# offense for the same fully-undocumented method. Off by default.
|
|
24
|
+
'SkipFullyUndocumented' => false
|
|
16
25
|
}.freeze
|
|
17
26
|
end
|
|
18
27
|
end
|
|
@@ -9,9 +9,13 @@ module Yard
|
|
|
9
9
|
# @example Output format (skip-lint)
|
|
10
10
|
# /path/to/file.rb:10: Platform::Analysis::Authors#initialize
|
|
11
11
|
class Parser < Parsers::Base
|
|
12
|
-
# Regex to extract file, line, and
|
|
12
|
+
# Regex to extract file, line, and object title from yard list output
|
|
13
13
|
# Format: /path/to/file.rb:10: ClassName#method_name
|
|
14
|
-
LOCATION_REGEX = /^(.+):(\d+):\s+(.+)
|
|
14
|
+
LOCATION_REGEX = /^(.+):(\d+):\s+(.+)$/
|
|
15
|
+
# Splits an object title into namespace and method name on the last
|
|
16
|
+
# # or . separator. Top-level methods (#foo) have an empty namespace;
|
|
17
|
+
# titles without a separator (e.g. Foo::Bar, CONST) are kept whole.
|
|
18
|
+
TITLE_REGEX = /\A(.*)[#.]([^#.]+)\z/
|
|
15
19
|
|
|
16
20
|
# @param yard_list [String] raw yard list results string
|
|
17
21
|
# @return [Array<Hash>] Array with undocumented method arguments details
|
|
@@ -26,8 +30,7 @@ module Yard
|
|
|
26
30
|
# Extract: file path, line number, class name, method name
|
|
27
31
|
file_path = match_data[1]
|
|
28
32
|
line_number = match_data[2].to_i
|
|
29
|
-
class_name = match_data[3]
|
|
30
|
-
method_name = match_data[4]
|
|
33
|
+
class_name, method_name = split_title(match_data[3])
|
|
31
34
|
|
|
32
35
|
{
|
|
33
36
|
location: file_path,
|
|
@@ -37,6 +40,17 @@ module Yard
|
|
|
37
40
|
}
|
|
38
41
|
end
|
|
39
42
|
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Splits a YARD object title into namespace and method name parts
|
|
47
|
+
# @param title [String] object title (e.g. "Foo#bar", "#bar", "CONST")
|
|
48
|
+
# @return [Array(String, String)] namespace and method name; for titles
|
|
49
|
+
# without a separator both parts are the full title
|
|
50
|
+
def split_title(title)
|
|
51
|
+
match = title.match(TITLE_REGEX)
|
|
52
|
+
match ? [match[1], match[2]] : [title, title]
|
|
53
|
+
end
|
|
40
54
|
end
|
|
41
55
|
end
|
|
42
56
|
end
|
|
@@ -27,14 +27,48 @@ module Yard
|
|
|
27
27
|
# doesn't need explicit @param documentation, matching attr_accessor behavior
|
|
28
28
|
return if object.is_attribute?
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# Splat (*args, **opts) and block (&block) parameters are excluded -
|
|
31
|
+
# blocks are documented with @yield rather than @param, matching the
|
|
32
|
+
# arity convention used everywhere else in the gem (e.g. method_allowed?).
|
|
33
|
+
params = object.parameters.reject { |p| p[0].to_s.start_with?('*', '&') }
|
|
34
|
+
return if params.empty?
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
# Opt-in: defer fully-undocumented methods to
|
|
37
|
+
# Documentation/UndocumentedObjects instead of reporting them here too.
|
|
38
|
+
return if config_or_default('SkipFullyUndocumented') && object.docstring.blank?
|
|
39
|
+
|
|
40
|
+
return unless arguments_missing_docs?(object, params)
|
|
35
41
|
|
|
36
42
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
37
43
|
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Decide whether a method's parameters are under-documented.
|
|
48
|
+
# @param object [YARD::CodeObjects::MethodObject] the method to check
|
|
49
|
+
# @param params [Array<Array>] non-splat/block parameters
|
|
50
|
+
# @return [Boolean] true if documentation is missing
|
|
51
|
+
def arguments_missing_docs?(object, params)
|
|
52
|
+
# Tags nested inside @overload blocks live on the overload's own
|
|
53
|
+
# docstring, so all_typed_tags collects those @param tags too.
|
|
54
|
+
param_tags = all_typed_tags(object.docstring, %w[param])
|
|
55
|
+
|
|
56
|
+
if config_or_default('CheckParameterNames')
|
|
57
|
+
documented = param_tags.map { |tag| normalize_param_name(tag.name) }
|
|
58
|
+
params.any? { |param| !documented.include?(normalize_param_name(param[0])) }
|
|
59
|
+
else
|
|
60
|
+
params.size > param_tags.size
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Normalize a parameter or @param name for comparison by stripping a
|
|
65
|
+
# trailing `:` - keyword parameters appear as `name:` in
|
|
66
|
+
# object.parameters but `name` in @param tag names.
|
|
67
|
+
# @param name [String, Symbol, nil] the raw name
|
|
68
|
+
# @return [String] the normalized name
|
|
69
|
+
def normalize_param_name(name)
|
|
70
|
+
name.to_s.sub(/:\z/, '')
|
|
71
|
+
end
|
|
38
72
|
end
|
|
39
73
|
end
|
|
40
74
|
end
|
|
@@ -62,6 +62,12 @@ module Yard
|
|
|
62
62
|
# @param excluded_methods [Array<String>] list of exclusion patterns
|
|
63
63
|
# @return [Boolean] true if method should be excluded
|
|
64
64
|
def method_excluded?(element, arity, excluded_methods)
|
|
65
|
+
# ExcludedMethods only applies to methods. A class, module, or
|
|
66
|
+
# constant element has no #/. separator, so never derive a
|
|
67
|
+
# "method name" from it - otherwise a pattern like /cache/ would
|
|
68
|
+
# silently suppress the offense for a class such as Memcached.
|
|
69
|
+
return false unless element.match?(/[#.]/)
|
|
70
|
+
|
|
65
71
|
# Extract method name from element (e.g., "Foo::Bar#baz" -> "baz")
|
|
66
72
|
method_name = element.split(/[#.]/).last
|
|
67
73
|
return false unless method_name
|
|
@@ -32,6 +32,11 @@ module Yard
|
|
|
32
32
|
|
|
33
33
|
return unless has_options_param
|
|
34
34
|
|
|
35
|
+
# A named (non-splat) parameter documented with a concrete
|
|
36
|
+
# non-Hash type (via its param tag) is not an options hash, so it
|
|
37
|
+
# does not need option tags.
|
|
38
|
+
return if options_param_documented_as_non_hash?(object)
|
|
39
|
+
|
|
35
40
|
# Check if @option tags are missing
|
|
36
41
|
option_tags = object.tags(:option)
|
|
37
42
|
return unless option_tags.empty?
|
|
@@ -40,6 +45,31 @@ module Yard
|
|
|
40
45
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
41
46
|
collector.puts params.map { |p| p.join(' ') }.join(', ')
|
|
42
47
|
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Whether a named options-style parameter is documented with a
|
|
52
|
+
# concrete non-Hash @param type (e.g. `@param option [Symbol]`).
|
|
53
|
+
# Double-splat (`**opts`) collectors are always hashes and excluded.
|
|
54
|
+
# @param object [YARD::CodeObjects::MethodObject] the method
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def options_param_documented_as_non_hash?(object)
|
|
57
|
+
param_tags = all_typed_tags(object.docstring, %w[param])
|
|
58
|
+
|
|
59
|
+
object.parameters.any? do |param|
|
|
60
|
+
name = param[0].to_s
|
|
61
|
+
next false if name.start_with?('**')
|
|
62
|
+
|
|
63
|
+
bare = name.gsub(/[*:]/, '')
|
|
64
|
+
next false unless bare.match?(/\A(options?|opts?|kwargs)\z/)
|
|
65
|
+
|
|
66
|
+
tag = param_tags.find { |t| t.name == bare }
|
|
67
|
+
types = tag&.types
|
|
68
|
+
next false if types.nil? || types.empty?
|
|
69
|
+
|
|
70
|
+
types.none? { |type| type.to_s.match?(/\AHash\b|\AHash[<{(]/) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
43
73
|
end
|
|
44
74
|
end
|
|
45
75
|
end
|
|
@@ -28,6 +28,7 @@ module Yard
|
|
|
28
28
|
severity: configured_severity,
|
|
29
29
|
type: self.class.offense_type,
|
|
30
30
|
name: offense_data[:name] || self.class.offense_name,
|
|
31
|
+
validator: validator_name,
|
|
31
32
|
message: build_message(offense_data),
|
|
32
33
|
location: offense_data[:location] || offense_data[:file],
|
|
33
34
|
location_line: offense_data[:line] || offense_data[:location_line] || 0
|
|
@@ -32,11 +32,21 @@ module Yard
|
|
|
32
32
|
# Skip def line and end
|
|
33
33
|
body_lines = lines[1...-1] || []
|
|
34
34
|
|
|
35
|
+
# Merge statement continuations (a line ending in a comma or
|
|
36
|
+
# backslash continues on the next line) so a multi-line
|
|
37
|
+
# `raise NotImplementedError, "message"` is judged as one
|
|
38
|
+
# statement rather than leaving a dangling argument line that
|
|
39
|
+
# looks like a real implementation.
|
|
40
|
+
body_lines = merge_continuations(body_lines)
|
|
41
|
+
|
|
42
|
+
# A line is an allowed (non-)implementation if it is a comment,
|
|
43
|
+
# the closing `end`, or matches one of the configured
|
|
44
|
+
# AllowedImplementations patterns (e.g. raising NotImplementedError).
|
|
45
|
+
allowed = allowed_implementation_patterns
|
|
35
46
|
has_real_implementation = body_lines.any? do |line|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
line != 'end'
|
|
47
|
+
next false if line.start_with?('#') || line == 'end'
|
|
48
|
+
|
|
49
|
+
allowed.none? { |pattern| line.match?(pattern) }
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
return unless has_real_implementation
|
|
@@ -44,6 +54,35 @@ module Yard
|
|
|
44
54
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
45
55
|
collector.puts 'has_implementation'
|
|
46
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# The configured AllowedImplementations patterns compiled to
|
|
61
|
+
# regexps. A body line matching any of these does not count as a
|
|
62
|
+
# real implementation. Invalid patterns are ignored.
|
|
63
|
+
# @return [Array<Regexp>]
|
|
64
|
+
def allowed_implementation_patterns
|
|
65
|
+
Array(config_or_default('AllowedImplementations')).filter_map do |pattern|
|
|
66
|
+
Regexp.new(pattern.to_s)
|
|
67
|
+
rescue RegexpError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Joins lines that are continuations of the previous statement (the
|
|
73
|
+
# previous line ends with a comma or a backslash) into a single
|
|
74
|
+
# logical line.
|
|
75
|
+
# @param lines [Array<String>] stripped body lines
|
|
76
|
+
# @return [Array<String>] body lines with continuations merged
|
|
77
|
+
def merge_continuations(lines)
|
|
78
|
+
lines.each_with_object([]) do |line, merged|
|
|
79
|
+
if !merged.empty? && merged.last.match?(/[,\\]\z/)
|
|
80
|
+
merged[-1] = "#{merged.last.chomp('\\').rstrip} #{line}"
|
|
81
|
+
else
|
|
82
|
+
merged << line
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
47
86
|
end
|
|
48
87
|
end
|
|
49
88
|
end
|
|
@@ -19,7 +19,12 @@ module Yard
|
|
|
19
19
|
allowed_list = allowed_apis
|
|
20
20
|
|
|
21
21
|
if object.has_tag?(:api)
|
|
22
|
-
|
|
22
|
+
# YARD tag text includes indented continuation/description lines
|
|
23
|
+
# (e.g. "private\nfor internal use only"). The @api value is a
|
|
24
|
+
# single token, so validate only the first word - otherwise a
|
|
25
|
+
# documented `@api private` is flagged as invalid, and the
|
|
26
|
+
# newline in the emitted value corrupts the parser's pairing.
|
|
27
|
+
api_value = object.tag(:api).text.to_s.split(/\s+/).first.to_s
|
|
23
28
|
unless allowed_list.include?(api_value)
|
|
24
29
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
25
30
|
collector.puts "invalid:#{api_value}"
|
|
@@ -37,6 +42,11 @@ module Yard
|
|
|
37
42
|
# hit the branch above and get their value validated. See issue #128.
|
|
38
43
|
return if object.type == :method && object.is_attribute?
|
|
39
44
|
|
|
45
|
+
# An api tag is expected on classes, modules, and methods (per
|
|
46
|
+
# this validator's documentation) - not on constants or other
|
|
47
|
+
# object types, which were being flagged as missing the tag.
|
|
48
|
+
return unless %i[class module method].include?(object.type)
|
|
49
|
+
|
|
40
50
|
# Only check public methods/classes if require_api_tags is enabled
|
|
41
51
|
visibility = object.visibility.to_s
|
|
42
52
|
if visibility == 'public' && !object.root?
|
|
@@ -54,13 +54,44 @@ module Yard
|
|
|
54
54
|
# (String, Integer) -> Array(String, Integer)
|
|
55
55
|
"Array#{type_string}"
|
|
56
56
|
else
|
|
57
|
-
# Hash<K, V> -> Hash{K => V}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
# Hash<K, V> -> Hash{K => V}, handling nested generics with
|
|
58
|
+
# balanced-bracket splitting so the suggestion stays valid.
|
|
59
|
+
convert_hash_short_to_long(type_string)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Converts Hash<K, V> to Hash{K => V}, recursing into the key and
|
|
64
|
+
# value and splitting on the top-level comma so nested types like
|
|
65
|
+
# Hash<Symbol, Hash<String, Integer>> are not mangled.
|
|
66
|
+
# @param type_string [String] the type string
|
|
67
|
+
# @return [String] the converted type string
|
|
68
|
+
def convert_hash_short_to_long(type_string)
|
|
69
|
+
match = type_string.match(/\AHash<(.+)>\z/m)
|
|
70
|
+
return type_string unless match
|
|
71
|
+
|
|
72
|
+
key, value = split_top_level(match[1])
|
|
73
|
+
return type_string unless value
|
|
74
|
+
|
|
75
|
+
"Hash{#{convert_hash_short_to_long(key.strip)} => " \
|
|
76
|
+
"#{convert_hash_short_to_long(value.strip)}}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Splits a generic body on its first top-level comma, respecting
|
|
80
|
+
# nested <>, {}, and () so nested generics are kept intact.
|
|
81
|
+
# @param str [String] the inside of a generic (without the brackets)
|
|
82
|
+
# @return [Array] [key, value], or [str, nil] when there is no
|
|
83
|
+
# top-level comma
|
|
84
|
+
def split_top_level(str)
|
|
85
|
+
depth = 0
|
|
86
|
+
str.each_char.with_index do |char, index|
|
|
87
|
+
case char
|
|
88
|
+
when '<', '{', '(' then depth += 1
|
|
89
|
+
when '>', '}', ')' then depth -= 1
|
|
90
|
+
when ','
|
|
91
|
+
return [str[0...index], str[(index + 1)..]] if depth.zero?
|
|
62
92
|
end
|
|
63
93
|
end
|
|
94
|
+
[str, nil]
|
|
64
95
|
end
|
|
65
96
|
|
|
66
97
|
# Converts long syntax to short syntax
|
|
@@ -20,9 +20,10 @@ module Yard
|
|
|
20
20
|
style = enforced_style
|
|
21
21
|
|
|
22
22
|
all_typed_tags(object.docstring, validated_tags).each do |tag|
|
|
23
|
-
|
|
23
|
+
types = tag_data(tag).types
|
|
24
|
+
next unless types
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
types.each do |type_str|
|
|
26
27
|
detected_style = detect_style(type_str)
|
|
27
28
|
|
|
28
29
|
# Report violations based on enforced style
|
|
@@ -41,22 +42,25 @@ module Yard
|
|
|
41
42
|
# @param type_str [String] the type string to check
|
|
42
43
|
# @return [String, nil] 'long' or 'short', or nil if not a collection type
|
|
43
44
|
def detect_style(type_str)
|
|
45
|
+
# The Hash/Array prefixes are anchored with a negative lookbehind
|
|
46
|
+
# so only the built-in classes match - not custom classes whose
|
|
47
|
+
# names merely contain them (MyHash, ByteArray).
|
|
44
48
|
# Hash types
|
|
45
49
|
# Hash<...> is short style (should be Hash{K => V})
|
|
46
|
-
if type_str =~ /Hash<.*>/
|
|
50
|
+
if type_str =~ /(?<![A-Za-z0-9_])Hash<.*>/
|
|
47
51
|
'short'
|
|
48
52
|
# Hash{...} is long style
|
|
49
|
-
elsif type_str =~ /Hash\{.*\}/
|
|
53
|
+
elsif type_str =~ /(?<![A-Za-z0-9_])Hash\{.*\}/
|
|
50
54
|
'long'
|
|
51
55
|
# {...} without Hash prefix is short style
|
|
52
56
|
elsif type_str =~ /^\{.*\}$/
|
|
53
57
|
'short'
|
|
54
58
|
# Array types
|
|
55
59
|
# Array<...> is long style
|
|
56
|
-
elsif type_str =~ /Array<.*>/
|
|
60
|
+
elsif type_str =~ /(?<![A-Za-z0-9_])Array<.*>/
|
|
57
61
|
'long'
|
|
58
62
|
# Array(...) is long style
|
|
59
|
-
elsif type_str =~ /Array\(.*\)/
|
|
63
|
+
elsif type_str =~ /(?<![A-Za-z0-9_])Array\(.*\)/
|
|
60
64
|
'long'
|
|
61
65
|
# <...> without Array prefix is short style
|
|
62
66
|
elsif type_str =~ /^<.*>$/
|
|
@@ -28,7 +28,7 @@ module Yard
|
|
|
28
28
|
code = example.text
|
|
29
29
|
next if code.nil? || code.empty?
|
|
30
30
|
|
|
31
|
-
example_name = example.name
|
|
31
|
+
example_name = example.name.to_s.empty? ? "Example #{index + 1}" : example.name
|
|
32
32
|
|
|
33
33
|
# Run linter (pass file path for context/config discovery)
|
|
34
34
|
offenses = runner.run(code, example_name, file_path: object.file)
|
|
@@ -10,7 +10,12 @@ module Yard
|
|
|
10
10
|
self.id = :example_syntax
|
|
11
11
|
self.defaults = {
|
|
12
12
|
'Enabled' => true,
|
|
13
|
-
'Severity' => 'warning'
|
|
13
|
+
'Severity' => 'warning',
|
|
14
|
+
# Opt-in: skip @example blocks that are interactive console
|
|
15
|
+
# transcripts (irb/pry sessions, their `=>` output, or shell `$`
|
|
16
|
+
# prompts) rather than runnable Ruby. Off by default so a real
|
|
17
|
+
# syntax error in a normal example is not accidentally hidden.
|
|
18
|
+
'SkipNonRuby' => false
|
|
14
19
|
}.freeze
|
|
15
20
|
end
|
|
16
21
|
end
|