yard-lint 1.6.1 → 1.8.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 +73 -1
- data/README.md +52 -8
- 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 +49 -1
- data/lib/yard/lint/templates/strict_config.yml +44 -1
- data/lib/yard/lint/todo_generator.rb +35 -14
- data/lib/yard/lint/validators/base.rb +50 -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 -11
- 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/underfilled_lines/config.rb +36 -0
- data/lib/yard/lint/validators/documentation/underfilled_lines/messages_builder.rb +31 -0
- data/lib/yard/lint/validators/documentation/underfilled_lines/parser.rb +64 -0
- data/lib/yard/lint/validators/documentation/underfilled_lines/result.rb +26 -0
- data/lib/yard/lint/validators/documentation/underfilled_lines/validator.rb +266 -0
- data/lib/yard/lint/validators/documentation/underfilled_lines.rb +74 -0
- 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 +12 -1
|
@@ -18,26 +18,30 @@ module Yard
|
|
|
18
18
|
def in_process_query(object, collector)
|
|
19
19
|
docstring_text = object.docstring.to_s
|
|
20
20
|
return if docstring_text.empty?
|
|
21
|
+
return if duplicate_docstring?(object)
|
|
21
22
|
|
|
22
23
|
errors = []
|
|
23
24
|
|
|
24
|
-
# Check for unclosed backticks
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# Check for unclosed inline backticks, ignoring fenced code blocks
|
|
26
|
+
# (``` ... ```): their fence characters and contents are not
|
|
27
|
+
# inline-code markers and otherwise inflate the count.
|
|
28
|
+
errors << 'unclosed_backtick' if inline_backtick_count(docstring_text).odd?
|
|
27
29
|
|
|
28
30
|
# Check for unclosed code blocks
|
|
29
31
|
code_block_count = docstring_text.scan(/^```/).count
|
|
30
32
|
errors << 'unclosed_code_block' if code_block_count.odd?
|
|
31
33
|
|
|
32
|
-
# Check for unclosed bold markers
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
# Check for unclosed bold markers, ignoring fenced code blocks and
|
|
35
|
+
# inline code spans (their contents are code, not markdown) as well
|
|
36
|
+
# as `**` runs surrounded by whitespace, which cannot delimit
|
|
37
|
+
# CommonMark emphasis (e.g. the exponent operator in `x ** y`).
|
|
38
|
+
errors << 'unclosed_bold' if bold_marker_count(docstring_text).odd?
|
|
36
39
|
|
|
37
|
-
# Check for invalid list markers
|
|
40
|
+
# Check for invalid list markers, reported with their absolute
|
|
41
|
+
# source line rather than a docstring-relative index
|
|
38
42
|
docstring_text.lines.each_with_index do |line, line_idx|
|
|
39
43
|
stripped = line.strip
|
|
40
|
-
errors << "invalid_list_marker:#{line_idx
|
|
44
|
+
errors << "invalid_list_marker:#{docstring_line(object, line_idx)}" if stripped.match?(/^[•·]/)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
return if errors.empty?
|
|
@@ -45,6 +49,59 @@ module Yard
|
|
|
45
49
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
46
50
|
collector.puts errors.join('|')
|
|
47
51
|
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Counts inline backticks, skipping fenced code blocks (``` ... ```)
|
|
56
|
+
# entirely - their fence characters and contents are not inline-code
|
|
57
|
+
# markers.
|
|
58
|
+
# @param text [String] the docstring text
|
|
59
|
+
# @return [Integer] number of inline backticks outside fenced blocks
|
|
60
|
+
def inline_backtick_count(text)
|
|
61
|
+
in_fence = false
|
|
62
|
+
count = 0
|
|
63
|
+
text.each_line do |line|
|
|
64
|
+
if line.strip.start_with?('```')
|
|
65
|
+
in_fence = !in_fence
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
next if in_fence
|
|
69
|
+
|
|
70
|
+
count += line.count('`')
|
|
71
|
+
end
|
|
72
|
+
count
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Counts `**` emphasis markers, skipping fenced code blocks and
|
|
76
|
+
# inline code spans entirely. A `**` run is only counted when it
|
|
77
|
+
# abuts a non-whitespace character on at least one side, since a run
|
|
78
|
+
# padded by whitespace on both sides can neither open nor close
|
|
79
|
+
# CommonMark emphasis - this excludes the exponent operator (`x ** y`)
|
|
80
|
+
# without dropping genuine `**bold**` markers.
|
|
81
|
+
# @param text [String] the docstring text
|
|
82
|
+
# @return [Integer] number of bold markers that can delimit emphasis
|
|
83
|
+
def bold_marker_count(text)
|
|
84
|
+
in_fence = false
|
|
85
|
+
count = 0
|
|
86
|
+
text.each_line do |line|
|
|
87
|
+
if line.strip.start_with?('```')
|
|
88
|
+
in_fence = !in_fence
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
next if in_fence
|
|
92
|
+
|
|
93
|
+
non_code = line.gsub(/`[^`]*`/, '')
|
|
94
|
+
non_code.scan(/\*\*/) do
|
|
95
|
+
match = Regexp.last_match
|
|
96
|
+
# Peek at the surrounding characters without consuming them, so
|
|
97
|
+
# adjacent runs like `**a**` (single-character bold) still pair up.
|
|
98
|
+
before = match.pre_match[-1]
|
|
99
|
+
after = match.post_match[0]
|
|
100
|
+
count += 1 if before&.match?(/\S/) || after&.match?(/\S/)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
count
|
|
104
|
+
end
|
|
48
105
|
end
|
|
49
106
|
end
|
|
50
107
|
end
|
|
@@ -23,9 +23,9 @@ module Yard
|
|
|
23
23
|
return unless object.is_explicit?
|
|
24
24
|
return if parent_class_allowed?(object)
|
|
25
25
|
|
|
26
|
-
# Check if @return tag is missing
|
|
27
|
-
|
|
28
|
-
return unless
|
|
26
|
+
# Check if @return tag is missing; tags nested inside @overload
|
|
27
|
+
# blocks live on the overload's own docstring, so check those too
|
|
28
|
+
return unless all_typed_tags(object.docstring, %w[return]).empty?
|
|
29
29
|
|
|
30
30
|
# Calculate arity (exclude splat and block parameters)
|
|
31
31
|
arity = object.parameters.reject { |p| p[0].to_s.start_with?('*', '&') }.size
|
|
@@ -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,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module UnderfilledLines
|
|
8
|
+
# Configuration for UnderfilledLines validator
|
|
9
|
+
class Config < ::Yard::Lint::Validators::Config
|
|
10
|
+
self.id = :underfilled_lines
|
|
11
|
+
self.defaults = {
|
|
12
|
+
'Enabled' => false,
|
|
13
|
+
'Severity' => 'convention',
|
|
14
|
+
# Target width. Re-wrapping prose at this width must save a line for an
|
|
15
|
+
# offense to be reported. Mirror your Documentation/LineLength MaxLength.
|
|
16
|
+
'MaxLength' => 120,
|
|
17
|
+
# Only flag when the widest non-final line of the paragraph leaves at
|
|
18
|
+
# least this many unused columns - avoids nitpicking near-full prose.
|
|
19
|
+
'MinTrailingSpace' => 20,
|
|
20
|
+
# Paragraphs shorter than this are never flagged (a single line cannot
|
|
21
|
+
# be "under-filled" - there is nothing to pull up onto it).
|
|
22
|
+
'MinParagraphLines' => 2,
|
|
23
|
+
# A non-final prose line ending in one of these characters is treated as
|
|
24
|
+
# a deliberate sentence/clause break, and its paragraph is left alone.
|
|
25
|
+
# Add ',' to also respect comma breaks (suppresses more, catches less).
|
|
26
|
+
'SentenceEndChars' => ['.', '?', '!', ':', ';'],
|
|
27
|
+
# Skip paragraphs containing non-ASCII text: String#length is not a
|
|
28
|
+
# reliable display width for CJK/full-width/emoji content.
|
|
29
|
+
'SkipNonAscii' => true
|
|
30
|
+
}.freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module UnderfilledLines
|
|
8
|
+
# Builds human-readable messages for UnderfilledLines violations.
|
|
9
|
+
class MessagesBuilder
|
|
10
|
+
class << self
|
|
11
|
+
# @param offense [Hash] offense details with :actual_lines, :reflowed_lines,
|
|
12
|
+
# :widest_fill, :max_length and :object_name
|
|
13
|
+
# @return [String] formatted message
|
|
14
|
+
def call(offense)
|
|
15
|
+
actual = offense[:actual_lines]
|
|
16
|
+
reflowed = offense[:reflowed_lines]
|
|
17
|
+
widest = offense[:widest_fill]
|
|
18
|
+
max_length = offense[:max_length]
|
|
19
|
+
object_name = offense[:object_name]
|
|
20
|
+
|
|
21
|
+
"Documentation paragraph uses #{actual} lines but fits in #{reflowed} " \
|
|
22
|
+
"at <=#{max_length} cols [widest line filled to #{widest}/#{max_length}] " \
|
|
23
|
+
"for '#{object_name}'"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module UnderfilledLines
|
|
8
|
+
# Parses UnderfilledLines validator output into structured violation hashes.
|
|
9
|
+
#
|
|
10
|
+
# Expected format (two lines per object with violations):
|
|
11
|
+
# file.rb:OBJECT_LINE: ObjectName
|
|
12
|
+
# MAX_LENGTH|START:ACTUAL:REFLOW:WIDEST|START:ACTUAL:REFLOW:WIDEST|...
|
|
13
|
+
class Parser < Parsers::Base
|
|
14
|
+
# @param output [String] raw validator output
|
|
15
|
+
# @return [Array<Hash>] array of violation hashes
|
|
16
|
+
def call(output, **)
|
|
17
|
+
return [] if output.nil? || output.empty?
|
|
18
|
+
|
|
19
|
+
violations = []
|
|
20
|
+
lines = output.lines.map(&:chomp)
|
|
21
|
+
|
|
22
|
+
i = 0
|
|
23
|
+
while i < lines.size
|
|
24
|
+
location_match = lines[i].match(/^(.+):(\d+): (.+)$/)
|
|
25
|
+
|
|
26
|
+
if location_match
|
|
27
|
+
file_path = location_match[1]
|
|
28
|
+
object_line = location_match[2].to_i
|
|
29
|
+
object_name = location_match[3]
|
|
30
|
+
|
|
31
|
+
i += 1
|
|
32
|
+
if i < lines.size
|
|
33
|
+
parts = lines[i].split('|')
|
|
34
|
+
max_length = parts.shift.to_i
|
|
35
|
+
|
|
36
|
+
parts.each do |part|
|
|
37
|
+
start, actual, reflow, widest = part.split(':', 4)
|
|
38
|
+
next unless start && actual && reflow && widest
|
|
39
|
+
|
|
40
|
+
violations << {
|
|
41
|
+
location: file_path,
|
|
42
|
+
line: start.to_i,
|
|
43
|
+
object_line: object_line,
|
|
44
|
+
object_name: object_name,
|
|
45
|
+
actual_lines: actual.to_i,
|
|
46
|
+
reflowed_lines: reflow.to_i,
|
|
47
|
+
widest_fill: widest.to_i,
|
|
48
|
+
max_length: max_length
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
i += 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
violations
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module UnderfilledLines
|
|
8
|
+
# Result wrapper for UnderfilledLines validator.
|
|
9
|
+
class Result < Results::Base
|
|
10
|
+
self.default_severity = 'convention'
|
|
11
|
+
self.offense_type = 'line'
|
|
12
|
+
self.offense_name = 'UnderfilledLines'
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @param offense [Hash] offense details from the parser
|
|
17
|
+
# @return [String] formatted message
|
|
18
|
+
def build_message(offense)
|
|
19
|
+
MessagesBuilder.call(offense)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|