yard-lint 1.12.0 → 1.13.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/CHANGELOG.md +199 -501
- data/README.md +94 -1
- data/bin/yard-lint +68 -32
- data/lib/yard/lint/config.rb +81 -5
- data/lib/yard/lint/config_validator.rb +7 -191
- data/lib/yard/lint/contractable/contract.rb +219 -0
- data/lib/yard/lint/contractable/result.rb +93 -0
- data/lib/yard/lint/contractable/rule.rb +10 -0
- data/lib/yard/lint/contracts/base.rb +57 -0
- data/lib/yard/lint/contracts/category.rb +71 -0
- data/lib/yard/lint/contracts/config.rb +81 -0
- data/lib/yard/lint/contracts/validator.rb +72 -0
- data/lib/yard/lint/errors.rb +3 -0
- data/lib/yard/lint/executor/in_process_registry.rb +1 -1
- data/lib/yard/lint/explainer.rb +42 -0
- data/lib/yard/lint/formatters/sarif.rb +218 -0
- data/lib/yard/lint/git.rb +5 -3
- data/lib/yard/lint/result_builder.rb +7 -6
- data/lib/yard/lint/runner.rb +2 -21
- data/lib/yard/lint/templates/default_config.yml +10 -0
- data/lib/yard/lint/templates/strict_config.yml +10 -0
- data/lib/yard/lint/todo_generator.rb +143 -12
- data/lib/yard/lint/validators/base.rb +3 -0
- data/lib/yard/lint/validators/documentation/reference_typo/config.rb +20 -0
- data/lib/yard/lint/validators/documentation/reference_typo/messages_builder.rb +24 -0
- data/lib/yard/lint/validators/documentation/reference_typo/parser.rb +44 -0
- data/lib/yard/lint/validators/documentation/reference_typo/result.rb +27 -0
- data/lib/yard/lint/validators/documentation/reference_typo/validator.rb +214 -0
- data/lib/yard/lint/validators/documentation/reference_typo.rb +39 -0
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/parser.rb +13 -12
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +84 -5
- data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +16 -3
- data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +3 -2
- data/lib/yard/lint/validators/tags/example_style/validator.rb +4 -4
- data/lib/yard/lint/validators/tags/example_syntax/validator.rb +4 -2
- data/lib/yard/lint/validators/tags/invalid_types/messages_builder.rb +15 -4
- data/lib/yard/lint/validators/tags/invalid_types/parser.rb +18 -7
- data/lib/yard/lint/validators/tags/invalid_types/validator.rb +22 -12
- data/lib/yard/lint/validators/tags/overload_consistency/config.rb +20 -0
- data/lib/yard/lint/validators/tags/overload_consistency/messages_builder.rb +32 -0
- data/lib/yard/lint/validators/tags/overload_consistency/parser.rb +49 -0
- data/lib/yard/lint/validators/tags/overload_consistency/result.rb +27 -0
- data/lib/yard/lint/validators/tags/overload_consistency/validator.rb +71 -0
- data/lib/yard/lint/validators/tags/overload_consistency.rb +53 -0
- data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +3 -3
- data/lib/yard/lint/validators/tags/type_syntax/parser.rb +13 -2
- data/lib/yard/lint/validators/tags/type_syntax/validator.rb +11 -1
- data/lib/yard/lint/version.rb +1 -1
- data/lib/yard/lint.rb +3 -57
- metadata +21 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'digest'
|
|
6
|
+
|
|
7
|
+
module Yard
|
|
8
|
+
module Lint
|
|
9
|
+
module Formatters
|
|
10
|
+
# Renders lint results as SARIF 2.1.0 (Static Analysis Results Interchange
|
|
11
|
+
# Format) - the JSON format GitHub code scanning and other result aggregators
|
|
12
|
+
# ingest, so yard-lint offenses can surface as PR annotations / Security-tab
|
|
13
|
+
# alerts instead of being buried in CI logs.
|
|
14
|
+
#
|
|
15
|
+
# This is output only: it does not change which offenses are reported, the
|
|
16
|
+
# exit code, or any configuration - it is an alternative rendering of the
|
|
17
|
+
# same result. Rule metadata (`rules[]`) is populated from each validator's
|
|
18
|
+
# own YARD documentation via {Explainer.describe}.
|
|
19
|
+
class Sarif
|
|
20
|
+
# SARIF 2.1.0 JSON schema URI, emitted as the document's `$schema`.
|
|
21
|
+
# @return [String]
|
|
22
|
+
SCHEMA_URI = 'https://json.schemastore.org/sarif-2.1.0.json'
|
|
23
|
+
|
|
24
|
+
# Tool information URI, emitted as the driver's `informationUri`.
|
|
25
|
+
# @return [String]
|
|
26
|
+
INFORMATION_URI = 'https://github.com/mensfeld/yard-lint'
|
|
27
|
+
|
|
28
|
+
# Help URI for each rule (the wiki validator reference).
|
|
29
|
+
# @return [String]
|
|
30
|
+
HELP_URI = 'https://github.com/mensfeld/yard-lint/wiki/Validators'
|
|
31
|
+
|
|
32
|
+
# SARIF supports only error/warning/note (plus none). yard-lint's
|
|
33
|
+
# `convention` maps to note, and `never` (report-only) maps to none.
|
|
34
|
+
# @return [Hash{String => String}]
|
|
35
|
+
SEVERITY_LEVELS = {
|
|
36
|
+
'error' => 'error',
|
|
37
|
+
'warning' => 'warning',
|
|
38
|
+
'convention' => 'note',
|
|
39
|
+
'never' => 'none'
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# Fallback SARIF level for any unrecognized severity.
|
|
43
|
+
# @return [String]
|
|
44
|
+
DEFAULT_LEVEL = 'warning'
|
|
45
|
+
|
|
46
|
+
# partialFingerprints key, versioned so the scheme can evolve without
|
|
47
|
+
# invalidating history. GitHub uses it to correlate results across runs.
|
|
48
|
+
# @return [String]
|
|
49
|
+
FINGERPRINT_KEY = 'yardLintOffense/v1'
|
|
50
|
+
|
|
51
|
+
# @param offenses [Array<Hash>] offense hashes (from `result.offenses`)
|
|
52
|
+
# @param base_dir [String] directory that offense paths are made relative
|
|
53
|
+
# to, so GitHub can map them onto the repository (default: current dir)
|
|
54
|
+
def initialize(offenses, base_dir: Dir.pwd)
|
|
55
|
+
@offenses = offenses
|
|
56
|
+
@base_dir = base_dir
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String] pretty-printed SARIF 2.1.0 JSON
|
|
60
|
+
def generate
|
|
61
|
+
JSON.pretty_generate(document)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Hash] the SARIF document as a Ruby hash
|
|
65
|
+
def document
|
|
66
|
+
rule_ids = @offenses.map { |offense| offense[:validator] }.compact.uniq
|
|
67
|
+
rule_index = rule_ids.each_with_index.to_h
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
'$schema' => SCHEMA_URI,
|
|
71
|
+
'version' => '2.1.0',
|
|
72
|
+
'runs' => [
|
|
73
|
+
{
|
|
74
|
+
'tool' => { 'driver' => driver(rule_ids) },
|
|
75
|
+
'results' => @offenses.map { |offense| result(offense, rule_index) }
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# @param rule_ids [Array<String>] distinct validator names, in result order
|
|
84
|
+
# @return [Hash] the SARIF tool driver, including rule metadata
|
|
85
|
+
def driver(rule_ids)
|
|
86
|
+
{
|
|
87
|
+
'name' => 'yard-lint',
|
|
88
|
+
'informationUri' => INFORMATION_URI,
|
|
89
|
+
'version' => Yard::Lint::VERSION,
|
|
90
|
+
'rules' => rule_ids.map { |id| rule(id) }
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param id [String] validator name (e.g. 'Tags/TypeSyntax')
|
|
95
|
+
# @return [Hash] a SARIF reportingDescriptor for the validator
|
|
96
|
+
def rule(id)
|
|
97
|
+
entry = {
|
|
98
|
+
'id' => id,
|
|
99
|
+
'name' => id.split('/').last,
|
|
100
|
+
'helpUri' => HELP_URI,
|
|
101
|
+
'defaultConfiguration' => { 'level' => level(default_severity(id)) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
described = Explainer.describe(id)
|
|
105
|
+
if described
|
|
106
|
+
entry['shortDescription'] = { 'text' => described[:short] }
|
|
107
|
+
entry['fullDescription'] = { 'text' => described[:full] }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
entry
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @param offense [Hash] an offense hash
|
|
114
|
+
# @param rule_index [Hash{String => Integer}] validator name to rule index
|
|
115
|
+
# @return [Hash] a SARIF result
|
|
116
|
+
def result(offense, rule_index)
|
|
117
|
+
uri = relative_uri(offense[:location])
|
|
118
|
+
entry = {}
|
|
119
|
+
|
|
120
|
+
# ruleId/ruleIndex are optional in SARIF; omit them (rather than emit
|
|
121
|
+
# null, which is schema-invalid) for the rare offense with no validator.
|
|
122
|
+
validator = offense[:validator]
|
|
123
|
+
if validator
|
|
124
|
+
entry['ruleId'] = validator
|
|
125
|
+
entry['ruleIndex'] = rule_index[validator]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
entry['level'] = level(offense[:severity])
|
|
129
|
+
entry['message'] = { 'text' => offense[:message].to_s }
|
|
130
|
+
entry['locations'] = [location(offense, uri)]
|
|
131
|
+
entry['partialFingerprints'] = { FINGERPRINT_KEY => fingerprint(offense, uri) }
|
|
132
|
+
entry
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param offense [Hash] an offense hash
|
|
136
|
+
# @param uri [String] the artifact URI (already made relative)
|
|
137
|
+
# @return [Hash] a SARIF location
|
|
138
|
+
def location(offense, uri)
|
|
139
|
+
{
|
|
140
|
+
'physicalLocation' => {
|
|
141
|
+
'artifactLocation' => { 'uri' => uri },
|
|
142
|
+
'region' => { 'startLine' => start_line(offense) }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# A line-independent fingerprint so GitHub correlates the same offense
|
|
148
|
+
# across commits without churn (close/reopen) when unrelated lines shift.
|
|
149
|
+
# Keys on the validator, file, and offending object, excluding the line
|
|
150
|
+
# number and (when an object is identified) the message, so a re-worded
|
|
151
|
+
# message does not churn alerts. Validators that do not identify an object
|
|
152
|
+
# fall back to a normalized message, so distinct offenses in one file
|
|
153
|
+
# still get distinct fingerprints instead of collapsing into one alert.
|
|
154
|
+
# @param offense [Hash] an offense hash
|
|
155
|
+
# @param uri [String] the artifact URI (already made relative)
|
|
156
|
+
# @return [String] a stable hex digest
|
|
157
|
+
def fingerprint(offense, uri)
|
|
158
|
+
element = offense[:element].to_s
|
|
159
|
+
# Tag the identity with its source so an element that happens to equal
|
|
160
|
+
# another offense's normalized message cannot collide.
|
|
161
|
+
identity = element.empty? ? "m:#{normalized_message(offense[:message])}" : "e:#{element}"
|
|
162
|
+
parts = [offense[:validator], uri, identity]
|
|
163
|
+
Digest::SHA256.hexdigest(parts.map(&:to_s).join("\x00"))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Message reduced to a stable identity for offenses with no :element:
|
|
167
|
+
# whitespace is collapsed so trivial formatting differences do not churn
|
|
168
|
+
# alerts. Case is preserved - messages embed case-sensitive names (types,
|
|
169
|
+
# methods, constants), so downcasing could merge genuinely distinct offenses.
|
|
170
|
+
# @param message [String, nil] the offense message
|
|
171
|
+
# @return [String] the normalized message
|
|
172
|
+
def normalized_message(message)
|
|
173
|
+
message.to_s.gsub(/\s+/, ' ').strip
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# @param severity [String, nil] a yard-lint severity
|
|
177
|
+
# @return [String] the SARIF level
|
|
178
|
+
def level(severity)
|
|
179
|
+
SEVERITY_LEVELS.fetch(severity.to_s, DEFAULT_LEVEL)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# SARIF regions are 1-based; guard against a missing or zero line.
|
|
183
|
+
# @param offense [Hash] an offense hash
|
|
184
|
+
# @return [Integer] a 1-based start line
|
|
185
|
+
def start_line(offense)
|
|
186
|
+
line = (offense[:location_line] || offense[:line]).to_i
|
|
187
|
+
line.positive? ? line : 1
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Make a path repository-relative so GitHub maps it onto a file. Paths
|
|
191
|
+
# already relative are kept; a path outside base_dir keeps its original
|
|
192
|
+
# value rather than emitting a `../` or absolute URI.
|
|
193
|
+
# @param location [String, nil] the offense location path
|
|
194
|
+
# @return [String] a relative URI where possible
|
|
195
|
+
def relative_uri(location)
|
|
196
|
+
path = location.to_s
|
|
197
|
+
return path if path.empty?
|
|
198
|
+
|
|
199
|
+
absolute = File.absolute_path(path, @base_dir)
|
|
200
|
+
relative = Pathname.new(absolute).relative_path_from(Pathname.new(@base_dir)).to_s
|
|
201
|
+
relative.start_with?('..') ? path : relative
|
|
202
|
+
rescue ArgumentError
|
|
203
|
+
# relative_path_from raises across platforms/mixed roots; fall back.
|
|
204
|
+
path
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The validator's default severity, from a defaults-only config, for the
|
|
208
|
+
# rule's defaultConfiguration.level.
|
|
209
|
+
# @param id [String] validator name
|
|
210
|
+
# @return [String] default severity
|
|
211
|
+
def default_severity(id)
|
|
212
|
+
@default_config ||= Config.new
|
|
213
|
+
@default_config.validator_severity(id)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
data/lib/yard/lint/git.rb
CHANGED
|
@@ -89,15 +89,17 @@ module Yard
|
|
|
89
89
|
filter_ruby_files(files.uniq, path)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
92
|
+
# Absolute path to the repository root (git reports paths relative to it).
|
|
93
|
+
# Used to make report paths repository-relative; falls back to the current
|
|
94
|
+
# directory outside a git repo.
|
|
95
95
|
# @return [String] repo root, or the current directory if it can't be determined
|
|
96
96
|
def repository_root
|
|
97
97
|
stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--show-toplevel')
|
|
98
98
|
status.success? ? stdout.strip : Dir.pwd
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
private
|
|
102
|
+
|
|
101
103
|
# Ensure we're in a git repository
|
|
102
104
|
# @raise [Error] if not in a git repository
|
|
103
105
|
def ensure_git_repository!
|
|
@@ -22,8 +22,7 @@ module Yard
|
|
|
22
22
|
validator_cfg = ConfigLoader.validator_config(validator_name)
|
|
23
23
|
return nil unless validator_module && validator_cfg
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
return nil if composite_child?(validator_name)
|
|
25
|
+
return nil if reported_by_composite?(validator_name)
|
|
27
26
|
|
|
28
27
|
# Handle composite validators (those that combine multiple validators)
|
|
29
28
|
unless validator_cfg.combines_with.empty?
|
|
@@ -36,15 +35,17 @@ module Yard
|
|
|
36
35
|
|
|
37
36
|
private
|
|
38
37
|
|
|
39
|
-
# Check if a
|
|
38
|
+
# Check if a composite parent reports this validator's offenses
|
|
39
|
+
# A child reports on its own only when --only selects it without its parent
|
|
40
40
|
# @param validator_name [String] validator name to check
|
|
41
|
-
# @return [Boolean] true if
|
|
42
|
-
def
|
|
41
|
+
# @return [Boolean] true if a composite parent reports it
|
|
42
|
+
def reported_by_composite?(validator_name)
|
|
43
43
|
ConfigLoader::ALL_VALIDATORS.any? do |parent_name|
|
|
44
44
|
parent_cfg = ConfigLoader.validator_config(parent_name)
|
|
45
45
|
next unless parent_cfg
|
|
46
|
+
next unless parent_cfg.combines_with.include?(validator_name)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
config.only_validators.empty? || config.only_validators.include?(parent_name)
|
|
48
49
|
end
|
|
49
50
|
end
|
|
50
51
|
|
data/lib/yard/lint/runner.rb
CHANGED
|
@@ -102,11 +102,7 @@ module Yard
|
|
|
102
102
|
validator_excludes = config.validator_exclude(validator_name)
|
|
103
103
|
return files if validator_excludes.empty?
|
|
104
104
|
|
|
105
|
-
files.reject
|
|
106
|
-
validator_excludes.any? do |pattern|
|
|
107
|
-
File.fnmatch(pattern, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
105
|
+
files.reject { |file| config.excluded_path?(file, validator_excludes) }
|
|
110
106
|
end
|
|
111
107
|
|
|
112
108
|
# Filter result offenses based on per-validator exclusions
|
|
@@ -124,23 +120,8 @@ module Yard
|
|
|
124
120
|
return nil if result.offenses.empty?
|
|
125
121
|
return result if validator_excludes.empty?
|
|
126
122
|
|
|
127
|
-
working_dir = Dir.pwd
|
|
128
|
-
|
|
129
123
|
filtered_offenses = result.offenses.reject do |offense|
|
|
130
|
-
|
|
131
|
-
next true unless file_path
|
|
132
|
-
|
|
133
|
-
# Convert to relative path for pattern matching
|
|
134
|
-
relative_path = if file_path.start_with?(working_dir)
|
|
135
|
-
file_path.sub(%r{^#{Regexp.escape(working_dir)}/}, '')
|
|
136
|
-
else
|
|
137
|
-
file_path
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Check if file matches any exclusion pattern
|
|
141
|
-
validator_excludes.any? do |pattern|
|
|
142
|
-
File.fnmatch(pattern, relative_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
143
|
-
end
|
|
124
|
+
config.excluded_path?(offense[:location] || offense[:file], validator_excludes)
|
|
144
125
|
end
|
|
145
126
|
|
|
146
127
|
# Return nil if no offenses remain after filtering
|
|
@@ -156,6 +156,11 @@ Documentation/DuplicateNamespaceComment:
|
|
|
156
156
|
Enabled: true
|
|
157
157
|
Severity: warning
|
|
158
158
|
|
|
159
|
+
Documentation/ReferenceTypo:
|
|
160
|
+
Description: 'Detects probable typos in method and constant references.'
|
|
161
|
+
Enabled: true
|
|
162
|
+
Severity: warning
|
|
163
|
+
|
|
159
164
|
# Tags validators
|
|
160
165
|
Tags/Order:
|
|
161
166
|
Description: 'Enforces consistent ordering of YARD tags.'
|
|
@@ -381,6 +386,11 @@ Tags/ForbiddenTags:
|
|
|
381
386
|
# - Object
|
|
382
387
|
# - Tag: api # Forbids @api tag entirely (no Types = any occurrence)
|
|
383
388
|
|
|
389
|
+
Tags/OverloadConsistency:
|
|
390
|
+
Description: 'Checks the tags of each @overload against its own signature.'
|
|
391
|
+
Enabled: true
|
|
392
|
+
Severity: warning
|
|
393
|
+
|
|
384
394
|
# Warnings validators - catches YARD parser errors
|
|
385
395
|
Warnings/SyntaxError:
|
|
386
396
|
Description: 'Detects Ruby files YARD cannot parse (syntax errors).'
|
|
@@ -154,6 +154,11 @@ Documentation/DuplicateNamespaceComment:
|
|
|
154
154
|
Enabled: true
|
|
155
155
|
Severity: error
|
|
156
156
|
|
|
157
|
+
Documentation/ReferenceTypo:
|
|
158
|
+
Description: 'Detects probable typos in method and constant references.'
|
|
159
|
+
Enabled: true
|
|
160
|
+
Severity: error
|
|
161
|
+
|
|
157
162
|
# Tags validators
|
|
158
163
|
Tags/Order:
|
|
159
164
|
Description: 'Enforces consistent ordering of YARD tags.'
|
|
@@ -379,6 +384,11 @@ Tags/ForbiddenTags:
|
|
|
379
384
|
# - Object
|
|
380
385
|
# - Tag: api # Forbids @api tag entirely (no Types = any occurrence)
|
|
381
386
|
|
|
387
|
+
Tags/OverloadConsistency:
|
|
388
|
+
Description: 'Checks the tags of each @overload against its own signature.'
|
|
389
|
+
Enabled: true
|
|
390
|
+
Severity: error
|
|
391
|
+
|
|
382
392
|
# Warnings validators - catches YARD parser errors
|
|
383
393
|
Warnings/SyntaxError:
|
|
384
394
|
Description: 'Detects Ruby files YARD cannot parse (syntax errors).'
|
|
@@ -14,12 +14,20 @@ module Yard
|
|
|
14
14
|
# @param force [Boolean] whether to overwrite existing todo file if present
|
|
15
15
|
# @param exclude_limit [Integer] minimum files in directory before grouping into wildcard patterns
|
|
16
16
|
# @param config_path [String, nil] config file to link the todo into
|
|
17
|
-
# (defaults to .yard-lint.yml in the
|
|
17
|
+
# (defaults to .yard-lint.yml in the config directory)
|
|
18
18
|
# @return [Hash] result with :message, :offense_count, :validator_count
|
|
19
19
|
def generate(path:, config:, force: false, exclude_limit: DEFAULT_EXCLUDE_LIMIT, config_path: nil)
|
|
20
20
|
new(path: path, config: config, force: force, exclude_limit: exclude_limit,
|
|
21
21
|
config_path: config_path).generate
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
# Report .yard-lint-todo.yml Exclude entries that no longer match any offense
|
|
25
|
+
# @param path [String] directory or file path containing Ruby files to analyze for violations
|
|
26
|
+
# @param config [Config] yard-lint configuration object with validator settings
|
|
27
|
+
# @return [Hash] result with :message and :unused_entries (validator name => unused patterns)
|
|
28
|
+
def check(path:, config:)
|
|
29
|
+
new(path: path, config: config, force: false, exclude_limit: DEFAULT_EXCLUDE_LIMIT).check
|
|
30
|
+
end
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
# Initialize a new TodoGenerator instance
|
|
@@ -33,8 +41,8 @@ module Yard
|
|
|
33
41
|
@config = config
|
|
34
42
|
@force = force
|
|
35
43
|
@exclude_limit = exclude_limit
|
|
36
|
-
@todo_path = File.join(
|
|
37
|
-
@config_path = config_path || File.join(
|
|
44
|
+
@todo_path = File.join(config.base_dir, '.yard-lint-todo.yml')
|
|
45
|
+
@config_path = config_path || File.join(config.base_dir, Config::DEFAULT_CONFIG_FILE)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
# Generate the .yard-lint-todo.yml file with exclusions for current violations
|
|
@@ -65,8 +73,130 @@ module Yard
|
|
|
65
73
|
build_success_result(violations_by_validator, lint_result[:total_offenses])
|
|
66
74
|
end
|
|
67
75
|
|
|
76
|
+
# Re-lint without the todo file exclusions and find entries that match no offense
|
|
77
|
+
# Offenses can depend on the other linted files and on their order, so an entry counts as
|
|
78
|
+
# used when it matches an offense of a whole-project run or of a run on the path only.
|
|
79
|
+
# The path only limits which entries are judged: those that cover a file under it,
|
|
80
|
+
# or no project file at all. The given config is not modified.
|
|
81
|
+
# @return [Hash] result with :message and :unused_entries (validator name => unused patterns)
|
|
82
|
+
# @raise [Errors::TodoFileNotFoundError] if the todo file does not exist
|
|
83
|
+
def check
|
|
84
|
+
todo_entries = load_todo_entries
|
|
85
|
+
audit_config = build_audit_config(todo_entries)
|
|
86
|
+
path_files = Yard::Lint.send(:expand_path, @path, audit_config)
|
|
87
|
+
files = Yard::Lint.send(:expand_path, '.', audit_config) | path_files
|
|
88
|
+
|
|
89
|
+
violations_by_validator = audit_violations([files, path_files], audit_config)
|
|
90
|
+
files = files.map { |file| make_relative_path(file) }
|
|
91
|
+
path_files = path_files.map { |file| make_relative_path(file) }
|
|
92
|
+
|
|
93
|
+
unused_entries = todo_entries.each_with_object({}) do |(validator_name, patterns), unused|
|
|
94
|
+
next unless judged?(validator_name, audit_config)
|
|
95
|
+
|
|
96
|
+
violating_files = violations_by_validator.fetch(validator_name, [])
|
|
97
|
+
stale = patterns.select do |pattern|
|
|
98
|
+
in_scope?(pattern, files, path_files) && violating_files.none? { |file| todo_match?(pattern, file) }
|
|
99
|
+
end
|
|
100
|
+
unused[validator_name] = stale if stale.any?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
{ message: build_check_message(unused_entries), unused_entries: unused_entries }
|
|
104
|
+
end
|
|
105
|
+
|
|
68
106
|
private
|
|
69
107
|
|
|
108
|
+
# Load per-validator Exclude patterns from the todo file
|
|
109
|
+
# @return [Hash{String => Array<String>}] validator names to their todo Exclude patterns
|
|
110
|
+
# @raise [Errors::TodoFileNotFoundError] if the todo file does not exist
|
|
111
|
+
def load_todo_entries
|
|
112
|
+
unless File.exist?(@todo_path)
|
|
113
|
+
raise Errors::TodoFileNotFoundError,
|
|
114
|
+
'.yard-lint-todo.yml not found. Use --auto-gen-config to generate it.'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
ConfigLoader.load_yaml_file(@todo_path).each_with_object({}) do |(validator_name, settings), entries|
|
|
118
|
+
next unless settings.is_a?(Hash) && settings['Exclude'].is_a?(Array)
|
|
119
|
+
|
|
120
|
+
entries[validator_name] = settings['Exclude'].map(&:to_s)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Build a copy of the config without the todo file's Exclude lists
|
|
125
|
+
# A per-validator Exclude replaces an inherited one, so a validator's todo list is in effect
|
|
126
|
+
# only when it is that validator's whole Exclude. Otherwise the user's Exclude stays as is.
|
|
127
|
+
# @param todo_entries [Hash{String => Array<String>}] validator names to todo Exclude patterns
|
|
128
|
+
# @return [Config] configuration for the audit run
|
|
129
|
+
def build_audit_config(todo_entries)
|
|
130
|
+
audit_config = Config.new(Marshal.load(Marshal.dump(@config.raw_config)))
|
|
131
|
+
audit_config.base_dir = @config.base_dir
|
|
132
|
+
audit_config.only_validators = @config.only_validators.dup
|
|
133
|
+
|
|
134
|
+
todo_entries.each do |validator_name, patterns|
|
|
135
|
+
next unless ConfigLoader::ALL_VALIDATORS.include?(validator_name)
|
|
136
|
+
next unless audit_config.validator_config(validator_name, 'Exclude') == patterns
|
|
137
|
+
|
|
138
|
+
audit_config.set_validator_config(validator_name, 'Exclude', [])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
audit_config
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Lint each file set and merge the violating files per validator
|
|
145
|
+
# @param file_sets [Array<Array<String>>] file sets to lint separately
|
|
146
|
+
# @param config [Config] configuration for the audit run
|
|
147
|
+
# @return [Hash{String => Array<String>}] validator names to files with offenses in any run
|
|
148
|
+
def audit_violations(file_sets, config)
|
|
149
|
+
file_sets.uniq(&:sort).each_with_object({}) do |files, merged|
|
|
150
|
+
run_linting(files: files, config: config)[:violations_by_validator].each do |validator_name, paths|
|
|
151
|
+
merged[validator_name] = merged.fetch(validator_name, []) | paths
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Whether the audit can judge a validator's todo entries
|
|
157
|
+
# Entries of validators left out by only_validators are not judged, as they were not run.
|
|
158
|
+
# @param validator_name [String] validator name from the todo file
|
|
159
|
+
# @param config [Config] configuration for the audit run
|
|
160
|
+
# @return [Boolean] true unless only_validators leaves the validator out
|
|
161
|
+
def judged?(validator_name, config)
|
|
162
|
+
config.only_validators.empty? || config.only_validators.include?(validator_name)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Whether a todo pattern belongs to the checked path
|
|
166
|
+
# @param pattern [String] todo Exclude pattern
|
|
167
|
+
# @param files [Array<String>] relative paths of all linted files
|
|
168
|
+
# @param path_files [Array<String>] relative paths of the files under the checked path
|
|
169
|
+
# @return [Boolean] true if the pattern covers a file under the path, or no linted file at all
|
|
170
|
+
def in_scope?(pattern, files, path_files)
|
|
171
|
+
path_files.any? { |file| todo_match?(pattern, file) } ||
|
|
172
|
+
files.none? { |file| todo_match?(pattern, file) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Whether a todo Exclude pattern matches a file
|
|
176
|
+
# @param pattern [String] todo Exclude pattern
|
|
177
|
+
# @param file [String] relative file path
|
|
178
|
+
# @return [Boolean] true if the pattern matches the file
|
|
179
|
+
def todo_match?(pattern, file)
|
|
180
|
+
File.fnmatch(pattern, file, Config::EXCLUDE_FLAGS)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Build the summary message for a todo check
|
|
184
|
+
# @param unused_entries [Hash{String => Array<String>}] validator names to unused patterns
|
|
185
|
+
# @return [String] human-readable summary
|
|
186
|
+
def build_check_message(unused_entries)
|
|
187
|
+
return 'No unused entries found in .yard-lint-todo.yml' if unused_entries.empty?
|
|
188
|
+
|
|
189
|
+
count = unused_entries.values.sum(&:size)
|
|
190
|
+
lines = ["Found #{count} unused entr#{count == 1 ? 'y' : 'ies'} in .yard-lint-todo.yml:"]
|
|
191
|
+
unused_entries.each do |validator_name, patterns|
|
|
192
|
+
lines << " #{validator_name}:"
|
|
193
|
+
patterns.each { |pattern| lines << " - '#{pattern}'" }
|
|
194
|
+
end
|
|
195
|
+
lines << ''
|
|
196
|
+
lines << 'Remove these entries, or run yard-lint --regenerate-todo to rebuild the file.'
|
|
197
|
+
lines.join("\n")
|
|
198
|
+
end
|
|
199
|
+
|
|
70
200
|
# Validate that the todo file doesn't already exist
|
|
71
201
|
# @return [void]
|
|
72
202
|
# @raise [Errors::TodoFileExistsError] if todo file exists and force is false
|
|
@@ -78,23 +208,25 @@ module Yard
|
|
|
78
208
|
end
|
|
79
209
|
|
|
80
210
|
# Run linting and collect violations per validator
|
|
211
|
+
# @param files [Array<String>, nil] files to lint (defaults to the files under the path)
|
|
212
|
+
# @param config [Config] configuration to lint with
|
|
81
213
|
# @return [Hash] hash with :violations_by_validator and :total_offenses keys
|
|
82
|
-
def run_linting
|
|
214
|
+
def run_linting(files: nil, config: @config)
|
|
83
215
|
# Run each validator individually to track violations per validator
|
|
84
216
|
# This is more reliable than trying to infer validator from offense name
|
|
85
|
-
files
|
|
86
|
-
runner = Runner.new(files,
|
|
217
|
+
files ||= Yard::Lint.send(:expand_path, @path, config)
|
|
218
|
+
runner = Runner.new(files, config)
|
|
87
219
|
|
|
88
220
|
# Run validators and collect raw results
|
|
89
221
|
raw_results = runner.send(:run_validators)
|
|
90
|
-
result_builder = ResultBuilder.new(
|
|
222
|
+
result_builder = ResultBuilder.new(config)
|
|
91
223
|
|
|
92
224
|
violations_by_validator = {}
|
|
93
225
|
total_offenses = 0
|
|
94
226
|
|
|
95
227
|
# Process each validator's results
|
|
96
228
|
ConfigLoader::ALL_VALIDATORS.each do |validator_name|
|
|
97
|
-
next unless
|
|
229
|
+
next unless config.validator_enabled?(validator_name)
|
|
98
230
|
|
|
99
231
|
validator_result = result_builder.build(validator_name, raw_results)
|
|
100
232
|
next unless validator_result
|
|
@@ -137,14 +269,13 @@ module Yard
|
|
|
137
269
|
end
|
|
138
270
|
end
|
|
139
271
|
|
|
140
|
-
# Convert
|
|
272
|
+
# Convert a path to the form Exclude patterns match (relative to the config directory)
|
|
141
273
|
# @param path [String, nil] absolute or relative file path
|
|
142
|
-
# @return [String, nil] relative
|
|
274
|
+
# @return [String, nil] path relative to the config directory, or nil if path is nil
|
|
143
275
|
def make_relative_path(path)
|
|
144
276
|
return path if path.nil?
|
|
145
277
|
|
|
146
|
-
|
|
147
|
-
path.start_with?(pwd) ? path.sub("#{pwd}/", '') : path
|
|
278
|
+
@config.relative_path(path)
|
|
148
279
|
end
|
|
149
280
|
|
|
150
281
|
# Build YAML content for the todo file
|
|
@@ -155,6 +155,9 @@ module Yard
|
|
|
155
155
|
# when the object does not come from module_function
|
|
156
156
|
def module_function_twin(object)
|
|
157
157
|
return nil unless object.respond_to?(:scope)
|
|
158
|
+
# `def CONST.name` leaves a Proxy or ConstantObject namespace with no
|
|
159
|
+
# children; Proxy#is_a? answers for the resolved object
|
|
160
|
+
return nil unless object.namespace.is_a?(YARD::CodeObjects::NamespaceObject)
|
|
158
161
|
|
|
159
162
|
twin = object.namespace.child(
|
|
160
163
|
name: object.name,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module ReferenceTypo
|
|
8
|
+
# Configuration for ReferenceTypo validator
|
|
9
|
+
class Config < ::Yard::Lint::Validators::Config
|
|
10
|
+
self.id = :reference_typo
|
|
11
|
+
self.defaults = {
|
|
12
|
+
'Enabled' => true,
|
|
13
|
+
'Severity' => 'warning'
|
|
14
|
+
}.freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module ReferenceTypo
|
|
8
|
+
# Builds human-readable messages for ReferenceTypo violations
|
|
9
|
+
class MessagesBuilder
|
|
10
|
+
class << self
|
|
11
|
+
# Formats a reference typo violation message
|
|
12
|
+
# @param offense [Hash] offense details with :reference and :suggestion keys
|
|
13
|
+
# @return [String] formatted message
|
|
14
|
+
def call(offense)
|
|
15
|
+
"Reference '#{offense[:reference]}' does not resolve " \
|
|
16
|
+
"(did you mean '#{offense[:suggestion]}'?)"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module ReferenceTypo
|
|
8
|
+
# Parser for ReferenceTypo validator output
|
|
9
|
+
class Parser < ::Yard::Lint::Parsers::Base
|
|
10
|
+
# Parses validator output and extracts reference typo violations
|
|
11
|
+
# Expected format (two lines per violation):
|
|
12
|
+
# file.rb:LINE: ObjectName
|
|
13
|
+
# reference|suggestion
|
|
14
|
+
# @param yard_output [String] raw validator output
|
|
15
|
+
# @return [Array<Hash>] array with violation details
|
|
16
|
+
def call(yard_output, **)
|
|
17
|
+
return [] if yard_output.nil? || yard_output.strip.empty?
|
|
18
|
+
|
|
19
|
+
lines = yard_output.split("\n").map(&:strip).reject(&:empty?)
|
|
20
|
+
|
|
21
|
+
lines.each_slice(2).filter_map do |location_line, details_line|
|
|
22
|
+
next unless location_line && details_line
|
|
23
|
+
|
|
24
|
+
location_match = location_line.match(/^(.+):(\d+): (.+)$/)
|
|
25
|
+
next unless location_match
|
|
26
|
+
|
|
27
|
+
reference, suggestion = details_line.split('|', 2)
|
|
28
|
+
next if reference.to_s.empty? || suggestion.to_s.empty?
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
location: location_match[1],
|
|
32
|
+
line: location_match[2].to_i,
|
|
33
|
+
object_name: location_match[3],
|
|
34
|
+
reference: reference,
|
|
35
|
+
suggestion: suggestion
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|