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.
@@ -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