yard-lint 1.1.0 → 1.2.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: 95017b70fd77952580b3bf10466c79efff2fb1bbd353d1ba49ce84eab7feb5e9
4
- data.tar.gz: 0e5015d8cd2b15b38ccc98d149b3b923997c3952ccc8d799050e51e226d8f14b
3
+ metadata.gz: 72ea0cf00858078029ca28144f64d7722abcf1a646e1365560542a064a37de24
4
+ data.tar.gz: 2d68009b8610f26b2bfaeb053692f36a714e96ff145cc60c71ed5b2c01c2a43c
5
5
  SHA512:
6
- metadata.gz: d4825147326c85f9af2faaa95200f797321e06d12bbfe4063ea5b0acd851496bc541546d0885ccdd3d674242670a573be396bc4ae454361a266cadc240c813e8
7
- data.tar.gz: cb1fa4762efdbecfc0265791a93ffb3ef4a9668e9a6905da29acb966f3bbd650ebd7b21ccb826a5dab2dd972762d4f642e17ecccd986d22354af4837633991cd
6
+ metadata.gz: 48313464b85f0d161d39cf151fbbc681fcb5b8271efcc87a371f72199f692c6a75bf24f478dd169ad53babe987c43f88987cc89d51835085db7180eb0906a2f1
7
+ data.tar.gz: c2fa9aa46f81613ee4aacdee0dd6aba4492d61bb9776daf11c7754b94aec7011a5337ccfbe937b136004d240974b0fa336c406d91bb0982dc322235c02b1fdc8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # YARD-Lint Changelog
2
2
 
3
+ ## 1.2.0 (2025-11-12)
4
+ - **[Fix]** Add Ruby 3.5+ compatibility without requiring IRB gem dependency
5
+ - Ruby 3.5 moved IRB out of default gems, requiring explicit installation
6
+ - YARD's legacy parser depends on `IRB::Notifier` for debug output
7
+ - Created lightweight `IRB::Notifier` shim to satisfy YARD without full IRB gem
8
+ - Shim tries to load real IRB first, only provides fallback if LoadError occurs
9
+ - Does not override or interfere with real IRB gem when present
10
+ - Safe to use in applications that depend on yard-lint and also use IRB
11
+ - Shim automatically loaded in subprocesses via RUBYOPT environment variable
12
+ - Avoids adding IRB and its transitive dependencies to supply chain
13
+ - All 977 tests pass on Ruby 3.5.0-preview1 without IRB gem
14
+ - **[Feature]** Add Documentation Coverage Statistics with minimum threshold enforcement
15
+ - `--min-coverage PERCENT` - Fail if documentation coverage is below threshold (0-100)
16
+ - `--stats` flag now displays coverage metrics (total objects, documented, undocumented, percentage)
17
+ - `MinCoverage` configuration option in `.yard-lint.yml` under `AllValidators` section
18
+ - CLI flag overrides config file setting for flexibility in CI/CD pipelines
19
+ - Coverage calculation uses YARD queries to count documented vs undocumented objects
20
+ - Works seamlessly with diff mode (--diff, --staged, --changed) to calculate coverage for changed files only
21
+ - Exit code 1 when coverage is below minimum threshold, even if no linting offenses found
22
+ - Summary-only output in --quiet mode shows coverage with pass/fail status
23
+ - Comprehensive unit and integration test coverage for all scenarios
24
+ - Performance optimized with auto-cleanup temp directories for large codebases
25
+ - **[Feature]** Add Diff Mode for incremental linting - only analyze files that changed
26
+ - `--diff [REF]` - Lint only files changed since REF (auto-detects main/master if not specified)
27
+ - `--staged` - Lint only staged files (git index)
28
+ - `--changed` - Lint only uncommitted files
29
+ - Enables practical usage in large legacy codebases
30
+ - Perfect for CI/CD pipelines (only check what changed in PR)
31
+ - Ideal for pre-commit hooks (only check staged files)
32
+ - Auto-detects main/master branch with fallback to master
33
+ - Applies global exclusion patterns to git diff results
34
+ - Silently skips deleted files
35
+ - Returns clean result when no files are changed
36
+ - Uses shell-based git commands via Open3 (no new dependencies)
37
+ - Configuration support via `AllValidators.DiffMode` section
38
+ - Mutually exclusive diff flags (--diff, --staged, --changed)
39
+
3
40
  ## 1.1.0 (2025-11-11)
4
41
  - **[Feature]** Add `Tags/ExampleSyntax` validator to validate Ruby syntax in `@example` tags
5
42
  - Uses Ruby 3.2's `RubyVM::InstructionSequence.compile()` to parse example code
data/README.md CHANGED
@@ -79,16 +79,124 @@ With options:
79
79
 
80
80
  ```bash
81
81
  # Use a specific config file
82
- yard-lint --config config/yard-lint.yml lib/
82
+ yard-lint lib/ --config config/yard-lint.yml
83
83
 
84
84
  # Output as JSON
85
- yard-lint --format json lib/ > report.json
85
+ yard-lint lib/ --format json > report.json
86
86
 
87
87
  # Generate config file (use --force to overwrite existing)
88
88
  yard-lint --init
89
89
  yard-lint --init --force
90
90
  ```
91
91
 
92
+ ### Diff Mode (Incremental Linting)
93
+
94
+ Lint only files that changed - perfect for large projects, CI/CD, and pre-commit hooks:
95
+
96
+ ```bash
97
+ # Lint only files changed since main branch (auto-detects main/master)
98
+ yard-lint lib/ --diff
99
+
100
+ # Lint only files changed since specific branch/commit
101
+ yard-lint lib/ --diff develop
102
+ yard-lint lib/ --diff HEAD~3
103
+
104
+ # Lint only staged files (perfect for pre-commit hooks)
105
+ yard-lint lib/ --staged
106
+
107
+ # Lint only uncommitted files
108
+ yard-lint lib/ --changed
109
+ ```
110
+
111
+ **Use Cases:**
112
+
113
+ **Pre-commit Hook:**
114
+ ```bash
115
+ #!/bin/bash
116
+ # .git/hooks/pre-commit
117
+ bundle exec yard-lint lib/ --staged --fail-on-severity error
118
+ ```
119
+
120
+ **GitHub Actions CI/CD:**
121
+ ```yaml
122
+ name: YARD Lint
123
+ on: [pull_request]
124
+ jobs:
125
+ lint:
126
+ runs-on: ubuntu-latest
127
+ steps:
128
+ - uses: actions/checkout@v4
129
+ with:
130
+ fetch-depth: 0 # Need full history for --diff
131
+ - name: Run YARD-Lint on changed files
132
+ run: bundle exec yard-lint lib/ --diff origin/${{ github.base_ref }}
133
+ ```
134
+
135
+ **Legacy Codebase Incremental Adoption:**
136
+ ```bash
137
+ # Only enforce rules on NEW code
138
+ yard-lint lib/ --diff main
139
+ ```
140
+
141
+ ### Documentation Coverage Statistics
142
+
143
+ Monitor and enforce minimum documentation coverage thresholds:
144
+
145
+ ```bash
146
+ # Show coverage statistics with --stats flag
147
+ yard-lint lib/ --stats
148
+
149
+ # Output:
150
+ # Documentation Coverage: 85.5%
151
+ # Total objects: 120
152
+ # Documented: 102
153
+ # Undocumented: 18
154
+
155
+ # Enforce minimum coverage threshold (fails if below)
156
+ yard-lint lib/ --min-coverage 80
157
+
158
+ # Use with diff mode to check coverage only for changed files
159
+ yard-lint lib/ --diff main --min-coverage 90
160
+
161
+ # Quiet mode shows only summary with coverage
162
+ yard-lint lib/ --quiet --min-coverage 80
163
+ ```
164
+
165
+ **Configuration File:**
166
+ ```yaml
167
+ # .yard-lint.yml
168
+ AllValidators:
169
+ # Fail if documentation coverage is below this percentage
170
+ MinCoverage: 80.0
171
+ ```
172
+
173
+ **CI/CD Pipeline Example:**
174
+ ```yaml
175
+ name: Documentation Quality
176
+ on: [pull_request]
177
+ jobs:
178
+ coverage:
179
+ runs-on: ubuntu-latest
180
+ steps:
181
+ - uses: actions/checkout@v4
182
+ with:
183
+ fetch-depth: 0
184
+ - name: Check documentation coverage for new code
185
+ run: |
186
+ bundle exec yard-lint \
187
+ lib/ \
188
+ --diff origin/${{ github.base_ref }} \
189
+ --min-coverage 90 \
190
+ --quiet
191
+ ```
192
+
193
+ **Key Features:**
194
+ - Calculates percentage of documented classes, modules, and methods
195
+ - CLI `--min-coverage` flag overrides config file setting
196
+ - Exit code 1 if coverage is below threshold
197
+ - Works with diff mode to enforce coverage only on changed files
198
+ - Performance optimized with auto-cleanup temp directories for large codebases
199
+
92
200
  ## Configuration
93
201
 
94
202
  YARD-Lint is configured via a `.yard-lint.yml` configuration file (similar to `.rubocop.yml`).
@@ -116,6 +224,13 @@ AllValidators:
116
224
  # Exit code behavior (error, warning, convention, never)
117
225
  FailOnSeverity: warning
118
226
 
227
+ # Diff mode settings
228
+ DiffMode:
229
+ # Default base ref for --diff (auto-detects main/master if not specified)
230
+ DefaultBaseRef: ~
231
+ # Include untracked files in diff mode (not yet implemented)
232
+ IncludeUntracked: false
233
+
119
234
  # Individual validator configuration
120
235
  Documentation/UndocumentedObjects:
121
236
  Description: 'Checks for classes, modules, and methods without documentation.'
@@ -273,7 +388,7 @@ YARD-Lint will automatically search for `.yard-lint.yml` in the current director
273
388
  You can specify a different config file:
274
389
 
275
390
  ```bash
276
- yard-lint --config path/to/config.yml lib/
391
+ yard-lint lib/ --config path/to/config.yml
277
392
  ```
278
393
 
279
394
  #### Configuration Inheritance
data/bin/yard-lint CHANGED
@@ -8,6 +8,7 @@ require 'yard-lint'
8
8
 
9
9
  options = {}
10
10
  config_file = nil
11
+ diff_mode = nil
11
12
 
12
13
  OptionParser.new do |opts|
13
14
  opts.banner = 'Usage: yard-lint [options] PATH'
@@ -28,10 +29,32 @@ OptionParser.new do |opts|
28
29
  options[:stats] = true
29
30
  end
30
31
 
32
+ opts.on('--min-coverage PERCENT', Float, 'Minimum documentation coverage required (0-100)') do |percent|
33
+ options[:min_coverage] = percent
34
+ end
35
+
31
36
  opts.on('--[no-]progress', 'Show progress indicator (default: auto)') do |value|
32
37
  options[:progress] = value
33
38
  end
34
39
 
40
+ opts.separator ''
41
+ opts.separator 'Diff mode options (mutually exclusive):'
42
+
43
+ opts.on('--diff [REF]', 'Lint only files changed since REF (default: main/master auto-detected)') do |ref|
44
+ diff_mode = { mode: :ref, base_ref: ref }
45
+ end
46
+
47
+ opts.on('--staged', 'Lint only staged files (git index)') do
48
+ diff_mode = { mode: :staged }
49
+ end
50
+
51
+ opts.on('--changed', 'Lint only uncommitted files') do
52
+ diff_mode = { mode: :changed }
53
+ end
54
+
55
+ opts.separator ''
56
+ opts.separator 'Other options:'
57
+
35
58
  opts.on('--init', 'Generate .yard-lint.yml config file with defaults') do
36
59
  options[:init] = true
37
60
  end
@@ -47,6 +70,13 @@ OptionParser.new do |opts|
47
70
 
48
71
  opts.on('-h', '--help', 'Show this help') do
49
72
  puts opts
73
+ puts
74
+ puts 'Examples:'
75
+ puts ' yard-lint lib/ # Lint all files in lib/'
76
+ puts ' yard-lint --diff main lib/ # Lint only files changed since main branch'
77
+ puts ' yard-lint --staged lib/ # Lint only staged files'
78
+ puts ' yard-lint --changed lib/ # Lint only uncommitted files'
79
+ puts ' yard-lint --format json lib/ # Output in JSON format'
50
80
  exit
51
81
  end
52
82
  end.parse!
@@ -96,12 +126,28 @@ end
96
126
  Yard::Lint::Validators::Base.reset_command_cache!
97
127
  Yard::Lint::Validators::Base.clear_yard_database!
98
128
 
99
- # Run the linter with config_file (it will be loaded internally)
100
- result = Yard::Lint.run(
101
- path: path,
102
- config_file: config_file,
103
- progress: options[:progress]
104
- )
129
+ # Load config and apply CLI overrides
130
+ config = if config_file
131
+ Yard::Lint::Config.from_file(config_file)
132
+ else
133
+ Yard::Lint::Config.load || Yard::Lint::Config.new
134
+ end
135
+
136
+ # Apply CLI min_coverage override if provided
137
+ config.min_coverage = options[:min_coverage] if options[:min_coverage]
138
+
139
+ # Run the linter
140
+ begin
141
+ result = Yard::Lint.run(
142
+ path: path,
143
+ config: config,
144
+ progress: options[:progress],
145
+ diff: diff_mode
146
+ )
147
+ rescue Yard::Lint::Git::Error => e
148
+ puts "Git error: #{e.message}"
149
+ exit 1
150
+ end
105
151
 
106
152
  # Format and display results
107
153
  case options[:format]
@@ -113,14 +159,40 @@ when 'json'
113
159
  })
114
160
  exit result.exit_code
115
161
  when 'text', nil
162
+ # Calculate coverage stats if requested or configured
163
+ coverage = result.documentation_coverage if options[:stats] || options[:min_coverage] || config.min_coverage
164
+
165
+ # Show coverage stats if available
166
+ if coverage && (options[:stats] || options[:quiet])
167
+ puts "\nDocumentation Coverage: #{coverage[:coverage].round(2)}%"
168
+ puts " Total objects: #{coverage[:total]}"
169
+ puts " Documented: #{coverage[:documented]}"
170
+ puts " Undocumented: #{coverage[:total] - coverage[:documented]}"
171
+
172
+ if config.min_coverage
173
+ if coverage[:coverage] >= config.min_coverage
174
+ puts " Status: ✓ Meets minimum (#{config.min_coverage}%)"
175
+ else
176
+ puts " Status: ✗ Below minimum (#{config.min_coverage}%)"
177
+ end
178
+ end
179
+ puts
180
+ end
181
+
116
182
  if result.clean?
117
- puts 'No offenses found'
183
+ # Still check coverage requirement even if no offenses
184
+ if coverage && config.min_coverage && coverage[:coverage] < config.min_coverage
185
+ puts "Error: Documentation coverage #{coverage[:coverage].round(2)}% is below minimum #{config.min_coverage}%"
186
+ exit result.exit_code
187
+ end
188
+
189
+ puts 'No offenses found' unless options[:quiet]
118
190
  exit 0
119
191
  else
120
192
  # Show statistics if requested or in quiet mode
121
193
  if options[:stats] || options[:quiet]
122
194
  stats = result.statistics
123
- puts "\n#{result.count} offense(s) detected"
195
+ puts "#{result.count} offense(s) detected"
124
196
  puts " Errors: #{stats[:error]}"
125
197
  puts " Warnings: #{stats[:warning]}"
126
198
  puts " Conventions: #{stats[:convention]}"
@@ -58,7 +58,10 @@ module Yard
58
58
  # @param command_string [String] the command to execute
59
59
  # @return [Hash] hash with stdout, stderr, exit_code keys
60
60
  def execute_command(command_string)
61
- stdout, stderr, status = Open3.capture3(command_string)
61
+ # Set up environment to load IRB shim before YARD (Ruby 3.5+ compatibility)
62
+ env = build_environment_with_shim
63
+
64
+ stdout, stderr, status = Open3.capture3(env, command_string)
62
65
  {
63
66
  stdout: stdout,
64
67
  stderr: stderr,
@@ -66,6 +69,19 @@ module Yard
66
69
  }
67
70
  end
68
71
 
72
+ # Build environment hash with RUBYOPT to load IRB shim
73
+ # This ensures the shim is loaded in subprocesses (like yard list commands)
74
+ # @return [Hash] environment variables for command execution
75
+ def build_environment_with_shim
76
+ shim_path = File.expand_path('ext/irb_notifier_shim.rb', __dir__)
77
+ rubyopt = "-r#{shim_path}"
78
+
79
+ # Preserve existing RUBYOPT if present
80
+ rubyopt = "#{ENV['RUBYOPT'].strip} #{rubyopt}" if ENV['RUBYOPT']
81
+
82
+ { 'RUBYOPT' => rubyopt }
83
+ end
84
+
69
85
  # Deep clone a hash to prevent modifications to cached data
70
86
  # @param hash [Hash] the hash to clone
71
87
  # @return [Hash] deep cloned hash
@@ -129,6 +129,19 @@ module Yard
129
129
  all_validators['FailOnSeverity'] || 'warning'
130
130
  end
131
131
 
132
+ # Diff mode default base ref (main or master)
133
+ # @return [String, nil] default base ref for diff mode
134
+ def diff_mode_default_base_ref
135
+ diff_config = all_validators['DiffMode'] || {}
136
+ diff_config['DefaultBaseRef']
137
+ end
138
+
139
+ # Minimum documentation coverage percentage required
140
+ # @return [Float, nil] minimum coverage percentage (0-100) or nil if not set
141
+ def min_coverage
142
+ all_validators['MinCoverage']
143
+ end
144
+
132
145
  # Check if a validator is enabled
133
146
  # @param validator_name [String] full validator name (e.g., 'Tags/Order')
134
147
  # @return [Boolean] true if validator is enabled
@@ -192,6 +205,13 @@ module Yard
192
205
  @raw_config['AllValidators']['FailOnSeverity'] = value
193
206
  end
194
207
 
208
+ # Set minimum coverage percentage
209
+ # @param value [Float] minimum coverage percentage (0-100)
210
+ def min_coverage=(value)
211
+ @raw_config['AllValidators'] ||= {}
212
+ @raw_config['AllValidators']['MinCoverage'] = value
213
+ end
214
+
195
215
  # Allow hash-like access for convenience
196
216
  # @param key [Symbol, String] attribute name to access
197
217
  # @return [Object, nil] attribute value or nil if not found
@@ -27,6 +27,15 @@ module Yard
27
27
  # Exit code behavior (error, warning, convention, never)
28
28
  FailOnSeverity: warning
29
29
 
30
+ # Minimum documentation coverage percentage (0-100)
31
+ # Fails if coverage is below this threshold
32
+ # MinCoverage: 80.0
33
+
34
+ # Diff mode settings
35
+ DiffMode:
36
+ # Default base ref for --diff (auto-detects main/master if not specified)
37
+ DefaultBaseRef: ~
38
+
30
39
  # Documentation validators
31
40
  Documentation/UndocumentedObjects:
32
41
  Description: 'Checks for classes, modules, and methods without documentation.'
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shim for IRB::Notifier to avoid IRB dependency in Ruby 3.5+
4
+ #
5
+ # YARD's legacy parser vendors old IRB code that depends on IRB::Notifier.
6
+ # In Ruby 3.5+, IRB is no longer part of the default gems and must be explicitly installed.
7
+ # This shim provides just enough functionality to keep YARD's legacy parser working
8
+ # without requiring the full IRB gem as a dependency.
9
+ #
10
+ # The notifier is only used for debug output in YARD's legacy parser, which we don't need.
11
+ #
12
+ # IMPORTANT: This shim only loads if IRB::Notifier is not already defined.
13
+ # If IRB gem is present, we use the real implementation instead.
14
+
15
+ # Only load the shim if IRB::Notifier is not already defined
16
+ unless defined?(IRB::Notifier)
17
+ # Try to load the real IRB notifier first
18
+ # If it fails (IRB not installed), we'll provide our shim
19
+ begin
20
+ # Suppress warnings during require attempt (Ruby 3.5+ warns about missing default gems)
21
+ original_verbose = $VERBOSE
22
+ $VERBOSE = nil
23
+ require 'irb/notifier'
24
+ rescue LoadError
25
+ # IRB not available, use our shim
26
+ # Mark as loaded to prevent further require attempts
27
+ $LOADED_FEATURES << 'irb/notifier.rb'
28
+
29
+ module IRB
30
+ # Minimal Notifier implementation that does nothing
31
+ # YARD's legacy parser uses this for debug output which we can safely ignore
32
+ class Notifier
33
+ # No-op message level constant
34
+ D_NOMSG = 0
35
+
36
+ class << self
37
+ # Returns a no-op notifier
38
+ # @param _prefix [String] notification prefix (ignored)
39
+ # @return [NoOpNotifier] a notifier that does nothing
40
+ def def_notifier(_prefix)
41
+ NoOpNotifier.new
42
+ end
43
+ end
44
+
45
+ # A notifier that silently discards all output
46
+ class NoOpNotifier
47
+ attr_accessor :level
48
+
49
+ def initialize
50
+ @level = Notifier::D_NOMSG
51
+ end
52
+
53
+ # Returns a no-op notifier for any sub-level
54
+ # @param _level [Integer] notification level (ignored)
55
+ # @param _prefix [String] notification prefix (ignored)
56
+ # @return [NoOpNotifier] a notifier that does nothing
57
+ def def_notifier(_level, _prefix)
58
+ NoOpNotifier.new
59
+ end
60
+
61
+ # Silently ignore pretty-print calls
62
+ # @param _obj [Object] object to pretty-print (ignored)
63
+ # @return [nil]
64
+ def pp(_obj)
65
+ nil
66
+ end
67
+
68
+ # Silently ignore print calls
69
+ # @param _args [Array] print arguments (ignored)
70
+ # @return [nil]
71
+ def print(*_args)
72
+ nil
73
+ end
74
+
75
+ # Silently ignore puts calls
76
+ # @param _args [Array] puts arguments (ignored)
77
+ # @return [nil]
78
+ def puts(*_args)
79
+ nil
80
+ end
81
+
82
+ # Silently ignore printf calls
83
+ # @param _args [Array] printf arguments (ignored)
84
+ # @return [nil]
85
+ def printf(*_args)
86
+ nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ ensure
92
+ # Restore original verbosity setting
93
+ $VERBOSE = original_verbose
94
+ end
95
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ # Git integration for diff mode functionality
6
+ module Git
7
+ # Custom error class for Git-related errors
8
+ class Error < StandardError; end
9
+
10
+ class << self
11
+ # Detect the default branch (main or master)
12
+ # @return [String] 'main', 'master', or nil if neither exists
13
+ def default_branch
14
+ # Try main first (modern default)
15
+ return 'main' if branch_exists?('main')
16
+ # Fall back to master (legacy default)
17
+ return 'master' if branch_exists?('master')
18
+
19
+ nil
20
+ end
21
+
22
+ # Check if a git ref exists
23
+ # @param ref [String] the git ref to check
24
+ # @return [Boolean] true if ref exists
25
+ def branch_exists?(ref)
26
+ _stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--verify', '--quiet', ref)
27
+ status.success?
28
+ end
29
+
30
+ # Get files changed since a base ref
31
+ # @param base_ref [String, nil] the base ref to compare against (nil for auto-detect)
32
+ # @param path [String] the path to filter files within
33
+ # @return [Array<String>] absolute paths to changed Ruby files
34
+ def changed_files(base_ref, path)
35
+ base_ref ||= default_branch
36
+ raise Error, 'Could not detect default branch (main/master)' unless base_ref
37
+
38
+ ensure_git_repository!
39
+
40
+ # Use three-dot diff to compare against merge base
41
+ stdout, stderr, status = Open3.capture3('git', 'diff', '--name-only', "#{base_ref}...HEAD")
42
+
43
+ unless status.success?
44
+ raise Error, "Git diff failed: #{stderr.strip}"
45
+ end
46
+
47
+ filter_ruby_files(stdout.split("\n"), path)
48
+ end
49
+
50
+ # Get staged files (files in git index)
51
+ # @param path [String] the path to filter files within
52
+ # @return [Array<String>] absolute paths to staged Ruby files
53
+ def staged_files(path)
54
+ ensure_git_repository!
55
+
56
+ # ACM filter: Added, Copied, Modified (exclude Deleted)
57
+ stdout, stderr, status = Open3.capture3(
58
+ 'git', 'diff', '--cached', '--name-only', '--diff-filter=ACM'
59
+ )
60
+
61
+ unless status.success?
62
+ raise Error, "Git diff failed: #{stderr.strip}"
63
+ end
64
+
65
+ filter_ruby_files(stdout.split("\n"), path)
66
+ end
67
+
68
+ # Get uncommitted files (all changes in working directory)
69
+ # @param path [String] the path to filter files within
70
+ # @return [Array<String>] absolute paths to uncommitted Ruby files
71
+ def uncommitted_files(path)
72
+ ensure_git_repository!
73
+
74
+ # Get both staged and unstaged changes
75
+ stdout, stderr, status = Open3.capture3('git', 'diff', '--name-only', 'HEAD')
76
+
77
+ unless status.success?
78
+ raise Error, "Git diff failed: #{stderr.strip}"
79
+ end
80
+
81
+ filter_ruby_files(stdout.split("\n"), path)
82
+ end
83
+
84
+ private
85
+
86
+ # Ensure we're in a git repository
87
+ # @raise [Error] if not in a git repository
88
+ def ensure_git_repository!
89
+ _stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--git-dir')
90
+
91
+ return if status.success?
92
+
93
+ raise Error, 'Not a git repository'
94
+ end
95
+
96
+ # Filter for Ruby files within path and convert to absolute paths
97
+ # @param files [Array<String>] relative file paths from git
98
+ # @param path [String] the base path to filter within
99
+ # @return [Array<String>] absolute paths to Ruby files that exist
100
+ def filter_ruby_files(files, path)
101
+ base_path = File.expand_path(path)
102
+
103
+ files
104
+ .select { |f| f.end_with?('.rb') }
105
+ .map { |f| File.expand_path(f) }
106
+ .select { |f| File.exist?(f) } # Skip deleted files
107
+ .select { |f| file_within_path?(f, base_path) }
108
+ end
109
+
110
+ # Check if file is within the specified path
111
+ # @param file [String] absolute file path
112
+ # @param base_path [String] absolute base path
113
+ # @return [Boolean] true if file is within base_path
114
+ def file_within_path?(file, base_path)
115
+ # Handle both directory and file base_path
116
+ if File.directory?(base_path)
117
+ file.start_with?(base_path + '/')
118
+ else
119
+ file == base_path
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -13,14 +13,16 @@ module Yard
13
13
  # Convention severity level constant
14
14
  SEVERITY_CONVENTION = 'convention'
15
15
 
16
- attr_reader :config
16
+ attr_reader :config, :files
17
17
 
18
18
  # Initialize aggregate result with array of validator results
19
19
  # @param results [Array<Results::Base>] array of validator result objects
20
20
  # @param config [Config, nil] configuration object
21
- def initialize(results, config = nil)
21
+ # @param files [Array<String>, nil] array of files that were analyzed
22
+ def initialize(results, config = nil, files = nil)
22
23
  @results = Array(results)
23
24
  @config = config
25
+ @files = Array(files)
24
26
  end
25
27
 
26
28
  # Get all offenses from all validators
@@ -60,10 +62,28 @@ module Yard
60
62
  stats
61
63
  end
62
64
 
65
+ # Calculate documentation coverage statistics
66
+ # @return [Hash] coverage statistics with :total, :documented, :coverage keys
67
+ def documentation_coverage
68
+ return @documentation_coverage if defined?(@documentation_coverage)
69
+
70
+ return nil unless @config && !@files.empty?
71
+
72
+ calculator = StatsCalculator.new(@config, @files)
73
+ @documentation_coverage = calculator.calculate
74
+ end
75
+
63
76
  # Determine exit code based on configured fail_on_severity
64
77
  # Uses the config object stored during initialization
65
78
  # @return [Integer] 0 for success, 1 for failure
66
79
  def exit_code
80
+ # Check minimum coverage requirement first
81
+ if @config&.min_coverage &&
82
+ documentation_coverage &&
83
+ documentation_coverage[:coverage] < @config.min_coverage
84
+ return 1
85
+ end
86
+
67
87
  return 0 if offenses.empty?
68
88
  return 0 unless @config # No config means don't fail
69
89
 
@@ -21,7 +21,7 @@ module Yard
21
21
  def run
22
22
  raw_results = run_validators
23
23
  parsed_results = parse_results(raw_results)
24
- build_result(parsed_results)
24
+ build_result(parsed_results, @selection)
25
25
  end
26
26
 
27
27
  private
@@ -116,9 +116,10 @@ module Yard
116
116
 
117
117
  # Build final result object
118
118
  # @param results [Array<Results::Base>] array of validator result objects
119
+ # @param files [Array<String>] array of files that were analyzed
119
120
  # @return [Results::Aggregate] aggregate result object
120
- def build_result(results)
121
- Results::Aggregate.new(results, config)
121
+ def build_result(results, files)
122
+ Results::Aggregate.new(results, config, files)
122
123
  end
123
124
  end
124
125
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ # Calculates documentation coverage statistics
6
+ # Runs YARD queries to count documented vs undocumented objects
7
+ class StatsCalculator
8
+ attr_reader :config, :files
9
+
10
+ # @param config [Yard::Lint::Config] configuration object
11
+ # @param files [Array<String>] files to analyze
12
+ def initialize(config, files)
13
+ @config = config
14
+ @files = Array(files).compact
15
+ end
16
+
17
+ # Calculate documentation coverage statistics
18
+ # @return [Hash] statistics with :total, :documented, :coverage keys
19
+ def calculate
20
+ return default_stats if files.empty?
21
+
22
+ raw_stats = run_yard_stats_query
23
+ return default_stats if raw_stats.empty?
24
+
25
+ parsed_stats = parse_stats_output(raw_stats)
26
+ filtered_stats = apply_exclusions(parsed_stats)
27
+
28
+ calculate_coverage_percentage(filtered_stats)
29
+ end
30
+
31
+ private
32
+
33
+ # Default stats for empty file lists
34
+ # @return [Hash]
35
+ def default_stats
36
+ { total: 0, documented: 0, coverage: 100.0 }
37
+ end
38
+
39
+ # Run YARD query to get object documentation status
40
+ # @return [String] YARD query output
41
+ def run_yard_stats_query
42
+ # Create temp file with file list
43
+ Tempfile.create(['yard_stats', '.txt']) do |f|
44
+ files.each { |file| f.puts(Shellwords.escape(file)) }
45
+ f.flush
46
+
47
+ query = build_stats_query
48
+
49
+ # Use temp directory for YARD database (auto-cleanup)
50
+ Dir.mktmpdir("yard_stats_#{Process.pid}_") do |temp_dir|
51
+ cmd = build_yard_command(f.path, query, temp_dir)
52
+
53
+ stdout, _stderr, status = Open3.capture3(cmd)
54
+
55
+ # Return empty string if YARD command fails
56
+ return '' unless status.exitstatus.zero?
57
+
58
+ stdout
59
+ end
60
+ end
61
+ end
62
+
63
+ # Build the YARD query for stats collection
64
+ # @return [String] YARD query string
65
+ def build_stats_query
66
+ <<~QUERY.chomp
67
+ type = object.type.to_s; state = object.docstring.all.empty? ? "undoc" : "doc"; puts "\#{type}:\#{state}"
68
+ QUERY
69
+ end
70
+
71
+ # Build complete YARD command
72
+ # @param file_list_path [String] path to file with list of files
73
+ # @param query [String] YARD query to execute
74
+ # @param temp_dir [String] temporary directory for YARD database
75
+ # @return [String] complete command string
76
+ def build_yard_command(file_list_path, query, temp_dir)
77
+ <<~CMD.tr("\n", ' ').strip
78
+ cat #{Shellwords.escape(file_list_path)} | xargs yard list
79
+ --charset utf-8
80
+ --markup markdown
81
+ --no-progress
82
+ --query #{Shellwords.escape(query)}
83
+ -q
84
+ -b #{Shellwords.escape(temp_dir)}
85
+ CMD
86
+ end
87
+
88
+ # Parse YARD stats output
89
+ # Format: "type:state" (e.g., "method:doc", "class:undoc")
90
+ # @param output [String] YARD command output
91
+ # @return [Hash] counts by type and state
92
+ def parse_stats_output(output)
93
+ stats = Hash.new { |h, k| h[k] = { documented: 0, undocumented: 0 } }
94
+
95
+ output.each_line do |line|
96
+ line.strip!
97
+ next if line.empty?
98
+
99
+ type, state = line.split(':', 2)
100
+ next unless type && state
101
+
102
+ if state == 'doc'
103
+ stats[type][:documented] += 1
104
+ elsif state == 'undoc'
105
+ stats[type][:undocumented] += 1
106
+ end
107
+ end
108
+
109
+ stats
110
+ end
111
+
112
+ # Apply validator exclusions to stats
113
+ # Respects ExcludedMethods and other validator-specific exclusions
114
+ # @param stats [Hash] parsed stats
115
+ # @return [Hash] filtered stats
116
+ def apply_exclusions(stats)
117
+ # Get excluded methods from UndocumentedObjects validator config
118
+ excluded_methods = config.validator_config('Documentation/UndocumentedObjects', 'ExcludedMethods') || []
119
+
120
+ return stats if excluded_methods.empty?
121
+
122
+ # For now, we can't easily filter out specific methods without re-parsing
123
+ # This would require running YARD query with method names
124
+ # TODO: Implement precise method-level filtering if needed
125
+
126
+ stats
127
+ end
128
+
129
+ # Calculate coverage percentage from stats
130
+ # @param stats [Hash] filtered stats by type
131
+ # @return [Hash] final coverage statistics
132
+ def calculate_coverage_percentage(stats)
133
+ total_documented = 0
134
+ total_undocumented = 0
135
+
136
+ stats.each_value do |counts|
137
+ total_documented += counts[:documented]
138
+ total_undocumented += counts[:undocumented]
139
+ end
140
+
141
+ total_objects = total_documented + total_undocumented
142
+
143
+ coverage = if total_objects.zero?
144
+ 100.0
145
+ else
146
+ (total_documented.to_f / total_objects * 100)
147
+ end
148
+
149
+ {
150
+ total: total_objects,
151
+ documented: total_documented,
152
+ coverage: coverage
153
+ }
154
+ end
155
+ end
156
+ end
157
+ 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.1.0'
6
+ VERSION = '1.2.0'
7
7
  end
8
8
  end
data/lib/yard/lint.rb CHANGED
@@ -19,10 +19,20 @@ module Yard
19
19
  # @param config_file [String, nil] path to config file
20
20
  # (auto-loads .yard-lint.yml if not specified)
21
21
  # @param progress [Boolean] show progress indicator (default: true for TTY)
22
+ # @param diff [Hash, nil] diff mode options
23
+ # - :mode [Symbol] one of :ref, :staged, :changed
24
+ # - :base_ref [String, nil] base ref for :ref mode (auto-detects main/master if nil)
22
25
  # @return [Yard::Lint::Result] result object with offenses
23
- def run(path:, config: nil, config_file: nil, progress: nil)
26
+ def run(path:, config: nil, config_file: nil, progress: nil, diff: nil)
24
27
  config ||= load_config(config_file)
25
- files = expand_path(path, config)
28
+
29
+ # Determine files to lint based on diff mode or normal path expansion
30
+ files = if diff
31
+ get_diff_files(diff, path, config)
32
+ else
33
+ expand_path(path, config)
34
+ end
35
+
26
36
  runner = Runner.new(files, config)
27
37
 
28
38
  # Enable progress by default if output is a TTY
@@ -45,6 +55,32 @@ module Yard
45
55
  end
46
56
  end
47
57
 
58
+ # Get files from git diff based on diff mode
59
+ # @param diff [Hash] diff mode options
60
+ # @param path [String, Array<String>] path or array of paths to filter within
61
+ # @param config [Yard::Lint::Config] configuration object
62
+ # @return [Array<String>] array of absolute file paths
63
+ def get_diff_files(diff, path, config)
64
+ # Get changed files from git based on mode
65
+ git_files = case diff[:mode]
66
+ when :ref
67
+ Git.changed_files(diff[:base_ref], path)
68
+ when :staged
69
+ Git.staged_files(path)
70
+ when :changed
71
+ Git.uncommitted_files(path)
72
+ else
73
+ raise ArgumentError, "Unknown diff mode: #{diff[:mode]}"
74
+ end
75
+
76
+ # Apply exclusion patterns
77
+ git_files.reject do |file|
78
+ config.exclude.any? do |pattern|
79
+ File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
80
+ end
81
+ end
82
+ end
83
+
48
84
  # Expand path/glob patterns into an array of files
49
85
  # @param path [String, Array<String>] path or array of paths
50
86
  # @param config [Yard::Lint::Config] configuration object
data/lib/yard-lint.rb CHANGED
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load IRB notifier shim before YARD to avoid IRB dependency in Ruby 3.5+
4
+ # This must be loaded before any YARD code is required
5
+ require_relative 'yard/lint/ext/irb_notifier_shim'
6
+
3
7
  require 'zeitwerk'
4
8
 
5
9
  # Setup Zeitwerk loader for gem
6
10
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
7
11
  loader.ignore(__FILE__)
12
+ loader.ignore("#{__dir__}/yard/lint/ext")
8
13
  loader.setup
9
14
 
10
15
  # Manually load the main module since it contains class-level methods
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: irb
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: yard
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -77,7 +63,9 @@ files:
77
63
  - lib/yard/lint/config_generator.rb
78
64
  - lib/yard/lint/config_loader.rb
79
65
  - lib/yard/lint/errors.rb
66
+ - lib/yard/lint/ext/irb_notifier_shim.rb
80
67
  - lib/yard/lint/formatters/progress.rb
68
+ - lib/yard/lint/git.rb
81
69
  - lib/yard/lint/parsers/base.rb
82
70
  - lib/yard/lint/parsers/one_line_base.rb
83
71
  - lib/yard/lint/parsers/two_line_base.rb
@@ -85,6 +73,7 @@ files:
85
73
  - lib/yard/lint/results/aggregate.rb
86
74
  - lib/yard/lint/results/base.rb
87
75
  - lib/yard/lint/runner.rb
76
+ - lib/yard/lint/stats_calculator.rb
88
77
  - lib/yard/lint/validators/base.rb
89
78
  - lib/yard/lint/validators/config.rb
90
79
  - lib/yard/lint/validators/documentation/markdown_syntax.rb