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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e80922a8d6a086601b78ff5aa7d15a1463a9b772b44c3315d440d35ab8e8e36
4
- data.tar.gz: ece101a1e691a92a8c524a6e1a7b63e6db623cb7c156680f0b85ca3129861821
3
+ metadata.gz: a3e167ca549d10a6391cdcee97de9e53356448146f58d045dbd7aed87466460c
4
+ data.tar.gz: 17197fe60701c4b0f93deefb4b9b933e7deba9eaa9499d201908649b09da1019
5
5
  SHA512:
6
- metadata.gz: cb60b66bee32b4f7abb20d0617e576044c98e2095967d08a4776553c3435f636dcd5ca3788e5ac2b5e3de2de501d9aade4b49e11d3b5173bd5c260a706e95808
7
- data.tar.gz: 01b284d20fc97cb09c362410e491544732264568e971f40194c498824210e6724bed6c72003ce1569fdc90bedfce93bb3e19e8520877158fd8b41f0eb058c2d2
6
+ metadata.gz: f274aa4a55a41ab329ba44a6c76d4d2c0c9a431ea8da8a2ded67ee2208adec3cf7a658d275fc5ef843511ddb23dfdae96575dee565cd7435b93330af614ef4b0
7
+ data.tar.gz: ad921b1b3156088cbaeaf760773bf1d6f947759d7b29555cb2133586d5ccf12f1b41f98968598bf3ed21e6b98755727b835088f05d760d131bc081e9adffea8a
data/CHANGELOG.md CHANGED
@@ -1,4 +1,72 @@
1
- # YARD-Lint Changelog
1
+ ## 1.7.0 (2026-06-15)
2
+ - **[Feature]** `Tags/ExampleSyntax` gained an opt-in `SkipNonRuby` option (default `false`). The validator compiles every `@example` body as Ruby, so an irb/pry session, its `=>` output, or a shell `$` transcript was reported as a syntax error. With `SkipNonRuby: true`, `@example` blocks that are interactive console transcripts are skipped. Off by default so a genuine syntax error in a normal example is not hidden.
3
+ - **[Feature]** `Tags/InvalidTypes` gained an opt-in `StrictConstantNames` option (default `false`). The type check was deliberately lenient - any syntactically valid constant name was accepted - so a misspelled class name like `Strng` was never flagged, defeating the validator's headline use case. With `StrictConstantNames: true`, a CamelCase type that is neither a loaded Ruby constant nor resolvable in the analyzed codebase's YARD registry is reported. It stays off by default (types defined only in un-analyzed dependencies would otherwise be flagged - add those to `ExtraTypes`); the strict template (`--init --strict`) enables it. `Boolean` is now always accepted as a pseudo-type.
4
+ - **[Feature]** `Tags/InformalNotation` gained an opt-in `SkipIndentedCodeBlocks` option (default `false`). The validator skipped fenced (` ``` `) code blocks but not 4-space/tab indented Markdown code blocks, so informal-looking text inside an indented code sample (e.g. `Note:`) was flagged. With `SkipIndentedCodeBlocks: true`, indented code lines are skipped too. Off by default because indented text is also used for list continuations and wrapped prose.
5
+ - **[Feature]** Added `Warnings/SyntaxError` (enabled by default, severity `error`). A Ruby file YARD cannot parse used to be silently skipped - it registered no objects, produced no offense, and a run could exit 0 over code that does not even parse. The parser error is now surfaced as an offense (with file and line), so the run exits non-zero. Disable with `Warnings/SyntaxError: { Enabled: false }`.
6
+ - **[Fix]** `Documentation/UndocumentedMethodArguments` now matches each parameter to a `@param` tag **by name** instead of only comparing counts, so a misnamed tag (e.g. `@param wrong` for `def push(item)`) is now caught - the count-only check accepted it. This is the new default; set `CheckParameterNames: false` to restore the lenient count-only comparison. A separate opt-in `SkipFullyUndocumented` (default `false`) skips methods with no documentation at all, leaving them to `Documentation/UndocumentedObjects` instead of reporting the same method twice.
7
+ - **[Fix]** `yard-lint --diff PATH` (e.g. `--diff lib/`) no longer fails with `fatal: ambiguous argument 'lib/...HEAD'`. Because `--diff [REF]` takes an optional argument, OptionParser consumed the path as the REF, leaving no path. The `--diff` argument is now treated as the path (with the base ref auto-detected) when it is not a resolvable git ref but is an existing path; an explicit ref (`--diff main`), ref-plus-path (`--diff main lib/`), and an unknown ref (which still errors) are unaffected.
8
+ - **[Fix]** `Documentation/TextSubstitution` no longer flags a forbidden string that appears inside an inline code span (`` `…` ``). It already skipped fenced ` ``` ` code blocks but matched against the raw line, so e.g. an em-dash inside `` `start — finish` `` was reported even though it is literal code, not prose to substitute. Matching now ignores inline code spans; a forbidden string elsewhere in prose on the same line is still flagged.
9
+ - **[Fix]** `Documentation/OrphanedDocComment` no longer treats `#`-leading lines inside heredocs or string literals as documentation comments. A line like `# @param x [Integer] …` sitting inside a SQL heredoc or a multi-line string was scanned as a real comment and reported as orphaned. The scanner now identifies comments from the Ruby lexer (`Ripper`) rather than by line prefix, so only genuine full-line comments are considered (with a regex fallback for unparseable sources). Behaviour for real comments is unchanged, pinned by an expanded characterization test suite.
10
+ - **[Fix]** `Documentation/OrphanedDocComment` no longer reports false positives for three more DSL constructs that YARD does document: a wrapped def (`memoize def value`, `module_function def …`), a receiver DSL call (`MyDSL.register :name do … end`), and a call whose comment carries a `@method`/`@attribute` tag naming the created object (e.g. `# @method dynamic_size` above `acts_as_counter do`). Genuinely orphaned tagged comments are still flagged.
11
+ - **[Fix]** `Tags/TagTypePosition` no longer misfires in two cases: a comment detached from a definition by a blank line is no longer scanned (YARD does not attach it), and under `EnforcedStyle: type_first` a valid `@option` (whose grammar is always `name [Type] :key`) is no longer flagged with the nonsensical suggestion to put the type first.
12
+ - **[Fix]** `Documentation/MarkdownSyntax` no longer reports the exponent operator as unclosed bold. A `**` run padded by whitespace on both sides (e.g. `x ** y`) cannot open or close CommonMark emphasis, so it is no longer counted toward the bold-marker balance. The check also now skips fenced code blocks (` ```...``` `), so a double-splat such as `def call(**opts)` inside an `@example` block is not mistaken for markdown. Genuine `**bold**`/`**unclosed` markers are still detected.
13
+ - **[Fix]** `--update` and `--auto-gen-config` now honor the `-c CONFIG` path. `--update -c custom.yml` updated (or errored on) `./.yard-lint.yml` instead of the named file, and `--auto-gen-config -c custom.yml` generated the todo from the custom config but linked `inherit_from` into a separate `./.yard-lint.yml` (leaving the custom config unconnected and shadowing it on later runs). Both now target the `-c` file.
14
+ - **[Fix]** Diff modes (`--diff`/`--staged`/`--changed`) now find changed files whose names contain non-ASCII characters. With git's default `core.quotepath=true`, such paths are emitted C-quoted (e.g. `"caf0351.rb"`), so the `.rb` suffix check failed and the file was silently skipped. git-quoted paths are now unquoted before filtering.
15
+ - **[Fix]** `Documentation/OrphanedDocComment` no longer treats a prose comment that merely begins with a magic-comment word (e.g. `# encoding: UTF-8 is assumed for all inputs`) as a real magic comment. It matched the prefix followed by anything, so such a line split the documentation block and the tagged part looked orphaned even though a definition followed. A magic comment is now required to have a single-token value.
16
+ - **[Fix]** `--changed` (uncommitted files) now includes untracked files. It used `git diff --name-only HEAD`, which lists only tracked changes, so a brand-new not-yet-staged `.rb` file was never linted despite being a working-directory change. Untracked, non-ignored files (`git ls-files --others --exclude-standard`) are now included.
17
+ - **[Fix]** An unnamed `@example` no longer corrupts `Tags/ExampleSyntax` / `Tags/ExampleStyle` output. YARD returns an empty string (not `nil`) for an unnamed example, so the `example.name || "Example N"` fallback never fired; the empty name line was then dropped by the parser, shifting fields - the syntax-error message ended up in the example-name slot and the actual error was lost. The fallback now triggers on an empty name.
18
+ - **[Fix]** Custom tags declared in a project's `.yardopts` (e.g. `--tag custom_tag:"Custom"`) are no longer reported as `Warnings/UnknownTag`. The in-process parser called `YARD.parse` directly and never loaded `.yardopts`, so a project's own tags - which the real `yard` command honors - were flagged as unknown. `.yardopts` tag-defining options (`--tag` and its `--type-tag`/`--name-tag`/etc. variants) are now registered before parsing.
19
+ - **[Fix]** `Tags/InformalNotation` now suggests `@note` (not `@deprecated`) for a `Warning:` notation. A warning is a caveat, not a deprecation; mapping it to `@deprecated` was incorrect. The code default and both shipped templates are updated consistently.
20
+ - **[Fix]** `--auto-gen-config` no longer writes offenses that are already silenced by a per-validator `Exclude` pattern into the generated baseline. `TodoGenerator#run_linting` built validator results directly, bypassing `Runner#filter_result_offenses`, so excluded files were counted and added to `.yard-lint-todo.yml`. The same exclude filtering (and no-location-offense dropping) a normal run applies is now used.
21
+ - **[Fix]** Documentation coverage is no longer reported as 100% when the `yard` stats subprocess fails. `StatsCalculator` returned an empty string on failure, which became the default stats (`coverage: 100.0`), so a `MinCoverage` gate silently passed; a subprocess killed by a signal also raised a `NoMethodError` on `nil.zero?`. Failure now yields unknown coverage (`nil`), and the exit-code check fails safe when a minimum is required but coverage cannot be determined.
22
+ - **[Fix]** Offenses with a `never` severity no longer cause a non-zero exit code under `FailOnSeverity: convention`. The convention branch counted *all* offenses via `offenses.any?`, while `#statistics` (and the error/warning branches) correctly exclude `never`. A validator set to `Severity: never` - meant to report without ever failing the build - now behaves consistently.
23
+ - **[Fix]** `--auto-gen-config` now emits a usable exclude pattern for grouped root-level files. `PathGrouper` produced `./**/*` for files at the project root (`File.dirname` is `.`), but `./**/*` matches nothing under `File.fnmatch` with `FNM_PATHNAME` - so the generated `.yard-lint-todo.yml` failed to exclude exactly the files it grouped, and yard-lint kept reporting them right after generating the baseline. The root group now uses `**/*`.
24
+ - **[Fix]** `Tags/RedundantParamDescription`'s `ParamToVerb` pattern now only fires when the word after "to" is actually a low-value verb (from the `LowValueVerbs` config). It previously flagged any `<param> to <anything>`, so meaningful noun phrases like `@param path [String] path to file` were reported as too generic.
25
+ - **[Fix]** `Tags/NonAsciiType` no longer flags non-ASCII characters inside string or quoted-symbol literal types (e.g. `["naïve", "plain"]`). Those are literal *values*, not Ruby type names, so non-ASCII is legitimate - and the sibling `Tags/TypeSyntax` already exempts them. Real type names with non-ASCII characters are still flagged.
26
+ - **[Fix]** The `Warnings/UnknownParameterName` "did you mean" suggestion engine now reads parameters from the right method and parses modern signatures. It scanned from 15 lines *before* the reported location and returned the first `def` found (so it read an earlier method's parameters), missed receiver methods like `def self.build(...)`, and mangled keyword defaults and defaults containing commas (`def f(mode: :fast, list = [1, 2], name)` parsed as `["mode fast", "list", "2]", "name"]`). It now starts at the reported def line, matches receiver/operator method names, and splits parameters with bracket awareness. The dead `yard list` fallback (which always returned `[]`, shelled out per offense, and created a `.yardoc/` directory) was removed.
27
+ - **[Fix]** The "did you mean" Levenshtein fallback in `Warnings/UnknownTag` and `Warnings/UnknownParameterName` no longer offers absurd suggestions for short, very different names (e.g. `@foo` → `@todo`, `@spec` → `@see`). It accepted any candidate within half the longer length; it now requires the edit distance to be strictly less than half, so a name differing by half its characters is not "corrected".
28
+ - **[Fix]** `AllValidators.DiffMode.DefaultBaseRef` is now honored. `--diff` with no explicit REF went straight to main/master auto-detection, ignoring the configured default (and erroring with "Could not detect default branch" on repos without main/master - exactly when the option is needed). The configured `DefaultBaseRef` is now used before falling back to auto-detection; an explicit REF still wins.
29
+ - **[Fix]** `Semantic/AbstractMethods` now honors its `AllowedImplementations` config option, which was defined in the defaults (and asserted by a test) but never read - the validator used a hardcoded heuristic, so customizing the option did nothing. Body lines are now checked against the configured patterns (compiled as regexps); a line matching any of them does not count as a real implementation.
30
+ - **[Fix]** `Tags/CollectionType` now produces a valid long-syntax suggestion for nested short-style hashes. The non-greedy `gsub(/Hash<(.+?)>/)` could not handle nesting, so `Hash<Symbol, Hash<String, Integer>>` was suggested as `Hash{Symbol => Hash<String, Integer}>` (mismatched brackets). Conversion now splits on the top-level comma with balanced-bracket awareness and recurses, yielding `Hash{Symbol => Hash{String => Integer}}`.
31
+ - **[Fix]** An unknown `--format` value is now rejected up front instead of after the entire (potentially long) lint run completes. `yard-lint --format xml lib/` previously analyzed everything and only then printed "Unknown format".
32
+ - **[Fix]** The config error for an unrecognizable validator name no longer points to a nonexistent `yard-lint --list-validators` flag (following that advice produced an invalid-option error). It now links the wiki Validators page.
33
+ - **[Fix]** Category-level configuration is now honored. A department key such as `Documentation:` with `Enabled: false` passed validation but was silently ignored, so the category's validators still ran. `Config#validator_enabled?` now consults a category-level `Enabled` setting (an explicit per-validator setting still takes precedence).
34
+ - **[Fix]** `Tags/ApiTags` no longer requires an `@api` tag on constants. The missing-tag check flagged any public non-root object, so a constant (e.g. `MAX_RETRIES = 5`) was reported as "missing @api tag" - although the validator's documentation promises it checks classes, modules, and methods. The check is now restricted to those three object types.
35
+ - **[Fix]** Diff modes (`--diff`, `--staged`, `--changed`) now find changed files when yard-lint is run from a repository subdirectory. `git diff --name-only` reports paths relative to the repo root, but the filter expanded them against the current working directory - so from a subdirectory every path failed `File.exist?` and the diff modes silently linted nothing (exiting 0). Paths are now expanded against the repository root (`git rev-parse --show-toplevel`).
36
+ - **[Fix]** Diamond-shaped config inheritance no longer raises a false `CircularDependencyError`. The loader tracked every file ever loaded instead of the files on the inheritance path currently being resolved, so two configs both inheriting one shared base (e.g. `inherit_from: [a.yml, b.yml]` where both inherit `common.yml`) aborted although no cycle exists. The tracker now acts as a proper recursion stack; true cycles are still detected.
37
+ - **[Fix]** `Tags/ExampleSyntax` no longer reports bogus syntax errors for example code containing a `# =>` sequence inside a string literal. The cleaner that strips YARD output markers used `line.sub(/\s*#\s*=>.*$/, '')`, truncating at the first `# =>` anywhere on the line - so `msg = "result # => not output"` became an unterminated string and was flagged as a syntax error. Stripping is now comment-aware: a `#` inside a string or character literal is left untouched.
38
+ - **[Fix]** `Tags/CollectionType` no longer flags custom classes whose names merely contain `Hash` or `Array` (e.g. `MyHash<String, Integer>`, `ByteArray<Integer>`). The style-detection regexes used unanchored substring matches (`/Hash<.*>/`, `/Array<.*>/`), so a custom collection type was misdetected and reported with a nonsense suggestion like `MyHash{String => Integer}`. The prefixes are now anchored with a negative lookbehind so only the built-in `Hash` and `Array` types match.
39
+ - **[Fix]** `Tags/Order` no longer crashes the entire run when `EnforcedOrder` is explicitly set to null in the config. The validator read the config value directly with no default fallback, so `EnforcedOrder: ~` replaced the seeded array with `nil` and `nil.dup` raised a `NoMethodError` that aborted the run. It now falls back to the default order.
40
+ - **[Fix]** `Tags/OptionTags` no longer demands `@option` tags for a parameter that merely has an options-like name but is documented as a non-Hash type. The check looked only at the parameter name, so a boolean keyword argument (`def run(options: false)` with `@param options [Boolean]`) or an array parameter (`@param opts [Array<String>]`) was wrongly required to document `@option`. A parameter documented with a concrete non-Hash `@param` type is now treated as not an options hash. Genuine options hashes (documented `[Hash]` or with no type) are still checked.
41
+ - **[Fix]** `Tags/TagGroupSeparator` no longer treats indented `@`-leading lines inside an `@example` body (e.g. an instance variable like `@result = compute`) as YARD tag groups. Real YARD tags begin at column 0 of the docstring, but the scanner stripped each line before checking for a leading `@`, so example/code content created phantom one-tag groups and spurious "missing a blank line between groups" offenses. The check now requires the `@` at column 0 of the unstripped line, matching how `Tags/Order` scans tags.
42
+ - **[Fix]** `Documentation/MarkdownSyntax` no longer reports a spurious "unclosed backtick" for a backtick inside a fenced code block. It counted every backtick in the docstring - including the ``` fences and code content - so a fenced block containing a lone backtick made the total odd. Inline backticks are now counted only outside fenced code blocks.
43
+ - **[Fix]** The in-process registry now restores YARD's global logger level even when parsing raises. The level was raised to fatal-only during parsing and reset as the final statement with no `ensure`, so an exception during `YARD.parse` left the logger silenced for the rest of the run.
44
+ - **[Fix]** `Documentation/UndocumentedOptions` no longer demands `@option` tags for a named parameter that merely matches an options-like name (`option`/`opts`/`options`/`kwargs`) but is documented as a non-Hash type (e.g. `@param option [Symbol]`). It looked only at the parameter name. A param documented with a concrete non-Hash `@param` type is now treated as not an options hash; double-splat (`**opts`) collectors and genuine `Hash` params are still checked.
45
+ - **[Fix]** `--auto-gen-config` no longer destroys the comments and formatting in an existing `.yard-lint.yml` when adding the `inherit_from` line. It round-tripped the whole config through `YAML.load_file(...).to_yaml`, deleting every comment and reformatting the file. It now edits the file textually: prepending an `inherit_from` block when none exists (preserving the rest verbatim), or inserting the todo entry into an existing block list.
46
+ - **[Fix]** `yard-lint --update` no longer silently drops `inherit_from` / `inherit_gem`. The updater rebuilt the config from `AllValidators` plus validator keys only and never copied or wrote the inheritance directives, so updating a config that inherits `.yard-lint-todo.yml` deleted that line and resurrected the entire baseline (every silenced offense reappeared). The directives are now preserved and written at the top of the regenerated config.
47
+ - **[Fix]** The one-line YARD-warning parsers (`Warnings/UnknownTag`, `Warnings/UnknownDirective`, `Warnings/InvalidTagFormat`, `Warnings/InvalidDirectiveFormat`) now extract the line number and message correctly when the source file path contains `line ` or ` in file `. The line regex `/line (\d*)/` matched the first `line ` anywhere (so a path like `.../command line tools/x.rb` yielded line 0), and the greedy message regex `/(.*) in file/` matched up to the last ` in file ` (leaking a path fragment into the message). The line regex now anchors on `near line (\d+)` and the message capture is non-greedy.
48
+ - **[Fix]** `Documentation/UndocumentedMethodArguments` no longer demands `@param` tags for block (`&block`) or splat (`*args`, `**opts`) parameters. The check compared the `@param` tag count against `object.parameters.size`, which counted blocks (documented with `@yield`, never `@param`) and splats - unlike every other arity computation in the gem, which excludes `*` and `&`. A method documenting its block with `@yield` (e.g. `def each(limit, &block)` with `@param limit` + `@yield`) was wrongly flagged as missing argument documentation. The count now excludes splat and block parameters, matching the gem-wide convention.
49
+ - **[Fix]** `Documentation/EmptyCommentLine` no longer attributes a file-header comment (separated from a definition by a blank line) to that definition. The upward scan skipped any number of blank lines before the comment block, so a header such as `# frozen_string_literal: true` + `#` was treated as a class's documentation and its bare `#` reported as an empty trailing line - although YARD only attaches a docstring that sits immediately above the definition. The scan now stops at a blank line above the definition.
50
+ - **[Fix]** `Documentation/BlankLineBeforeDefinition` no longer treats shebangs (`#!...`), tool sigils/directives (Sorbet `# typed:`, `# rubocop:...`, `# standard:...`), or a bare `#` comment as documentation. The upward scan stopped at the first non-magic comment and called it a doc block, so a blank line between such a non-doc comment and a definition produced a spurious "blank line between documentation and definition" offense for an undocumented class (and its suggested fix - removing the blank - would have turned the directive into the docstring). These lines are now skipped like magic comments.
51
+ - **[Fix]** The CLI now reports a clean error instead of a raw Ruby backtrace for an unknown command-line flag and for a missing `-c` config file. `OptionParser::ParseError` escaped the unrescued `.parse!` (so `yard-lint --bogus` dumped `OptionParser::InvalidOption` with a backtrace), and `ConfigFileNotFoundError` escaped the config-load rescue, which only handled `InvalidConfigError` (so `yard-lint -c missing.yml` dumped a backtrace). Both now print a one-line error and exit 1.
52
+ - **[Fix]** `Semantic/AbstractMethods` no longer flags an `@abstract` method whose body is a multi-line `raise NotImplementedError, "message"`. The body heuristic inspected each stripped source line independently, so the continuation line holding the message string looked like a real implementation (unless the message happened to contain the word `raise`). Statement continuations (a line ending in a comma or backslash) are now merged before the check.
53
+ - **[Fix]** `Tags/RedundantParamDescription` no longer treats any word that merely starts with "a", "an", or "the" as an article. The article regex was an unanchored prefix match, so meaningful descriptions like `@param user [User] authenticated user` or `@param id [Integer] auto-generated id` were flagged as "just restates the parameter name" (the bug also infected the `PossessiveParam` and `ArticleParamPhrase` patterns, which reuse the same regex). Articles now match whole words only; genuinely redundant descriptions like `the name` for `name` are still flagged.
54
+ - **[Fix]** `Tags/ApiTags` now validates only the `@api` value itself, not any indented continuation/description lines. YARD tag text includes continuation lines, so `@api private` followed by a description (e.g. `# for internal use only`) produced the value `"private\ninternal use only"`, which failed the allowed-value check (a false "invalid @api value" offense) and - because the emitted value contained a newline - corrupted the parser's two-line pairing, silently dropping later offenses. Only the first whitespace-delimited token is now validated.
55
+ - **[Fix]** `Semantic/AbstractMethods` and `Tags/OptionTags` offenses now include the `validator` field (the full config key, e.g. `"Semantic/AbstractMethods"`). Both validators override `Results::Base#build_offenses` and omitted the `validator: validator_name` merge the base class performs, so the text and quickfix formatters printed an empty validator path for their offenses, and anything keying on `offense[:validator]` mis-handled them.
56
+ - **[Fix]** `Warnings/UnknownTag` now renders a "did you mean" suggestion that resolves to a YARD directive with the `@!` prefix instead of a plain `@`. The suggestion dictionary merges tags and directives, but every suggestion was rendered as `@name` - so a directive suggestion (e.g. for `@parsee`) came out as `@parse`, and following it just produced another unknown-tag offense because the valid form is the directive `@!parse`. Directive-only suggestions are now prefixed with `@!`.
57
+ - **[Fix]** The shipped config templates (`--init`, `--init --strict`) no longer silently narrow validation compared to running with no config file at all. Because validator-config arrays/hashes are replaced (not merged), the stale template lists disabled real checks after materializing a config: `Tags/InvalidTypes`, `Tags/TypeSyntax`, `Tags/CollectionType`, and `Tags/NonAsciiType` lost `yieldparam`/`yieldreturn`/`raise` coverage in `ValidatedTags`, `Tags/InformalNotation` lost the `IMPORTANT`/`Important` patterns, and the strict template lost `Tags/RedundantParamDescription`'s `ArticleParamPhrase` pattern. Both templates are synced with code defaults and a parity test now prevents future drift (intentional deviations are explicitly allowlisted); two in-code fallback literals were also replaced with references to their `Config.defaults`.
58
+ - **[Fix]** `Documentation/UndocumentedObjects`'s `ExcludedMethods` option no longer silently suppresses offenses for classes, modules, or constants. The exclusion derived a method name with `element.split(/[#.]/).last`, which for a namespace element (no `#`/`.` separator) returned the full object path - so a method pattern like `/cache/` matched and hid the undocumented-class offense for e.g. `Memcached` while still reporting its methods. `ExcludedMethods` now only applies to method elements.
59
+ - **[Fix]** Missing `inherit_from` targets are no longer silently ignored. If an inherited config file (most importantly the `.yard-lint-todo.yml` baseline) was renamed, deleted, or typoed, the loader skipped it without any indication, so every baselined offense reappeared with no clue why. A warning naming the missing file (and the path it resolved to) is now printed to stderr; the rest of the config still loads.
60
+ - **[Fix]** Config files using YAML anchors/aliases (the common RuboCop-style shared-section idiom) no longer crash with an unrescued `Psych::AliasesNotEnabled` backtrace on Psych 4+ (Ruby >= 3.1); aliases are now explicitly enabled for all config loads (`.yard-lint.yml`, inherited files, `--update`, `--auto-gen-config`). Malformed YAML now raises the gem's own `InvalidConfigError` with the file path and parser message instead of leaking a raw `Psych::SyntaxError` backtrace.
61
+ - **[Fix]** Docstring-content offenses now point at the offending documentation line instead of the definition line. `Documentation/TextSubstitution` and `Tags/InformalNotation` computed a per-line offset within the docstring but reported every offense at the `def`/`class` line (e.g. an em-dash on source line 6 was reported at line 10); `Documentation/MarkdownSyntax` rendered invalid list markers with a docstring-relative index presented as a source line ("at line 4" for source line 8). All three now resolve the docstring offset against `docstring.line_range`, so editor/quickfix integrations jump to the right line.
62
+ - **[Fix]** `Tags/MissingYield` no longer flags methods that use `yield:` as a symbol hash key or keyword-argument label (e.g. `{ yield: true }` or `call(yield: 1)`). The detection regex guarded `:yield` and `.yield` but not the label form, so such methods were told to document a block they never yield to. A negative lookahead now excludes `yield` immediately followed by a colon; real block yields (including `yield ::Const`, which has a space before the colons) are still detected.
63
+ - **[Fix]** One documentation problem now produces one offense. A docstring on `attr_accessor` belongs to both generated methods (reader and writer), so a single issue was reported twice: YARD warning capture (e.g. `Warnings/UnknownTag`) emitted two identical offenses, and docstring-content validators (`Documentation/LineLength`, `Documentation/MarkdownSyntax`, `Documentation/TextSubstitution`, `Tags/InformalNotation`) scanned the shared docstring once per generated method. Identical (validator, name, location, line, message) tuples are now deduplicated in the aggregate result, and content-scanning validators process each docstring location once per run.
64
+ - **[Fix]** `@option` tag types and descriptions are now actually validated. YARD's `OptionTag` stores the documented option on a nested pair tag (`tag.pair.types` / `tag.pair.text`) - `tag.types` and `tag.text` are `nil` on the tag itself - so four validators silently skipped every `@option` tag despite listing `option` in their checked-tags configuration: `Tags/InvalidTypes` (invalid types in `@option` passed), `Tags/CollectionType` (collection style unenforced), `Tags/ForbiddenTags` (forbidden type patterns unmatched), and `Tags/RedundantParamDescription` (redundant option descriptions unchecked). A shared `tag_data` helper now resolves the data-carrying tag, and the redundancy check uses the option key (e.g. `:mode`) as the documented name.
65
+ - **[Fix]** Tags written inside `@overload` blocks now count as documentation. YARD stores them on the overload's own docstring, invisible to `object.tags`/`docstring.tags`, so methods documented entirely via `@overload` produced false positives: `Documentation/MissingReturn` demanded a `@return` the overload already had, `Documentation/UndocumentedMethodArguments` did not count overload `@param` tags, and `Tags/OptionTags` did not see overload `@option` tags. `Tags/ForbiddenTags` had the inverse problem - forbidden tags hidden inside `@overload` blocks escaped detection. All four validators now traverse overload docstrings via the shared `all_typed_tags` helper.
66
+ - **[Fix]** `Tags/Order` and `Tags/TagGroupSeparator` parsers now pair each offense location directly with its own payload (expected tag order / missing separators). Previously they zipped two parallel arrays by index, so a location line the downstream parser could not handle shifted every following offense onto another object's payload — reporting e.g. a method with a different method's expected tag order — while silently dropping the last offense. The historical trigger (top-level method titles failing the location regex) was fixed separately; this change removes the fragile pairing itself, so any future unparseable line drops only its own offense.
67
+ - **[Fix]** `Tags/Order` and `Tags/TagGroupSeparator` now check class, module, and constant docstrings. Both validators called `object.is_alias?` before checking the object type; `is_alias?` only exists on method objects, and on namespace objects YARD's `method_missing` raises `NameError` (not `NoMethodError`), which the query executor's data-error rescue silently swallowed — so every class, module, and constant was skipped by these validators. The alias check is now guarded by an `object.type == :method` test. Fixing this immediately surfaced (and fixed) two real tag-order offenses in yard-lint's own documentation that the bug had been hiding.
68
+ - **[Fix]** Offenses on top-level (root namespace) methods and on constants are no longer silently discarded. The location regex shared by the `Documentation/UndocumentedMethodArguments` and `Tags/InvalidTypes` parsers required a `#` or `.` separator in the object title, so titles like `#my_method` (top-level method) or `MAX_RETRIES` (constant) never matched and their offenses were dropped after the validator had already detected them. `Tags/Order` was affected too via the shared parser: a dropped location line shifted the parallel expected-order list, so surviving offenses could be reported with another method's expected tag order. The parsers now accept any object title and split namespace/method name on the last separator when one is present.
69
+ - **[Fix]** Boolean validator options explicitly set to `false` in `.yard-lint.yml` are no longer silently ignored. The shared config fallback (`Validators::Base#config_or_default` and `Config#get_validator_config_with_default`) used `value || default`, so a user-configured `false` fell through to the truthy default — e.g. `Tags/InformalNotation: RequireStartOfLine: false` had no effect and mid-line informal notation was never reported. The fallback now only applies when the key is genuinely unset (`nil`).
2
70
 
3
71
  ## 1.6.1 (2026-06-11)
4
72
  - **[Fix]** `Documentation/OrphanedDocComment` no longer reports false positives for documented DSL-style method calls (e.g. `ransacker :foo do … end`, `validates :name`, `scope :active, -> { … }`). YARD's DSL handler turns such a call into a documentable method object when the preceding comment carries an implicit-docstring tag (`@return`, `@overload`, `@method`, `@attribute`, `@scope`, `@visibility`), so the comment is attached rather than dropped. The validator now recognises this case: a tagged comment before a DSL call is only flagged when the call is one YARD's handler ignores (`include`, `extend`, `private :sym`, etc.) or the comment lacks an implicit-docstring tag (in which case YARD really does drop it). Also fixes a pre-existing false positive for the bare `attr :name` attribute form.
data/README.md CHANGED
@@ -19,10 +19,11 @@ Accurate documentation isn't just for human developers anymore. [Research shows]
19
19
 
20
20
  YARD-Lint validates your YARD documentation for:
21
21
 
22
- - **Documentation Completeness** - Undocumented classes, modules, methods, parameters, boolean return values, and missing `@return` tags; orphaned doc comments with YARD tags that YARD silently drops
23
- - **Type Accuracy** - Invalid type definitions, malformed type syntax, non-ASCII characters in types, tuple types, and literal types (symbols, strings, numbers)
22
+ - **Documentation Completeness** - Undocumented classes, modules, methods, parameters (matched to `@param` tags by name, so a misnamed `@param` is caught), boolean return values, and missing `@return` tags; orphaned doc comments with YARD tags that YARD silently drops
23
+ - **Type Accuracy** - Invalid type definitions, malformed type syntax, non-ASCII characters in types, tuple types, literal types (symbols, strings, numbers), and misspelled class names (opt-in via `StrictConstantNames`)
24
24
  - **Tag Validation** - Incorrect tag ordering, meaningless tags, invalid tag positions, unknown tags with suggestions, forbidden tag patterns, undocumented `yield` calls (opt-in)
25
- - **Code Examples** - Syntax validation in `@example` tags, optional style validation with RuboCop/StandardRB
25
+ - **Parse Errors** - Files YARD cannot parse (syntax errors) are reported instead of silently skipped, so a run never passes over code that does not parse
26
+ - **Code Examples** - Syntax validation in `@example` tags (with an opt-in skip for irb/pry/shell console transcripts), optional style validation with RuboCop/StandardRB
26
27
  - **Semantic Correctness** - Abstract methods with implementations, redundant descriptions
27
28
  - **Style & Formatting** - Empty comment lines, blank lines before definitions, informal notation patterns, tag group separators, configurable documentation line length (opt-in)
28
29
  - **Smart Suggestions** - "Did you mean" suggestions for typos in parameter names, tags, and configuration settings
data/bin/yard-lint CHANGED
@@ -10,7 +10,8 @@ options = {}
10
10
  config_file = nil
11
11
  diff_mode = nil
12
12
 
13
- OptionParser.new do |opts|
13
+ begin
14
+ OptionParser.new do |opts|
14
15
  opts.banner = 'Usage: yard-lint [options] [PATH]'
15
16
 
16
17
  opts.on('-c', '--config FILE', 'Path to config file (default: .yard-lint.yml)') do |file|
@@ -125,7 +126,25 @@ OptionParser.new do |opts|
125
126
  puts ' cat lib/foo.rb | yard-lint --stdin lib/foo.rb # Lint source piped from stdin'
126
127
  exit
127
128
  end
128
- end.parse!
129
+ end.parse!
130
+ rescue OptionParser::ParseError => e
131
+ warn "Error: #{e.message}"
132
+ warn 'Run `yard-lint --help` for usage.'
133
+ exit 1
134
+ end
135
+
136
+ # Disambiguate `--diff PATH`. `--diff [REF]` takes an optional argument, so
137
+ # `yard-lint --diff lib/` makes OptionParser consume `lib/` as the REF, leaving
138
+ # no PATH (the run then tries `git diff lib/...HEAD` and fails). When the --diff
139
+ # argument is not a resolvable git ref but is an existing path and no explicit
140
+ # path was given, treat it as the PATH and let the base ref auto-detect.
141
+ if diff_mode && diff_mode[:mode] == :ref && diff_mode[:base_ref] && ARGV.empty?
142
+ candidate = diff_mode[:base_ref]
143
+ if File.exist?(candidate) && !Yard::Lint::Git.branch_exists?(candidate)
144
+ ARGV.unshift(candidate)
145
+ diff_mode[:base_ref] = nil
146
+ end
147
+ end
129
148
 
130
149
  # Handle --init flag
131
150
  if options[:init]
@@ -146,7 +165,7 @@ end
146
165
  # Handle --update flag
147
166
  if options[:update]
148
167
  begin
149
- result = Yard::Lint::ConfigUpdater.update(strict: options[:strict])
168
+ result = Yard::Lint::ConfigUpdater.update(path: config_file, strict: options[:strict])
150
169
 
151
170
  if result[:added].any? || result[:removed].any?
152
171
  puts 'Updated .yard-lint.yml:'
@@ -185,7 +204,8 @@ if options[:auto_gen_config]
185
204
  path: path,
186
205
  config: config,
187
206
  force: options[:force] || false,
188
- exclude_limit: options[:exclude_limit] || 15
207
+ exclude_limit: options[:exclude_limit] || 15,
208
+ config_path: config_file
189
209
  )
190
210
 
191
211
  puts result[:message]
@@ -225,6 +245,9 @@ begin
225
245
  else
226
246
  Yard::Lint::Config.load || Yard::Lint::Config.new
227
247
  end
248
+ rescue Yard::Lint::Errors::ConfigFileNotFoundError => e
249
+ puts "Configuration Error: #{e.message}"
250
+ exit 1
228
251
  rescue Yard::Lint::Errors::InvalidConfigError => e
229
252
  puts "Configuration Error:\n#{e.message}"
230
253
  exit 1
@@ -251,6 +274,14 @@ if options[:only]
251
274
  config.only_validators = options[:only]
252
275
  end
253
276
 
277
+ # Validate the output format before doing any (potentially long) work, rather
278
+ # than running the whole lint and only then rejecting an unknown format.
279
+ valid_formats = %w[text json quickfix]
280
+ if options[:format] && !valid_formats.include?(options[:format])
281
+ puts "Error: Unknown format '#{options[:format]}'. Valid formats: #{valid_formats.join(', ')}"
282
+ exit 1
283
+ end
284
+
254
285
  # Suppress progress for quickfix to avoid contaminating machine-readable output
255
286
  options[:progress] = false if options[:format] == 'quickfix' && options[:progress].nil?
256
287
 
@@ -155,6 +155,20 @@ module Yard
155
155
  # If --only is specified, it takes full control
156
156
  return only_validators.include?(validator_name) if only_validators.any?
157
157
 
158
+ # An explicit per-validator Enabled in the user's config wins.
159
+ raw_validator = @raw_config[validator_name]
160
+ if raw_validator.is_a?(Hash) && raw_validator.key?('Enabled')
161
+ return raw_validator['Enabled'] != false
162
+ end
163
+
164
+ # Otherwise honor a category-level Enabled (e.g. `Documentation:
165
+ # { Enabled: false }`), which previously validated but was ignored.
166
+ category = validator_name.split('/').first
167
+ raw_category = @raw_config[category]
168
+ if raw_category.is_a?(Hash) && raw_category.key?('Enabled')
169
+ return raw_category['Enabled'] != false
170
+ end
171
+
158
172
  validator_config = validators[validator_name] || {}
159
173
  validator_config['Enabled'] != false # Default to true
160
174
  end
@@ -243,10 +257,13 @@ module Yard
243
257
  # @param key [String] configuration key
244
258
  # @return [Object, nil] configuration value or default
245
259
  def get_validator_config_with_default(validator_name, key)
246
- validator_config(validator_name, key) || begin
247
- validator_cfg = ConfigLoader.validator_config(validator_name)
248
- validator_cfg&.defaults&.dig(key)
249
- end
260
+ value = validator_config(validator_name, key)
261
+ # A nil? check (not ||) so that explicitly configured false values
262
+ # are honored instead of falling back to a truthy default
263
+ return value unless value.nil?
264
+
265
+ validator_cfg = ConfigLoader.validator_config(validator_name)
266
+ validator_cfg&.defaults&.dig(key)
250
267
  end
251
268
 
252
269
  # Get AllValidators section
@@ -79,6 +79,19 @@ module Yard
79
79
  def load(path)
80
80
  new(path).load
81
81
  end
82
+
83
+ # Load a YAML config file with alias/anchor support and clean errors.
84
+ # Psych 4+ rejects aliases by default, breaking the common RuboCop-style
85
+ # shared-section idiom; malformed YAML would otherwise escape as a raw
86
+ # Psych::SyntaxError backtrace.
87
+ # @param path [String] path to the YAML file
88
+ # @return [Hash] parsed YAML, or an empty hash for empty files
89
+ # @raise [Errors::InvalidConfigError] when the file contains invalid YAML
90
+ def load_yaml_file(path)
91
+ YAML.load_file(path, aliases: true) || {}
92
+ rescue Psych::SyntaxError => e
93
+ raise Errors::InvalidConfigError, "Invalid YAML in #{path}: #{e.message}"
94
+ end
82
95
  end
83
96
 
84
97
  # All validator names (auto-discovered from codebase structure)
@@ -110,20 +123,27 @@ module Yard
110
123
  # @return [Hash] configuration hash with inheritance resolved
111
124
  # @raise [Yard::Lint::Errors::CircularDependencyError] if circular dependency detected
112
125
  def load_file(path)
113
- # Prevent circular dependencies
126
+ # A cycle exists only when the file is an ancestor on the inheritance
127
+ # path currently being loaded - @loaded_files acts as that stack.
128
+ # A file reachable through several branches (diamond inheritance,
129
+ # e.g. two configs sharing a common base) is legal.
114
130
  if @loaded_files.include?(path)
115
131
  raise Errors::CircularDependencyError, "Circular dependency detected: #{path}"
116
132
  end
117
133
 
118
134
  @loaded_files << path
119
135
 
120
- yaml = YAML.load_file(path) || {}
136
+ begin
137
+ yaml = self.class.load_yaml_file(path)
121
138
 
122
- # Handle inheritance
123
- base_config = load_inherited_configs(yaml, File.dirname(path))
139
+ # Handle inheritance
140
+ base_config = load_inherited_configs(yaml, File.dirname(path))
124
141
 
125
- # Merge current config over inherited config
126
- merge_configs(base_config, yaml)
142
+ # Merge current config over inherited config
143
+ merge_configs(base_config, yaml)
144
+ ensure
145
+ @loaded_files.pop
146
+ end
127
147
  end
128
148
 
129
149
  # Load all inherited configurations
@@ -141,6 +161,11 @@ module Yard
141
161
  if File.exist?(inherited_path)
142
162
  inherited = load_file(inherited_path)
143
163
  config = merge_configs(config, inherited)
164
+ else
165
+ # Stay loud: silently skipping makes a renamed/deleted todo
166
+ # baseline resurrect every baselined offense with no clue why
167
+ $stderr.puts "Warning: inherit_from file '#{file}' not found " \
168
+ "(resolved to #{inherited_path}) - skipping"
144
169
  end
145
170
  end
146
171
  end
@@ -69,7 +69,7 @@ module Yard
69
69
  # Load the existing user config
70
70
  # @return [Hash] parsed YAML config
71
71
  def load_existing_config
72
- YAML.load_file(@path) || {}
72
+ ConfigLoader.load_yaml_file(@path)
73
73
  end
74
74
 
75
75
  # Load the template config
@@ -96,6 +96,14 @@ module Yard
96
96
  # Build merged config
97
97
  merged = {}
98
98
 
99
+ # Preserve inheritance directives (inherit_from / inherit_gem) so that
100
+ # --update does not silently drop them - otherwise a .yard-lint-todo.yml
101
+ # baseline (or an inherited gem config) is lost and every silenced
102
+ # offense reappears.
103
+ %w[inherit_from inherit_gem].each do |key|
104
+ merged[key] = existing[key] if existing.key?(key)
105
+ end
106
+
99
107
  # Copy AllValidators from existing, falling back to template
100
108
  merged['AllValidators'] = existing['AllValidators'] || template['AllValidators']
101
109
 
@@ -158,6 +166,19 @@ module Yard
158
166
  lines << '# See https://github.com/mensfeld/yard-lint for documentation'
159
167
  lines << ''
160
168
 
169
+ # Write inheritance directives first (these must be preserved across updates)
170
+ if config['inherit_from']
171
+ lines << 'inherit_from:'
172
+ Array(config['inherit_from']).each { |file| lines << " - #{file}" }
173
+ lines << ''
174
+ end
175
+
176
+ if config['inherit_gem'].is_a?(Hash)
177
+ lines << 'inherit_gem:'
178
+ config['inherit_gem'].each { |gem_name, gem_file| lines << " #{gem_name}: #{gem_file}" }
179
+ lines << ''
180
+ end
181
+
161
182
  # Write AllValidators section
162
183
  if config['AllValidators']
163
184
  lines << '# Global settings for all validators'
@@ -202,7 +202,8 @@ module Yard
202
202
  if suggestions.any?
203
203
  @errors << " Did you mean: #{suggestions.first}?"
204
204
  else
205
- @errors << ' Run `yard-lint --list-validators` to see all available validators'
205
+ @errors << ' See https://github.com/mensfeld/yard-lint/wiki/Validators ' \
206
+ 'for all available validators'
206
207
  end
207
208
  end
208
209
 
@@ -30,36 +30,44 @@ module Yard
30
30
 
31
31
  YARD::Registry.clear
32
32
 
33
+ # Register custom tags declared in .yardopts so the parser does not
34
+ # warn about them (matching how the real `yard` command behaves).
35
+ register_yardopts_tags
36
+
33
37
  # Suppress YARD's default output by setting log level high
34
38
  # YARD uses its own logging levels, 4 is ERROR/FATAL level
35
39
  original_level = YARD::Logger.instance.level
36
40
  YARD::Logger.instance.level = 4 # Only show fatal errors
37
41
 
38
- if source
39
- virtual_path = files.first
40
- # First pass: register directive/macro definitions from the in-memory source.
41
- # We set parser.file manually so registered objects carry the virtual path.
42
- parse_source_string(source, virtual_path)
43
- # Clear checksums so the second pass is not skipped
44
- YARD::Registry.checksums.clear
45
- # Second pass: full parse with all directives available
46
- @warnings = capture_warnings { parse_source_string(source, virtual_path) }
47
- else
48
- # First pass: parse all files to process directive definitions
49
- YARD.parse(files)
50
-
51
- # Clear checksums to force reparsing without clearing the registry.
52
- # This allows macro definitions from the first pass to be available
53
- # during the second pass, enabling proper directive expansion regardless of parse order.
54
- YARD::Registry.checksums.clear
42
+ begin
43
+ if source
44
+ virtual_path = files.first
45
+ # First pass: register directive/macro definitions from the in-memory source.
46
+ # We set parser.file manually so registered objects carry the virtual path.
47
+ parse_source_string(source, virtual_path)
48
+ # Clear checksums so the second pass is not skipped
49
+ YARD::Registry.checksums.clear
50
+ # Second pass: full parse with all directives available
51
+ @warnings = capture_warnings { parse_source_string(source, virtual_path) }
52
+ else
53
+ # First pass: parse all files to process directive definitions
54
+ YARD.parse(files)
55
+
56
+ # Clear checksums to force reparsing without clearing the registry.
57
+ # This allows macro definitions from the first pass to be available
58
+ # during the second pass, enabling proper directive expansion regardless of parse order.
59
+ YARD::Registry.checksums.clear
60
+
61
+ # Second pass: reparse files now that all directive definitions are available
62
+ @warnings = capture_warnings { YARD.parse(files) }
63
+ end
55
64
 
56
- # Second pass: reparse files now that all directive definitions are available
57
- @warnings = capture_warnings { YARD.parse(files) }
65
+ @parsed = true
66
+ ensure
67
+ # Restore the log level even if parsing raises, so a single bad
68
+ # file does not leave YARD's global logger silenced for the run.
69
+ YARD::Logger.instance.level = original_level
58
70
  end
59
-
60
- @parsed = true
61
-
62
- YARD::Logger.instance.level = original_level
63
71
  end
64
72
  end
65
73
 
@@ -124,6 +132,33 @@ module Yard
124
132
 
125
133
  private
126
134
 
135
+ # Reads .yardopts (if present in the working directory) and registers
136
+ # any custom tags it declares, so YARD does not emit "Unknown tag"
137
+ # warnings for tags the project has defined - the same tags the real
138
+ # `yard` command would honor.
139
+ # @return [void]
140
+ def register_yardopts_tags
141
+ yardopts = File.join(Dir.pwd, '.yardopts')
142
+ return unless File.exist?(yardopts)
143
+
144
+ require 'shellwords'
145
+ args = Shellwords.split(File.read(yardopts))
146
+ index = 0
147
+ while index < args.length
148
+ # Tag-defining options: --tag, --type-tag, --type-name-tag,
149
+ # --name-tag, --title-tag all make the tag name "known".
150
+ if args[index] =~ /\A--(?:type-|type-name-|name-|title-)?tag\z/ && args[index + 1]
151
+ name = args[index + 1].split(':', 2).first
152
+ YARD::Tags::Library.define_tag(name, name.to_sym) if name && !name.empty?
153
+ index += 2
154
+ else
155
+ index += 1
156
+ end
157
+ end
158
+ rescue StandardError => e
159
+ warn "[YARD::Lint] Failed to load .yardopts tags: #{e.message}" if ENV['DEBUG']
160
+ end
161
+
127
162
  # Parse Ruby source from a string and register objects under a virtual path.
128
163
  # YARD::Parser::SourceParser#parse accepts a StringIO but keeps @file as '(stdin)'
129
164
  # unless we set it explicitly before parsing.
@@ -9,6 +9,7 @@ module Yard
9
9
  # Patterns matching the 'general' regexps from each warning validator's parser.
10
10
  # These patterns identify which validator should handle each warning.
11
11
  PATTERNS = {
12
+ 'Warnings/SyntaxError' => /^\[warn\]: Syntax error in /,
12
13
  'Warnings/UnknownTag' => /^\[warn\]: Unknown tag.*@.*near line/,
13
14
  'Warnings/UnknownParameterName' => /^\[warn\]: @param tag has unknown parameter name/,
14
15
  'Warnings/DuplicatedParameterName' => /^\[warn\]: @param tag has duplicate parameter name/,
data/lib/yard/lint/git.rb CHANGED
@@ -71,18 +71,33 @@ module Yard
71
71
  def uncommitted_files(path)
72
72
  ensure_git_repository!
73
73
 
74
- # Get both staged and unstaged changes
74
+ # Get both staged and unstaged changes to tracked files
75
75
  stdout, stderr, status = Open3.capture3('git', 'diff', '--name-only', 'HEAD')
76
76
 
77
77
  unless status.success?
78
78
  raise Error, "Git diff failed: #{stderr.strip}"
79
79
  end
80
80
 
81
- filter_ruby_files(stdout.split("\n"), path)
81
+ files = stdout.split("\n")
82
+
83
+ # Also include untracked (but not git-ignored) files - "all changes in
84
+ # the working directory" should cover newly added, not-yet-staged files.
85
+ others_stdout, _stderr, others_status =
86
+ Open3.capture3('git', 'ls-files', '--others', '--exclude-standard')
87
+ files += others_stdout.split("\n") if others_status.success?
88
+
89
+ filter_ruby_files(files.uniq, path)
82
90
  end
83
91
 
84
92
  private
85
93
 
94
+ # Absolute path to the repository root (git reports paths relative to it)
95
+ # @return [String] repo root, or the current directory if it can't be determined
96
+ def repository_root
97
+ stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--show-toplevel')
98
+ status.success? ? stdout.strip : Dir.pwd
99
+ end
100
+
86
101
  # Ensure we're in a git repository
87
102
  # @raise [Error] if not in a git repository
88
103
  def ensure_git_repository!
@@ -99,14 +114,40 @@ module Yard
99
114
  # @return [Array<String>] absolute paths to Ruby files that exist
100
115
  def filter_ruby_files(files, path)
101
116
  base_path = File.expand_path(path)
117
+ # git reports paths relative to the repository root, which is not
118
+ # necessarily the current working directory, so expand against the
119
+ # repo root - otherwise running from a subdirectory finds nothing.
120
+ root = repository_root
102
121
 
103
122
  files
123
+ .map { |f| unquote_git_path(f) }
104
124
  .select { |f| f.end_with?('.rb') }
105
- .map { |f| File.expand_path(f) }
125
+ .map { |f| File.expand_path(f, root) }
106
126
  .select { |f| File.exist?(f) } # Skip deleted files
107
127
  .select { |f| file_within_path?(f, base_path) }
108
128
  end
109
129
 
130
+ # Unquotes a path as git emits it with the default core.quotepath=true:
131
+ # paths containing non-ASCII (or special) bytes are wrapped in double
132
+ # quotes with C-style escapes (e.g. "caf\303\251.rb"). Without this such
133
+ # files end with `"` rather than `.rb` and are silently dropped.
134
+ # @param path [String] a path from git output
135
+ # @return [String] the unquoted path
136
+ def unquote_git_path(path)
137
+ return path unless path.start_with?('"') && path.end_with?('"')
138
+
139
+ inner = path[1..-2]
140
+ unescaped = inner.gsub(/\\(?:(\d{3})|(.))/) do
141
+ octal = Regexp.last_match(1)
142
+ if octal
143
+ octal.to_i(8).chr
144
+ else
145
+ { 't' => "\t", 'n' => "\n", 'r' => "\r" }.fetch(Regexp.last_match(2), Regexp.last_match(2))
146
+ end
147
+ end
148
+ unescaped.force_encoding(Encoding::UTF_8)
149
+ end
150
+
110
151
  # Check if file is within the specified path
111
152
  # @param file [String] absolute file path
112
153
  # @param base_path [String] absolute base path
@@ -33,7 +33,10 @@ module Yard
33
33
 
34
34
  by_dir.each do |dir, dir_files|
35
35
  if should_group_directory?(dir, dir_files.uniq, limit)
36
- result << "#{dir}/**/*"
36
+ # For root-level files File.dirname is ".", and "./**/*" matches
37
+ # nothing under File.fnmatch (FNM_PATHNAME), so the generated todo
38
+ # file would fail to exclude them. Use a plain recursive glob.
39
+ result << (dir == '.' ? '**/*' : "#{dir}/**/*")
37
40
  else
38
41
  result.concat(dir_files.uniq)
39
42
  end