yard-lint 1.12.0 → 1.13.1
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 +202 -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 +26 -8
- 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,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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
module ReferenceTypo
|
|
8
|
+
# Result wrapper for ReferenceTypo validator
|
|
9
|
+
class Result < Results::Base
|
|
10
|
+
self.default_severity = 'warning'
|
|
11
|
+
self.offense_type = 'line'
|
|
12
|
+
self.offense_name = 'ReferenceTypo'
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Builds a human-readable message for the offense
|
|
17
|
+
# @param offense [Hash] offense details
|
|
18
|
+
# @return [String] formatted message
|
|
19
|
+
def build_message(offense)
|
|
20
|
+
MessagesBuilder.call(offense)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'did_you_mean'
|
|
4
|
+
|
|
5
|
+
module Yard
|
|
6
|
+
module Lint
|
|
7
|
+
module Validators
|
|
8
|
+
module Documentation
|
|
9
|
+
module ReferenceTypo
|
|
10
|
+
# Validates that references in documentation are not probable typos
|
|
11
|
+
class Validator < Base
|
|
12
|
+
# Same matching rules YARD uses for `{name}` / `{name title}` links
|
|
13
|
+
LINK_PATTERN = /(?<![\\!])\{(?!\})(\S+?)(?:\s[^}]*)?\}/
|
|
14
|
+
|
|
15
|
+
# Code spans, fenced and indented code blocks hold code, not links
|
|
16
|
+
CODE_PATTERN = /```.*?```|`[^`\n]*`|^(?: {4}|\t)[^\n]*/m
|
|
17
|
+
|
|
18
|
+
# `Foo::Bar#baz`, `Foo.baz` or a namespace-relative `#baz`
|
|
19
|
+
METHOD_REFERENCE = /\A(?<namespace>(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*)?(?<separator>[#.])(?<name>[A-Za-z_]\w*[?!=]?)\z/
|
|
20
|
+
|
|
21
|
+
# `Foo`, `Foo::Bar` or `::Foo::BAR`
|
|
22
|
+
CONSTANT_REFERENCE = /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/
|
|
23
|
+
|
|
24
|
+
in_process visibility: :all
|
|
25
|
+
|
|
26
|
+
# Execute query for a single object during in-process execution.
|
|
27
|
+
# @param object [YARD::CodeObjects::Base] the code object to query
|
|
28
|
+
# @param collector [Executor::ResultCollector] collector for output
|
|
29
|
+
def in_process_query(object, collector)
|
|
30
|
+
return if object.type == :method && object.is_alias?
|
|
31
|
+
return if module_function_copy?(object)
|
|
32
|
+
return if duplicate_docstring?(object)
|
|
33
|
+
|
|
34
|
+
references(object).uniq.each do |reference|
|
|
35
|
+
suggestion = suggest(object, reference)
|
|
36
|
+
next unless suggestion
|
|
37
|
+
|
|
38
|
+
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
39
|
+
collector.puts "#{reference}|#{suggestion}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# @param object [YARD::CodeObjects::Base] documented object
|
|
46
|
+
# @return [Array<String>] link targets and `@see` names in its docs
|
|
47
|
+
def references(object)
|
|
48
|
+
overloads = object.tags(:overload)
|
|
49
|
+
tags = object.tags + overloads.flat_map { |overload| overload.docstring.tags }
|
|
50
|
+
|
|
51
|
+
texts = [object.docstring.to_s] + overloads.map { |overload| overload.docstring.to_s }
|
|
52
|
+
tags.each do |tag|
|
|
53
|
+
texts << tag.text.to_s unless tag.tag_name == 'example'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
links = texts.flat_map do |text|
|
|
57
|
+
text.gsub(CODE_PATTERN, '').scan(LINK_PATTERN).map(&:first)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
links + tags.select { |tag| tag.tag_name == 'see' }.map { |tag| tag.name.to_s }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param object [YARD::CodeObjects::Base] documented object
|
|
64
|
+
# @param reference [String] referenced name
|
|
65
|
+
# @return [String, nil] corrected reference for a probable typo
|
|
66
|
+
def suggest(object, reference)
|
|
67
|
+
if (match = METHOD_REFERENCE.match(reference))
|
|
68
|
+
return if resolve(object, reference)
|
|
69
|
+
|
|
70
|
+
suggest_method(object, match)
|
|
71
|
+
elsif CONSTANT_REFERENCE.match?(reference)
|
|
72
|
+
suggest_constant(object, reference)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param object [YARD::CodeObjects::Base] documented object
|
|
77
|
+
# @param match [MatchData] parts of a method reference
|
|
78
|
+
# @return [String, nil] corrected method reference
|
|
79
|
+
def suggest_method(object, match)
|
|
80
|
+
namespace_name = match[:namespace]
|
|
81
|
+
namespace = namespace_name ? resolve(object, namespace_name) : lexical_namespace(object)
|
|
82
|
+
|
|
83
|
+
unless namespace
|
|
84
|
+
suggestion = namespace_name && suggest_constant(object, namespace_name)
|
|
85
|
+
|
|
86
|
+
return suggestion && "#{suggestion}#{match[:separator]}#{match[:name]}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return unless namespace.is_a?(YARD::CodeObjects::NamespaceObject)
|
|
90
|
+
|
|
91
|
+
scope = match[:separator] == '#' ? :instance : :class
|
|
92
|
+
return if external_method?(namespace, match[:name], scope)
|
|
93
|
+
|
|
94
|
+
candidates = namespace.meths(scope: scope).map { |meth| meth.name.to_s }
|
|
95
|
+
# YARD does not resolve a method found more than one namespace up
|
|
96
|
+
return if candidates.include?(match[:name])
|
|
97
|
+
|
|
98
|
+
suggestion = spell_check(match[:name], candidates)
|
|
99
|
+
|
|
100
|
+
suggestion && "#{namespace_name}#{match[:separator]}#{suggestion}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param object [YARD::CodeObjects::Base] documented object
|
|
104
|
+
# @param reference [String] constant reference
|
|
105
|
+
# @return [String, nil] corrected constant reference
|
|
106
|
+
def suggest_constant(object, reference)
|
|
107
|
+
return if resolve(object, reference)
|
|
108
|
+
return if runtime_constant?(reference)
|
|
109
|
+
|
|
110
|
+
parent_name, _, name = reference.rpartition('::')
|
|
111
|
+
|
|
112
|
+
if parent_name.empty?
|
|
113
|
+
return if reference.start_with?('::')
|
|
114
|
+
|
|
115
|
+
candidates = visible_constants(lexical_namespace(object))
|
|
116
|
+
else
|
|
117
|
+
parent = resolve(object, parent_name)
|
|
118
|
+
return unless parent.is_a?(YARD::CodeObjects::NamespaceObject)
|
|
119
|
+
|
|
120
|
+
candidates = constant_names(parent)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
suggestion = spell_check(name, candidates)
|
|
124
|
+
return unless suggestion
|
|
125
|
+
|
|
126
|
+
parent_name.empty? ? suggestion : "#{parent_name}::#{suggestion}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param object [YARD::CodeObjects::Base] lookup context
|
|
130
|
+
# @param name [String] name to resolve
|
|
131
|
+
# @return [YARD::CodeObjects::Base, nil] resolved registry object
|
|
132
|
+
def resolve(object, name)
|
|
133
|
+
YARD::Registry.resolve(object, name, true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @param object [YARD::CodeObjects::Base] documented object
|
|
137
|
+
# @return [YARD::CodeObjects::Base, YARD::CodeObjects::Proxy] namespace links resolve against
|
|
138
|
+
def lexical_namespace(object)
|
|
139
|
+
object.is_a?(YARD::CodeObjects::NamespaceObject) ? object : object.namespace
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Root-level names are left out: a bare name near one of them is as
|
|
143
|
+
# likely a constant of a gem YARD did not parse as it is a typo
|
|
144
|
+
# @param namespace [YARD::CodeObjects::Base, YARD::CodeObjects::Proxy] innermost namespace
|
|
145
|
+
# @return [Array<String>] constant names visible from it
|
|
146
|
+
def visible_constants(namespace)
|
|
147
|
+
names = []
|
|
148
|
+
|
|
149
|
+
# `def CONST.name` and `class Unknown::Name` leave unresolved
|
|
150
|
+
# namespaces, which have no children to list
|
|
151
|
+
while namespace.is_a?(YARD::CodeObjects::NamespaceObject) && !namespace.root?
|
|
152
|
+
names.concat(constant_names(namespace))
|
|
153
|
+
namespace = namespace.namespace
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
names.uniq
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @param namespace [YARD::CodeObjects::NamespaceObject] namespace to list
|
|
160
|
+
# @return [Array<String>] names of its constants, classes and modules
|
|
161
|
+
def constant_names(namespace)
|
|
162
|
+
namespace.children.filter_map do |child|
|
|
163
|
+
child.name.to_s if %i[constant class module].include?(child.type)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A method may come from an ancestor YARD does not know about, so it
|
|
168
|
+
# is checked against the loaded Ruby ancestors as well
|
|
169
|
+
# @param namespace [YARD::CodeObjects::NamespaceObject] referenced namespace
|
|
170
|
+
# @param name [String] referenced method name
|
|
171
|
+
# @param scope [Symbol] :instance or :class
|
|
172
|
+
# @return [Boolean] true when the method may exist outside the registry
|
|
173
|
+
def external_method?(namespace, name, scope)
|
|
174
|
+
ancestors = namespace.inheritance_tree(true).grep(YARD::CodeObjects::Proxy)
|
|
175
|
+
ancestors << YARD::CodeObjects::Proxy.new(:root, 'Object')
|
|
176
|
+
ancestors << YARD::CodeObjects::Proxy.new(:root, 'Class') if scope == :class
|
|
177
|
+
|
|
178
|
+
ancestors.any? do |proxy|
|
|
179
|
+
constant = runtime_constant(proxy.path)
|
|
180
|
+
# An ancestor that is not loaded here may define anything
|
|
181
|
+
next true unless constant.is_a?(Module)
|
|
182
|
+
|
|
183
|
+
constant.method_defined?(name) || constant.private_method_defined?(name)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @param path [String] constant path
|
|
188
|
+
# @return [Boolean] true when the constant is defined in this Ruby process
|
|
189
|
+
def runtime_constant?(path)
|
|
190
|
+
!runtime_constant(path).nil?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @param path [String] constant path
|
|
194
|
+
# @return [Object, nil] the constant's value, if it is loaded
|
|
195
|
+
def runtime_constant(path)
|
|
196
|
+
Object.const_get(path.delete_prefix('::'))
|
|
197
|
+
rescue NameError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @param name [String] misspelled name
|
|
202
|
+
# @param candidates [Array<String>] known names
|
|
203
|
+
# @return [String, nil] closest known name
|
|
204
|
+
def spell_check(name, candidates)
|
|
205
|
+
return if candidates.empty?
|
|
206
|
+
|
|
207
|
+
DidYouMean::SpellChecker.new(dictionary: candidates).correct(name).first
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yard
|
|
4
|
+
module Lint
|
|
5
|
+
module Validators
|
|
6
|
+
module Documentation
|
|
7
|
+
# ReferenceTypo validator
|
|
8
|
+
#
|
|
9
|
+
# Detects probable typos in method and constant names referenced from
|
|
10
|
+
# documentation: `{Foo#bar}` links in docstring and tag text, and `@see`
|
|
11
|
+
# targets. A reference is reported only when it does not resolve in the
|
|
12
|
+
# YARD registry, its namespace does, and a close match exists there.
|
|
13
|
+
# References into namespaces YARD does not know (stdlib, other gems) are
|
|
14
|
+
# skipped, as are methods a loaded Ruby ancestor may define. A bare
|
|
15
|
+
# constant is compared only with constants of its enclosing namespaces,
|
|
16
|
+
# not with top-level ones. This validator is enabled by default.
|
|
17
|
+
#
|
|
18
|
+
# @example Bad - link to a misspelled method
|
|
19
|
+
# # Delegates to {Parser#prase}
|
|
20
|
+
# def call
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Good - link to an existing method
|
|
24
|
+
# # Delegates to {Parser#parse}
|
|
25
|
+
# def call
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# ## Configuration
|
|
29
|
+
#
|
|
30
|
+
# To disable this validator:
|
|
31
|
+
#
|
|
32
|
+
# Documentation/ReferenceTypo:
|
|
33
|
+
# Enabled: false
|
|
34
|
+
module ReferenceTypo
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -8,10 +8,15 @@ module Yard
|
|
|
8
8
|
module UndocumentedBooleanMethods
|
|
9
9
|
# Class used to extract details about undocumented boolean methods
|
|
10
10
|
# @example Output format (skip-lint)
|
|
11
|
-
# Platform::Analysis::Authors#valid?
|
|
11
|
+
# lib/platform/analysis/authors.rb:12: Platform::Analysis::Authors#valid?
|
|
12
12
|
class Parser < Parsers::Base
|
|
13
|
-
# Regex to extract
|
|
14
|
-
|
|
13
|
+
# Regex to extract file, line and method title from the validator output
|
|
14
|
+
# Format: file.rb:LINE: Namespace#method?, Namespace.method?, or #method? and
|
|
15
|
+
# ::method? at the root
|
|
16
|
+
LINE_REGEX = /^(.+):(\d+): (.+)$/
|
|
17
|
+
|
|
18
|
+
# Method name: the last segment of the title
|
|
19
|
+
METHOD_NAME_REGEX = /[^#.:]+\z/
|
|
15
20
|
|
|
16
21
|
# @param yard_list [String] raw yard list results string
|
|
17
22
|
# @return [Array<Hash>] Array with undocumented boolean methods details
|
|
@@ -20,18 +25,14 @@ module Yard
|
|
|
20
25
|
.split("\n")
|
|
21
26
|
.reject(&:empty?)
|
|
22
27
|
.filter_map do |line|
|
|
23
|
-
match_data = line.match(
|
|
28
|
+
match_data = line.match(LINE_REGEX)
|
|
24
29
|
next unless match_data
|
|
25
30
|
|
|
26
|
-
# Handle both instance (#) and class (.) methods
|
|
27
|
-
location = match_data[1] || match_data[3]
|
|
28
|
-
method_name = match_data[2] || match_data[4]
|
|
29
|
-
|
|
30
31
|
{
|
|
31
|
-
location:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
location: match_data[1],
|
|
33
|
+
line: match_data[2].to_i,
|
|
34
|
+
element: match_data[3],
|
|
35
|
+
method_name: match_data[3][METHOD_NAME_REGEX]
|
|
35
36
|
}
|
|
36
37
|
end
|
|
37
38
|
end
|
|
@@ -6,13 +6,22 @@ module Yard
|
|
|
6
6
|
module Documentation
|
|
7
7
|
module UndocumentedBooleanMethods
|
|
8
8
|
# Runs a query that will pick all the boolean methods (ending with ?) that
|
|
9
|
-
# do not have a return
|
|
9
|
+
# do not have a return documented
|
|
10
10
|
class Validator < Base
|
|
11
|
+
# A top-level tag line; indented lines belong to the tag above them
|
|
12
|
+
TAG_REGEX = /\A@(!?[\w.]+)/
|
|
13
|
+
# A macro use or definition, capturing the macro name
|
|
14
|
+
MACRO_REGEX = /\A@!?macro(?:\s+\[[^\]]*\])?\s+(\S+)/
|
|
15
|
+
# Macros may reference other macros; stop at this depth
|
|
16
|
+
MAX_MACRO_DEPTH = 5
|
|
17
|
+
|
|
18
|
+
private_constant :TAG_REGEX, :MACRO_REGEX, :MAX_MACRO_DEPTH
|
|
19
|
+
|
|
11
20
|
# Enable in-process execution for this validator
|
|
12
21
|
in_process visibility: :public
|
|
13
22
|
|
|
14
23
|
# Execute query for a single object during in-process execution.
|
|
15
|
-
# Finds boolean methods (ending with ?) without @return tag
|
|
24
|
+
# Finds boolean methods (ending with ?) without a user-written @return tag.
|
|
16
25
|
# @param object [YARD::CodeObjects::Base] the code object to query
|
|
17
26
|
# @param collector [Executor::ResultCollector] collector for output
|
|
18
27
|
# @return [void]
|
|
@@ -26,12 +35,82 @@ module Yard
|
|
|
26
35
|
# Only check boolean methods (ending with ?)
|
|
27
36
|
return unless object.name.to_s.end_with?('?')
|
|
28
37
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
return
|
|
38
|
+
# A module_function pair is reported once, on its public class method
|
|
39
|
+
twin = module_function_twin(object)
|
|
40
|
+
return if twin && object.scope == :instance
|
|
41
|
+
|
|
42
|
+
# The copied half's raw text includes YARD's inferred `@return`
|
|
43
|
+
docstring = module_function_copy?(object) ? twin.docstring : object.docstring
|
|
44
|
+
|
|
45
|
+
# Undocumented methods are reported by UndocumentedObjects
|
|
46
|
+
return if docstring.all.empty?
|
|
47
|
+
# YARD adds `@return [Boolean]` to every `?` method without one, so the
|
|
48
|
+
# tags cannot tell; read the raw docstring instead
|
|
49
|
+
return if user_return?(docstring.all)
|
|
32
50
|
|
|
33
51
|
collector.puts "#{object.file}:#{object.line}: #{object.title}"
|
|
34
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Tells if the raw docstring text has a `@return` written by the user: at the
|
|
57
|
+
# top level, inside an `@overload`, or carried by a macro. Text inside other
|
|
58
|
+
# tags (e.g. an `@example` body) never counts.
|
|
59
|
+
# @param text [String] raw docstring text
|
|
60
|
+
# @param depth [Integer] macro nesting depth
|
|
61
|
+
# @return [Boolean] true when a user-written `@return` is present
|
|
62
|
+
def user_return?(text, depth = 0)
|
|
63
|
+
tag_blocks(text).any? do |line, body|
|
|
64
|
+
case line[TAG_REGEX, 1]
|
|
65
|
+
when 'return'
|
|
66
|
+
true
|
|
67
|
+
when 'overload'
|
|
68
|
+
user_return?(dedent(body), depth)
|
|
69
|
+
when 'macro', '!macro'
|
|
70
|
+
macro_return?(line, depth)
|
|
71
|
+
else
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Tells if the macro named in the line carries a `@return`
|
|
78
|
+
# @param line [String] the `@macro` or `@!macro` line
|
|
79
|
+
# @param depth [Integer] macro nesting depth
|
|
80
|
+
# @return [Boolean] true when the macro text has a `@return` or the macro
|
|
81
|
+
# is not in the registry
|
|
82
|
+
def macro_return?(line, depth)
|
|
83
|
+
return false if depth >= MAX_MACRO_DEPTH
|
|
84
|
+
|
|
85
|
+
name = line[MACRO_REGEX, 1]
|
|
86
|
+
macro = name && YARD::CodeObjects::MacroObject.find(name)
|
|
87
|
+
# Only the selected files are parsed, so a macro from another file is
|
|
88
|
+
# unknown here; count it as written to report the same in every run
|
|
89
|
+
return true unless macro
|
|
90
|
+
|
|
91
|
+
user_return?(macro.macro_data.to_s, depth + 1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Splits the text into top-level tag lines and the indented lines below them
|
|
95
|
+
# @param text [String] raw docstring text
|
|
96
|
+
# @return [Array<Array(String, Array<String>)>] tag line and body lines
|
|
97
|
+
def tag_blocks(text)
|
|
98
|
+
text.lines.map(&:chomp).each_with_object([]) do |line, blocks|
|
|
99
|
+
if line.match?(TAG_REGEX)
|
|
100
|
+
blocks << [line, []]
|
|
101
|
+
elsif blocks.any? && (line.start_with?(/\s/) || line.empty?)
|
|
102
|
+
blocks.last.last << line
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Removes the common leading whitespace from the lines
|
|
108
|
+
# @param lines [Array<String>] lines to dedent
|
|
109
|
+
# @return [String] dedented text
|
|
110
|
+
def dedent(lines)
|
|
111
|
+
indent = lines.reject { |l| l.strip.empty? }.map { |l| l[/\A\s*/].size }.min || 0
|
|
112
|
+
lines.map { |l| l[indent..] || '' }.join("\n")
|
|
113
|
+
end
|
|
35
114
|
end
|
|
36
115
|
end
|
|
37
116
|
end
|
|
@@ -6,9 +6,13 @@ module Yard
|
|
|
6
6
|
module Documentation
|
|
7
7
|
# UndocumentedBooleanMethods validator
|
|
8
8
|
#
|
|
9
|
-
# Ensures that boolean methods (methods ending with `?`) have an
|
|
10
|
-
# `@return
|
|
11
|
-
#
|
|
9
|
+
# Ensures that documented boolean methods (methods ending with `?`) have an
|
|
10
|
+
# explicit `@return` tag, at the top level or inside an `@overload`. Boolean
|
|
11
|
+
# methods should clearly document that they return true or false values.
|
|
12
|
+
# YARD adds `@return [Boolean]` to such methods on its own, so this validator
|
|
13
|
+
# reads the tags written in the comment. Methods without any documentation
|
|
14
|
+
# are reported by `Documentation/UndocumentedObjects`. This validator is
|
|
15
|
+
# enabled by default.
|
|
12
16
|
#
|
|
13
17
|
# @example Bad - Missing @return tag on boolean method
|
|
14
18
|
# # Checks if the user is active
|
|
@@ -23,6 +27,15 @@ module Yard
|
|
|
23
27
|
# @active
|
|
24
28
|
# end
|
|
25
29
|
#
|
|
30
|
+
# @example Good - Boolean return documented inside @overload
|
|
31
|
+
# # Checks if the user is active
|
|
32
|
+
# # @overload active?(strict)
|
|
33
|
+
# # @param strict [Boolean] whether to require a confirmed account
|
|
34
|
+
# # @return [Boolean] true if the user is active
|
|
35
|
+
# def active?(strict)
|
|
36
|
+
# @active
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
26
39
|
# ## Configuration
|
|
27
40
|
#
|
|
28
41
|
# To disable this validator:
|
|
@@ -37,8 +37,9 @@ module Yard
|
|
|
37
37
|
# does not need option tags.
|
|
38
38
|
return if options_param_documented_as_non_hash?(object)
|
|
39
39
|
|
|
40
|
-
# Check if @option tags are missing
|
|
41
|
-
|
|
40
|
+
# Check if @option tags are missing; tags nested inside overload
|
|
41
|
+
# blocks live on the overload's own docstring, so check those too
|
|
42
|
+
option_tags = all_typed_tags(object.docstring, %w[option])
|
|
42
43
|
return unless option_tags.empty?
|
|
43
44
|
|
|
44
45
|
# Output method location and parameter info
|
|
@@ -16,14 +16,14 @@ module Yard
|
|
|
16
16
|
# @param collector [Executor::ResultCollector] collector for output
|
|
17
17
|
# @return [void]
|
|
18
18
|
def in_process_query(object, collector)
|
|
19
|
-
|
|
19
|
+
# Tags nested inside overload blocks live on the overload's own
|
|
20
|
+
# docstring, so check those too
|
|
21
|
+
example_tags = all_typed_tags(object.docstring, %w[example])
|
|
22
|
+
return if example_tags.empty?
|
|
20
23
|
|
|
21
24
|
# Get or initialize runner (memoized for performance)
|
|
22
25
|
return unless runner
|
|
23
26
|
|
|
24
|
-
# Process each example
|
|
25
|
-
example_tags = object.tags(:example)
|
|
26
|
-
|
|
27
27
|
example_tags.each_with_index do |example, index|
|
|
28
28
|
code = example.text
|
|
29
29
|
next if code.nil? || code.empty?
|
|
@@ -16,10 +16,12 @@ module Yard
|
|
|
16
16
|
# @param collector [Executor::ResultCollector] collector for output
|
|
17
17
|
# @return [void]
|
|
18
18
|
def in_process_query(object, collector)
|
|
19
|
-
|
|
19
|
+
# Tags nested inside overload blocks live on the overload's own
|
|
20
|
+
# docstring, so check those too
|
|
21
|
+
example_tags = all_typed_tags(object.docstring, %w[example])
|
|
22
|
+
return if example_tags.empty?
|
|
20
23
|
|
|
21
24
|
skip_non_ruby = config_or_default('SkipNonRuby')
|
|
22
|
-
example_tags = object.tags(:example)
|
|
23
25
|
|
|
24
26
|
example_tags.each_with_index do |example, index|
|
|
25
27
|
code = example.text
|
|
@@ -15,10 +15,9 @@ module Yard
|
|
|
15
15
|
violations = offense[:tag_violations]
|
|
16
16
|
|
|
17
17
|
if violations&.any?
|
|
18
|
-
details = violations.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"#{label}: #{types}"
|
|
18
|
+
details = violations.group_by { |v| v[:param] ? "#{v[:tag]} #{v[:param]}" : v[:tag] }
|
|
19
|
+
.map do |label, entries|
|
|
20
|
+
"#{label}: #{entries.map { |v| describe_types(v) }.join(', ')}"
|
|
22
21
|
end.join('; ')
|
|
23
22
|
|
|
24
23
|
"The `#{offense[:method_name]}` has invalid type(s): #{details}"
|
|
@@ -26,6 +25,18 @@ module Yard
|
|
|
26
25
|
"The `#{offense[:method_name]}` has at least one tag with an invalid type definition."
|
|
27
26
|
end
|
|
28
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @param violation [Hash] entry with :types (invalid names) and :type_string (full type, may be nil)
|
|
32
|
+
# @return [String] the invalid names, plus the full type when it differs from them
|
|
33
|
+
def describe_types(violation)
|
|
34
|
+
names = violation[:types].map { |t| "`#{t}`" }.join(', ')
|
|
35
|
+
type_string = violation[:type_string]
|
|
36
|
+
return names if type_string.nil? || violation[:types] == [type_string]
|
|
37
|
+
|
|
38
|
+
"#{names} in `#{type_string}`"
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
42
|
end
|
|
@@ -8,7 +8,9 @@ module Yard
|
|
|
8
8
|
# Parser for invalid tag types output.
|
|
9
9
|
# The validator emits two lines per offense:
|
|
10
10
|
# /path/to/file.rb:10: ClassName#method_name
|
|
11
|
-
# @tagname param_name:BadType1,BadType2|@tagname2:BadType3
|
|
11
|
+
# @tagname param_name:BadType1,BadType2:Type|@tagname2:BadType3:Type2
|
|
12
|
+
# One entry per type with invalid names. '%', '|', ',', ':' and newlines
|
|
13
|
+
# inside tags, params, names and types are escaped as %25, %7C, %2C, %3A and %0A
|
|
12
14
|
class Parser < Parsers::Base
|
|
13
15
|
# @return [Regexp] matches "file:line: ObjectTitle"
|
|
14
16
|
LOCATION_REGEX = /^(.+):(\d+):\s+(.+)$/
|
|
@@ -71,20 +73,29 @@ module Yard
|
|
|
71
73
|
match ? [match[1], match[2]] : [title, title]
|
|
72
74
|
end
|
|
73
75
|
|
|
74
|
-
# Parse "tagname param:
|
|
76
|
+
# Parse "tagname param:Bad1,Bad2:Type|tagname2:Bad3:Type2" into structured data
|
|
75
77
|
# @param line [String] the violations line
|
|
76
78
|
# @return [Array<Hash>] each entry has :tag, :param (may be nil), :types
|
|
79
|
+
# (the invalid names) and :type_string (the full type, may be nil)
|
|
77
80
|
def parse_tag_violations(line)
|
|
78
81
|
line.split('|').map do |entry|
|
|
79
|
-
label, types_str = entry.split(':',
|
|
80
|
-
|
|
82
|
+
label, types_str, type_string = entry.split(':', 3)
|
|
83
|
+
tag, param = label.split(' ', 2)
|
|
81
84
|
{
|
|
82
|
-
tag:
|
|
83
|
-
param:
|
|
84
|
-
types: (types_str || '').split(',').reject(&:empty?)
|
|
85
|
+
tag: unescape_field(tag),
|
|
86
|
+
param: param && unescape_field(param),
|
|
87
|
+
types: (types_str || '').split(',').reject(&:empty?).map { |name| unescape_field(name) },
|
|
88
|
+
type_string: type_string && unescape_field(type_string)
|
|
85
89
|
}
|
|
86
90
|
end
|
|
87
91
|
end
|
|
92
|
+
|
|
93
|
+
# Reverts the validator's escaping
|
|
94
|
+
# @param text [String] escaped field text
|
|
95
|
+
# @return [String] raw field text
|
|
96
|
+
def unescape_field(text)
|
|
97
|
+
text.gsub(/%(?:25|7C|2C|3A|0A)/, '%25' => '%', '%7C' => '|', '%2C' => ',', '%3A' => ':', '%0A' => "\n")
|
|
98
|
+
end
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
end
|