yard-lint 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 670e09e8becdbcef0090965d61aeb47e488601119775ed33b5b6a99d50e04145
4
- data.tar.gz: 0c1b99fa7aa6357948895c7e120adae3a59f3a0f0bc6da6fc48c5191a62d78f3
3
+ metadata.gz: 7e34cf3a1c023498c1fe762550cde88ce865a5bac461b2a8d40540ce883f39bc
4
+ data.tar.gz: 60a3553d7e7728c74eab25b31886c8a5acb5018a1abd933d82ab0df3e00737c8
5
5
  SHA512:
6
- metadata.gz: 164679ca8bdee70f43561bfeecaa3bee67705c046f77466833469920142733f1311cf19ac5d5856db08f6372200760eb8bb6fbc29170c84b2f48722fc803bbff
7
- data.tar.gz: b87edd3236cb9aed11e4e61764507678c77fd3a3a1eb887874ac59e9cd255e7e78cc19152a56d8c471762028054268b3d593727ec2f8c7b781ca3060eec9be88
6
+ metadata.gz: ccf09341a5bae5a35815fada582e1eaa3856e2997638f7c61a5b8e80ebdfac49c0c8e263ec469694da719501aa2ef23d4aa81750da1b7e101d614a652a14e7a3
7
+ data.tar.gz: b3b0b92de7f0a63506abf5e90e4baef22479098c694e0c7a7fcde5a7f9f4939ad0f7630c7f068ac9ec6c3fa43176df10c22904dc86b0954a6c1dafa452d98577
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 1.12.0 (2026-09-23)
2
+ - **[Feature]** Added `--explain VALIDATOR` - prints a validator's description, default severity, configuration options, and Bad/Good examples directly in the terminal, so you no longer need the wiki to understand or tune a rule. The explanation is extracted from the validator's own YARD documentation block (e.g. `lib/yard/lint/validators/tags/type_syntax.rb`), parsed with YARD - the linter reads its own YARD docs to explain itself, which keeps those doc blocks accurate. Unknown names get "did you mean" suggestions and the full validator list, mirroring `--only`.
3
+ - **[Bugfix]** `Tags/TagSeparator` and `Tags/TagGroupSeparator` no longer report false positives caused by `module_function`. `module_function` makes YARD register one definition twice: as a public class method and as a private instance method. One of the two is built with `CodeObjects::Base#copy_to`, which assigns it a normalized copy of the authored docstring that preserves content but not layout. The normalized copy drops the blank lines between tags and folds in tags inherited from the enclosing namespace, so a correctly separated docstring was reported as `description -> api, api -> param, param -> return`. Both validators now lint the half whose docstring YARD read from source and skip the normalized copy. A genuinely unseparated `module_function` method is still reported, exactly once.
4
+ - **[Bugfix]** `Tags/TagSeparator` and `Tags/TagGroupSeparator` now report a docstring once however many objects YARD generates from it. An `attr_accessor` registers a reader and a writer sharing one comment block, so a layout offense in that comment was reported once for each of them. `Results::Aggregate` did not collapse the pair because each message names its own method. Both validators now report each docstring location once, using the `duplicate_docstring?` helper the base validator already provides.
5
+
1
6
  ## 1.11.0 (2026-08-11)
2
7
  - [Maintenance] Re-release of `1.10.3` as `1.11.0` due to new features being present.
3
8
 
data/README.md CHANGED
@@ -143,6 +143,14 @@ yard-lint lib/ --only Tags/Order,Documentation/UndocumentedObjects
143
143
 
144
144
  **Learn more:** [Advanced Usage Guide](https://github.com/mensfeld/yard-lint/wiki/Advanced-Usage)
145
145
 
146
+ ### Explain a Validator
147
+
148
+ Print what a validator checks, its default severity, configuration options, and Bad/Good examples directly in the terminal - no need to open the wiki. The explanation is sourced from the validator's own YARD documentation.
149
+
150
+ ```bash
151
+ yard-lint --explain Tags/TypeSyntax
152
+ ```
153
+
146
154
  ### Lint from stdin (LSP / Editor Integration)
147
155
 
148
156
  Pass source bytes directly without reading from disk. The `path` argument is still required - it governs config resolution, exclusion matching, and offense location reporting.
@@ -538,6 +546,7 @@ Diff Mode:
538
546
 
539
547
  Validators:
540
548
  --only VALIDATORS Run only specified validators (comma-separated)
549
+ --explain VALIDATOR Explain what a validator checks and how to configure it
541
550
 
542
551
  Configuration Generation:
543
552
  --init Generate .yard-lint.yml config file
data/bin/yard-lint CHANGED
@@ -64,6 +64,10 @@ begin
64
64
  options[:only] = validators.split(',').map(&:strip)
65
65
  end
66
66
 
67
+ opts.on('--explain VALIDATOR', 'Explain what a validator checks and how to configure it') do |validator|
68
+ options[:explain] = validator
69
+ end
70
+
67
71
  opts.separator ''
68
72
  opts.separator 'Other options:'
69
73
 
@@ -112,6 +116,7 @@ begin
112
116
  puts ' yard-lint lib/ # Lint all files in lib/'
113
117
  puts ' yard-lint --only Tags/TypeSyntax lib/ # Run only one validator on lib/'
114
118
  puts ' yard-lint --only Tags/Order,Tags/TypeSyntax # Run specific validators'
119
+ puts ' yard-lint --explain Tags/TypeSyntax # Explain what a validator checks'
115
120
  puts ' yard-lint --diff main # Lint files changed since main branch'
116
121
  puts ' yard-lint --staged # Lint only staged files'
117
122
  puts ' yard-lint --changed # Lint only uncommitted files'
@@ -133,6 +138,33 @@ rescue OptionParser::ParseError => e
133
138
  exit 1
134
139
  end
135
140
 
141
+ # Report one or more unknown validator names with "did you mean" suggestions,
142
+ # the full list of available validators, and a pointer to --explain. Shared by
143
+ # --only and --explain so the two stay consistent.
144
+ report_unknown_validators = lambda do |names|
145
+ checker = DidYouMean::SpellChecker.new(dictionary: Yard::Lint::ConfigLoader::ALL_VALIDATORS)
146
+ messages = names.map do |name|
147
+ suggestions = checker.correct(name)
148
+ suggestions.any? ? " #{name} (did you mean: #{suggestions.first}?)" : " #{name}"
149
+ end
150
+ puts 'Error: Unknown validator(s):'
151
+ puts messages.join("\n")
152
+ puts "\nAvailable validators:"
153
+ Yard::Lint::ConfigLoader::ALL_VALIDATORS.each { |v| puts " #{v}" }
154
+ puts "\nRun `yard-lint --explain VALIDATOR` to see what a validator checks."
155
+ end
156
+
157
+ # Handle --explain flag (early exit; needs no config or path)
158
+ if options[:explain]
159
+ name = options[:explain]
160
+ unless Yard::Lint::ConfigLoader::ALL_VALIDATORS.include?(name)
161
+ report_unknown_validators.call([name])
162
+ exit 1
163
+ end
164
+ puts Yard::Lint::Explainer.call(name)
165
+ exit 0
166
+ end
167
+
136
168
  # Disambiguate `--diff PATH`. `--diff [REF]` takes an optional argument, so
137
169
  # `yard-lint --diff lib/` makes OptionParser consume `lib/` as the REF, leaving
138
170
  # no PATH (the run then tries `git diff lib/...HEAD` and fails). When the --diff
@@ -260,15 +292,7 @@ config.min_coverage = options[:min_coverage] if options[:min_coverage]
260
292
  if options[:only]
261
293
  unknown = options[:only] - Yard::Lint::ConfigLoader::ALL_VALIDATORS
262
294
  if unknown.any?
263
- checker = DidYouMean::SpellChecker.new(dictionary: Yard::Lint::ConfigLoader::ALL_VALIDATORS)
264
- messages = unknown.map do |name|
265
- suggestions = checker.correct(name)
266
- suggestions.any? ? " #{name} (did you mean: #{suggestions.first}?)" : " #{name}"
267
- end
268
- puts "Error: Unknown validator(s):"
269
- puts messages.join("\n")
270
- puts "\nAvailable validators:"
271
- Yard::Lint::ConfigLoader::ALL_VALIDATORS.each { |v| puts " #{v}" }
295
+ report_unknown_validators.call(unknown)
272
296
  exit 1
273
297
  end
274
298
  config.only_validators = options[:only]
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ # Renders a human-readable explanation of a single validator for the terminal.
6
+ #
7
+ # The explanation is sourced from the validator's own YARD documentation block
8
+ # (the module file, e.g. `validators/tags/type_syntax.rb`) - yard-lint reads its
9
+ # own YARD docs to explain itself. This keeps the explanation next to the code it
10
+ # documents rather than in a separate file that can drift, and gives the doc
11
+ # blocks a standing reason to stay accurate.
12
+ #
13
+ # @example
14
+ # puts Yard::Lint::Explainer.call("Tags/TypeSyntax")
15
+ class Explainer
16
+ # Keys in a validator's defaults that are surfaced in the header rather than
17
+ # listed as tunable configuration options.
18
+ META_KEYS = %w[Enabled Severity].freeze
19
+
20
+ # Build the explanation for a validator.
21
+ # @param name [String] validator name (e.g. 'Tags/TypeSyntax')
22
+ # @return [String] formatted, terminal-ready explanation
23
+ def self.call(name)
24
+ new(name).call
25
+ end
26
+
27
+ # @param name [String] validator name (e.g. 'Tags/TypeSyntax')
28
+ def initialize(name)
29
+ @name = name
30
+ end
31
+
32
+ # @return [String] formatted, terminal-ready explanation
33
+ # @raise [ArgumentError] if name is not a known validator
34
+ def call
35
+ unless ConfigLoader::ALL_VALIDATORS.include?(@name)
36
+ raise ArgumentError, "Unknown validator: #{@name.inspect}"
37
+ end
38
+
39
+ [header, body].compact.join("\n")
40
+ end
41
+
42
+ private
43
+
44
+ # @return [String] the metadata header (name, defaults) for the validator
45
+ def header
46
+ lines = [@name.to_s]
47
+ lines << " Enabled by default: #{default_config.validator_enabled?(@name)}"
48
+ lines << " Default severity: #{default_config.validator_severity(@name)}"
49
+
50
+ config_keys = validator_defaults.keys - META_KEYS
51
+ lines << " Configuration keys: #{config_keys.join(', ')}" if config_keys.any?
52
+ lines.join("\n")
53
+ end
54
+
55
+ # @return [String] the documentation body (description, config, examples)
56
+ def body
57
+ doc = documentation
58
+ return raw_comment_fallback if doc.nil?
59
+
60
+ sections = ["\n#{doc[:description]}"]
61
+ sections << render_examples(doc[:examples]) if doc[:examples].any?
62
+ sections.join("\n")
63
+ end
64
+
65
+ # @param examples [Array(String, String)] pairs of example label and code
66
+ # @return [String] the rendered "Examples:" section
67
+ def render_examples(examples)
68
+ rendered = examples.map do |name, text|
69
+ code = text.each_line.map { |line| " #{line}" }.join
70
+ " #{name}\n#{code}".rstrip
71
+ end
72
+ "\nExamples:\n#{rendered.join("\n\n")}"
73
+ end
74
+
75
+ # A config carrying only built-in defaults (no user overrides), used as the
76
+ # authoritative source for the validator's default enabled state and
77
+ # severity so the header never drifts from how the linter actually resolves
78
+ # them (see Config#validator_enabled? / #validator_severity).
79
+ # @return [Config] the defaults-only config
80
+ def default_config
81
+ @default_config ||= Config.new
82
+ end
83
+
84
+ # @return [Hash] the validator's default configuration
85
+ def validator_defaults
86
+ config = ConfigLoader.validator_config(@name)
87
+ config&.defaults || {}
88
+ end
89
+
90
+ # Parse the validator's module file with YARD and extract its description
91
+ # and examples. Runs in an isolated registry - any objects a caller parsed
92
+ # before us are saved and restored - so explaining a validator never
93
+ # clobbers the shared YARD::Registry.
94
+ # @return [Hash, nil] { description: String, examples: Array((String, String)) }
95
+ # or nil if no docstring is available
96
+ def documentation
97
+ path = source_path
98
+ return nil unless path && File.exist?(path)
99
+
100
+ saved = YARD::Registry.all
101
+ YARD::Registry.clear
102
+ begin
103
+ YARD.parse_string(File.read(path))
104
+ docstring = YARD::Registry.at(object_path)&.docstring
105
+ return nil if docstring.nil? || docstring.to_s.strip.empty?
106
+
107
+ {
108
+ description: docstring.to_s,
109
+ examples: docstring.tags(:example).map { |tag| [tag.name, tag.text.to_s] }
110
+ }
111
+ ensure
112
+ YARD::Registry.clear
113
+ saved.each { |object| YARD::Registry.register(object) }
114
+ end
115
+ end
116
+
117
+ # @return [String] the fully-qualified code object path (e.g.
118
+ # 'Yard::Lint::Validators::Tags::TypeSyntax')
119
+ def object_path
120
+ category, validator = @name.split('/')
121
+ "Yard::Lint::Validators::#{category}::#{validator}"
122
+ end
123
+
124
+ # Resolve the validator module file from its name by inverting the casing
125
+ # convention used in ConfigLoader.discover_validators.
126
+ # @return [String] absolute path to the validator module file
127
+ def source_path
128
+ category, validator = @name.split('/')
129
+ File.join(__dir__, 'validators', snake_case(category), "#{snake_case(validator)}.rb")
130
+ end
131
+
132
+ # Convert a PascalCase segment to snake_case (inverse of the
133
+ # `split('_').map(&:capitalize).join` used during discovery).
134
+ # @param string [String] PascalCase name (e.g. 'TypeSyntax')
135
+ # @return [String] snake_case name (e.g. 'type_syntax')
136
+ def snake_case(string)
137
+ string.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
138
+ end
139
+
140
+ # Defensive fallback: print the raw leading comment block from the module
141
+ # file when YARD yields no docstring. Every validator currently ships a doc
142
+ # block, so this should not normally be reached.
143
+ # @return [String, nil] the raw comment text, or nil if it cannot be read
144
+ def raw_comment_fallback
145
+ path = source_path
146
+ return nil unless path && File.exist?(path)
147
+
148
+ comment = []
149
+ File.foreach(path) do |line|
150
+ stripped = line.strip
151
+ if stripped.start_with?('#')
152
+ comment << stripped.sub(/\A#\s?/, '')
153
+ elsif stripped.start_with?('module ') && comment.any?
154
+ break
155
+ else
156
+ comment.clear
157
+ end
158
+ end
159
+ comment.any? ? "\n#{comment.join("\n")}" : nil
160
+ end
161
+ end
162
+ end
163
+ end
@@ -110,6 +110,66 @@ module Yard
110
110
  !@scanned_docstrings.add?(key)
111
111
  end
112
112
 
113
+ # Whether the object is a synthesized copy whose docstring has been
114
+ # normalized and should not be linted.
115
+ #
116
+ # `module_function` makes YARD register one definition twice: as a public
117
+ # class method and as a private instance method.
118
+ #
119
+ # One of the two is built with `CodeObjects::Base#copy_to` which assigns a
120
+ # docstring which is a normalized copy of the original, authored docstring
121
+ # that preserves content but not layout.
122
+ #
123
+ # The normalized docstring drops the blank lines between tags, moves param
124
+ # types ahead of param names, and folds in tags inherited from the enclosing
125
+ # namespace (such as an api tag). Linting the normalized docstring reports
126
+ # layout offenses against text no one wrote.
127
+ #
128
+ # The authored docstring is the one YARD tracks back to a range of source
129
+ # lines.
130
+ #
131
+ # For `module_function def name` neither the original object nor its copy
132
+ # carries a line range. For this form, the instance method holds the original
133
+ # docstring (YARD::Handlers::Ruby::ModuleFunctionHandler copies it to the
134
+ # class method).
135
+ #
136
+ # @param object [YARD::CodeObjects::Base] the code object to check
137
+ # @return [Boolean] true if the object's docstring is a normalized copy
138
+ def module_function_copy?(object)
139
+ return false unless object.type == :method
140
+ # Checked before the twin lookup, which scans the namespace's children:
141
+ # an object YARD tracked back to source lines is authored, whatever it
142
+ # is paired with, and most objects are
143
+ return false if object.docstring.line_range
144
+
145
+ twin = module_function_twin(object)
146
+ return false unless twin
147
+ return true if twin.docstring.line_range
148
+
149
+ object.scope == :class
150
+ end
151
+
152
+ # Finds the other object registered for a module_function definition.
153
+ # @param object [YARD::CodeObjects::Base] one half of the pair
154
+ # @return [YARD::CodeObjects::MethodObject, nil] the other half, or nil
155
+ # when the object does not come from module_function
156
+ def module_function_twin(object)
157
+ return nil unless object.respond_to?(:scope)
158
+
159
+ twin = object.namespace.child(
160
+ name: object.name,
161
+ scope: (object.scope == :class) ? :instance : :class
162
+ )
163
+
164
+ return nil unless twin
165
+ return nil unless twin.file == object.file && twin.line == object.line
166
+ # Both halves point at the same definition, but only the class method
167
+ # is flagged as a module function by YARD.
168
+ return nil unless [object, twin].any? { |o| o.respond_to?(:module_function?) && o.module_function? }
169
+
170
+ twin
171
+ end
172
+
113
173
  # Converts a zero-based line offset within a docstring's text into an
114
174
  # absolute line number in the source file, so offenses can point at
115
175
  # the offending documentation line instead of the definition line.
@@ -24,6 +24,14 @@ module Yard
24
24
  # is_alias? exists only on method objects; on namespace objects
25
25
  # YARD's method_missing raises NameError, so guard by type first
26
26
  return if object.type == :method && object.is_alias?
27
+ # The rebuilt half of a module_function pair carries no authored
28
+ # layout, only YARD's normalization of it. Checked before
29
+ # duplicate_docstring? so the normalized half never claims the
30
+ # location its authored twin needs
31
+ return if module_function_copy?(object)
32
+ # Objects sharing one comment block (an attr_accessor's reader and
33
+ # writer) would otherwise report the same offense once each
34
+ return if duplicate_docstring?(object)
27
35
 
28
36
  docstring = object.docstring.all
29
37
  return if docstring.nil? || docstring.empty?
@@ -26,6 +26,14 @@ module Yard
26
26
  # is_alias? exists only on method objects; on namespace objects
27
27
  # YARD's method_missing raises NameError, so guard by type first
28
28
  return if object.type == :method && object.is_alias?
29
+ # The rebuilt half of a module_function pair carries no authored
30
+ # layout, only YARD's normalization of it. Checked before
31
+ # duplicate_docstring? so the normalized half never claims the
32
+ # location its authored twin needs
33
+ return if module_function_copy?(object)
34
+ # Objects sharing one comment block (an attr_accessor's reader and
35
+ # writer) would otherwise report the same offense once each
36
+ return if duplicate_docstring?(object)
29
37
 
30
38
  docstring = object.docstring.all
31
39
  return if docstring.nil? || docstring.empty?
@@ -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.11.0'
6
+ VERSION = '1.12.0'
7
7
  end
8
8
  end
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.11.0
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -62,6 +62,7 @@ files:
62
62
  - lib/yard/lint/executor/query_executor.rb
63
63
  - lib/yard/lint/executor/result_collector.rb
64
64
  - lib/yard/lint/executor/warning_dispatcher.rb
65
+ - lib/yard/lint/explainer.rb
65
66
  - lib/yard/lint/ext/irb_notifier_shim.rb
66
67
  - lib/yard/lint/formatters/progress.rb
67
68
  - lib/yard/lint/git.rb
@@ -327,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
327
328
  - !ruby/object:Gem::Version
328
329
  version: '0'
329
330
  requirements: []
330
- rubygems_version: 4.0.16
331
+ rubygems_version: 4.0.20
331
332
  specification_version: 4
332
333
  summary: YARD documentation linter and validator
333
334
  test_files: []