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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +73 -3
- data/README.md +154 -527
- data/Rakefile +11 -8
- data/bin/yard-lint +64 -5
- data/lib/yard/lint/config.rb +4 -0
- data/lib/yard/lint/config_validator.rb +230 -0
- data/lib/yard/lint/errors.rb +6 -0
- data/lib/yard/lint/executor/in_process_registry.rb +9 -0
- data/lib/yard/lint/path_grouper.rb +70 -0
- data/lib/yard/lint/result_builder.rb +19 -5
- data/lib/yard/lint/results/base.rb +3 -3
- data/lib/yard/lint/templates/default_config.yml +31 -0
- data/lib/yard/lint/templates/strict_config.yml +31 -0
- data/lib/yard/lint/todo_generator.rb +261 -0
- data/lib/yard/lint/validators/base.rb +1 -1
- data/lib/yard/lint/validators/documentation/missing_return/config.rb +23 -0
- data/lib/yard/lint/validators/documentation/missing_return/messages_builder.rb +23 -0
- data/lib/yard/lint/validators/documentation/missing_return/parser.rb +128 -0
- data/lib/yard/lint/validators/documentation/missing_return/result.rb +25 -0
- data/lib/yard/lint/validators/documentation/missing_return/validator.rb +40 -0
- data/lib/yard/lint/validators/documentation/missing_return.rb +49 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +1 -1
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +1 -1
- data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +3 -0
- data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +1 -1
- data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +8 -2
- data/lib/yard/lint/validators/tags/collection_type/validator.rb +33 -14
- data/lib/yard/lint/validators/tags/example_style/config.rb +33 -0
- data/lib/yard/lint/validators/tags/example_style/linter_detector.rb +71 -0
- data/lib/yard/lint/validators/tags/example_style/messages_builder.rb +29 -0
- data/lib/yard/lint/validators/tags/example_style/parser.rb +88 -0
- data/lib/yard/lint/validators/tags/example_style/result.rb +42 -0
- data/lib/yard/lint/validators/tags/example_style/rubocop_runner.rb +210 -0
- data/lib/yard/lint/validators/tags/example_style/validator.rb +87 -0
- data/lib/yard/lint/validators/tags/example_style.rb +61 -0
- data/lib/yard/lint/validators/tags/forbidden_tags/config.rb +21 -0
- data/lib/yard/lint/validators/tags/forbidden_tags/messages_builder.rb +34 -0
- data/lib/yard/lint/validators/tags/forbidden_tags/parser.rb +51 -0
- data/lib/yard/lint/validators/tags/forbidden_tags/result.rb +28 -0
- data/lib/yard/lint/validators/tags/forbidden_tags/validator.rb +66 -0
- data/lib/yard/lint/validators/tags/forbidden_tags.rb +68 -0
- data/lib/yard/lint/validators/tags/invalid_types/validator.rb +1 -1
- data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +1 -1
- data/lib/yard/lint/validators/tags/type_syntax/validator.rb +19 -0
- data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +1 -1
- data/lib/yard/lint/version.rb +1 -1
- data/mise.toml +2 -0
- data/package-lock.json +329 -0
- data/package.json +7 -0
- data/proxy_types +0 -0
- data/renovate.json +18 -1
- metadata +30 -3
- 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
|
|
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 =
|
|
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
|