yard-lint 1.3.0 → 1.5.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +73 -3
  4. data/README.md +154 -527
  5. data/Rakefile +11 -8
  6. data/bin/yard-lint +64 -5
  7. data/lib/yard/lint/config.rb +4 -0
  8. data/lib/yard/lint/config_validator.rb +230 -0
  9. data/lib/yard/lint/errors.rb +6 -0
  10. data/lib/yard/lint/executor/in_process_registry.rb +9 -0
  11. data/lib/yard/lint/path_grouper.rb +70 -0
  12. data/lib/yard/lint/result_builder.rb +19 -5
  13. data/lib/yard/lint/results/base.rb +3 -3
  14. data/lib/yard/lint/templates/default_config.yml +31 -0
  15. data/lib/yard/lint/templates/strict_config.yml +31 -0
  16. data/lib/yard/lint/todo_generator.rb +261 -0
  17. data/lib/yard/lint/validators/base.rb +1 -1
  18. data/lib/yard/lint/validators/documentation/missing_return/config.rb +23 -0
  19. data/lib/yard/lint/validators/documentation/missing_return/messages_builder.rb +23 -0
  20. data/lib/yard/lint/validators/documentation/missing_return/parser.rb +128 -0
  21. data/lib/yard/lint/validators/documentation/missing_return/result.rb +25 -0
  22. data/lib/yard/lint/validators/documentation/missing_return/validator.rb +40 -0
  23. data/lib/yard/lint/validators/documentation/missing_return.rb +49 -0
  24. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +1 -1
  25. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +1 -1
  26. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +3 -0
  27. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +1 -1
  28. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +8 -2
  29. data/lib/yard/lint/validators/tags/collection_type/validator.rb +33 -14
  30. data/lib/yard/lint/validators/tags/example_style/config.rb +33 -0
  31. data/lib/yard/lint/validators/tags/example_style/linter_detector.rb +71 -0
  32. data/lib/yard/lint/validators/tags/example_style/messages_builder.rb +29 -0
  33. data/lib/yard/lint/validators/tags/example_style/parser.rb +88 -0
  34. data/lib/yard/lint/validators/tags/example_style/result.rb +42 -0
  35. data/lib/yard/lint/validators/tags/example_style/rubocop_runner.rb +210 -0
  36. data/lib/yard/lint/validators/tags/example_style/validator.rb +87 -0
  37. data/lib/yard/lint/validators/tags/example_style.rb +61 -0
  38. data/lib/yard/lint/validators/tags/forbidden_tags/config.rb +21 -0
  39. data/lib/yard/lint/validators/tags/forbidden_tags/messages_builder.rb +34 -0
  40. data/lib/yard/lint/validators/tags/forbidden_tags/parser.rb +51 -0
  41. data/lib/yard/lint/validators/tags/forbidden_tags/result.rb +28 -0
  42. data/lib/yard/lint/validators/tags/forbidden_tags/validator.rb +66 -0
  43. data/lib/yard/lint/validators/tags/forbidden_tags.rb +68 -0
  44. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +1 -1
  45. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +1 -1
  46. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +19 -0
  47. data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +1 -1
  48. data/lib/yard/lint/version.rb +1 -1
  49. data/mise.toml +2 -0
  50. data/package-lock.json +329 -0
  51. data/package.json +7 -0
  52. data/proxy_types +0 -0
  53. data/renovate.json +18 -1
  54. metadata +30 -3
  55. data/.coditsu/ci.yml +0 -3
@@ -5,13 +5,13 @@ module Yard
5
5
  module Validators
6
6
  module Tags
7
7
  module CollectionType
8
- # Validates Hash collection type syntax in YARD tags
8
+ # Validates Hash and Array collection type syntax in YARD tags
9
9
  class Validator < Base
10
10
  # Enable in-process execution
11
11
  in_process visibility: :public
12
12
 
13
13
  # Execute query for a single object during in-process execution.
14
- # Validates Hash collection type syntax based on EnforcedStyle.
14
+ # Validates collection type syntax based on EnforcedStyle.
15
15
  # @param object [YARD::CodeObjects::Base] the code object to query
16
16
  # @param collector [Executor::ResultCollector] collector for output
17
17
  # @return [void]
@@ -25,18 +25,7 @@ module Yard
25
25
  next unless tag.types
26
26
 
27
27
  tag.types.each do |type_str|
28
- detected_style = nil
29
-
30
- # Check for Hash<...> syntax (angle brackets)
31
- if type_str =~ /Hash<.*>/
32
- detected_style = 'short'
33
- # Check for Hash{...} syntax (curly braces)
34
- elsif type_str =~ /Hash\{.*\}/
35
- detected_style = 'long'
36
- # Check for {...} syntax without Hash prefix
37
- elsif type_str =~ /^\{.*\}$/
38
- detected_style = 'short'
39
- end
28
+ detected_style = detect_style(type_str)
40
29
 
41
30
  # Report violations based on enforced style
42
31
  if detected_style && detected_style != style
@@ -50,6 +39,36 @@ module Yard
50
39
 
51
40
  private
52
41
 
42
+ # Detects the collection style used in a type string
43
+ # @param type_str [String] the type string to check
44
+ # @return [String, nil] 'long' or 'short', or nil if not a collection type
45
+ def detect_style(type_str)
46
+ # Hash types
47
+ # Hash<...> is short style (should be Hash{K => V})
48
+ if type_str =~ /Hash<.*>/
49
+ 'short'
50
+ # Hash{...} is long style
51
+ elsif type_str =~ /Hash\{.*\}/
52
+ 'long'
53
+ # {...} without Hash prefix is short style
54
+ elsif type_str =~ /^\{.*\}$/
55
+ 'short'
56
+ # Array types
57
+ # Array<...> is long style
58
+ elsif type_str =~ /Array<.*>/
59
+ 'long'
60
+ # Array(...) is long style
61
+ elsif type_str =~ /Array\(.*\)/
62
+ 'long'
63
+ # <...> without Array prefix is short style
64
+ elsif type_str =~ /^<.*>$/
65
+ 'short'
66
+ # (...) without Array prefix is short style (tuple shorthand)
67
+ elsif type_str =~ /^\(.*\)$/
68
+ 'short'
69
+ end
70
+ end
71
+
53
72
  # Gets the enforced collection style from configuration
54
73
  # @return [String] 'long' or 'short'
55
74
  def enforced_style
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleStyle
8
+ # Configuration for ExampleStyle validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :example_style
11
+ self.defaults = {
12
+ 'Enabled' => false, # Opt-in validator
13
+ 'Severity' => 'convention',
14
+ 'Linter' => 'auto', # 'auto', 'rubocop', 'standard', or 'none'
15
+ 'SkipPatterns' => [],
16
+ 'DisabledCops' => [
17
+ # File-level cops that don't make sense for code snippets
18
+ 'Style/FrozenStringLiteralComment',
19
+ 'Layout/TrailingWhitespace',
20
+ 'Layout/EndOfLine',
21
+ 'Layout/TrailingEmptyLines',
22
+ 'Metrics/MethodLength',
23
+ 'Metrics/AbcSize',
24
+ 'Metrics/CyclomaticComplexity',
25
+ 'Metrics/PerceivedComplexity'
26
+ ]
27
+ }.freeze
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleStyle
8
+ # Detects available linters (RuboCop or StandardRB) in the project
9
+ class LinterDetector
10
+ class << self
11
+ # Detect which linter to use based on configuration and project setup
12
+ # @param config_linter [String] configured linter preference ('auto', 'rubocop', 'standard', 'none')
13
+ # @param project_root [String] project root directory for file checks
14
+ # @return [Symbol] detected linter (:rubocop, :standard, or :none)
15
+ def detect(config_linter, project_root: Dir.pwd)
16
+ return :none if config_linter == 'none'
17
+ return :rubocop if config_linter == 'rubocop' && rubocop_available?
18
+ return :standard if config_linter == 'standard' && standard_available?
19
+
20
+ # Auto-detection logic
21
+ return :none unless config_linter == 'auto'
22
+
23
+ # Priority 1: Check for .standard.yml config file
24
+ return :standard if File.exist?(File.join(project_root, '.standard.yml')) && standard_available?
25
+
26
+ # Priority 2: Check for .rubocop.yml config file
27
+ return :rubocop if File.exist?(File.join(project_root, '.rubocop.yml')) && rubocop_available?
28
+
29
+ # Priority 3: Check Gemfile for 'standard' gem
30
+ gemfile_path = File.join(project_root, 'Gemfile')
31
+ if File.exist?(gemfile_path)
32
+ gemfile_content = File.read(gemfile_path)
33
+ return :standard if gemfile_content.match?(/gem\s+['"]standard['"]/) && standard_available?
34
+ return :rubocop if gemfile_content.match?(/gem\s+['"]rubocop['"]/) && rubocop_available?
35
+ end
36
+
37
+ # Priority 4: Check for Gemfile.lock
38
+ gemfile_lock_path = File.join(project_root, 'Gemfile.lock')
39
+ if File.exist?(gemfile_lock_path)
40
+ lock_content = File.read(gemfile_lock_path)
41
+ return :standard if lock_content.include?('standard (') && standard_available?
42
+ return :rubocop if lock_content.include?('rubocop (') && rubocop_available?
43
+ end
44
+
45
+ :none
46
+ end
47
+
48
+ # Check if RuboCop gem is available
49
+ # @return [Boolean] true if RuboCop can be loaded
50
+ def rubocop_available?
51
+ require 'rubocop'
52
+ true
53
+ rescue LoadError
54
+ false
55
+ end
56
+
57
+ # Check if StandardRB gem is available
58
+ # @return [Boolean] true if StandardRB can be loaded
59
+ def standard_available?
60
+ require 'standard'
61
+ true
62
+ rescue LoadError
63
+ false
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleStyle
8
+ # Builds messages for example style offenses
9
+ class MessagesBuilder
10
+ class << self
11
+ # Build message for example style offense
12
+ # @param offense [Hash] offense data with :example_name, :cop_name, :message keys
13
+ # @return [String] formatted message
14
+ def call(offense)
15
+ example_name = offense[:example_name]
16
+ cop_name = offense[:cop_name]
17
+ message = offense[:message]
18
+ object_name = offense[:object_name]
19
+
20
+ "Object `#{object_name}` has style offense in @example " \
21
+ "'#{example_name}': #{cop_name}: #{message}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleStyle
8
+ # Parser for @example style validation results
9
+ class Parser < Parsers::Base
10
+ # @param yard_output [String] raw yard output with example style issues
11
+ # @return [Array<Hash>] array with example style violation details
12
+ def call(yard_output)
13
+ return [] if yard_output.nil? || yard_output.empty?
14
+
15
+ # Normalize line endings (handle Windows \r\n)
16
+ normalized_output = yard_output.gsub("\r\n", "\n")
17
+ lines = normalized_output.split("\n").reject(&:empty?)
18
+ results = []
19
+
20
+ # Output format is exactly 5 lines per offense:
21
+ # 1. file.rb:10: ClassName#method_name
22
+ # 2. style_offense
23
+ # 3. Example name
24
+ # 4. Cop name (e.g., Style/StringLiterals)
25
+ # 5. Offense message
26
+ # Next offense starts with another file.rb:line: pattern
27
+
28
+ i = 0
29
+ while i < lines.length
30
+ location_line = lines[i]
31
+
32
+ # Parse location line: "file.rb:10: ClassName#method_name"
33
+ match = location_line.match(%r{^([a-zA-Z./~].+):(\d+): (.+)$})
34
+ unless match
35
+ i += 1
36
+ next
37
+ end
38
+
39
+ file = match[1]
40
+ line = match[2].to_i
41
+ object_name = match[3]
42
+
43
+ # Next line should be status
44
+ i += 1
45
+ break if i >= lines.length
46
+
47
+ status_line = lines[i]
48
+ next unless status_line == 'style_offense'
49
+
50
+ # Next line is example name
51
+ i += 1
52
+ break if i >= lines.length
53
+
54
+ example_name = lines[i]
55
+
56
+ # Next line is cop name
57
+ i += 1
58
+ break if i >= lines.length
59
+
60
+ cop_name = lines[i]
61
+
62
+ # Next line is message
63
+ i += 1
64
+ break if i >= lines.length
65
+
66
+ message = lines[i]
67
+
68
+ results << {
69
+ name: 'ExampleStyle',
70
+ object_name: object_name,
71
+ example_name: example_name,
72
+ cop_name: cop_name,
73
+ message: message,
74
+ location: file,
75
+ line: line
76
+ }
77
+
78
+ i += 1
79
+ end
80
+
81
+ results
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleStyle
8
+ # Result object for example style validation
9
+ class Result < Results::Base
10
+ self.default_severity = 'convention'
11
+ self.offense_type = 'line'
12
+ self.offense_name = 'ExampleStyleOffense'
13
+
14
+ # Build human-readable message for example style offense
15
+ # @param offense [Hash] offense data with :example_name, :cop_name, :message keys
16
+ # @return [String] formatted message
17
+ def build_message(offense)
18
+ MessagesBuilder.call(offense)
19
+ end
20
+
21
+ private
22
+
23
+ # Override to build offenses with dynamic names from parsed data
24
+ # @return [Array<Hash>] array of offense hashes
25
+ def build_offenses
26
+ @parsed_data.map do |offense_data|
27
+ {
28
+ severity: configured_severity,
29
+ type: self.class.offense_type,
30
+ name: offense_data[:name] || self.class.offense_name,
31
+ message: build_message(offense_data),
32
+ location: offense_data[:location] || offense_data[:file],
33
+ location_line: offense_data[:line] || offense_data[:location_line] || 0
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+
6
+ module Yard
7
+ module Lint
8
+ module Validators
9
+ module Tags
10
+ module ExampleStyle
11
+ # Runs RuboCop or StandardRB on code snippets and returns offenses
12
+ class RubocopRunner
13
+ # @param linter [Symbol] which linter to use (:rubocop or :standard)
14
+ # @param disabled_cops [Array<String>] list of cop names to disable
15
+ # @param skip_patterns [Array<String>] regex patterns to skip examples
16
+ def initialize(linter:, disabled_cops: [], skip_patterns: [])
17
+ @linter = linter
18
+ @disabled_cops = disabled_cops
19
+ @skip_patterns = compile_patterns(skip_patterns)
20
+ end
21
+
22
+ # Run linter on code example and return offenses
23
+ # @param code [String] Ruby code extracted from @example tag to check for style violations
24
+ # @param example_name [String] name of the example for context
25
+ # @param file_path [String] path to the file being linted (for config discovery)
26
+ # @return [Array<Hash>] array of offense hashes with :cop_name, :message, :line, :column keys
27
+ def run(code, example_name, file_path: nil)
28
+ return [] if should_skip?(example_name)
29
+
30
+ cleaned_code = clean_code(code)
31
+ return [] if cleaned_code.empty?
32
+
33
+ case @linter
34
+ when :rubocop
35
+ run_rubocop(cleaned_code, file_path: file_path)
36
+ when :standard
37
+ run_standard(cleaned_code, file_path: file_path)
38
+ else
39
+ []
40
+ end
41
+ rescue StandardError => e
42
+ warn "[YARD::Lint] ExampleStyle: Error running #{@linter}: #{e.message}" if ENV['DEBUG']
43
+ []
44
+ end
45
+
46
+ private
47
+
48
+ # Compile skip patterns into regex objects
49
+ # @param patterns [Array<String>] array of regex pattern strings
50
+ # @return [Array<Regexp>] array of compiled regex objects
51
+ def compile_patterns(patterns)
52
+ patterns.filter_map do |pattern|
53
+ # Pattern format: '/pattern/flags' or 'pattern'
54
+ if pattern.start_with?('/') && pattern.match?(%r{^/(.+)/([imx]*)$})
55
+ match = pattern.match(%r{^/(.+)/([imx]*)$})
56
+ Regexp.new(match[1], parse_flags(match[2]))
57
+ else
58
+ Regexp.new(pattern)
59
+ end
60
+ rescue RegexpError => e
61
+ warn "[YARD::Lint] ExampleStyle: Invalid skip pattern '#{pattern}': #{e.message}" if ENV['DEBUG']
62
+ nil
63
+ end
64
+ end
65
+
66
+ # Parse regex flags from string
67
+ # @param flags_str [String] flags string like 'i' or 'im'
68
+ # @return [Integer] OR'd flag constants
69
+ def parse_flags(flags_str)
70
+ flags = 0
71
+ flags |= Regexp::IGNORECASE if flags_str.include?('i')
72
+ flags |= Regexp::MULTILINE if flags_str.include?('m')
73
+ flags |= Regexp::EXTENDED if flags_str.include?('x')
74
+ flags
75
+ end
76
+
77
+ # Find RuboCop config file in starting directory or parent directories
78
+ # @param start_dir [String] directory to start searching from
79
+ # @return [String, nil] path to config file or nil if not found
80
+ def find_rubocop_config(start_dir = Dir.pwd)
81
+ current_dir = start_dir
82
+ config_names = ['.rubocop.yml', '.rubocop.yaml']
83
+
84
+ # Search up the directory tree
85
+ loop do
86
+ config_names.each do |name|
87
+ config_path = File.join(current_dir, name)
88
+ return config_path if File.exist?(config_path)
89
+ end
90
+
91
+ parent_dir = File.dirname(current_dir)
92
+ break if parent_dir == current_dir # Reached root
93
+
94
+ current_dir = parent_dir
95
+ end
96
+
97
+ nil
98
+ end
99
+
100
+ # Check if example should be skipped based on skip patterns
101
+ # @param example_name [String] name of the example
102
+ # @return [Boolean] true if should skip
103
+ def should_skip?(example_name)
104
+ @skip_patterns.any? { |pattern| example_name.match?(pattern) }
105
+ end
106
+
107
+ # Clean code by removing output indicators and extra whitespace
108
+ # Mirrors the logic from ExampleSyntax validator
109
+ # @param code [String] raw code from @example tag
110
+ # @return [String] cleaned code with preserved trailing newline
111
+ def clean_code(code)
112
+ return '' if code.nil? || code.empty?
113
+
114
+ # Strip output indicators (#=>) and everything after it
115
+ code_lines = code.split("\n").map do |line|
116
+ line.sub(/\s*#\s*=>.*$/, '')
117
+ end
118
+
119
+ # Join lines and add single trailing newline for RuboCop
120
+ cleaned = code_lines.join("\n").strip
121
+ cleaned.empty? ? '' : "#{cleaned}\n"
122
+ end
123
+
124
+ # Run RuboCop on code and parse JSON output
125
+ # @param code [String] Ruby code snippet to analyze with RuboCop
126
+ # @param file_path [String] path to the file being linted (for config discovery)
127
+ # @return [Array<Hash>] array of offense hashes
128
+ def run_rubocop(code, file_path: nil)
129
+ # Determine working directory from file path
130
+ work_dir = file_path ? File.dirname(file_path) : Dir.pwd
131
+
132
+ # Build RuboCop command
133
+ cmd = ['rubocop', '--format', 'json', '--stdin', 'example.rb']
134
+
135
+ # Add config file if it exists in the file's directory
136
+ config_file = find_rubocop_config(work_dir)
137
+ cmd += ['--config', config_file] if config_file
138
+
139
+ # Add disabled cops (comma-separated)
140
+ unless @disabled_cops.empty?
141
+ cmd += ['--except', @disabled_cops.join(',')]
142
+ end
143
+
144
+ stdout, = Open3.capture3(*cmd, stdin_data: code, chdir: work_dir)
145
+
146
+ # RuboCop returns non-zero exit status when offenses are found
147
+ # We only care about actual errors (not offense findings)
148
+ return [] if stdout.empty?
149
+
150
+ parse_rubocop_output(stdout)
151
+ rescue Errno::ENOENT
152
+ warn '[YARD::Lint] ExampleStyle: rubocop command not found' if ENV['DEBUG']
153
+ []
154
+ end
155
+
156
+ # Run StandardRB on code and parse JSON output
157
+ # @param code [String] Ruby code snippet to analyze with StandardRB
158
+ # @param file_path [String] path to the file being linted (for config discovery)
159
+ # @return [Array<Hash>] array of offense hashes
160
+ def run_standard(code, file_path: nil)
161
+ # Determine working directory from file path
162
+ work_dir = file_path ? File.dirname(file_path) : Dir.pwd
163
+
164
+ # StandardRB doesn't support --except for individual cops
165
+ # Users must configure exclusions in .standard.yml
166
+ cmd = ['standardrb', '--format', 'json', '--stdin', 'example.rb']
167
+
168
+ stdout, = Open3.capture3(*cmd, stdin_data: code, chdir: work_dir)
169
+
170
+ # StandardRB returns non-zero exit status when offenses are found
171
+ return [] if stdout.empty?
172
+
173
+ parse_rubocop_output(stdout) # StandardRB uses RuboCop's JSON format
174
+ rescue Errno::ENOENT
175
+ warn '[YARD::Lint] ExampleStyle: standardrb command not found' if ENV['DEBUG']
176
+ []
177
+ end
178
+
179
+ # Parse RuboCop/StandardRB JSON output
180
+ # @param json_output [String] JSON output from linter
181
+ # @return [Array<Hash>] array of offense hashes
182
+ def parse_rubocop_output(json_output)
183
+ result = JSON.parse(json_output)
184
+
185
+ # RuboCop JSON format: { "files": [ { "offenses": [...] } ] }
186
+ files = result['files'] || []
187
+ return [] if files.empty?
188
+
189
+ file = files.first
190
+ offenses = file['offenses'] || []
191
+
192
+ offenses.map do |offense|
193
+ {
194
+ cop_name: offense['cop_name'],
195
+ message: offense['message'],
196
+ line: offense['location']['line'],
197
+ column: offense['location']['column'],
198
+ severity: offense['severity']
199
+ }
200
+ end
201
+ rescue JSON::ParserError => e
202
+ warn "[YARD::Lint] ExampleStyle: Failed to parse linter output: #{e.message}" if ENV['DEBUG']
203
+ []
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleStyle
8
+ # Validator to check code style in @example tags using RuboCop/StandardRB
9
+ class Validator < Base
10
+ # Enable in-process execution with all visibility
11
+ in_process visibility: :all
12
+
13
+ # Execute query for a single object during in-process execution.
14
+ # Checks code style in @example tags using RuboCop or StandardRB.
15
+ # @param object [YARD::CodeObjects::Base] the code object to query
16
+ # @param collector [Executor::ResultCollector] collector for output
17
+ # @return [void]
18
+ def in_process_query(object, collector)
19
+ return unless object.has_tag?(:example)
20
+
21
+ # Get or initialize runner (memoized for performance)
22
+ return unless runner
23
+
24
+ # Process each example
25
+ example_tags = object.tags(:example)
26
+
27
+ example_tags.each_with_index do |example, index|
28
+ code = example.text
29
+ next if code.nil? || code.empty?
30
+
31
+ example_name = example.name || "Example #{index + 1}"
32
+
33
+ # Run linter (pass file path for context/config discovery)
34
+ offenses = runner.run(code, example_name, file_path: object.file)
35
+
36
+ # Output each offense
37
+ offenses.each do |offense|
38
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
39
+ collector.puts 'style_offense'
40
+ collector.puts example_name
41
+ collector.puts offense[:cop_name]
42
+ collector.puts offense[:message]
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Memoized runner instance
50
+ # @return [RubocopRunner, nil] runner instance or nil if no linter available
51
+ def runner
52
+ return @runner if defined?(@runner)
53
+
54
+ # Detect linter
55
+ linter_type = config_or_default('Linter')
56
+ detected_linter = LinterDetector.detect(linter_type)
57
+
58
+ # Gracefully skip if no linter available
59
+ if detected_linter == :none
60
+ warn_once_about_missing_linter if ENV['DEBUG']
61
+ @runner = nil
62
+ return nil
63
+ end
64
+
65
+ # Initialize and memoize runner
66
+ disabled_cops = config_or_default('DisabledCops')
67
+ skip_patterns = config_or_default('SkipPatterns')
68
+ @runner = RubocopRunner.new(
69
+ linter: detected_linter,
70
+ disabled_cops: disabled_cops,
71
+ skip_patterns: skip_patterns
72
+ )
73
+ end
74
+
75
+ # Warn once about missing linter (class-level tracking)
76
+ def warn_once_about_missing_linter
77
+ return if self.class.instance_variable_get(:@warned_about_linter)
78
+
79
+ warn '[YARD::Lint] ExampleStyle validator enabled but no linter (RuboCop/StandardRB) found. Skipping.'
80
+ self.class.instance_variable_set(:@warned_about_linter, true)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end