yard-lint 1.0.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +160 -268
  4. data/bin/yard-lint +100 -8
  5. data/lib/yard/lint/command_cache.rb +17 -1
  6. data/lib/yard/lint/config.rb +20 -2
  7. data/lib/yard/lint/config_generator.rb +200 -0
  8. data/lib/yard/lint/ext/irb_notifier_shim.rb +95 -0
  9. data/lib/yard/lint/git.rb +125 -0
  10. data/lib/yard/lint/results/aggregate.rb +22 -2
  11. data/lib/yard/lint/runner.rb +4 -3
  12. data/lib/yard/lint/stats_calculator.rb +157 -0
  13. data/lib/yard/lint/validators/base.rb +36 -0
  14. data/lib/yard/lint/validators/documentation/markdown_syntax/config.rb +20 -0
  15. data/lib/yard/lint/validators/documentation/markdown_syntax/messages_builder.rb +44 -0
  16. data/lib/yard/lint/validators/documentation/markdown_syntax/parser.rb +53 -0
  17. data/lib/yard/lint/validators/documentation/markdown_syntax/result.rb +25 -0
  18. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +38 -0
  19. data/lib/yard/lint/validators/documentation/markdown_syntax.rb +37 -0
  20. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +26 -1
  21. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +26 -1
  22. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +131 -2
  23. data/lib/yard/lint/validators/documentation/undocumented_options/config.rb +20 -0
  24. data/lib/yard/lint/validators/documentation/undocumented_options/parser.rb +53 -0
  25. data/lib/yard/lint/validators/documentation/undocumented_options/result.rb +29 -0
  26. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +38 -0
  27. data/lib/yard/lint/validators/documentation/undocumented_options.rb +40 -0
  28. data/lib/yard/lint/validators/semantic/abstract_methods.rb +31 -1
  29. data/lib/yard/lint/validators/tags/api_tags.rb +34 -1
  30. data/lib/yard/lint/validators/tags/collection_type/config.rb +2 -1
  31. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +40 -11
  32. data/lib/yard/lint/validators/tags/collection_type/parser.rb +6 -5
  33. data/lib/yard/lint/validators/tags/collection_type/validator.rb +26 -7
  34. data/lib/yard/lint/validators/tags/collection_type.rb +38 -2
  35. data/lib/yard/lint/validators/tags/example_syntax/config.rb +20 -0
  36. data/lib/yard/lint/validators/tags/example_syntax/messages_builder.rb +28 -0
  37. data/lib/yard/lint/validators/tags/example_syntax/parser.rb +79 -0
  38. data/lib/yard/lint/validators/tags/example_syntax/result.rb +42 -0
  39. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +88 -0
  40. data/lib/yard/lint/validators/tags/example_syntax.rb +42 -0
  41. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +2 -2
  42. data/lib/yard/lint/validators/tags/invalid_types.rb +25 -1
  43. data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +2 -4
  44. data/lib/yard/lint/validators/tags/meaningless_tag.rb +31 -3
  45. data/lib/yard/lint/validators/tags/option_tags/validator.rb +7 -1
  46. data/lib/yard/lint/validators/tags/option_tags.rb +26 -1
  47. data/lib/yard/lint/validators/tags/order.rb +25 -1
  48. data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +33 -0
  49. data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +61 -0
  50. data/lib/yard/lint/validators/tags/redundant_param_description/parser.rb +67 -0
  51. data/lib/yard/lint/validators/tags/redundant_param_description/result.rb +25 -0
  52. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +148 -0
  53. data/lib/yard/lint/validators/tags/redundant_param_description.rb +168 -0
  54. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +2 -4
  55. data/lib/yard/lint/validators/tags/tag_type_position.rb +39 -2
  56. data/lib/yard/lint/validators/tags/type_syntax.rb +26 -2
  57. data/lib/yard/lint/validators/warnings/duplicated_parameter_name.rb +26 -1
  58. data/lib/yard/lint/validators/warnings/invalid_directive_format.rb +26 -1
  59. data/lib/yard/lint/validators/warnings/invalid_tag_format.rb +25 -1
  60. data/lib/yard/lint/validators/warnings/unknown_directive.rb +26 -1
  61. data/lib/yard/lint/validators/warnings/unknown_parameter_name.rb +23 -1
  62. data/lib/yard/lint/validators/warnings/unknown_tag.rb +26 -1
  63. data/lib/yard/lint/version.rb +1 -1
  64. data/lib/yard/lint.rb +38 -2
  65. data/lib/yard-lint.rb +5 -0
  66. metadata +28 -1
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,40 @@ 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
+
58
+ opts.on('--init', 'Generate .yard-lint.yml config file with defaults') do
59
+ options[:init] = true
60
+ end
61
+
62
+ opts.on('--force', 'Force overwrite when using --init') do
63
+ options[:force] = true
64
+ end
65
+
35
66
  opts.on('-v', '--version', 'Show version') do
36
67
  puts "yard-lint #{Yard::Lint::VERSION}"
37
68
  exit
@@ -39,10 +70,29 @@ OptionParser.new do |opts|
39
70
 
40
71
  opts.on('-h', '--help', 'Show this help') do
41
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'
42
80
  exit
43
81
  end
44
82
  end.parse!
45
83
 
84
+ # Handle --init flag
85
+ if options[:init]
86
+ if Yard::Lint::ConfigGenerator.generate(force: options[:force])
87
+ puts 'Created .yard-lint.yml with default configuration'
88
+ exit 0
89
+ else
90
+ puts 'Error: .yard-lint.yml already exists'
91
+ puts 'Use --init --force to overwrite'
92
+ exit 1
93
+ end
94
+ end
95
+
46
96
  # Get path argument
47
97
  path = ARGV[0]
48
98
 
@@ -76,12 +126,28 @@ end
76
126
  Yard::Lint::Validators::Base.reset_command_cache!
77
127
  Yard::Lint::Validators::Base.clear_yard_database!
78
128
 
79
- # Run the linter with config_file (it will be loaded internally)
80
- result = Yard::Lint.run(
81
- path: path,
82
- config_file: config_file,
83
- progress: options[:progress]
84
- )
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
85
151
 
86
152
  # Format and display results
87
153
  case options[:format]
@@ -93,14 +159,40 @@ when 'json'
93
159
  })
94
160
  exit result.exit_code
95
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
+
96
182
  if result.clean?
97
- 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]
98
190
  exit 0
99
191
  else
100
192
  # Show statistics if requested or in quiet mode
101
193
  if options[:stats] || options[:quiet]
102
194
  stats = result.statistics
103
- puts "\n#{result.count} offense(s) detected"
195
+ puts "#{result.count} offense(s) detected"
104
196
  puts " Errors: #{stats[:error]}"
105
197
  puts " Warnings: #{stats[:warning]}"
106
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
@@ -199,8 +219,6 @@ module Yard
199
219
  respond_to?(key) ? send(key) : nil
200
220
  end
201
221
 
202
- private
203
-
204
222
  # Generic helper to set validator configuration
205
223
  # @param validator_name [String] full validator name (e.g., 'Tags/Order')
206
224
  # @param key [String] configuration key
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ # Generates default .yard-lint.yml configuration file
6
+ class ConfigGenerator
7
+ # Default configuration template
8
+ DEFAULT_CONFIG = <<~YAML
9
+ # YARD-Lint Configuration
10
+ # See https://github.com/mensfeld/yard-lint for documentation
11
+
12
+ # Global settings for all validators
13
+ AllValidators:
14
+ # YARD command-line options (applied to all validators by default)
15
+ YardOptions:
16
+ - --private
17
+ - --protected
18
+
19
+ # Global file exclusion patterns
20
+ Exclude:
21
+ - '\\.git'
22
+ - 'vendor/**/*'
23
+ - 'node_modules/**/*'
24
+ - 'spec/**/*'
25
+ - 'test/**/*'
26
+
27
+ # Exit code behavior (error, warning, convention, never)
28
+ FailOnSeverity: warning
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
+
39
+ # Documentation validators
40
+ Documentation/UndocumentedObjects:
41
+ Description: 'Checks for classes, modules, and methods without documentation.'
42
+ Enabled: true
43
+ Severity: warning
44
+ ExcludedMethods:
45
+ - 'initialize/0' # Exclude parameter-less initialize
46
+ - '/^_/' # Exclude private methods (by convention)
47
+
48
+ Documentation/UndocumentedMethodArguments:
49
+ Description: 'Checks for method parameters without @param tags.'
50
+ Enabled: true
51
+ Severity: warning
52
+
53
+ Documentation/UndocumentedBooleanMethods:
54
+ Description: 'Checks that question mark methods document their boolean return.'
55
+ Enabled: true
56
+ Severity: warning
57
+
58
+ Documentation/UndocumentedOptions:
59
+ Description: 'Detects methods with options hash parameters but no @option tags.'
60
+ Enabled: true
61
+ Severity: warning
62
+
63
+ Documentation/MarkdownSyntax:
64
+ Description: 'Detects common markdown syntax errors in documentation.'
65
+ Enabled: true
66
+ Severity: warning
67
+
68
+ # Tags validators
69
+ Tags/Order:
70
+ Description: 'Enforces consistent ordering of YARD tags.'
71
+ Enabled: true
72
+ Severity: convention
73
+ EnforcedOrder:
74
+ - param
75
+ - option
76
+ - return
77
+ - raise
78
+ - example
79
+
80
+ Tags/InvalidTypes:
81
+ Description: 'Validates type definitions in @param, @return, @option tags.'
82
+ Enabled: true
83
+ Severity: warning
84
+ ValidatedTags:
85
+ - param
86
+ - option
87
+ - return
88
+
89
+ Tags/TypeSyntax:
90
+ Description: 'Validates YARD type syntax using YARD parser.'
91
+ Enabled: true
92
+ Severity: warning
93
+ ValidatedTags:
94
+ - param
95
+ - option
96
+ - return
97
+ - yieldreturn
98
+
99
+ Tags/MeaninglessTag:
100
+ Description: 'Detects @param/@option tags on classes, modules, or constants.'
101
+ Enabled: true
102
+ Severity: warning
103
+ CheckedTags:
104
+ - param
105
+ - option
106
+ InvalidObjectTypes:
107
+ - class
108
+ - module
109
+ - constant
110
+
111
+ Tags/CollectionType:
112
+ Description: 'Validates Hash collection syntax consistency.'
113
+ Enabled: true
114
+ Severity: convention
115
+ EnforcedStyle: long # 'long' for Hash{K => V} (YARD standard), 'short' for {K => V}
116
+ ValidatedTags:
117
+ - param
118
+ - option
119
+ - return
120
+ - yieldreturn
121
+
122
+ Tags/TagTypePosition:
123
+ Description: 'Validates type annotation position in tags.'
124
+ Enabled: true
125
+ Severity: convention
126
+ CheckedTags:
127
+ - param
128
+ - option
129
+ # EnforcedStyle: 'type_after_name' (YARD standard: @param name [Type])
130
+ # or 'type_first' (@param [Type] name)
131
+ EnforcedStyle: type_after_name
132
+
133
+ Tags/ApiTags:
134
+ Description: 'Enforces @api tags on public objects.'
135
+ Enabled: false # Opt-in validator
136
+ Severity: warning
137
+ AllowedApis:
138
+ - public
139
+ - private
140
+ - internal
141
+
142
+ Tags/OptionTags:
143
+ Description: 'Requires @option tags for methods with options parameters.'
144
+ Enabled: true
145
+ Severity: warning
146
+
147
+ # Warnings validators - catches YARD parser errors
148
+ Warnings/UnknownTag:
149
+ Description: 'Detects unknown YARD tags.'
150
+ Enabled: true
151
+ Severity: error
152
+
153
+ Warnings/UnknownDirective:
154
+ Description: 'Detects unknown YARD directives.'
155
+ Enabled: true
156
+ Severity: error
157
+
158
+ Warnings/InvalidTagFormat:
159
+ Description: 'Detects malformed tag syntax.'
160
+ Enabled: true
161
+ Severity: error
162
+
163
+ Warnings/InvalidDirectiveFormat:
164
+ Description: 'Detects malformed directive syntax.'
165
+ Enabled: true
166
+ Severity: error
167
+
168
+ Warnings/DuplicatedParameterName:
169
+ Description: 'Detects duplicate @param tags.'
170
+ Enabled: true
171
+ Severity: error
172
+
173
+ Warnings/UnknownParameterName:
174
+ Description: 'Detects @param tags for non-existent parameters.'
175
+ Enabled: true
176
+ Severity: error
177
+
178
+ # Semantic validators
179
+ Semantic/AbstractMethods:
180
+ Description: 'Ensures @abstract methods do not have real implementations.'
181
+ Enabled: true
182
+ Severity: warning
183
+ YAML
184
+
185
+ # Generate .yard-lint.yml file in current directory
186
+ # @param force [Boolean] overwrite existing file if true
187
+ # @return [Boolean] true if file was created, false if already exists
188
+ def self.generate(force: false)
189
+ config_path = File.join(Dir.pwd, Config::DEFAULT_CONFIG_FILE)
190
+
191
+ if File.exist?(config_path) && !force
192
+ false
193
+ else
194
+ File.write(config_path, DEFAULT_CONFIG)
195
+ true
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -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