yard-lint 1.10.2 → 1.10.3

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +10 -0
  4. data/lib/yard/lint/templates/default_config.yml +21 -0
  5. data/lib/yard/lint/templates/strict_config.yml +21 -0
  6. data/lib/yard/lint/validators/base.rb +4 -4
  7. data/lib/yard/lint/validators/documentation/blank_line_before_definition/config.rb +7 -1
  8. data/lib/yard/lint/validators/documentation/blank_line_before_definition/result.rb +1 -0
  9. data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +66 -2
  10. data/lib/yard/lint/validators/documentation/line_length/validator.rb +1 -1
  11. data/lib/yard/lint/validators/documentation/line_length.rb +1 -1
  12. data/lib/yard/lint/validators/documentation/orphaned_doc_comment/validator.rb +6 -0
  13. data/lib/yard/lint/validators/documentation/text_substitution/parser.rb +1 -1
  14. data/lib/yard/lint/validators/documentation/text_substitution.rb +6 -6
  15. data/lib/yard/lint/validators/documentation/undocumented_objects/config.rb +1 -0
  16. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +48 -4
  17. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +29 -1
  18. data/lib/yard/lint/validators/tags/tag_separator/config.rb +22 -0
  19. data/lib/yard/lint/validators/tags/tag_separator/messages_builder.rb +49 -0
  20. data/lib/yard/lint/validators/tags/tag_separator/parser.rb +60 -0
  21. data/lib/yard/lint/validators/tags/tag_separator/result.rb +28 -0
  22. data/lib/yard/lint/validators/tags/tag_separator/validator.rb +141 -0
  23. data/lib/yard/lint/validators/tags/tag_separator.rb +67 -0
  24. data/lib/yard/lint/version.rb +1 -1
  25. data/lib/yard/lint.rb +1 -1
  26. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1b9f3a39ce5fb15c2a1ace664e45d393fb70e429220085ebd4a7881aa2496cc
4
- data.tar.gz: 207fb47ddd283bd62755c6f5736652166caed19a342f601ce78cfe73c0aad5e9
3
+ metadata.gz: b257fdb7ca6477899e3be3411289568fea84f44fbc3e06ae10e0803e236c4443
4
+ data.tar.gz: 44609a51c40daa3f6b9a0a85ea647448768d4ad34bbd4b99358effee51170f19
5
5
  SHA512:
6
- metadata.gz: 7898934b15acdea1b39a244e6224969cef3848427b02db1711c87480ff8459b481920eb6a378fe25296bf0c2dc708d7f3b63ec7c8bd25effb7ea92d8c21179df
7
- data.tar.gz: cb7525e59baebfe5cbb1e192ed91d38c6c75d04a4fd1ed9c60db1616794e66b67b46b39cefd5fa734565b30bb7bb4419fe672cbe798b2019443c0d5f59e7f7ae
6
+ metadata.gz: b4c98789f3b1bfab2c0cfa1fbdf69801adf0d7b942517fb99f7bb6c75ff3a95f050d7561a543b1d0b6e0ea749d6d63e18b0d6efa179c8d0f4e7d07637d825fed
7
+ data.tar.gz: 9bfe860758ef6de5770acfacc2e821661f883491f923d169c7fa32d942c6979f88d68df8f57fc284b461a0e5e5d7661c58200df9c1a3018d5f8a8d50e66149c2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 1.10.3 (2026-08-05)
2
+ - **[Feature]** Added `Tags/TagSeparator` (opt-in, disabled by default, severity `convention`) - a stricter sibling of `Tags/TagGroupSeparator`. Where `TagGroupSeparator` only inserts a blank line between different tag *groups* (and therefore can never separate two tags of the same type, such as sibling `@param` tags), `TagSeparator` requires a blank line between *every* pair of consecutive tags. Tags listed under the `Exempt` option may immediately follow the previous tag without a blank line - useful for `@option` tags, which document keys of a preceding `@param` hash and read best when clustered directly beneath it. `RequireAfterDescription` additionally requires a blank line between the description and the first tag.
3
+ - **[Feature]** `Documentation/UndocumentedObjects` gained an `ExcludedObjects` option that matches the fully-qualified name of any object - class, module, method, or constant - via exact names or `/regex/` patterns anchored to the full path. This closes a gap where constants could never be excluded: `ExcludedMethods` matches only the trailing method name and deliberately ignores classes/modules/constants (so a pattern like `/cache/` cannot silently suppress a class such as `Memcached`), which left no way to skip a constant like `ATui::Input::KEY_A_Z` or to target a single object by its full path. `ExcludedObjects` (e.g. `'/^ATui::Input::KEY_/'`, `'MyApp::Config::DEFAULTS'`, or arity notation `'MyApp::Api#call/1'`) now handles those cases; `ExcludedMethods` behavior is unchanged (#299).
4
+ - **[Feature]** `Documentation/BlankLineBeforeDefinition` gained an `IgnoredCommentPatterns` option: comment lines matching any configured pattern are not treated as documentation, so a blank line below them is no longer reported as a detached docstring. This resolves false positives on comments that are not docs - commented-out code in a constant block (`# KEY_F12 = ...` then a blank line then `KEY_C_A = ...`), `# FIXME` notes, or section separators - without the linter having to guess which comments are code. Each entry is a `/regex/` (matched as a regular expression) or a plain string (matched as a literal at word boundaries, so `api` does not match inside `rapid`); invalid regexes are skipped. Genuine detached prose docstrings are still reported. Defaults to empty, so existing behaviour is unchanged (#300).
5
+ - **[Bugfix]** `Documentation/BlankLineBeforeDefinition` offenses now carry their validator name. The result builder overrode the shared offense construction but omitted the `validator` field, so the text output rendered the offense as ` : Blank line ...` with an empty validator slot, leaving no way to tell which validator to disable. The name (`Documentation/BlankLineBeforeDefinition`) is now populated like every other validator (#300).
6
+ - **[Bugfix]** `Documentation/OrphanedDocComment` no longer flags a tagged doc comment above a class variable assignment (`@@name = ...`) as orphaned. YARD's `ClassVariableHandler` registers class variables as documentable code objects and attaches the preceding comment to them (a bare `@private`/`@api` tag included), so the comment is not dropped - it was only the validator's definition matcher that recognized constants (`FOO = ...`) but not class variables. Class variable assignments are now recognized as documentable, so `# @private`/`@@logger = ...` internal-marker pairs are left alone. Single instance-variable assignments (`@cached = ...`), which YARD does not document, are still reported.
7
+
1
8
  ## 1.10.2 (2026-07-24)
2
9
  - **[Bugfix]** `Semantic/AbstractMethods` no longer flags an `@abstract` method whose body is `fail NotImplementedError`. `fail` is a built-in alias of `raise` (`Kernel#fail`), so `fail NotImplementedError` is an identical abstract-method guard to `raise NotImplementedError`, but the `AllowedImplementations` patterns are written with `raise` and were matched literally. A leading `fail` keyword is now normalized to `raise` before matching, so both forms are recognized (and any custom `raise`-based `AllowedImplementations` pattern automatically covers its `fail` alias). Identifiers such as `failure` and non-leading `fail(...)` calls are left untouched.
3
10
 
data/README.md CHANGED
@@ -228,9 +228,19 @@ Documentation/UndocumentedObjects:
228
228
  Description: 'Checks for classes, modules, and methods without documentation.'
229
229
  Enabled: true
230
230
  Severity: warning
231
+ # Exclude methods by their (unqualified) name, name/arity, or /regex/
231
232
  ExcludedMethods:
232
233
  - 'initialize/0'
233
234
  - '/^_/'
235
+ # Exclude any object (class, module, method, or constant) by its
236
+ # fully-qualified name, full-path name/arity, or a /regex/ matched against
237
+ # the full path. Use this to skip constants (ExcludedMethods never applies to
238
+ # them) or to target a single object without matching same-named objects
239
+ # elsewhere.
240
+ ExcludedObjects:
241
+ - 'MyApp::Config::DEFAULTS' # exact fully-qualified constant
242
+ - '/^MyApp::Keys::KEY_/' # every KEY_* constant under MyApp::Keys
243
+ - 'MyApp::Api#call/1' # only MyApp::Api#call with exactly 1 param
234
244
  # Skip classes inheriting from these base classes (exact full-path match)
235
245
  AllowedParentClasses:
236
246
  - StandardError
@@ -36,6 +36,13 @@ Documentation/UndocumentedObjects:
36
36
  ExcludedMethods:
37
37
  - 'initialize/0' # Exclude parameter-less initialize
38
38
  - '/^_/' # Exclude private methods (by convention)
39
+ # ExcludedObjects matches the fully-qualified name of ANY object (class,
40
+ # module, method, or constant). Unlike ExcludedMethods it can exclude
41
+ # constants and be matched against a full path (exact name, name/arity, or /regex/).
42
+ # ExcludedObjects:
43
+ # - 'MyApp::Config::DEFAULTS' # exact fully-qualified constant
44
+ # - '/^MyApp::Keys::KEY_/' # every KEY_* constant under MyApp::Keys
45
+ # - 'MyApp::Api#call/1' # only MyApp::Api#call with exactly 1 param
39
46
  # AllowedParentClasses:
40
47
  # - StandardError # Skip error subclasses
41
48
  # - ApplicationRecord # Skip Rails model subclasses
@@ -107,6 +114,13 @@ Documentation/BlankLineBeforeDefinition:
107
114
  EnabledPatterns:
108
115
  SingleBlankLine: true
109
116
  OrphanedDocs: true
117
+ # Comment lines matching any of these patterns are NOT treated as
118
+ # documentation, so a blank line below them is not reported as a detached
119
+ # docstring. Each entry is a /regex/ or a literal matched at word boundaries.
120
+ # Handy for commented-out code in constant blocks, FIXME notes, separators, etc.
121
+ # IgnoredCommentPatterns:
122
+ # - '/\A#\s*\w[\w:]*\s*=/' # commented-out assignments, e.g. "# KEY = 1"
123
+ # - 'FIXME' # a comment with the word FIXME
110
124
 
111
125
  Documentation/LineLength:
112
126
  Description: 'Detects documentation comment lines that exceed the configured maximum length.'
@@ -346,6 +360,13 @@ Tags/TagGroupSeparator:
346
360
  yield: [yield, yieldparam, yieldreturn]
347
361
  RequireAfterDescription: false
348
362
 
363
+ Tags/TagSeparator:
364
+ Description: 'Enforces a blank line between every consecutive YARD tag.'
365
+ Enabled: false # Opt-in validator
366
+ Severity: convention
367
+ Exempt: []
368
+ RequireAfterDescription: false
369
+
349
370
  Tags/ForbiddenTags:
350
371
  Description: 'Detects forbidden tag and type combinations.'
351
372
  Enabled: false # Opt-in validator
@@ -40,6 +40,13 @@ Documentation/UndocumentedObjects:
40
40
  ExcludedMethods:
41
41
  - 'initialize/0' # Exclude parameter-less initialize
42
42
  - '/^_/' # Exclude private methods (by convention)
43
+ # ExcludedObjects matches the fully-qualified name of ANY object (class,
44
+ # module, method, or constant). Unlike ExcludedMethods it can exclude
45
+ # constants and be matched against a full path (exact name, name/arity, or /regex/).
46
+ # ExcludedObjects:
47
+ # - 'MyApp::Config::DEFAULTS' # exact fully-qualified constant
48
+ # - '/^MyApp::Keys::KEY_/' # every KEY_* constant under MyApp::Keys
49
+ # - 'MyApp::Api#call/1' # only MyApp::Api#call with exactly 1 param
43
50
  # AllowedParentClasses:
44
51
  # - StandardError # Skip error subclasses
45
52
  # - ApplicationRecord # Skip Rails model subclasses
@@ -110,6 +117,13 @@ Documentation/BlankLineBeforeDefinition:
110
117
  EnabledPatterns:
111
118
  SingleBlankLine: true
112
119
  OrphanedDocs: true
120
+ # Comment lines matching any of these patterns are NOT treated as
121
+ # documentation, so a blank line below them is not reported as a detached
122
+ # docstring. Each entry is a /regex/ or a literal matched at word boundaries.
123
+ # Handy for commented-out code in constant blocks, FIXME notes, separators, etc.
124
+ # IgnoredCommentPatterns:
125
+ # - '/\A#\s*\w[\w:]*\s*=/' # commented-out assignments, e.g. "# KEY = 1"
126
+ # - 'FIXME' # a comment with the word FIXME
113
127
 
114
128
  Documentation/LineLength:
115
129
  Description: 'Detects documentation comment lines that exceed the configured maximum length.'
@@ -344,6 +358,13 @@ Tags/TagGroupSeparator:
344
358
  yield: [yield, yieldparam, yieldreturn]
345
359
  RequireAfterDescription: false
346
360
 
361
+ Tags/TagSeparator:
362
+ Description: 'Enforces a blank line between every consecutive YARD tag.'
363
+ Enabled: false # Opt-in validator
364
+ Severity: error
365
+ Exempt: []
366
+ RequireAfterDescription: false
367
+
347
368
  Tags/ForbiddenTags:
348
369
  Description: 'Detects forbidden tag and type combinations.'
349
370
  Enabled: false # Opt-in validator
@@ -186,7 +186,7 @@ module Yard
186
186
  superclass_path = superclass.respond_to?(:path) ? superclass.path.to_s : superclass.to_s
187
187
  next false if superclass_path.empty?
188
188
  # Every Ruby class without an explicit parent implicitly inherits from Object,
189
- # so matching it would exempt all classes never the intent.
189
+ # so matching it would exempt all classes - never the intent.
190
190
  # BasicObject is the root of the hierarchy and is guarded for the same reason.
191
191
  next false if superclass_path == 'Object' || superclass_path == 'BasicObject'
192
192
 
@@ -199,10 +199,10 @@ module Yard
199
199
  # object without reporting an offense.
200
200
  #
201
201
  # Three pattern forms are supported (matching ExcludedMethods convention):
202
- # - Exact name: 'call' matches any arity
203
- # - Arity: 'initialize/1' matches only the given parameter count
202
+ # - Exact name: 'call' - matches any arity
203
+ # - Arity: 'initialize/1' - matches only the given parameter count
204
204
  # (required + optional, excluding * and &)
205
- # - Regex: '/^perform/' matches against the bare method name
205
+ # - Regex: '/^perform/' - matches against the bare method name
206
206
  #
207
207
  # Invalid regex patterns are silently ignored. The empty regex '//' is always
208
208
  # rejected (it would match every method, making the option useless).
@@ -15,7 +15,13 @@ module Yard
15
15
  'EnabledPatterns' => {
16
16
  'SingleBlankLine' => true,
17
17
  'OrphanedDocs' => true
18
- }
18
+ },
19
+ # Comment lines matching any of these patterns are not treated as
20
+ # documentation, so a blank line below them is not reported as a
21
+ # detached docstring. Each entry is a `/regex/` or a literal matched
22
+ # at word boundaries. Useful for commented-out code, `# FIXME`
23
+ # notes, etc.
24
+ 'IgnoredCommentPatterns' => []
19
25
  }.freeze
20
26
  end
21
27
  end
@@ -30,6 +30,7 @@ module Yard
30
30
  severity: severity,
31
31
  type: self.class.offense_type,
32
32
  name: computed_offense_name,
33
+ validator: validator_name,
33
34
  message: build_message(offense_data),
34
35
  location: offense_data[:location] || offense_data[:file],
35
36
  location_line: offense_data[:line] || offense_data[:location_line] || 0
@@ -136,7 +136,8 @@ module Yard
136
136
  end
137
137
 
138
138
  # Check if a comment line is not YARD documentation (magic comment,
139
- # shebang, tool sigil/directive, or a bare `#` separator).
139
+ # shebang, tool sigil/directive, a bare `#` separator, or a line the
140
+ # user has excluded via `IgnoredCommentPatterns`).
140
141
  # @param line [String] stripped comment line
141
142
  # @return [Boolean] true if the line should not count as documentation
142
143
  def non_documentation_comment?(line)
@@ -144,7 +145,70 @@ module Yard
144
145
  line.start_with?('#!') || # shebang
145
146
  line.match?(/\A#\s*(rubocop|standard):/i) || # linter directives
146
147
  line.match?(/\A#\s*typed:/i) || # Sorbet sigil
147
- line.match?(/\A#+\s*\z/) # bare # separator
148
+ line.match?(/\A#+\s*\z/) || # bare # separator
149
+ ignored_comment?(line) # user-configured exclusions
150
+ end
151
+
152
+ # Whether the comment line matches one of the user-configured
153
+ # `IgnoredCommentPatterns`. Such a line is not treated as
154
+ # documentation, so it never counts as a docstring detached from the
155
+ # definition below it. This lets a project silence the blank-line
156
+ # offense for comments that are not docs - commented-out code in a
157
+ # constant block, `# FIXME` notes, section separators - without the
158
+ # linter having to guess what is and is not code (see issue #300).
159
+ # @param line [String] stripped comment line
160
+ # @return [Boolean] true if the line matches an ignored pattern
161
+ def ignored_comment?(line)
162
+ ignored_comment_patterns.any? { |pattern| pattern.match?(line) }
163
+ end
164
+
165
+ # Compiled `IgnoredCommentPatterns` from configuration. Each entry is
166
+ # either a `/regex/` (compiled as a regular expression) or a plain
167
+ # string (matched as a whole word). Blank entries, the empty regex
168
+ # `//`, and invalid regexes are dropped (an empty regex would match
169
+ # every comment, making the option useless - consistent with the
170
+ # other pattern-list options). Memoized for the lifetime of the
171
+ # validator.
172
+ # @return [Array<Regexp>] compiled patterns
173
+ def ignored_comment_patterns
174
+ @ignored_comment_patterns ||=
175
+ Array(config_or_default('IgnoredCommentPatterns'))
176
+ .compact
177
+ .map { |pattern| pattern.to_s.strip }
178
+ .reject(&:empty?)
179
+ .reject { |pattern| pattern == '//' }
180
+ .filter_map { |pattern| compile_comment_pattern(pattern) }
181
+ end
182
+
183
+ # Compile a single `IgnoredCommentPatterns` entry into a Regexp.
184
+ # A `/.../` entry is a regular expression. Anything else is a literal
185
+ # matched at word boundaries, so a fragment like `api` does not match
186
+ # inside `rapid` (which would silently suppress a real docstring); use
187
+ # a `/regex/` entry for substring or case-insensitive matching. An
188
+ # invalid regex is skipped (returns nil) rather than raising; the empty
189
+ # regex `//` is filtered out before it reaches here.
190
+ # @param pattern [String] one configured pattern
191
+ # @return [Regexp, nil]
192
+ def compile_comment_pattern(pattern)
193
+ if (match = pattern.match(%r{\A/(.+)/\z}))
194
+ Regexp.new(match[1])
195
+ else
196
+ Regexp.new(word_bounded_literal(pattern))
197
+ end
198
+ rescue RegexpError
199
+ nil
200
+ end
201
+
202
+ # Escape a literal pattern and anchor it at word boundaries, but only
203
+ # on a side that ends in a word character so punctuation-only patterns
204
+ # (e.g. `...` section separators) still match.
205
+ # @param pattern [String] the literal pattern
206
+ # @return [String] escaped, boundary-anchored regex source
207
+ def word_bounded_literal(pattern)
208
+ escaped = Regexp.escape(pattern)
209
+ escaped = "\\b#{escaped}" if pattern.match?(/\A\w/)
210
+ escaped = "#{escaped}\\b" if pattern.match?(/\w\z/)
211
+ escaped
148
212
  end
149
213
 
150
214
  # Check if the given pattern is enabled in configuration
@@ -9,7 +9,7 @@ module Yard
9
9
  #
10
10
  # Uses YARD's `docstring.line_range` to locate the exact source lines belonging to
11
11
  # each docstring block. This handles block comments, wrapped tag descriptions, and
12
- # macro expansion correctly no arithmetic reconstruction needed.
12
+ # macro expansion correctly - no arithmetic reconstruction needed.
13
13
  class Validator < Validators::Base
14
14
  in_process visibility: :all
15
15
 
@@ -11,7 +11,7 @@ module Yard
11
11
  # above a documentable Ruby construct). YARD's parsed docstring is used to
12
12
  # determine which lines belong to a docstring, avoiding fragile backwards-scanning.
13
13
  #
14
- # Disabled by default enable it and set MaxLength to taste.
14
+ # Disabled by default - enable it and set MaxLength to taste.
15
15
  #
16
16
  # @example Bad - line exceeds MaxLength (120)
17
17
  # # This documentation line is too long and exceeds the configured maximum length.
@@ -29,12 +29,18 @@ module Yard
29
29
  # Also matches define_method which YARD handles via a built-in dynamic handler.
30
30
  # `attr\b` is matched after the `attr_*` variants so the bare `attr :name` form
31
31
  # (handled by YARD's attribute handler) is recognised without swallowing them.
32
+ # Class variable assignments (`@@name = ...`) are matched too: YARD's
33
+ # ClassVariableHandler registers them as documentable objects and attaches the
34
+ # preceding comment (including a bare `@private`/`@api` tag), so such a comment is
35
+ # not orphaned.
32
36
  DEFINITION_PATTERN = /
33
37
  \A\s*(private\s+|protected\s+|public\s+)?
34
38
  (def |class |module |attr_reader|attr_writer|attr_accessor|attr_internal|attr\b|alias_method\b|alias\b|define_method\b)
35
39
  |
36
40
  \A\s*[A-Z][A-Za-z0-9_:]*\s*=
37
41
  |
42
+ \A\s*@@[A-Za-z_]\w*\s*=
43
+ |
38
44
  \A\s*\w+\s+def\b
39
45
  /x.freeze
40
46
 
@@ -22,7 +22,7 @@ module Yard
22
22
  def call(yard_output, **_kwargs)
23
23
  return [] if yard_output.nil? || yard_output.strip.empty?
24
24
 
25
- # Do not strip lines forbidden/replacement may have significant whitespace
25
+ # Do not strip lines - forbidden/replacement may have significant whitespace
26
26
  lines = yard_output.split("\n")
27
27
  violations = []
28
28
 
@@ -8,17 +8,17 @@ module Yard
8
8
  #
9
9
  # Detects forbidden characters or strings in YARD documentation comments
10
10
  # and suggests replacements. The primary use case is detecting AI-generated
11
- # em-dashes (—, U+2014) and en-dashes (–, U+2013) where a plain hyphen (-)
11
+ # em-dashes (-, U+2014) and en-dashes (-, U+2013) where a plain hyphen (-)
12
12
  # is preferred, but any string-to-string substitution rule can be configured.
13
13
  #
14
- # All substitution rules are checked on every line multiple violations can
14
+ # All substitution rules are checked on every line - multiple violations can
15
15
  # be reported for the same line when more than one forbidden string appears.
16
16
  # Fenced code blocks (``` ... ```) are skipped.
17
17
  #
18
- # Disabled by default enable it and configure Substitutions to taste.
18
+ # Disabled by default - enable it and configure Substitutions to taste.
19
19
  #
20
20
  # @example Bad - em-dash used in documentation
21
- # # Connects the start and end of the range.
21
+ # # Connects the start - and end of the range.
22
22
  # def connect(start, finish)
23
23
  # end
24
24
  #
@@ -39,8 +39,8 @@ module Yard
39
39
  # Documentation/TextSubstitution:
40
40
  # Enabled: true
41
41
  # Substitutions:
42
- # "": "-" # em-dash (U+2014)
43
- # "": "-" # en-dash (U+2013)
42
+ # "-": "-" # em-dash (U+2014)
43
+ # "-": "-" # en-dash (U+2013)
44
44
  # "…": "..." # ellipsis (U+2026)
45
45
  #
46
46
  # To disable:
@@ -12,6 +12,7 @@ module Yard
12
12
  'Enabled' => true,
13
13
  'Severity' => 'warning',
14
14
  'ExcludedMethods' => ['initialize/0'],
15
+ 'ExcludedObjects' => [],
15
16
  'AllowedParentClasses' => []
16
17
  }.freeze
17
18
  self.combines_with = ['Documentation/UndocumentedBooleanMethods'].freeze
@@ -24,11 +24,15 @@ module Yard
24
24
  'ExcludedMethods'
25
25
  ) || []
26
26
 
27
- # Ensure excluded_methods is an Array
28
- excluded_methods = Array(excluded_methods)
27
+ excluded_objects = config&.validator_config(
28
+ 'Documentation/UndocumentedObjects',
29
+ 'ExcludedObjects'
30
+ ) || []
29
31
 
30
- # Sanitize patterns: remove nil, empty, whitespace-only, and normalize
31
- excluded_methods = sanitize_patterns(excluded_methods)
32
+ # Ensure patterns are Arrays and sanitize: remove nil, empty,
33
+ # whitespace-only, and normalize
34
+ excluded_methods = sanitize_patterns(Array(excluded_methods))
35
+ excluded_objects = sanitize_patterns(Array(excluded_objects))
32
36
 
33
37
  yard_list_output
34
38
  .split("\n")
@@ -41,6 +45,14 @@ module Yard
41
45
  element = match[3]
42
46
  arity = match[4]&.to_i
43
47
 
48
+ # Skip if the fully-qualified object name is excluded. Unlike
49
+ # ExcludedMethods (which matches only the trailing method name
50
+ # and never applies to classes/modules/constants),
51
+ # ExcludedObjects matches the whole element - so it can
52
+ # exclude constants (e.g. "Foo::KEY_A") and lets a regex be
53
+ # anchored to the full path.
54
+ next if object_excluded?(element, arity, excluded_objects)
55
+
44
56
  # Skip if method is in excluded list
45
57
  next if method_excluded?(element, arity, excluded_methods)
46
58
 
@@ -88,6 +100,38 @@ module Yard
88
100
  end
89
101
  end
90
102
 
103
+ # Checks if an object should be excluded based on ExcludedObjects config.
104
+ # Matches against the fully-qualified object name (the whole element),
105
+ # so it applies to every object type - classes, modules, methods, and
106
+ # constants alike. Supports exact full names, arity notation (for
107
+ # methods), and regex patterns matched against the full path.
108
+ # @param element [String] the fully-qualified object name
109
+ # (e.g. "Foo::Bar#baz", "Foo::Bar", "Foo::KEY_A")
110
+ # @param arity [Integer, nil] number of parameters for a method element
111
+ # (required + optional, excluding splat and block); nil for
112
+ # classes, modules, and constants
113
+ # @param excluded_objects [Array<String>] list of exclusion patterns
114
+ # @return [Boolean] true if the object should be excluded
115
+ def object_excluded?(element, arity, excluded_objects)
116
+ excluded_objects.any? do |pattern|
117
+ case pattern
118
+ when %r{^/(.+)/$}
119
+ # Regex pattern matched against the full path: '/^Foo::KEY_/'
120
+ match_regex_pattern(element, Regexp.last_match(1))
121
+ when %r{/\d+$}
122
+ # Arity pattern on a full path: 'Foo::Bar#baz/1' matches the
123
+ # method Foo::Bar#baz only when it takes exactly one parameter.
124
+ # A full path contains a single '/' (the arity delimiter), so
125
+ # match_arity_pattern splits it correctly. Constants and other
126
+ # objects have a nil arity and never match an arity pattern.
127
+ match_arity_pattern(element, arity, pattern)
128
+ else
129
+ # Exact full-name match: 'Foo::Bar::KEY_A'
130
+ element == pattern
131
+ end
132
+ end
133
+ end
134
+
91
135
  # Sanitize exclusion patterns
92
136
  # @param patterns [Array] raw patterns from config
93
137
  # @return [Array<String>] cleaned and validated patterns
@@ -8,7 +8,35 @@ module Yard
8
8
  #
9
9
  # Checks for missing documentation on classes, modules, and methods.
10
10
  # This validator supports flexible method exclusions through the `ExcludedMethods`
11
- # configuration option.
11
+ # configuration option, and full-object exclusions (including constants)
12
+ # through the `ExcludedObjects` option.
13
+ #
14
+ # ## `ExcludedMethods` vs `ExcludedObjects`
15
+ #
16
+ # `ExcludedMethods` matches only the **unqualified method name** (the part
17
+ # after the final `#`/`.`) and never applies to classes, modules, or
18
+ # constants. This keeps a pattern like `/cache/` from silently suppressing
19
+ # a class such as `Memcached`, but it means constants cannot be excluded and
20
+ # a method cannot be targeted by its full path.
21
+ #
22
+ # `ExcludedObjects` matches the **fully-qualified name** of any object -
23
+ # class, module, method, or constant - and supports exact names, arity
24
+ # notation (for methods), and regex patterns matched against the full path:
25
+ #
26
+ # Documentation/UndocumentedObjects:
27
+ # ExcludedObjects:
28
+ # - 'MyApp::Config::DEFAULTS' # exact fully-qualified constant
29
+ # - '/^MyApp::Keys::KEY_/' # every KEY_* constant under MyApp::Keys
30
+ # - '/^MyApp::Internal#/' # every method on MyApp::Internal
31
+ # - 'MyApp::Api#call/1' # only MyApp::Api#call with exactly 1 param
32
+ #
33
+ # Arity notation follows the same rules as `ExcludedMethods` (see below):
34
+ # the count is `required + optional` parameters, excluding splat (`*`) and
35
+ # block (`&`). Classes, modules, and constants have no arity and never match
36
+ # an arity pattern.
37
+ #
38
+ # Use `ExcludedObjects` when you need to skip constants (issue #299) or to
39
+ # exclude one specific object without affecting same-named objects elsewhere.
12
40
  #
13
41
  # ## Pattern Types
14
42
  #
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagSeparator
8
+ # Configuration for TagSeparator validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :tag_separator
11
+ self.defaults = {
12
+ "Enabled" => false,
13
+ "Severity" => "convention",
14
+ "Exempt" => [],
15
+ "RequireAfterDescription" => false
16
+ }.freeze
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagSeparator
8
+ # Builds messages for missing tag separator offenses.
9
+ class MessagesBuilder
10
+ class << self
11
+ # Build message for missing tag separator.
12
+ #
13
+ # @param offense [Hash] offense data with :method_name and :separators keys
14
+ #
15
+ # @return [String] formatted message
16
+ def call(offense)
17
+ transitions = parse_transitions(offense[:separators])
18
+
19
+ if transitions.size == 1
20
+ from, to = transitions.first
21
+ "The `#{offense[:method_name]}` is missing a blank line between the " \
22
+ "`#{from}` and `#{to}` tags."
23
+ else
24
+ formatted = transitions.map { |from, to| "`#{from}` -> `#{to}`" }.join(", ")
25
+ "The `#{offense[:method_name]}` is missing blank lines between tags: " \
26
+ "#{formatted}."
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Parses transition string into array of [from, to] pairs.
33
+ #
34
+ # @param separators [String] string in format "from->to,from->to"
35
+ #
36
+ # @return [Array<Array<String>>] array of [from, to] pairs
37
+ def parse_transitions(separators)
38
+ separators
39
+ .to_s
40
+ .split(",")
41
+ .map { |transition| transition.split("->") }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagSeparator
8
+ # Parser for extracting tag separator violations from raw validator output.
9
+ #
10
+ # @example Output format (skip-lint)
11
+ # /path/to/file.rb:10: ClassName#method_name
12
+ # param->param,param->return
13
+ class Parser < Parsers::Base
14
+ # Parses raw validator output into structured offense data.
15
+ #
16
+ # @param raw_output [String] raw validator output string
17
+ #
18
+ # @return [Array<Hash>] array of hashes with offense details
19
+ def call(raw_output)
20
+ return [] if raw_output.nil? || raw_output.empty?
21
+
22
+ entries_by_location = {}
23
+
24
+ raw_output.split("\n").each_slice(2).each do |location, separators|
25
+ next if location.nil? || separators.nil?
26
+
27
+ # The location line is already a unique identifier for the
28
+ # documented object (file, line and title); using it directly
29
+ # as the key avoids collisions that a normalizing regexp could
30
+ # accidentally introduce between distinct paths/methods.
31
+ key = location.strip
32
+
33
+ if separators == "valid"
34
+ entries_by_location[key] = "valid"
35
+ else
36
+ entries_by_location[key] ||= [location, separators]
37
+ end
38
+ end
39
+
40
+ entries_by_location.delete_if { |_key, value| value == "valid" }
41
+
42
+ location_parser = Validators::Documentation::UndocumentedMethodArguments::Parser.new
43
+
44
+ # Parse each location together with its own separators so that an
45
+ # unparseable location line drops only its own offense instead of
46
+ # shifting the separators of all offenses that follow it
47
+ entries_by_location.values.filter_map do |location, separators|
48
+ offense = location_parser.call(location).first
49
+ next unless offense
50
+
51
+ offense[:separators] = separators
52
+ offense
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagSeparator
8
+ # Result object for tag separator validation.
9
+ # Transforms parsed separator violations into offense objects.
10
+ class Result < Results::Base
11
+ self.default_severity = "convention"
12
+ self.offense_type = "method"
13
+ self.offense_name = "MissingTagSeparator"
14
+
15
+ # Build human-readable message for tag separator offense.
16
+ #
17
+ # @param offense [Hash] offense data with :method_name and :separators keys
18
+ #
19
+ # @return [String] formatted message
20
+ def build_message(offense)
21
+ MessagesBuilder.call(offense)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagSeparator
8
+ # Validates that a blank line separates every pair of consecutive YARD tags.
9
+ #
10
+ # Unlike {TagGroupSeparator}, which only separates different tag groups,
11
+ # this validator requires a blank line between all consecutive tags,
12
+ # including same-type tags such as sibling @param tags. Tags listed in
13
+ # the Exempt option may immediately follow the previous tag.
14
+ class Validator < Base
15
+ # Enable in-process execution with all visibility
16
+ in_process visibility: :all
17
+
18
+ # Execute query for a single object during in-process execution.
19
+ # Checks if consecutive tags are separated by blank lines.
20
+ #
21
+ # @param object [YARD::CodeObjects::Base] the code object to query
22
+ # @param collector [Executor::ResultCollector] collector for output
23
+ #
24
+ # @return [void]
25
+ def in_process_query(object, collector)
26
+ # is_alias? exists only on method objects; on namespace objects
27
+ # YARD's method_missing raises NameError, so guard by type first
28
+ return if object.type == :method && object.is_alias?
29
+
30
+ docstring = object.docstring.all
31
+ return if docstring.nil? || docstring.empty?
32
+
33
+ transitions = offending_transitions(docstring)
34
+
35
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
36
+
37
+ if transitions.empty?
38
+ collector.puts "valid"
39
+ else
40
+ collector.puts transitions.map { |transition| "#{transition[:from]}->#{transition[:to]}" }.join(",")
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Find all locations where a blank line separator is missing between tags
47
+ #
48
+ # In YARD, a docstring's free-form description can only appear before the
49
+ # first `@tag`. Once the parser hits a tag, everything that follows
50
+ # (until the next tag) is considered part of that tag's content.
51
+ #
52
+ # A description is optional.
53
+ #
54
+ # A segment is a part of the docstring. It is either the free-form
55
+ # description or a YARD tag.
56
+ #
57
+ # The first segment never results in an offense.
58
+ #
59
+ # @param docstring [String] the raw docstring content
60
+ #
61
+ # @return [Array<Hash>] array of hashes with :from and :to tag names
62
+ def offending_transitions(docstring)
63
+ lines = docstring.split("\n")
64
+ transitions = []
65
+
66
+ previous_segment_name = nil # nil means we are processing the first segment
67
+
68
+ had_blank_line = true
69
+
70
+ lines.each do |line|
71
+ if line.strip.empty?
72
+ had_blank_line = true
73
+ next
74
+ end
75
+
76
+ # YARD tags begin at column 0 of the docstring. Indented @-leading
77
+ # lines are tag continuation or @example/code content (e.g. an
78
+ # instance variable like `@result`), not new tags.
79
+ tag_name = line[/\A@(\S+)/, 1]
80
+
81
+ if tag_name
82
+ if offending_transition?(previous_segment_name, had_blank_line, tag_name)
83
+ transitions << {from: previous_segment_name, to: tag_name}
84
+ end
85
+
86
+ previous_segment_name = tag_name
87
+ elsif previous_segment_name.nil?
88
+ previous_segment_name = "description"
89
+ end
90
+
91
+ had_blank_line = false
92
+ end
93
+
94
+ transitions
95
+ end
96
+
97
+ # Whether a missing separator should be reported between the previous
98
+ # segment and the current tag
99
+ #
100
+ # @param previous_segment_name [String, nil] the previous tag name, 'description', or nil
101
+ # @param had_blank_line [Boolean] whether a blank line preceded the current tag
102
+ # @param tag_name [String] the current tag name
103
+ #
104
+ # @return [Boolean] whether to report a missing separator
105
+ def offending_transition?(previous_segment_name, had_blank_line, tag_name)
106
+ # The first segment never results in an offense
107
+ return false if previous_segment_name.nil?
108
+
109
+ # If a blank line occurs between the previous segment and the current
110
+ # tag there is no offense
111
+ return false if had_blank_line
112
+
113
+ # In a description->tag transition, exempt does not apply. This
114
+ # transition is only an offense if the RequireAfterDescription option
115
+ # is set.
116
+ return require_after_description? if previous_segment_name == "description"
117
+
118
+ return false if exempt.include?(tag_name)
119
+
120
+ true
121
+ end
122
+
123
+ # @return [Array<String>] tag names that may follow without a blank line
124
+ def exempt
125
+ @exempt ||= config.validator_config("Tags/TagSeparator", "Exempt") ||
126
+ Config.defaults["Exempt"]
127
+ end
128
+
129
+ # @return [Boolean] whether to require separator after description
130
+ def require_after_description?
131
+ @require_after_description ||= config.validator_config(
132
+ "Tags/TagSeparator",
133
+ "RequireAfterDescription"
134
+ ) || false
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ # TagSeparator validator
8
+ #
9
+ # Enforces a blank line between *every* pair of consecutive YARD tags,
10
+ # including consecutive tags of the same type (e.g. two `@param` tags).
11
+ # This is stricter than {TagGroupSeparator}, which only separates
12
+ # different tag *groups* and therefore can never separate same-type tags
13
+ # such as sibling `@param` tags. This validator is disabled by default.
14
+ #
15
+ # @example Bad - No blank line between consecutive @param tags
16
+ # # @param organization_id [String] the organization ID
17
+ # # @param id [String] the pet ID
18
+ # # @return [Pet] the pet object
19
+ # def call(organization_id, id)
20
+ # end
21
+ #
22
+ # @example Good - Blank line separates every tag
23
+ # # @param organization_id [String] the organization ID
24
+ # #
25
+ # # @param id [String] the pet ID
26
+ # #
27
+ # # @return [Pet] the pet object
28
+ # def call(organization_id, id)
29
+ # end
30
+ #
31
+ # ## Configuration
32
+ #
33
+ # To enable this validator:
34
+ #
35
+ # Tags/TagSeparator:
36
+ # Enabled: true
37
+ #
38
+ # To allow certain tags to immediately follow the previous tag without a
39
+ # blank line, list them under `Exempt`. This is useful for `@option`
40
+ # tags, which document keys of a preceding `@param` hash and read best
41
+ # when clustered directly beneath it:
42
+ #
43
+ # Tags/TagSeparator:
44
+ # Enabled: true
45
+ # Exempt:
46
+ # - option
47
+ #
48
+ # With the configuration above, this is valid:
49
+ #
50
+ # # @param opts [Hash] the options
51
+ # # @option opts [String] :name the name
52
+ # # @option opts [Integer] :age the age
53
+ # #
54
+ # # @return [void]
55
+ #
56
+ # To also require a blank line between the description and the first tag,
57
+ # set `RequireAfterDescription`:
58
+ #
59
+ # Tags/TagSeparator:
60
+ # Enabled: true
61
+ # RequireAfterDescription: true
62
+ module TagSeparator
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -3,6 +3,6 @@
3
3
  module Yard
4
4
  module Lint
5
5
  # @return [String] version of the YARD Lint gem
6
- VERSION = '1.10.2'
6
+ VERSION = '1.10.3'
7
7
  end
8
8
  end
data/lib/yard/lint.rb CHANGED
@@ -25,7 +25,7 @@ module Yard
25
25
  # - :mode [Symbol] one of :ref, :staged, :changed
26
26
  # - :base_ref [String, nil] base ref for :ref mode (auto-detects main/master if nil)
27
27
  # @param source [String, nil] in-memory source content; when given, the file is not
28
- # read from disk `path` must be a single .rb file path (used for config/exclusion
28
+ # read from disk - `path` must be a single .rb file path (used for config/exclusion
29
29
  # resolution and offense location reporting only)
30
30
  # @return [Yard::Lint::Result] result object with offenses
31
31
  def run(path:, config: nil, config_file: nil, progress: nil, diff: nil, source: nil)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yard-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.2
4
+ version: 1.10.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -247,6 +247,12 @@ files:
247
247
  - lib/yard/lint/validators/tags/tag_group_separator/parser.rb
248
248
  - lib/yard/lint/validators/tags/tag_group_separator/result.rb
249
249
  - lib/yard/lint/validators/tags/tag_group_separator/validator.rb
250
+ - lib/yard/lint/validators/tags/tag_separator.rb
251
+ - lib/yard/lint/validators/tags/tag_separator/config.rb
252
+ - lib/yard/lint/validators/tags/tag_separator/messages_builder.rb
253
+ - lib/yard/lint/validators/tags/tag_separator/parser.rb
254
+ - lib/yard/lint/validators/tags/tag_separator/result.rb
255
+ - lib/yard/lint/validators/tags/tag_separator/validator.rb
250
256
  - lib/yard/lint/validators/tags/tag_type_position.rb
251
257
  - lib/yard/lint/validators/tags/tag_type_position/config.rb
252
258
  - lib/yard/lint/validators/tags/tag_type_position/messages_builder.rb
@@ -304,6 +310,8 @@ metadata:
304
310
  homepage_uri: https://github.com/mensfeld/yard-lint
305
311
  source_code_uri: https://github.com/mensfeld/yard-lint
306
312
  changelog_uri: https://github.com/mensfeld/yard-lint/blob/master/CHANGELOG.md
313
+ bug_tracker_uri: https://github.com/mensfeld/yard-lint/issues
314
+ documentation_uri: https://github.com/mensfeld/yard-lint
307
315
  rubygems_mfa_required: 'true'
308
316
  rdoc_options: []
309
317
  require_paths: