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
|
@@ -18,15 +18,22 @@ module Yard
|
|
|
18
18
|
def in_process_query(object, collector)
|
|
19
19
|
return unless object.has_tag?(:example)
|
|
20
20
|
|
|
21
|
+
skip_non_ruby = config_or_default('SkipNonRuby')
|
|
21
22
|
example_tags = object.tags(:example)
|
|
22
23
|
|
|
23
24
|
example_tags.each_with_index do |example, index|
|
|
24
25
|
code = example.text
|
|
25
26
|
next if code.nil? || code.empty?
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
+
# Opt-in: an interactive console transcript is not runnable Ruby,
|
|
29
|
+
# so skip it rather than reporting its prompts as syntax errors.
|
|
30
|
+
next if skip_non_ruby && console_transcript?(code)
|
|
31
|
+
|
|
32
|
+
# Clean the code: strip YARD output indicators (# =>) and
|
|
33
|
+
# everything after them, but only when the "# =>" is a real
|
|
34
|
+
# trailing comment - not when it appears inside a string literal
|
|
28
35
|
code_lines = code.split("\n").map do |line|
|
|
29
|
-
line
|
|
36
|
+
strip_output_marker(line)
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
cleaned_code = code_lines.join("\n").strip
|
|
@@ -51,7 +58,7 @@ module Yard
|
|
|
51
58
|
$VERBOSE = nil
|
|
52
59
|
RubyVM::InstructionSequence.compile(cleaned_code)
|
|
53
60
|
rescue SyntaxError => e
|
|
54
|
-
example_name = example.name
|
|
61
|
+
example_name = example.name.to_s.empty? ? "Example #{index + 1}" : example.name
|
|
55
62
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
56
63
|
collector.puts 'syntax_error'
|
|
57
64
|
collector.puts example_name
|
|
@@ -67,6 +74,70 @@ module Yard
|
|
|
67
74
|
end
|
|
68
75
|
end
|
|
69
76
|
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Matches a line that begins (after optional whitespace) with an
|
|
81
|
+
# interactive console prompt or REPL output marker:
|
|
82
|
+
# >> / ?> irb input / continuation
|
|
83
|
+
# => irb/pry result output
|
|
84
|
+
# irb( irb> irb prompt
|
|
85
|
+
# pry( / [n] pry( pry prompt
|
|
86
|
+
# $ shell prompt
|
|
87
|
+
CONSOLE_PROMPT = /\A\s*(?:>>|\?>|=>|\$\s|irb[\s(>]|(?:\[\d+\]\s*)?pry[\s(>])/.freeze
|
|
88
|
+
|
|
89
|
+
private_constant :CONSOLE_PROMPT
|
|
90
|
+
|
|
91
|
+
# Check whether an @example body is an interactive console transcript
|
|
92
|
+
# (irb/pry session or shell prompt) rather than runnable Ruby.
|
|
93
|
+
# @param code [String] the raw @example text
|
|
94
|
+
# @return [Boolean] true if any line looks like a console prompt/output
|
|
95
|
+
def console_transcript?(code)
|
|
96
|
+
code.each_line.any? { |line| line.match?(CONSOLE_PROMPT) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Removes a trailing YARD `# => result` output marker from a line of
|
|
100
|
+
# example code, but only when the `#` actually starts a comment - a
|
|
101
|
+
# `#` inside a string or character literal is left untouched, so a
|
|
102
|
+
# string such as `"result # => x"` is not corrupted into an
|
|
103
|
+
# unterminated literal.
|
|
104
|
+
# @param line [String] a single line of example source
|
|
105
|
+
# @return [String] the line with a trailing output marker removed
|
|
106
|
+
def strip_output_marker(line)
|
|
107
|
+
in_single = false
|
|
108
|
+
in_double = false
|
|
109
|
+
i = 0
|
|
110
|
+
|
|
111
|
+
while i < line.length
|
|
112
|
+
char = line[i]
|
|
113
|
+
|
|
114
|
+
if (in_single || in_double) && char == '\\'
|
|
115
|
+
i += 2
|
|
116
|
+
next
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if in_single
|
|
120
|
+
in_single = false if char == "'"
|
|
121
|
+
elsif in_double
|
|
122
|
+
in_double = false if char == '"'
|
|
123
|
+
elsif char == "'"
|
|
124
|
+
in_single = true
|
|
125
|
+
elsif char == '"'
|
|
126
|
+
in_double = true
|
|
127
|
+
elsif char == '#'
|
|
128
|
+
# A comment starts here (outside any string). Strip it only
|
|
129
|
+
# when it is a YARD output marker; leave ordinary comments,
|
|
130
|
+
# which the Ruby parser handles fine.
|
|
131
|
+
return line[0...i].rstrip if line[i..].match?(/\A#\s*=>/)
|
|
132
|
+
|
|
133
|
+
break
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
i += 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
line
|
|
140
|
+
end
|
|
70
141
|
end
|
|
71
142
|
end
|
|
72
143
|
end
|
|
@@ -18,7 +18,11 @@ module Yard
|
|
|
18
18
|
patterns = forbidden_patterns
|
|
19
19
|
return if patterns.empty?
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# Include tags nested inside @overload blocks - they live on the
|
|
22
|
+
# overload's own docstring and are invisible to docstring.tags
|
|
23
|
+
forbidden_tag_names = patterns.map { |pattern| pattern['Tag'] }.compact.uniq
|
|
24
|
+
|
|
25
|
+
all_typed_tags(object.docstring, forbidden_tag_names).each do |tag|
|
|
22
26
|
patterns.each do |pattern|
|
|
23
27
|
next unless matches_pattern?(tag, pattern)
|
|
24
28
|
|
|
@@ -45,7 +49,7 @@ module Yard
|
|
|
45
49
|
pattern_types = pattern['Types']
|
|
46
50
|
return true if pattern_types.nil? || pattern_types.empty?
|
|
47
51
|
|
|
48
|
-
tag_types = tag.types || []
|
|
52
|
+
tag_types = tag_data(tag).types || []
|
|
49
53
|
(tag_types & pattern_types).any?
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -54,7 +58,7 @@ module Yard
|
|
|
54
58
|
# @param pattern [Hash] the matched pattern
|
|
55
59
|
# @return [String] details line for parser
|
|
56
60
|
def build_details(tag, pattern)
|
|
57
|
-
types_text = (tag.types || []).join(',')
|
|
61
|
+
types_text = (tag_data(tag).types || []).join(',')
|
|
58
62
|
pattern_types = (pattern['Types'] || []).join(',')
|
|
59
63
|
"#{tag.tag_name}|#{types_text}|#{pattern_types}"
|
|
60
64
|
end
|
|
@@ -13,6 +13,11 @@ module Yard
|
|
|
13
13
|
'Severity' => 'warning',
|
|
14
14
|
'CaseSensitive' => false,
|
|
15
15
|
'RequireStartOfLine' => true,
|
|
16
|
+
# Opt-in: also skip 4-space (or tab) indented Markdown code blocks,
|
|
17
|
+
# not just fenced (```) blocks. Off by default because indented
|
|
18
|
+
# content is also used for list continuations and wrapped prose,
|
|
19
|
+
# which would then be skipped too.
|
|
20
|
+
'SkipIndentedCodeBlocks' => false,
|
|
16
21
|
'Patterns' => {
|
|
17
22
|
'Note' => '@note',
|
|
18
23
|
'IMPORTANT' => '@note',
|
|
@@ -22,7 +27,7 @@ module Yard
|
|
|
22
27
|
'FIXME' => '@todo',
|
|
23
28
|
'See' => '@see',
|
|
24
29
|
'See also' => '@see',
|
|
25
|
-
'Warning' => '@
|
|
30
|
+
'Warning' => '@note',
|
|
26
31
|
'Deprecated' => '@deprecated',
|
|
27
32
|
'Author' => '@author',
|
|
28
33
|
'Version' => '@version',
|
|
@@ -19,16 +19,19 @@ module Yard
|
|
|
19
19
|
def in_process_query(object, collector)
|
|
20
20
|
docstring_text = object.docstring.to_s
|
|
21
21
|
return if docstring_text.empty?
|
|
22
|
+
return if duplicate_docstring?(object)
|
|
22
23
|
|
|
23
24
|
patterns = config_patterns
|
|
24
25
|
case_sensitive = config_case_sensitive
|
|
25
26
|
require_start = config_require_start_of_line
|
|
27
|
+
skip_indented = config_skip_indented_code_blocks
|
|
26
28
|
|
|
27
29
|
found_patterns = find_informal_patterns(
|
|
28
30
|
docstring_text,
|
|
29
31
|
patterns,
|
|
30
32
|
case_sensitive,
|
|
31
|
-
require_start
|
|
33
|
+
require_start,
|
|
34
|
+
skip_indented
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
return if found_patterns.empty?
|
|
@@ -37,7 +40,8 @@ module Yard
|
|
|
37
40
|
# Line 1: file:line: object_title
|
|
38
41
|
# Line 2: pattern|replacement|line_offset|line_text (pipe-separated)
|
|
39
42
|
found_patterns.each do |match|
|
|
40
|
-
|
|
43
|
+
line = docstring_line(object, match[:line_offset])
|
|
44
|
+
collector.puts "#{object.file}:#{line}: #{object.title}"
|
|
41
45
|
collector.puts "#{match[:pattern]}|#{match[:replacement]}|" \
|
|
42
46
|
"#{match[:line_offset]}|#{match[:line_text]}"
|
|
43
47
|
end
|
|
@@ -50,8 +54,9 @@ module Yard
|
|
|
50
54
|
# @param patterns [Hash] pattern => replacement mapping
|
|
51
55
|
# @param case_sensitive [Boolean] whether to match case-sensitively
|
|
52
56
|
# @param require_start [Boolean] whether pattern must be at start of line
|
|
57
|
+
# @param skip_indented [Boolean] whether to also skip indented code blocks
|
|
53
58
|
# @return [Array<Hash>] array of matches with pattern, replacement, line_offset, line_text
|
|
54
|
-
def find_informal_patterns(docstring_text, patterns, case_sensitive, require_start)
|
|
59
|
+
def find_informal_patterns(docstring_text, patterns, case_sensitive, require_start, skip_indented = false)
|
|
55
60
|
found_patterns = []
|
|
56
61
|
matched_lines = Set.new
|
|
57
62
|
in_code_block = false
|
|
@@ -66,6 +71,9 @@ module Yard
|
|
|
66
71
|
# Skip lines inside code blocks
|
|
67
72
|
next if in_code_block
|
|
68
73
|
|
|
74
|
+
# Optionally skip 4-space/tab indented Markdown code blocks
|
|
75
|
+
next if skip_indented && indented_code_line?(line)
|
|
76
|
+
|
|
69
77
|
# Skip lines already matched (avoids duplicate reports for similar patterns)
|
|
70
78
|
next if matched_lines.include?(line_offset)
|
|
71
79
|
|
|
@@ -87,6 +95,14 @@ module Yard
|
|
|
87
95
|
found_patterns
|
|
88
96
|
end
|
|
89
97
|
|
|
98
|
+
# Check if a line is part of a Markdown indented code block
|
|
99
|
+
# (4+ leading spaces or a leading tab, with non-whitespace content).
|
|
100
|
+
# @param line [String] the raw docstring line
|
|
101
|
+
# @return [Boolean] true if the line is indented code
|
|
102
|
+
def indented_code_line?(line)
|
|
103
|
+
line.match?(/\A(?: {4}|\t)/) && !line.strip.empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
90
106
|
# Check if a line matches the informal pattern
|
|
91
107
|
# @param line [String] the line to check
|
|
92
108
|
# @param pattern [String] the pattern to match (without colon)
|
|
@@ -125,6 +141,11 @@ module Yard
|
|
|
125
141
|
def config_require_start_of_line
|
|
126
142
|
config_or_default('RequireStartOfLine')
|
|
127
143
|
end
|
|
144
|
+
|
|
145
|
+
# @return [Boolean] whether to skip indented Markdown code blocks
|
|
146
|
+
def config_skip_indented_code_blocks
|
|
147
|
+
config_or_default('SkipIndentedCodeBlocks')
|
|
148
|
+
end
|
|
128
149
|
end
|
|
129
150
|
end
|
|
130
151
|
end
|
|
@@ -12,7 +12,13 @@ module Yard
|
|
|
12
12
|
'Enabled' => true,
|
|
13
13
|
'Severity' => 'warning',
|
|
14
14
|
'ValidatedTags' => %w[param option return yieldreturn yieldparam raise],
|
|
15
|
-
'ExtraTypes' => []
|
|
15
|
+
'ExtraTypes' => [],
|
|
16
|
+
# Opt-in: when true, a CamelCase type name that is neither a loaded
|
|
17
|
+
# Ruby constant nor resolvable in the analyzed codebase's YARD
|
|
18
|
+
# registry is flagged (catches typos like `Strng`). Off by default
|
|
19
|
+
# because YARD does not load the project, so types defined only in
|
|
20
|
+
# un-analyzed dependencies would otherwise be reported.
|
|
21
|
+
'StrictConstantNames' => false
|
|
16
22
|
}.freeze
|
|
17
23
|
end
|
|
18
24
|
end
|
|
@@ -10,8 +10,12 @@ module Yard
|
|
|
10
10
|
# /path/to/file.rb:10: ClassName#method_name
|
|
11
11
|
# @tagname param_name:BadType1,BadType2|@tagname2:BadType3
|
|
12
12
|
class Parser < Parsers::Base
|
|
13
|
-
# @return [Regexp] matches "file:line:
|
|
14
|
-
LOCATION_REGEX = /^(.+):(\d+):\s+(.+)
|
|
13
|
+
# @return [Regexp] matches "file:line: ObjectTitle"
|
|
14
|
+
LOCATION_REGEX = /^(.+):(\d+):\s+(.+)$/
|
|
15
|
+
# @return [Regexp] splits a title into namespace and method name on
|
|
16
|
+
# the last # or . separator; titles without one (e.g. CONST,
|
|
17
|
+
# Foo::Bar) are kept whole
|
|
18
|
+
TITLE_REGEX = /\A(.*)[#.]([^#.]+)\z/
|
|
15
19
|
# @return [Regexp] matches the tag violations line (starts with @)
|
|
16
20
|
TAG_VIOLATIONS_REGEX = /^@/
|
|
17
21
|
|
|
@@ -23,18 +27,22 @@ module Yard
|
|
|
23
27
|
i = 0
|
|
24
28
|
|
|
25
29
|
while i < lines.size
|
|
26
|
-
|
|
30
|
+
# Violation lines (starting with @) must never be consumed as
|
|
31
|
+
# location lines, even if they happen to match the loose regex
|
|
32
|
+
match = lines[i].match?(TAG_VIOLATIONS_REGEX) ? nil : lines[i].match(LOCATION_REGEX)
|
|
27
33
|
|
|
28
34
|
unless match
|
|
29
35
|
i += 1
|
|
30
36
|
next
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
class_name, method_name = split_title(match[3])
|
|
40
|
+
|
|
33
41
|
offense = {
|
|
34
42
|
location: match[1],
|
|
35
43
|
line: match[2].to_i,
|
|
36
|
-
class_name:
|
|
37
|
-
method_name:
|
|
44
|
+
class_name: class_name,
|
|
45
|
+
method_name: method_name,
|
|
38
46
|
tag_violations: []
|
|
39
47
|
}
|
|
40
48
|
|
|
@@ -54,6 +62,15 @@ module Yard
|
|
|
54
62
|
|
|
55
63
|
private
|
|
56
64
|
|
|
65
|
+
# Splits a YARD object title into namespace and method name parts
|
|
66
|
+
# @param title [String] object title (e.g. "Foo#bar", "#bar", "CONST")
|
|
67
|
+
# @return [Array(String, String)] namespace and method name; for titles
|
|
68
|
+
# without a separator both parts are the full title
|
|
69
|
+
def split_title(title)
|
|
70
|
+
match = title.match(TITLE_REGEX)
|
|
71
|
+
match ? [match[1], match[2]] : [title, title]
|
|
72
|
+
end
|
|
73
|
+
|
|
57
74
|
# Parse "tagname param:Type1,Type2|tagname2:Type3" into structured data
|
|
58
75
|
# @param line [String] the violations line
|
|
59
76
|
# @return [Array<Hash>] each entry has :tag, :param (may be nil), :types
|
|
@@ -22,6 +22,7 @@ module Yard
|
|
|
22
22
|
undefined
|
|
23
23
|
unspecified
|
|
24
24
|
unknown
|
|
25
|
+
Boolean
|
|
25
26
|
].freeze
|
|
26
27
|
|
|
27
28
|
private_constant :ALLOWED_DEFAULTS
|
|
@@ -34,17 +35,18 @@ module Yard
|
|
|
34
35
|
def in_process_query(object, collector)
|
|
35
36
|
checked_tags = config_or_default('ValidatedTags')
|
|
36
37
|
extra_types = config_or_default('ExtraTypes')
|
|
38
|
+
strict = config_or_default('StrictConstantNames')
|
|
37
39
|
allowed_types = ALLOWED_DEFAULTS + extra_types
|
|
38
40
|
|
|
39
41
|
# Collect per-tag violations to surface in the offense message.
|
|
40
42
|
# Each entry is "tagname param_name:Type1,Type2" (param_name omitted when nil).
|
|
41
43
|
tag_violations = all_typed_tags(object.docstring, checked_tags).filter_map do |tag|
|
|
42
|
-
bad = (tag.types || [])
|
|
44
|
+
bad = (tag_data(tag).types || [])
|
|
43
45
|
.compact
|
|
44
46
|
.flat_map { |type| extract_type_names(type) }
|
|
45
47
|
.uniq
|
|
46
48
|
.reject { |type| allowed_types.include?(type) }
|
|
47
|
-
.reject { |type| type_defined?(type) }
|
|
49
|
+
.reject { |type| type_defined?(type, strict: strict) }
|
|
48
50
|
.reject { |type| type.include?('#') }
|
|
49
51
|
next if bad.empty?
|
|
50
52
|
|
|
@@ -73,17 +75,16 @@ module Yard
|
|
|
73
75
|
# Check if a type is defined in Ruby runtime or YARD registry
|
|
74
76
|
# In in-process mode, parsed classes are in YARD registry but not loaded into Ruby
|
|
75
77
|
# @param type [String] type name to check
|
|
78
|
+
# @param strict [Boolean] when true, a syntactically valid but unknown
|
|
79
|
+
# constant name (e.g. a typo) is treated as undefined instead of recognized
|
|
76
80
|
# @return [Boolean] true if type is defined (or at least recognized as a valid type)
|
|
77
|
-
def type_defined?(type)
|
|
81
|
+
def type_defined?(type, strict: false)
|
|
78
82
|
# Symbol and string literal types (:foo, "bar") are valid hash key notations
|
|
79
83
|
return true if type.start_with?(':', '"', "'")
|
|
80
84
|
|
|
81
|
-
# Check Ruby runtime first
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# the type is considered "recognized" and should not be flagged as invalid.
|
|
85
|
-
# This allows common types like "Boolean" which aren't actual Ruby classes
|
|
86
|
-
# but are still recognized by Ruby as valid constant names to check.
|
|
85
|
+
# Check Ruby runtime first.
|
|
86
|
+
# const_defined? returns true (loaded constant), false (valid name but
|
|
87
|
+
# not loaded), or raises NameError (invalid constant syntax).
|
|
87
88
|
begin
|
|
88
89
|
const_result = Kernel.const_defined?(type)
|
|
89
90
|
rescue NameError
|
|
@@ -91,7 +92,20 @@ module Yard
|
|
|
91
92
|
# These aren't valid Ruby constants, so we can't check them this way
|
|
92
93
|
const_result = nil
|
|
93
94
|
end
|
|
94
|
-
|
|
95
|
+
|
|
96
|
+
if strict
|
|
97
|
+
# Strict mode: only a constant actually loaded in this process
|
|
98
|
+
# (Ruby core/stdlib) is accepted outright. A valid-but-unloaded
|
|
99
|
+
# name (const_result == false) or invalid syntax (nil) still gets
|
|
100
|
+
# the YARD registry check below, so codebase-defined types pass but
|
|
101
|
+
# unknown CamelCase names (typos) are flagged.
|
|
102
|
+
return true if const_result == true
|
|
103
|
+
else
|
|
104
|
+
# Lenient (default): any syntactically valid constant name is
|
|
105
|
+
# accepted, because YARD does not load the project's code so most
|
|
106
|
+
# real types are not const_defined? in this process.
|
|
107
|
+
return true unless const_result.nil?
|
|
108
|
+
end
|
|
95
109
|
|
|
96
110
|
# Check YARD registry (for classes defined in parsed files)
|
|
97
111
|
# This may fail for malformed type strings or registry issues
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ripper'
|
|
4
|
+
|
|
3
5
|
module Yard
|
|
4
6
|
module Lint
|
|
5
7
|
module Validators
|
|
@@ -12,11 +14,15 @@ module Yard
|
|
|
12
14
|
|
|
13
15
|
# Matches the `yield` keyword. The negative lookbehind `(?<![:.])`
|
|
14
16
|
# prevents matching method calls like `Fiber.yield` or `yielder.yield`
|
|
15
|
-
# and symbol literals like `:yield`.
|
|
16
|
-
#
|
|
17
|
+
# and symbol literals like `:yield`. The negative lookahead `(?!:)`
|
|
18
|
+
# prevents matching the label form - a symbol hash key or keyword
|
|
19
|
+
# argument like `{ yield: true }` (a real block yield is never
|
|
20
|
+
# directly followed by a colon: `yield ::Const` has a space).
|
|
21
|
+
# Word boundaries ensure `yield_self` and similar identifiers are
|
|
22
|
+
# not matched.
|
|
17
23
|
# Known limitation: `yield` inside regex literals (e.g. /yield/) is
|
|
18
24
|
# not stripped before scanning; it is rare enough to be acceptable.
|
|
19
|
-
YIELD_PATTERN = /(?<![:.])\byield\b/.freeze
|
|
25
|
+
YIELD_PATTERN = /(?<![:.])\byield\b(?!:)/.freeze
|
|
20
26
|
|
|
21
27
|
# @return [Regexp] matches full-line Ruby comments
|
|
22
28
|
COMMENT_LINE_PATTERN = /\A\s*#/.freeze
|
|
@@ -58,8 +64,42 @@ module Yard
|
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
# @param source [String] raw method source code
|
|
61
|
-
# @return [Boolean] true if the
|
|
67
|
+
# @return [Boolean] true if the method itself yields (ignoring yields
|
|
68
|
+
# that belong to a nested method definition)
|
|
62
69
|
def source_contains_yield?(source)
|
|
70
|
+
sexp = ::Ripper.sexp(source)
|
|
71
|
+
# If the isolated source does not parse on its own, fall back to the
|
|
72
|
+
# line scan rather than silently missing a yield.
|
|
73
|
+
return yield_in_lines?(source) if sexp.nil?
|
|
74
|
+
|
|
75
|
+
method_level_yield?(sexp)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Walks the Ripper S-expression for a `yield` that belongs to the
|
|
79
|
+
# method being analysed - one nested directly inside this definition
|
|
80
|
+
# rather than inside a nested `def`. `def_depth` counts enclosing
|
|
81
|
+
# method definitions; the analysed method's own body is depth 1, so a
|
|
82
|
+
# yield inside a nested `def` (depth 2+) is correctly ignored.
|
|
83
|
+
# @param node [Object] a Ripper S-expression node
|
|
84
|
+
# @param def_depth [Integer] number of enclosing `def`/`defs` nodes
|
|
85
|
+
# @return [Boolean] true if a yield at the method's own level is found
|
|
86
|
+
def method_level_yield?(node, def_depth = 0)
|
|
87
|
+
return false unless node.is_a?(::Array)
|
|
88
|
+
|
|
89
|
+
case node.first
|
|
90
|
+
when :yield, :yield0
|
|
91
|
+
return def_depth == 1
|
|
92
|
+
when :def, :defs
|
|
93
|
+
return node.any? { |child| method_level_yield?(child, def_depth + 1) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
node.any? { |child| method_level_yield?(child, def_depth) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Line-based fallback used when Ripper cannot parse the source.
|
|
100
|
+
# @param source [String] raw method source code
|
|
101
|
+
# @return [Boolean] true if a `yield` keyword appears on a code line
|
|
102
|
+
def yield_in_lines?(source)
|
|
63
103
|
source.each_line.any? do |line|
|
|
64
104
|
next false if line.match?(COMMENT_LINE_PATTERN)
|
|
65
105
|
|
|
@@ -13,14 +13,6 @@ module Yard
|
|
|
13
13
|
#
|
|
14
14
|
# This validator is disabled by default (opt-in).
|
|
15
15
|
#
|
|
16
|
-
# @note Does not flag the inverse case (has `@yield` tag but no actual
|
|
17
|
-
# `yield` in source) - that is intentional for abstract/overridable methods.
|
|
18
|
-
#
|
|
19
|
-
# @note Known limitation: `yield` appearing inside heredoc bodies or
|
|
20
|
-
# multi-line string literals may produce false positives. These cases
|
|
21
|
-
# are rare enough in practice that the validator does not attempt to
|
|
22
|
-
# handle them.
|
|
23
|
-
#
|
|
24
16
|
# @example Bad - method yields but block is undocumented
|
|
25
17
|
# # Iterates over items
|
|
26
18
|
# # @param items [Array] the items
|
|
@@ -43,6 +35,14 @@ module Yard
|
|
|
43
35
|
# items.each { |item| yield item }
|
|
44
36
|
# end
|
|
45
37
|
#
|
|
38
|
+
# @note Does not flag the inverse case (has `@yield` tag but no actual
|
|
39
|
+
# `yield` in source) - that is intentional for abstract/overridable methods.
|
|
40
|
+
#
|
|
41
|
+
# @note Known limitation: `yield` appearing inside heredoc bodies or
|
|
42
|
+
# multi-line string literals may produce false positives. These cases
|
|
43
|
+
# are rare enough in practice that the validator does not attempt to
|
|
44
|
+
# handle them.
|
|
45
|
+
#
|
|
46
46
|
# ## Configuration
|
|
47
47
|
#
|
|
48
48
|
# Tags/MissingYield:
|
|
@@ -14,6 +14,15 @@ module Yard
|
|
|
14
14
|
# Pattern to match non-ASCII characters
|
|
15
15
|
NON_ASCII_PATTERN = /[^\x00-\x7F]/
|
|
16
16
|
|
|
17
|
+
# Matches a string literal type (e.g. "naïve"). Such literals are
|
|
18
|
+
# values, not Ruby type names, so non-ASCII inside them is valid.
|
|
19
|
+
STRING_LITERAL = /\A("[^"]*"|'[^']*')\z/
|
|
20
|
+
# Matches a quoted-symbol literal type (e.g. :"naïve"); non-ASCII
|
|
21
|
+
# inside the quoted value is likewise valid.
|
|
22
|
+
QUOTED_SYMBOL_LITERAL = /\A:("[^"]*"|'[^']*')\z/
|
|
23
|
+
|
|
24
|
+
private_constant :STRING_LITERAL, :QUOTED_SYMBOL_LITERAL
|
|
25
|
+
|
|
17
26
|
# Execute query for a single object during in-process execution.
|
|
18
27
|
# Checks type specifications for non-ASCII characters.
|
|
19
28
|
# @param object [YARD::CodeObjects::Base] the code object to query
|
|
@@ -21,12 +30,15 @@ module Yard
|
|
|
21
30
|
# @return [void]
|
|
22
31
|
def in_process_query(object, collector)
|
|
23
32
|
validated_tags = config.validator_config('Tags/NonAsciiType', 'ValidatedTags') ||
|
|
24
|
-
|
|
33
|
+
Config.defaults['ValidatedTags']
|
|
25
34
|
|
|
26
35
|
all_typed_tags(object.docstring, validated_tags).each do |tag|
|
|
27
36
|
next unless tag.types
|
|
28
37
|
|
|
29
38
|
tag.types.each do |type_str|
|
|
39
|
+
# Skip literal value types - non-ASCII is valid inside them.
|
|
40
|
+
next if type_str.match?(STRING_LITERAL) || type_str.match?(QUOTED_SYMBOL_LITERAL)
|
|
41
|
+
|
|
30
42
|
non_ascii_chars = type_str.scan(NON_ASCII_PATTERN).uniq
|
|
31
43
|
next if non_ascii_chars.empty?
|
|
32
44
|
|
|
@@ -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
|
|
@@ -28,8 +28,15 @@ module Yard
|
|
|
28
28
|
|
|
29
29
|
return unless has_options_param
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
|
|
31
|
+
# A parameter that merely has an options-like name but is
|
|
32
|
+
# documented as a non-Hash type (e.g. a Boolean keyword argument
|
|
33
|
+
# or an Array) is not an options hash, so do not demand @option.
|
|
34
|
+
return if options_param_documented_as_non_hash?(object, parameter_names)
|
|
35
|
+
|
|
36
|
+
# Check if method has any @option tags; tags nested inside
|
|
37
|
+
# overload blocks live on the overload's own docstring, so
|
|
38
|
+
# check those too
|
|
39
|
+
option_tags = all_typed_tags(object.docstring, %w[option])
|
|
33
40
|
|
|
34
41
|
return unless option_tags.empty?
|
|
35
42
|
|
|
@@ -39,6 +46,27 @@ module Yard
|
|
|
39
46
|
|
|
40
47
|
private
|
|
41
48
|
|
|
49
|
+
# Whether an options-named parameter is documented with a concrete
|
|
50
|
+
# non-Hash @param type. Such a parameter (e.g. `@param options
|
|
51
|
+
# [Boolean]`) is not an options hash and should not require @option.
|
|
52
|
+
# @param object [YARD::CodeObjects::MethodObject] the method
|
|
53
|
+
# @param parameter_names [Array<String>] configured options names
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def options_param_documented_as_non_hash?(object, parameter_names)
|
|
56
|
+
param_tags = all_typed_tags(object.docstring, %w[param])
|
|
57
|
+
|
|
58
|
+
object.parameters.any? do |param|
|
|
59
|
+
name = param[0].to_s.gsub(/[*:]/, '')
|
|
60
|
+
next false unless parameter_names.include?(name)
|
|
61
|
+
|
|
62
|
+
tag = param_tags.find { |t| t.name == name }
|
|
63
|
+
types = tag&.types
|
|
64
|
+
next false if types.nil? || types.empty?
|
|
65
|
+
|
|
66
|
+
types.none? { |type| type.to_s.match?(/\AHash\b|\AHash[<{(]/) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
42
70
|
# @return [Array<String>] parameter names that should have @option tags
|
|
43
71
|
def config_parameter_names
|
|
44
72
|
config.validator_config('Tags/OptionTags', 'ParameterNames') || Config.defaults['ParameterNames']
|
|
@@ -40,12 +40,19 @@ module Yard
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
base_hash.delete_if { |_key, value| value == 'valid' }
|
|
43
|
-
order = base_hash.values.map(&:last)
|
|
44
43
|
|
|
45
|
-
Validators::Documentation::UndocumentedMethodArguments::Parser
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
location_parser = Validators::Documentation::UndocumentedMethodArguments::Parser.new
|
|
45
|
+
|
|
46
|
+
# Parse each location together with its own ordering so that an
|
|
47
|
+
# unparseable location line drops only its own offense instead of
|
|
48
|
+
# shifting the orderings of all offenses that follow it
|
|
49
|
+
base_hash.values.filter_map do |location, ordering|
|
|
50
|
+
element = location_parser.call(location).first
|
|
51
|
+
next unless element
|
|
52
|
+
|
|
53
|
+
element[:order] = ordering
|
|
54
|
+
element
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
private
|
|
@@ -17,7 +17,9 @@ module Yard
|
|
|
17
17
|
# @param collector [Executor::ResultCollector] collector for output
|
|
18
18
|
# @return [void]
|
|
19
19
|
def in_process_query(object, collector)
|
|
20
|
-
|
|
20
|
+
# is_alias? exists only on method objects; on namespace objects
|
|
21
|
+
# YARD's method_missing raises NameError, so guard by type first
|
|
22
|
+
return if object.type == :method && object.is_alias?
|
|
21
23
|
|
|
22
24
|
# Extract @tag names from docstring
|
|
23
25
|
tag_pattern = /^@(\S+)/
|
|
@@ -51,7 +53,10 @@ module Yard
|
|
|
51
53
|
|
|
52
54
|
# @return [Array<String>] tags order
|
|
53
55
|
def tags_order
|
|
54
|
-
|
|
56
|
+
# Fall back to the default when EnforcedOrder is unset or
|
|
57
|
+
# explicitly null in the config - otherwise `nil.dup` crashes
|
|
58
|
+
# the entire run.
|
|
59
|
+
config_or_default('EnforcedOrder')
|
|
55
60
|
end
|
|
56
61
|
end
|
|
57
62
|
end
|