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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -1
  3. data/README.md +52 -8
  4. data/bin/yard-lint +35 -4
  5. data/lib/yard/lint/config.rb +21 -4
  6. data/lib/yard/lint/config_loader.rb +31 -6
  7. data/lib/yard/lint/config_updater.rb +22 -1
  8. data/lib/yard/lint/config_validator.rb +2 -1
  9. data/lib/yard/lint/executor/in_process_registry.rb +58 -23
  10. data/lib/yard/lint/executor/warning_dispatcher.rb +1 -0
  11. data/lib/yard/lint/git.rb +44 -3
  12. data/lib/yard/lint/path_grouper.rb +4 -1
  13. data/lib/yard/lint/results/aggregate.rb +15 -7
  14. data/lib/yard/lint/stats_calculator.rb +8 -2
  15. data/lib/yard/lint/templates/default_config.yml +49 -1
  16. data/lib/yard/lint/templates/strict_config.yml +44 -1
  17. data/lib/yard/lint/todo_generator.rb +35 -14
  18. data/lib/yard/lint/validators/base.rb +50 -1
  19. data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +17 -2
  20. data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +5 -2
  21. data/lib/yard/lint/validators/documentation/line_length/validator.rb +1 -11
  22. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +66 -9
  23. data/lib/yard/lint/validators/documentation/missing_return/validator.rb +3 -3
  24. data/lib/yard/lint/validators/documentation/orphaned_doc_comment/validator.rb +79 -11
  25. data/lib/yard/lint/validators/documentation/orphaned_doc_comment.rb +4 -4
  26. data/lib/yard/lint/validators/documentation/text_substitution/validator.rb +10 -2
  27. data/lib/yard/lint/validators/documentation/underfilled_lines/config.rb +36 -0
  28. data/lib/yard/lint/validators/documentation/underfilled_lines/messages_builder.rb +31 -0
  29. data/lib/yard/lint/validators/documentation/underfilled_lines/parser.rb +64 -0
  30. data/lib/yard/lint/validators/documentation/underfilled_lines/result.rb +26 -0
  31. data/lib/yard/lint/validators/documentation/underfilled_lines/validator.rb +266 -0
  32. data/lib/yard/lint/validators/documentation/underfilled_lines.rb +74 -0
  33. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/config.rb +10 -1
  34. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +18 -4
  35. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +38 -4
  36. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +6 -0
  37. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -0
  38. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +1 -0
  39. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +43 -4
  40. data/lib/yard/lint/validators/tags/api_tags/validator.rb +11 -1
  41. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +36 -5
  42. data/lib/yard/lint/validators/tags/collection_type/validator.rb +10 -6
  43. data/lib/yard/lint/validators/tags/example_style/validator.rb +1 -1
  44. data/lib/yard/lint/validators/tags/example_syntax/config.rb +6 -1
  45. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +74 -3
  46. data/lib/yard/lint/validators/tags/forbidden_tags/validator.rb +7 -3
  47. data/lib/yard/lint/validators/tags/informal_notation/config.rb +6 -1
  48. data/lib/yard/lint/validators/tags/informal_notation/validator.rb +24 -3
  49. data/lib/yard/lint/validators/tags/invalid_types/config.rb +7 -1
  50. data/lib/yard/lint/validators/tags/invalid_types/parser.rb +22 -5
  51. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +24 -10
  52. data/lib/yard/lint/validators/tags/missing_yield/validator.rb +44 -4
  53. data/lib/yard/lint/validators/tags/missing_yield.rb +8 -8
  54. data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +13 -1
  55. data/lib/yard/lint/validators/tags/option_tags/result.rb +1 -0
  56. data/lib/yard/lint/validators/tags/option_tags/validator.rb +30 -2
  57. data/lib/yard/lint/validators/tags/order/parser.rb +12 -5
  58. data/lib/yard/lint/validators/tags/order/validator.rb +7 -2
  59. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +21 -8
  60. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +12 -5
  61. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +9 -7
  62. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +8 -2
  63. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +1 -1
  64. data/lib/yard/lint/validators/warnings/invalid_directive_format/parser.rb +2 -2
  65. data/lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb +2 -2
  66. data/lib/yard/lint/validators/warnings/syntax_error/config.rb +22 -0
  67. data/lib/yard/lint/validators/warnings/syntax_error/parser.rb +28 -0
  68. data/lib/yard/lint/validators/warnings/syntax_error/result.rb +27 -0
  69. data/lib/yard/lint/validators/warnings/syntax_error/validator.rb +15 -0
  70. data/lib/yard/lint/validators/warnings/syntax_error.rb +34 -0
  71. data/lib/yard/lint/validators/warnings/unknown_directive/parser.rb +2 -2
  72. data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +51 -46
  73. data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +20 -3
  74. data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +2 -2
  75. data/lib/yard/lint/version.rb +1 -1
  76. data/lib/yard/lint.rb +4 -1
  77. 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
- backtick_count = docstring_text.scan(/`/).count
26
- errors << 'unclosed_backtick' if backtick_count.odd?
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 (excluding code sections)
33
- non_code_text = docstring_text.gsub(/`[^`]*`/, '')
34
- bold_count = non_code_text.scan(/\*\*/).count
35
- errors << 'unclosed_bold' if bold_count.odd?
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 + 1}" if stripped.match?(/^[•·]/)
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
- return_tag = object.tag(:return)
28
- return unless return_tag.nil?
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 = /\A\s*(?<method>[a-z_]\w*[!?]?)(?:\s+|\s*\(\s*)(?::\w|:["']|["'])/.freeze
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
- lines = File.readlines(file, chomp: true)
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
- while i < lines.length && comment_line?(lines[i])
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
- stripped = line.strip
115
- stripped.start_with?('#') && !magic_comment?(stripped)
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
- stripped_line.match?(/\A#\s*(frozen[_-]string[_-]literal|encoding|warn[_-]indent|shareable[_-]constant[_-]value)\s*:/i)
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) || (has_implicit_tag && dsl_method_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
- collector.puts "#{object.file}:#{object.line}: #{object.title}"
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 line.include?(forbidden)
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