rubocop-yardoc 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +71 -0
- data/CONTRIBUTING.md +213 -0
- data/LICENSE +21 -0
- data/README.md +71 -0
- data/config/default.yml +62 -0
- data/config/obsoletion.yml +3 -0
- data/lib/rubocop/cop/mixin/node_name_help.rb +26 -0
- data/lib/rubocop/cop/mixin/param_help.rb +45 -0
- data/lib/rubocop/cop/mixin/yard_help.rb +35 -0
- data/lib/rubocop/cop/yardoc/base.rb +44 -0
- data/lib/rubocop/cop/yardoc/class_description.rb +52 -0
- data/lib/rubocop/cop/yardoc/column_params.rb +174 -0
- data/lib/rubocop/cop/yardoc/constant_description.rb +74 -0
- data/lib/rubocop/cop/yardoc/method_description.rb +93 -0
- data/lib/rubocop/cop/yardoc/module_description.rb +52 -0
- data/lib/rubocop/cop/yardoc/param_description_casing.rb +82 -0
- data/lib/rubocop/cop/yardoc/param_documentation.rb +93 -0
- data/lib/rubocop/cop/yardoc/separate_tags_blocks.rb +161 -0
- data/lib/rubocop/cop/yardoc/supported_tags.rb +109 -0
- data/lib/rubocop/cop/yardoc/tag_order.rb +83 -0
- data/lib/rubocop/cop/yardoc/yield_documentation.rb +94 -0
- data/lib/rubocop/cop/yardoc_cops.rb +19 -0
- data/lib/rubocop/yardoc/plugin.rb +39 -0
- data/lib/rubocop/yardoc/version.rb +8 -0
- data/lib/rubocop/yardoc.rb +7 -0
- data/lib/rubocop-yardoc.rb +9 -0
- metadata +130 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensures all classes have YARD documentation.
|
|
7
|
+
#
|
|
8
|
+
# Classes can be ignored by exact name, qualified name, or regexp pattern.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# class MyClass
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# # A useful class.
|
|
17
|
+
# class MyClass
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Ignore: ['MyClass', '/Internal/', 'MyModule::MyClass']
|
|
21
|
+
# # good (ignored)
|
|
22
|
+
# class MyClass; end
|
|
23
|
+
#
|
|
24
|
+
# # good (ignored)
|
|
25
|
+
# class MyInternalClass; end
|
|
26
|
+
#
|
|
27
|
+
# module MyModule
|
|
28
|
+
# # good (ignored)
|
|
29
|
+
# class MyCustomClass; end
|
|
30
|
+
# # good (ignored)
|
|
31
|
+
# class MyClass; end
|
|
32
|
+
# end
|
|
33
|
+
class ClassDescription < Base
|
|
34
|
+
include NodeNameHelp
|
|
35
|
+
|
|
36
|
+
MSG = 'Missing YARD documentation for class `%<name>s`.'
|
|
37
|
+
|
|
38
|
+
# Executed for each class definition
|
|
39
|
+
#
|
|
40
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
41
|
+
def on_class(node)
|
|
42
|
+
full_name = fully_qualified_name(node) # MyClass::my_method
|
|
43
|
+
name = full_name.split('::').last # my_method
|
|
44
|
+
return if ignored?(full_name, ignore_list) || ignored?(name, ignore_list)
|
|
45
|
+
return if documented?(node)
|
|
46
|
+
|
|
47
|
+
add_offense(node, message: format(MSG, name: full_name))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensures @param tags are correctly formatted and aligned.
|
|
7
|
+
#
|
|
8
|
+
# Rules enforced:
|
|
9
|
+
# - Type must appear in square brackets directly after the param name
|
|
10
|
+
# - All @param type columns must be aligned
|
|
11
|
+
# - All @param description columns must be aligned
|
|
12
|
+
# - Multiline descriptions must align with the first description character
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# # @param foo [String] a foo
|
|
17
|
+
# # @param longer_name [Integer] a number
|
|
18
|
+
# # @param bar [String] A multiline, non aligned
|
|
19
|
+
# # description
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# # @param foo [String] a foo
|
|
23
|
+
# # @param longer_name [Integer] a number
|
|
24
|
+
# # @param bar [String] A multiline, aligned
|
|
25
|
+
# # description
|
|
26
|
+
class ColumnParams < Base
|
|
27
|
+
extend AutoCorrector
|
|
28
|
+
include ParamHelp
|
|
29
|
+
include YardHelp
|
|
30
|
+
|
|
31
|
+
MSG_TYPE_MISSING = '@param `%<name>s` must have a type in [brackets].'
|
|
32
|
+
MSG_ALIGNMENT = '@param tags must have aligned types and descriptions.'
|
|
33
|
+
MSG_MULTILINE = 'Multiline @param description must align with the first description character.'
|
|
34
|
+
|
|
35
|
+
# Executed for every method definition
|
|
36
|
+
#
|
|
37
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
38
|
+
def on_def(node)
|
|
39
|
+
param_lines = extract_param_lines(node)
|
|
40
|
+
return if param_lines.empty?
|
|
41
|
+
|
|
42
|
+
check_types_present(node, param_lines) unless config.for_cop('Yardoc/ParamDocumentation')['Enabled']
|
|
43
|
+
check_alignment(param_lines)
|
|
44
|
+
check_multiline_alignment(node, param_lines)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias on_defs on_def
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Parses raw comment lines into structured param line data.
|
|
52
|
+
#
|
|
53
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<Hash>]
|
|
56
|
+
def extract_param_lines(node) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
57
|
+
param_tags_and_positions(node).filter_map do |param|
|
|
58
|
+
stripped = param[:source].sub(/^#\s*/, '')
|
|
59
|
+
|
|
60
|
+
match = stripped.match(/^@param\s+(\S+)(\s+\[([^\]]*)\])?(\s+(.*))?$/)
|
|
61
|
+
next unless match
|
|
62
|
+
|
|
63
|
+
# Use the match values as the Yardoc tag data may be formatted differently
|
|
64
|
+
{
|
|
65
|
+
range: param[:range],
|
|
66
|
+
line: param[:source],
|
|
67
|
+
comment: param[:comment],
|
|
68
|
+
yard_tag: param[:yard_tag],
|
|
69
|
+
name: match[1],
|
|
70
|
+
type: match[3],
|
|
71
|
+
text: match[5] || '',
|
|
72
|
+
type_col: param[:source].index('['),
|
|
73
|
+
desc_col: match[5] ? param[:source].index(match[5]) : nil,
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Checks that every param has a type
|
|
79
|
+
#
|
|
80
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
81
|
+
# @param param_lines [Array<Hash>] Hashes containing informations about the parameters
|
|
82
|
+
def check_types_present(node, param_lines)
|
|
83
|
+
param_lines.each do |pl|
|
|
84
|
+
next unless pl[:type].nil?
|
|
85
|
+
|
|
86
|
+
add_offense(node, message: format(MSG_TYPE_MISSING, name: pl[:name]))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Checks for the alignment of comments types and descriptions
|
|
91
|
+
#
|
|
92
|
+
# @param param_lines [Array<Hash>] Hashes containing informations about the parameters
|
|
93
|
+
def check_alignment(param_lines) # rubocop:disable Metrics/MethodLength
|
|
94
|
+
return if param_lines.count <= 1
|
|
95
|
+
|
|
96
|
+
longest_type_pos, longest_text_pos = param_tag_bits_positions(param_lines)
|
|
97
|
+
|
|
98
|
+
param_lines.each do |line|
|
|
99
|
+
expected = "# @param #{line[:name]}".ljust(longest_type_pos, ' ') +
|
|
100
|
+
"[#{line[:type]}]"
|
|
101
|
+
expected = expected.ljust(longest_text_pos, ' ') + line[:text]
|
|
102
|
+
|
|
103
|
+
next if line[:line] == expected
|
|
104
|
+
|
|
105
|
+
add_offense(line[:range], message: MSG_ALIGNMENT) do |corrector|
|
|
106
|
+
corrector.replace(line[:comment], expected)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Finds the ideal starting position for all types and descriptions
|
|
112
|
+
#
|
|
113
|
+
# @param param_lines [Array<Hash>] Hashes containing informations about the parameters
|
|
114
|
+
#
|
|
115
|
+
# @return [Array<Integer>] Types position, descriptions position
|
|
116
|
+
def param_tag_bits_positions(param_lines) # rubocop:disable Metrics/AbcSize
|
|
117
|
+
longest_type_pos = param_lines.max_by { |l| l[:name].length }[:name].length + 10
|
|
118
|
+
longest_text_pos = param_lines.max_by { |l| l[:name].length + (l[:type]&.length || 0) }
|
|
119
|
+
# Type may not have been defined
|
|
120
|
+
longest_text_pos = longest_text_pos[:type] ? longest_text_pos[:type].length + longest_type_pos + 3 : 0
|
|
121
|
+
|
|
122
|
+
[longest_type_pos, longest_text_pos]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Checks for the alignment of multiline comments
|
|
126
|
+
#
|
|
127
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
128
|
+
# @param param_lines [Array<Hash>] Hashes containing informations about the parameters
|
|
129
|
+
def check_multiline_alignment(node, param_lines) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
130
|
+
longest_text_pos = param_tag_bits_positions(param_lines)[1]
|
|
131
|
+
|
|
132
|
+
comments = processed_source.ast_with_comments[node]
|
|
133
|
+
comments.each_with_index do |comment, index|
|
|
134
|
+
next unless comment.text.match?(/^# @param /)
|
|
135
|
+
|
|
136
|
+
# Search for continuations
|
|
137
|
+
comments[(index + 1)..].each do |next_comment|
|
|
138
|
+
break unless next_comment.text.match?(/^#\s{3,}/)
|
|
139
|
+
|
|
140
|
+
text = next_comment.text.sub(/#\s+/, '')
|
|
141
|
+
expected = "##{' ' * (longest_text_pos - 1)}#{text}"
|
|
142
|
+
|
|
143
|
+
next if next_comment.text == expected
|
|
144
|
+
|
|
145
|
+
add_offense(next_comment, message: MSG_MULTILINE) do |corrector|
|
|
146
|
+
corrector.replace(next_comment, expected)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Returns lines immediately following a param line that are
|
|
153
|
+
# plain comment continuations (not new tags).
|
|
154
|
+
#
|
|
155
|
+
# @param comments [Array<String>] All comment lines
|
|
156
|
+
# @param param_line [String] The @param line to find continuations for
|
|
157
|
+
#
|
|
158
|
+
# @return [Array<String>] Continuation lines
|
|
159
|
+
def following_continuation_lines(comments, param_line)
|
|
160
|
+
index = comments.index(param_line)
|
|
161
|
+
return [] if index.nil?
|
|
162
|
+
|
|
163
|
+
result = []
|
|
164
|
+
comments[(index + 1)..].each do |line|
|
|
165
|
+
break unless line.match?(/^#\s{3,}/)
|
|
166
|
+
|
|
167
|
+
result << line
|
|
168
|
+
end
|
|
169
|
+
result
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensures all constants have YARD documentation.
|
|
7
|
+
#
|
|
8
|
+
# They can be ignored via `Ignore`, by providing the name or a regexp
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# MY_CONST = 10
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# # That's 9+1
|
|
16
|
+
# MY_CONST = 10
|
|
17
|
+
#
|
|
18
|
+
# @example Ignores: ['MY_CONST']
|
|
19
|
+
# # bad
|
|
20
|
+
# A_CONST = 'something'
|
|
21
|
+
#
|
|
22
|
+
# # good (ignored)
|
|
23
|
+
# MY_CONST = 10
|
|
24
|
+
#
|
|
25
|
+
# class MyClass
|
|
26
|
+
# # good (ignored)
|
|
27
|
+
# MY_CONST
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @example Ignores: [/^MY_CONST/]
|
|
31
|
+
# # bad
|
|
32
|
+
# A_CONST = 'something'
|
|
33
|
+
#
|
|
34
|
+
# # good (ignored)
|
|
35
|
+
# MY_CONST = 10
|
|
36
|
+
#
|
|
37
|
+
# class MyClass
|
|
38
|
+
# # bad
|
|
39
|
+
# MY_CONST
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# @example Ignores: ['MyClass::MY_CONST']
|
|
43
|
+
# # bad
|
|
44
|
+
# A_CONST = 'something'
|
|
45
|
+
#
|
|
46
|
+
# # bad
|
|
47
|
+
# MY_CONST = 10
|
|
48
|
+
#
|
|
49
|
+
# class MyClass
|
|
50
|
+
# # good (ignored)
|
|
51
|
+
# MY_CONST
|
|
52
|
+
# end
|
|
53
|
+
class ConstantDescription < Base
|
|
54
|
+
include NodeNameHelp
|
|
55
|
+
|
|
56
|
+
MSG = 'Missing YARD documentation for constant `%<name>s`.'
|
|
57
|
+
|
|
58
|
+
# Executed for each constant definition
|
|
59
|
+
#
|
|
60
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
61
|
+
def on_casgn(node)
|
|
62
|
+
_scope, name, = node.children
|
|
63
|
+
name = name.to_s
|
|
64
|
+
full_name = fully_qualified_name(node)
|
|
65
|
+
full_name += "::#{name}"
|
|
66
|
+
return if ignored?(name, ignore_list) || ignored?(full_name, ignore_list)
|
|
67
|
+
return if documented?(node)
|
|
68
|
+
|
|
69
|
+
add_offense(node, message: format(MSG, name: name))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensures all public methods have a description.
|
|
7
|
+
#
|
|
8
|
+
# This does not ensure parameters and other documentation, only the description
|
|
9
|
+
#
|
|
10
|
+
# Private/protected methods are skipped unless `DocumentPrivate: true`.
|
|
11
|
+
# Methods can be ignored via `Ignore`.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad
|
|
15
|
+
# def my_method; end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# # Does something useful.
|
|
19
|
+
# def my_method; end
|
|
20
|
+
#
|
|
21
|
+
# @example DocumentPrivate: true
|
|
22
|
+
# # bad
|
|
23
|
+
# private
|
|
24
|
+
#
|
|
25
|
+
# def secret; end
|
|
26
|
+
#
|
|
27
|
+
# @example Ignore: [a_method]
|
|
28
|
+
# # bad
|
|
29
|
+
# def something; end
|
|
30
|
+
#
|
|
31
|
+
# # good (ignored)
|
|
32
|
+
# def a_method; end
|
|
33
|
+
#
|
|
34
|
+
# class MyClass
|
|
35
|
+
# # good (ignored)
|
|
36
|
+
# def a_method; end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# @example Ignore: [/^a_m/]
|
|
40
|
+
# # bad
|
|
41
|
+
# def something; end
|
|
42
|
+
#
|
|
43
|
+
# # good (ignored)
|
|
44
|
+
# def a_method; end
|
|
45
|
+
#
|
|
46
|
+
# class MyClass
|
|
47
|
+
# # good (ignored)
|
|
48
|
+
# def a_method; end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# @example Ignore: [MyClass::a_method]
|
|
52
|
+
# # bad
|
|
53
|
+
# def something; end
|
|
54
|
+
#
|
|
55
|
+
# # bad
|
|
56
|
+
# def a_method; end
|
|
57
|
+
#
|
|
58
|
+
# class MyClass
|
|
59
|
+
# # good (ignored)
|
|
60
|
+
# def a_method; end
|
|
61
|
+
# end
|
|
62
|
+
class MethodDescription < Base
|
|
63
|
+
include VisibilityHelp
|
|
64
|
+
include NodeNameHelp
|
|
65
|
+
|
|
66
|
+
MSG = 'Missing YARD documentation for method `%<name>s`.'
|
|
67
|
+
|
|
68
|
+
# Executed for each method definition
|
|
69
|
+
#
|
|
70
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
71
|
+
def on_def(node)
|
|
72
|
+
name = node.method_name.to_s
|
|
73
|
+
full_name = fully_qualified_name(node)
|
|
74
|
+
full_name += "::#{name}"
|
|
75
|
+
return if ignored?(name, ignore_list) || ignored?(full_name, ignore_list)
|
|
76
|
+
return if skip_private? && node_visibility(node) == :private
|
|
77
|
+
return if documented?(node)
|
|
78
|
+
|
|
79
|
+
add_offense(node, message: format(MSG, name: name))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
alias on_defs on_def
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Whether to skip documentation on private methods
|
|
87
|
+
def skip_private?
|
|
88
|
+
!cop_config.fetch('DocumentPrivate', false)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensures all modules have YARD documentation.
|
|
7
|
+
#
|
|
8
|
+
# Modules can be ignored by exact name, qualified name, or regexp pattern.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# module MyModule
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# # A useful module.
|
|
17
|
+
# module MyModule
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Ignore: ['MyModule', '/Internal/', 'MyModule::MySubModule']
|
|
21
|
+
# # good (ignored)
|
|
22
|
+
# module MyModule; end
|
|
23
|
+
#
|
|
24
|
+
# # good (ignored)
|
|
25
|
+
# module MyInternalModule; end
|
|
26
|
+
#
|
|
27
|
+
# module MyModule
|
|
28
|
+
# # good (ignored)
|
|
29
|
+
# module MySubModule; end
|
|
30
|
+
# # good (ignored)
|
|
31
|
+
# module MyModule; end
|
|
32
|
+
# end
|
|
33
|
+
class ModuleDescription < Base
|
|
34
|
+
include NodeNameHelp
|
|
35
|
+
|
|
36
|
+
MSG = 'Missing YARD documentation for module `%<name>s`.'
|
|
37
|
+
|
|
38
|
+
# Executed for each module definition
|
|
39
|
+
#
|
|
40
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
41
|
+
def on_module(node)
|
|
42
|
+
full_name = fully_qualified_name(node)
|
|
43
|
+
name = full_name.split('::').last
|
|
44
|
+
return if ignored?(name, ignore_list) || ignored?(full_name, ignore_list)
|
|
45
|
+
return if documented?(node)
|
|
46
|
+
|
|
47
|
+
add_offense(node, message: format(MSG, name: full_name))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensure every "param" and "return" tag starts with an uppercased letter
|
|
7
|
+
#
|
|
8
|
+
# It ignores description starting with non latin characters
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# # @param x [Integer] desc
|
|
13
|
+
# # @return [void] desc
|
|
14
|
+
# def my_method(x); end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# # @param x [Integer] Desc
|
|
18
|
+
# # @return [void] Desc
|
|
19
|
+
# def my_method(x); end
|
|
20
|
+
#
|
|
21
|
+
class ParamDescriptionCasing < Base
|
|
22
|
+
extend AutoCorrector
|
|
23
|
+
|
|
24
|
+
# Represents a comment entry (it may be a multi-line comment)
|
|
25
|
+
#
|
|
26
|
+
# @return [Struct] With :comment, :text, :tag, :line, :blank?
|
|
27
|
+
CommentEntry = Struct.new(:comment, :text, :tag, :line, :blank, keyword_init: true) do
|
|
28
|
+
# Whether the comment is empty
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def blank?
|
|
32
|
+
blank
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
MSG = 'First letter of a @param or @return tag should be in uppercase'
|
|
37
|
+
|
|
38
|
+
# Matches a "param" tag line
|
|
39
|
+
PARAM_REGEXP = /(?<definition>\A#\s+@param\s+[^\[]+(?:\s+\[[^\]]+\])?\s+)(?<desc>[a-z].*)/
|
|
40
|
+
# Matches a "return" tag line
|
|
41
|
+
RETURN_REGEXP = /(?<definition>\A#\s+@return\s+(?:\[[^\]]+\])?\s+)(?<desc>[a-z].*)/
|
|
42
|
+
|
|
43
|
+
# Executed for each method definition
|
|
44
|
+
#
|
|
45
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
46
|
+
def on_def(node)
|
|
47
|
+
return unless documented? node
|
|
48
|
+
|
|
49
|
+
processed_source.comments
|
|
50
|
+
.each do |line|
|
|
51
|
+
match = line.text.match(PARAM_REGEXP) || line.text.match(RETURN_REGEXP)
|
|
52
|
+
next unless match
|
|
53
|
+
|
|
54
|
+
range = offense_range(line, match[:definition])
|
|
55
|
+
add_offense(range) do |corrector|
|
|
56
|
+
corrector.replace(range, match[:desc][0].upcase)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
alias on_defs on_def
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Returns the position of the first character of the description in the line
|
|
66
|
+
#
|
|
67
|
+
# @param comment [Parser::Source::Comment] Offending comment
|
|
68
|
+
# @param param_lead [String] Part of the comment to definition, e.g.: "# @param x [Integer]"
|
|
69
|
+
#
|
|
70
|
+
# @return [Parser::Source::Range]
|
|
71
|
+
def offense_range(comment, param_lead)
|
|
72
|
+
comment_range = comment.loc.expression
|
|
73
|
+
at_pos = param_lead.length
|
|
74
|
+
|
|
75
|
+
Parser::Source::Range.new processed_source.buffer,
|
|
76
|
+
comment_range.begin_pos + at_pos,
|
|
77
|
+
comment_range.begin_pos + at_pos + 1
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yardoc
|
|
6
|
+
# Ensures all method parameters are documented with @param tags.
|
|
7
|
+
#
|
|
8
|
+
# By default, each @param must include a name, type, and description.
|
|
9
|
+
# Set `RequireDescription: false` to only require name and type.
|
|
10
|
+
#
|
|
11
|
+
# @example RequireDescription: true (default)
|
|
12
|
+
# # bad
|
|
13
|
+
# # @param name [String]
|
|
14
|
+
# def greet(name); end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# # @param name [String] the name to greet
|
|
18
|
+
# def greet(name); end
|
|
19
|
+
#
|
|
20
|
+
# @example RequireDescription: false
|
|
21
|
+
# # good
|
|
22
|
+
# # @param name [String]
|
|
23
|
+
# def greet(name); end
|
|
24
|
+
class ParamDocumentation < Base
|
|
25
|
+
include ParamHelp
|
|
26
|
+
include YardHelp
|
|
27
|
+
|
|
28
|
+
MSG_MISSING = 'Missing @param documentation for `%<names>s`.'
|
|
29
|
+
MSG_NO_TYPE = '@param `%<name>s` is missing a type (e.g. [String]).'
|
|
30
|
+
MSG_NO_DESCRIPTION = '@param `%<name>s` is missing a description.'
|
|
31
|
+
|
|
32
|
+
# Executed for every method definition
|
|
33
|
+
#
|
|
34
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
35
|
+
def on_def(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
36
|
+
return unless documented?(node)
|
|
37
|
+
|
|
38
|
+
def_params = method_params(node)
|
|
39
|
+
param_tags = param_tags_and_positions(node)
|
|
40
|
+
|
|
41
|
+
undocumented_params = []
|
|
42
|
+
|
|
43
|
+
def_params.each do |param_name|
|
|
44
|
+
tag = param_tags.find { |t| t[:name] == param_name.to_s }
|
|
45
|
+
|
|
46
|
+
# Undocumented params are a one-offense error as we cannot add multiple offenses on the same node
|
|
47
|
+
# FIXME: add a separate offense for every undocumented param with the argument position in the
|
|
48
|
+
# method definition.
|
|
49
|
+
if tag.nil?
|
|
50
|
+
undocumented_params << param_name
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if tag[:yard_tag].types.nil? || tag[:yard_tag].types.empty?
|
|
55
|
+
add_offense(tag[:range], message: format(MSG_NO_TYPE, name: param_name))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if require_description? && (tag[:yard_tag].text.nil? || tag[:yard_tag].text.strip.empty?)
|
|
59
|
+
add_offense(tag[:range], message: format(MSG_NO_DESCRIPTION, name: param_name))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return if undocumented_params.empty?
|
|
64
|
+
|
|
65
|
+
add_offense(node, message: format(MSG_MISSING, names: undocumented_params.join('`, `')))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias on_defs on_def
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Whether the description is needed for parameters
|
|
73
|
+
def require_description?
|
|
74
|
+
cop_config.fetch('RequireDescription', true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Extracts method parameters for given node
|
|
78
|
+
#
|
|
79
|
+
# @param node [RuboCop::AST::Node] The AST node
|
|
80
|
+
def method_params(node)
|
|
81
|
+
ignored_argument_types = [:blockarg, :shadowarg, :forward_arg]
|
|
82
|
+
|
|
83
|
+
args_node = node.arguments
|
|
84
|
+
args_node.children.filter_map do |arg|
|
|
85
|
+
next if ignored_argument_types.include?(arg.type)
|
|
86
|
+
|
|
87
|
+
arg.children.first
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|