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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/README.md +4 -3
  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 +34 -1
  16. data/lib/yard/lint/templates/strict_config.yml +34 -1
  17. data/lib/yard/lint/todo_generator.rb +35 -14
  18. data/lib/yard/lint/validators/base.rb +39 -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 -0
  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/undocumented_method_arguments/config.rb +10 -1
  28. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +18 -4
  29. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +38 -4
  30. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +6 -0
  31. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -0
  32. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +1 -0
  33. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +43 -4
  34. data/lib/yard/lint/validators/tags/api_tags/validator.rb +11 -1
  35. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +36 -5
  36. data/lib/yard/lint/validators/tags/collection_type/validator.rb +10 -6
  37. data/lib/yard/lint/validators/tags/example_style/validator.rb +1 -1
  38. data/lib/yard/lint/validators/tags/example_syntax/config.rb +6 -1
  39. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +74 -3
  40. data/lib/yard/lint/validators/tags/forbidden_tags/validator.rb +7 -3
  41. data/lib/yard/lint/validators/tags/informal_notation/config.rb +6 -1
  42. data/lib/yard/lint/validators/tags/informal_notation/validator.rb +24 -3
  43. data/lib/yard/lint/validators/tags/invalid_types/config.rb +7 -1
  44. data/lib/yard/lint/validators/tags/invalid_types/parser.rb +22 -5
  45. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +24 -10
  46. data/lib/yard/lint/validators/tags/missing_yield/validator.rb +44 -4
  47. data/lib/yard/lint/validators/tags/missing_yield.rb +8 -8
  48. data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +13 -1
  49. data/lib/yard/lint/validators/tags/option_tags/result.rb +1 -0
  50. data/lib/yard/lint/validators/tags/option_tags/validator.rb +30 -2
  51. data/lib/yard/lint/validators/tags/order/parser.rb +12 -5
  52. data/lib/yard/lint/validators/tags/order/validator.rb +7 -2
  53. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +21 -8
  54. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +12 -5
  55. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +9 -7
  56. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +8 -2
  57. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +1 -1
  58. data/lib/yard/lint/validators/warnings/invalid_directive_format/parser.rb +2 -2
  59. data/lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb +2 -2
  60. data/lib/yard/lint/validators/warnings/syntax_error/config.rb +22 -0
  61. data/lib/yard/lint/validators/warnings/syntax_error/parser.rb +28 -0
  62. data/lib/yard/lint/validators/warnings/syntax_error/result.rb +27 -0
  63. data/lib/yard/lint/validators/warnings/syntax_error/validator.rb +15 -0
  64. data/lib/yard/lint/validators/warnings/syntax_error.rb +34 -0
  65. data/lib/yard/lint/validators/warnings/unknown_directive/parser.rb +2 -2
  66. data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +51 -46
  67. data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +20 -3
  68. data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +2 -2
  69. data/lib/yard/lint/version.rb +1 -1
  70. data/lib/yard/lint.rb +4 -1
  71. metadata +6 -1
@@ -26,9 +26,14 @@ module Yard
26
26
  end
27
27
 
28
28
  # Get all offenses from all validators
29
- # @return [Array<Hash>] flattened array of all offenses
29
+ # Identical offenses are reported once: objects sharing one docstring
30
+ # (e.g. the reader and writer generated by attr_accessor) produce the
31
+ # same offense for each generated method
32
+ # @return [Array<Hash>] flattened array of unique offenses
30
33
  def offenses
31
- @results.flat_map(&:offenses)
34
+ @offenses ||= @results.flat_map(&:offenses).uniq do |offense|
35
+ offense.values_at(:validator, :name, :location, :location_line, :message)
36
+ end
32
37
  end
33
38
 
34
39
  # Total number of offenses
@@ -78,10 +83,11 @@ module Yard
78
83
  # @return [Integer] 0 for success, 1 for failure
79
84
  def exit_code
80
85
  # Check minimum coverage requirement first
81
- if @config&.min_coverage &&
82
- documentation_coverage &&
83
- documentation_coverage[:coverage] < @config.min_coverage
84
- return 1
86
+ if @config&.min_coverage
87
+ coverage = documentation_coverage
88
+ # Fail safe: if a minimum is required but coverage could not be
89
+ # determined (e.g. the YARD stats subprocess failed), do not pass.
90
+ return 1 if coverage.nil? || coverage[:coverage] < @config.min_coverage
85
91
  end
86
92
 
87
93
  return 0 if offenses.empty?
@@ -95,7 +101,9 @@ module Yard
95
101
  when SEVERITY_WARNING
96
102
  (statistics[:error] + statistics[:warning]).positive? ? 1 : 0
97
103
  when SEVERITY_CONVENTION
98
- offenses.any? ? 1 : 0
104
+ # Exclude 'never'-severity offenses, which are meant to run without
105
+ # ever failing the build (they are also omitted from #statistics).
106
+ (statistics[:error] + statistics[:warning] + statistics[:convention]).positive? ? 1 : 0
99
107
  else
100
108
  0
101
109
  end
@@ -20,6 +20,10 @@ module Yard
20
20
  return default_stats if files.empty?
21
21
 
22
22
  raw_stats = run_yard_stats_query
23
+ # nil means the YARD subprocess failed - return nil (coverage unknown)
24
+ # rather than default_stats, which would falsely report 100% coverage
25
+ # and let a MinCoverage gate pass.
26
+ return nil if raw_stats.nil?
23
27
  return default_stats if raw_stats.empty?
24
28
 
25
29
  parsed_stats = parse_stats_output(raw_stats)
@@ -52,8 +56,10 @@ module Yard
52
56
 
53
57
  stdout, _stderr, status = Open3.capture3(cmd)
54
58
 
55
- # Return empty string if YARD command fails
56
- return '' unless status.exitstatus.zero?
59
+ # Return nil if the YARD command fails (non-zero exit, or killed by
60
+ # a signal so exitstatus is nil) - distinct from genuinely empty
61
+ # output, so coverage is reported as unknown rather than 100%.
62
+ return nil unless status.exitstatus&.zero?
57
63
 
58
64
  stdout
59
65
  end
@@ -44,6 +44,12 @@ Documentation/UndocumentedMethodArguments:
44
44
  Description: 'Checks for method parameters without @param tags.'
45
45
  Enabled: true
46
46
  Severity: warning
47
+ # Match each parameter to a @param tag by name (catches a misnamed @param).
48
+ # Set to false to fall back to a lenient count-only comparison.
49
+ CheckParameterNames: true
50
+ # Opt-in: skip methods with no documentation at all (reported by
51
+ # Documentation/UndocumentedObjects) instead of flagging them here too.
52
+ SkipFullyUndocumented: false
47
53
  # AllowedParentClasses:
48
54
  # - StandardError
49
55
  # AllowedMethods: skip @param checks for specific methods (exact name, name/arity, /regex/)
@@ -142,6 +148,13 @@ Tags/InvalidTypes:
142
148
  - param
143
149
  - option
144
150
  - return
151
+ - yieldreturn
152
+ - yieldparam
153
+ - raise
154
+ # Opt-in: flag CamelCase type names that are neither loaded Ruby constants
155
+ # nor defined in the analyzed codebase (catches typos like `Strng`). Off by
156
+ # default because types from un-analyzed dependencies would also be flagged.
157
+ StrictConstantNames: false
145
158
 
146
159
  Tags/TypeSyntax:
147
160
  Description: 'Validates YARD type syntax using YARD parser.'
@@ -152,6 +165,8 @@ Tags/TypeSyntax:
152
165
  - option
153
166
  - return
154
167
  - yieldreturn
168
+ - yieldparam
169
+ - raise
155
170
 
156
171
  Tags/MeaninglessTag:
157
172
  Description: 'Detects @param/@option tags on classes, modules, or constants.'
@@ -180,6 +195,8 @@ Tags/CollectionType:
180
195
  - option
181
196
  - return
182
197
  - yieldreturn
198
+ - yieldparam
199
+ - raise
183
200
 
184
201
  Tags/TagTypePosition:
185
202
  Description: 'Validates type annotation position in tags.'
@@ -210,6 +227,10 @@ Tags/ExampleSyntax:
210
227
  Description: 'Validates Ruby syntax in @example tags.'
211
228
  Enabled: true
212
229
  Severity: warning
230
+ # Opt-in: skip @example blocks that are interactive console transcripts
231
+ # (irb/pry sessions, their `=>` output, or shell `$` prompts) rather than
232
+ # runnable Ruby. Off by default so a real syntax error is not hidden.
233
+ SkipNonRuby: false
213
234
 
214
235
  Tags/ExampleStyle:
215
236
  Description: 'Validates code style in @example tags using RuboCop/StandardRB.'
@@ -258,14 +279,20 @@ Tags/InformalNotation:
258
279
  Severity: warning
259
280
  CaseSensitive: false
260
281
  RequireStartOfLine: true
282
+ # Opt-in: also skip 4-space/tab indented Markdown code blocks (not just
283
+ # fenced ``` blocks). Off by default - indented text is also used for list
284
+ # continuations and wrapped prose, which would then be skipped too.
285
+ SkipIndentedCodeBlocks: false
261
286
  Patterns:
262
287
  Note: '@note'
288
+ IMPORTANT: '@note'
289
+ Important: '@note'
263
290
  Todo: '@todo'
264
291
  TODO: '@todo'
265
292
  FIXME: '@todo'
266
293
  See: '@see'
267
294
  See also: '@see'
268
- Warning: '@deprecated'
295
+ Warning: '@note'
269
296
  Deprecated: '@deprecated'
270
297
  Author: '@author'
271
298
  Version: '@version'
@@ -284,6 +311,7 @@ Tags/NonAsciiType:
284
311
  - return
285
312
  - yieldreturn
286
313
  - yieldparam
314
+ - raise
287
315
 
288
316
  Tags/TagGroupSeparator:
289
317
  Description: 'Enforces blank line separators between different YARD tag groups.'
@@ -313,6 +341,11 @@ Tags/ForbiddenTags:
313
341
  # - Tag: api # Forbids @api tag entirely (no Types = any occurrence)
314
342
 
315
343
  # Warnings validators - catches YARD parser errors
344
+ Warnings/SyntaxError:
345
+ Description: 'Detects Ruby files YARD cannot parse (syntax errors).'
346
+ Enabled: true
347
+ Severity: error
348
+
316
349
  Warnings/UnknownTag:
317
350
  Description: 'Detects unknown YARD tags.'
318
351
  Enabled: true
@@ -48,6 +48,11 @@ Documentation/UndocumentedMethodArguments:
48
48
  Description: 'Checks for method parameters without @param tags.'
49
49
  Enabled: true
50
50
  Severity: error
51
+ # Match each parameter to a @param tag by name (catches a misnamed @param).
52
+ CheckParameterNames: true
53
+ # Opt-in: skip methods with no documentation at all (reported by
54
+ # Documentation/UndocumentedObjects) instead of flagging them here too.
55
+ SkipFullyUndocumented: false
51
56
  # AllowedParentClasses:
52
57
  # - StandardError
53
58
  # AllowedMethods: skip @param checks for specific methods (exact name, name/arity, /regex/)
@@ -146,6 +151,13 @@ Tags/InvalidTypes:
146
151
  - param
147
152
  - option
148
153
  - return
154
+ - yieldreturn
155
+ - yieldparam
156
+ - raise
157
+ # The strict preset opts in: flag CamelCase type names that are neither loaded
158
+ # Ruby constants nor defined in the analyzed codebase (catches typos). Add any
159
+ # legitimate dependency types to ExtraTypes to silence false positives.
160
+ StrictConstantNames: true
149
161
 
150
162
  Tags/TypeSyntax:
151
163
  Description: 'Validates YARD type syntax using YARD parser.'
@@ -156,6 +168,8 @@ Tags/TypeSyntax:
156
168
  - option
157
169
  - return
158
170
  - yieldreturn
171
+ - yieldparam
172
+ - raise
159
173
 
160
174
  Tags/MeaninglessTag:
161
175
  Description: 'Detects @param/@option tags on classes, modules, or constants.'
@@ -184,6 +198,8 @@ Tags/CollectionType:
184
198
  - option
185
199
  - return
186
200
  - yieldreturn
201
+ - yieldparam
202
+ - raise
187
203
 
188
204
  Tags/TagTypePosition:
189
205
  Description: 'Validates type annotation position in tags.'
@@ -214,6 +230,10 @@ Tags/ExampleSyntax:
214
230
  Description: 'Validates Ruby syntax in @example tags.'
215
231
  Enabled: true
216
232
  Severity: error
233
+ # Opt-in: skip @example blocks that are interactive console transcripts
234
+ # (irb/pry sessions, their `=>` output, or shell `$` prompts) rather than
235
+ # runnable Ruby. Off by default so a real syntax error is not hidden.
236
+ SkipNonRuby: false
217
237
 
218
238
  Tags/ExampleStyle:
219
239
  Description: 'Validates code style in @example tags using RuboCop/StandardRB.'
@@ -254,6 +274,7 @@ Tags/RedundantParamDescription:
254
274
  IdPattern: true
255
275
  DirectionalDate: true
256
276
  TypeGeneric: true
277
+ ArticleParamPhrase: true
257
278
 
258
279
  Tags/InformalNotation:
259
280
  Description: 'Detects informal tag notation patterns like "Note:" instead of @note.'
@@ -261,14 +282,20 @@ Tags/InformalNotation:
261
282
  Severity: error
262
283
  CaseSensitive: false
263
284
  RequireStartOfLine: true
285
+ # Opt-in: also skip 4-space/tab indented Markdown code blocks (not just
286
+ # fenced ``` blocks). Off by default - indented text is also used for list
287
+ # continuations and wrapped prose, which would then be skipped too.
288
+ SkipIndentedCodeBlocks: false
264
289
  Patterns:
265
290
  Note: '@note'
291
+ IMPORTANT: '@note'
292
+ Important: '@note'
266
293
  Todo: '@todo'
267
294
  TODO: '@todo'
268
295
  FIXME: '@todo'
269
296
  See: '@see'
270
297
  See also: '@see'
271
- Warning: '@deprecated'
298
+ Warning: '@note'
272
299
  Deprecated: '@deprecated'
273
300
  Author: '@author'
274
301
  Version: '@version'
@@ -287,6 +314,7 @@ Tags/NonAsciiType:
287
314
  - return
288
315
  - yieldreturn
289
316
  - yieldparam
317
+ - raise
290
318
 
291
319
  Tags/TagGroupSeparator:
292
320
  Description: 'Enforces blank line separators between different YARD tag groups.'
@@ -316,6 +344,11 @@ Tags/ForbiddenTags:
316
344
  # - Tag: api # Forbids @api tag entirely (no Types = any occurrence)
317
345
 
318
346
  # Warnings validators - catches YARD parser errors
347
+ Warnings/SyntaxError:
348
+ Description: 'Detects Ruby files YARD cannot parse (syntax errors).'
349
+ Enabled: true
350
+ Severity: error
351
+
319
352
  Warnings/UnknownTag:
320
353
  Description: 'Detects unknown YARD tags.'
321
354
  Enabled: true
@@ -13,9 +13,12 @@ module Yard
13
13
  # @param config [Config] yard-lint configuration object with validator settings
14
14
  # @param force [Boolean] whether to overwrite existing todo file if present
15
15
  # @param exclude_limit [Integer] minimum files in directory before grouping into wildcard patterns
16
+ # @param config_path [String, nil] config file to link the todo into
17
+ # (defaults to .yard-lint.yml in the current directory)
16
18
  # @return [Hash] result with :message, :offense_count, :validator_count
17
- def generate(path:, config:, force: false, exclude_limit: DEFAULT_EXCLUDE_LIMIT)
18
- new(path: path, config: config, force: force, exclude_limit: exclude_limit).generate
19
+ def generate(path:, config:, force: false, exclude_limit: DEFAULT_EXCLUDE_LIMIT, config_path: nil)
20
+ new(path: path, config: config, force: force, exclude_limit: exclude_limit,
21
+ config_path: config_path).generate
19
22
  end
20
23
  end
21
24
 
@@ -24,13 +27,14 @@ module Yard
24
27
  # @param config [Config] yard-lint configuration object
25
28
  # @param force [Boolean] whether to overwrite existing todo file
26
29
  # @param exclude_limit [Integer] minimum files before grouping into patterns
27
- def initialize(path:, config:, force:, exclude_limit:)
30
+ # @param config_path [String, nil] config file to link the todo into
31
+ def initialize(path:, config:, force:, exclude_limit:, config_path: nil)
28
32
  @path = path
29
33
  @config = config
30
34
  @force = force
31
35
  @exclude_limit = exclude_limit
32
36
  @todo_path = File.join(Dir.pwd, '.yard-lint-todo.yml')
33
- @config_path = File.join(Dir.pwd, Config::DEFAULT_CONFIG_FILE)
37
+ @config_path = config_path || File.join(Dir.pwd, Config::DEFAULT_CONFIG_FILE)
34
38
  end
35
39
 
36
40
  # Generate the .yard-lint-todo.yml file with exclusions for current violations
@@ -93,6 +97,12 @@ module Yard
93
97
  next unless @config.validator_enabled?(validator_name)
94
98
 
95
99
  validator_result = result_builder.build(validator_name, raw_results)
100
+ next unless validator_result
101
+
102
+ # Apply per-validator Exclude patterns (and drop no-location offenses)
103
+ # exactly as a normal run does, so the baseline does not silence files
104
+ # that are already excluded.
105
+ validator_result = runner.send(:filter_result_offenses, validator_name, validator_result)
96
106
  next unless validator_result && validator_result.offenses.any?
97
107
 
98
108
  # Extract file paths from offenses, skipping those without a location
@@ -206,17 +216,28 @@ module Yard
206
216
  # Update existing config file to add inherit_from todo file
207
217
  # @return [void]
208
218
  def update_existing_config
209
- config_yaml = YAML.load_file(@config_path) || {}
210
- inherit_from = Array(config_yaml['inherit_from'] || [])
211
-
212
- # Add todo file to inherit_from if not already present
213
- unless inherit_from.include?('.yard-lint-todo.yml')
214
- inherit_from.unshift('.yard-lint-todo.yml')
215
- config_yaml['inherit_from'] = inherit_from
219
+ config_yaml = ConfigLoader.load_yaml_file(@config_path)
220
+ return if Array(config_yaml['inherit_from']).include?('.yard-lint-todo.yml')
221
+
222
+ raw = File.read(@config_path)
223
+
224
+ # Edit the file textually rather than round-tripping through to_yaml,
225
+ # which would strip every comment and reformat the user's config.
226
+ updated =
227
+ if config_yaml.key?('inherit_from') && raw.match?(/^inherit_from:[ \t]*$/)
228
+ # Existing block list - prepend our entry as the first item.
229
+ raw.sub(/^inherit_from:[ \t]*$\n/) { "#{Regexp.last_match(0)} - .yard-lint-todo.yml\n" }
230
+ elsif config_yaml.key?('inherit_from')
231
+ # Existing inline/array form (rare) - fall back to a structured
232
+ # rewrite; comments cannot be preserved in this case.
233
+ config_yaml['inherit_from'] = ['.yard-lint-todo.yml'] + Array(config_yaml['inherit_from'])
234
+ config_yaml.to_yaml
235
+ else
236
+ # No inherit_from yet - prepend a block, preserving the file verbatim.
237
+ "inherit_from:\n - .yard-lint-todo.yml\n\n#{raw}"
238
+ end
216
239
 
217
- # Write updated config
218
- File.write(@config_path, config_yaml.to_yaml)
219
- end
240
+ File.write(@config_path, updated)
220
241
  end
221
242
 
222
243
  # Create a minimal config file that inherits from todo file
@@ -97,6 +97,41 @@ module Yard
97
97
  tags
98
98
  end
99
99
 
100
+ # Tracks docstring locations already processed by this validator
101
+ # instance. Objects generated from a single comment block (e.g. the
102
+ # reader and writer created by attr_accessor) share one docstring;
103
+ # content-scanning validators use this to report each docstring once.
104
+ # @param object [YARD::CodeObjects::Base] the code object to check
105
+ # @return [Boolean] true if this object's docstring was already seen
106
+ def duplicate_docstring?(object)
107
+ @scanned_docstrings ||= Set.new
108
+ key = [object.file, object.docstring.line_range&.first || object.line]
109
+
110
+ !@scanned_docstrings.add?(key)
111
+ end
112
+
113
+ # Converts a zero-based line offset within a docstring's text into an
114
+ # absolute line number in the source file, so offenses can point at
115
+ # the offending documentation line instead of the definition line.
116
+ # @param object [YARD::CodeObjects::Base] the documented object
117
+ # @param line_offset [Integer] zero-based offset within the docstring text
118
+ # @return [Integer] absolute source line number
119
+ def docstring_line(object, line_offset)
120
+ start_line = object.docstring.line_range&.first || object.line
121
+
122
+ start_line + line_offset
123
+ end
124
+
125
+ # Returns the tag that actually carries a tag's types and description.
126
+ # For most tags that is the tag itself, but @option tags wrap their
127
+ # data in a nested pair tag - tag.types and tag.text are nil on the
128
+ # OptionTag itself, with the documented option living on tag.pair.
129
+ # @param tag [YARD::Tags::Tag] tag whose data holder should be resolved
130
+ # @return [YARD::Tags::Tag] the tag holding types/text data
131
+ def tag_data(tag)
132
+ tag.respond_to?(:pair) && tag.pair ? tag.pair : tag
133
+ end
134
+
100
135
  # Checks whether the object's enclosing class (or the object itself if it is
101
136
  # a class) has a superclass that appears in the validator's AllowedParentClasses
102
137
  # configuration list. When true, validators skip the object so that classes
@@ -236,7 +271,10 @@ module Yard
236
271
 
237
272
  return defaults[key] unless validator_name
238
273
 
239
- config.validator_config(validator_name, key) || defaults[key]
274
+ value = config.validator_config(validator_name, key)
275
+ # A nil? check (not ||) so that explicitly configured false values
276
+ # are honored instead of falling back to a truthy default
277
+ value.nil? ? defaults[key] : value
240
278
  end
241
279
  end
242
280
  end
@@ -50,8 +50,11 @@ module Yard
50
50
  if stripped.empty?
51
51
  blank_count += 1
52
52
  elsif stripped.start_with?('#')
53
- # Skip Ruby magic comments - they're not YARD documentation
54
- next if magic_comment?(stripped)
53
+ # Skip lines that are not YARD documentation: magic comments,
54
+ # shebangs, tool sigils/directives (Sorbet, RuboCop, Standard),
55
+ # and bare `#` separators. Treating them as a doc block caused
56
+ # spurious blank-line offenses for undocumented definitions.
57
+ next if non_documentation_comment?(stripped)
55
58
 
56
59
  has_doc_block = true
57
60
  break
@@ -72,6 +75,18 @@ module Yard
72
75
  line.match?(/^#\s*(frozen[_-]string[_-]literal|encoding|warn[_-]indent|shareable[_-]constant[_-]value)\s*:/i)
73
76
  end
74
77
 
78
+ # Check if a comment line is not YARD documentation (magic comment,
79
+ # shebang, tool sigil/directive, or a bare `#` separator).
80
+ # @param line [String] stripped comment line
81
+ # @return [Boolean] true if the line should not count as documentation
82
+ def non_documentation_comment?(line)
83
+ magic_comment?(line) ||
84
+ line.start_with?('#!') || # shebang
85
+ line.match?(/\A#\s*(rubocop|standard):/i) || # linter directives
86
+ line.match?(/\A#\s*typed:/i) || # Sorbet sigil
87
+ line.match?(/\A#+\s*\z/) # bare # separator
88
+ end
89
+
75
90
  # Check if the given pattern is enabled in configuration
76
91
  # @param violation_type [String] 'single' or 'orphaned'
77
92
  # @return [Boolean] whether the pattern is enabled
@@ -33,8 +33,11 @@ module Yard
33
33
  stripped = line.strip
34
34
 
35
35
  if stripped.empty? && comment_end.nil?
36
- # Skip empty lines before finding comment block
37
- next
36
+ # A blank line between the definition and any comment above
37
+ # means there is no attached docstring (YARD only attaches a
38
+ # comment immediately above the definition), so stop scanning -
39
+ # otherwise a detached file header is mistaken for the doc.
40
+ break
38
41
  elsif stripped.start_with?('#')
39
42
  comment_end ||= i
40
43
  comment_start = i
@@ -23,6 +23,7 @@ module Yard
23
23
 
24
24
  line_range = object.docstring.line_range
25
25
  return unless line_range
26
+ return if duplicate_docstring?(object)
26
27
 
27
28
  max_length = config_or_default('MaxLength').to_i
28
29
  source_lines = cached_lines(object.file)
@@ -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