forked from rubocop/rubocop
/
documentation.rb
186 lines (158 loc) · 5.42 KB
/
documentation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# This cop checks for missing top-level documentation of classes and
# modules. Classes with no body are exempt from the check and so are
# namespace modules - modules that have nothing in their bodies except
# classes, other modules, constant definitions or constant visibility
# declarations.
#
# The documentation requirement is annulled if the class or module has
# a "#:nodoc:" comment next to it. Likewise, "#:nodoc: all" does the
# same for all its children.
#
# @example
# # bad
# class Person
# # ...
# end
#
# module Math
# end
#
# # good
# # Description/Explanation of Person class
# class Person
# # ...
# end
#
# # allowed
# # Class without body
# class Person
# end
#
# # Namespace - A namespace can be a class or a module
# # Containing a class
# module Namespace
# # Description/Explanation of Person class
# class Person
# # ...
# end
# end
#
# # Containing constant visibility declaration
# module Namespace
# class Private
# end
#
# private_constant :Private
# end
#
# # Containing constant definition
# module Namespace
# Public = Class.new
# end
#
# # Macro calls
# module Namespace
# extend Foo
# end
#
# @example AllowedConstants: ['ClassMethods']
#
# # good
# module A
# module ClassMethods
# # ...
# end
# end
#
class Documentation < Base
include DocumentationComment
include RangeHelp
MSG = 'Missing top-level documentation comment for `%<type>s %<identifier>s`.'
# @!method constant_definition?(node)
def_node_matcher :constant_definition?, '{class module casgn}'
# @!method outer_module(node)
def_node_search :outer_module, '(const (const nil? _) _)'
# @!method constant_visibility_declaration?(node)
def_node_matcher :constant_visibility_declaration?, <<~PATTERN
(send nil? {:public_constant :private_constant} ({sym str} _))
PATTERN
def on_class(node)
return unless node.body
check(node, node.body)
end
def on_module(node)
check(node, node.body)
end
private
def check(node, body)
return if namespace?(body)
return if documentation_comment?(node)
return if constant_allowed?(node)
return if nodoc_self_or_outer_module?(node)
return if macro_only?(body)
range = range_between(node.loc.expression.begin_pos, node.loc.name.end_pos)
message = format(MSG, type: node.type, identifier: identifier(node))
add_offense(range, message: message)
end
def nodoc_self_or_outer_module?(node)
nodoc_comment?(node) ||
compact_namespace?(node) && nodoc_comment?(outer_module(node).first)
end
def macro_only?(body)
body.respond_to?(:macro?) && body.macro? ||
body.respond_to?(:children) && body.children&.all? { |child| macro_only?(child) }
end
def namespace?(node)
return false unless node
if node.begin_type?
node.children.all? { |child| constant_declaration?(child) }
else
constant_definition?(node)
end
end
def constant_declaration?(node)
constant_definition?(node) || constant_visibility_declaration?(node)
end
def constant_allowed?(node)
allowed_constants.include?(node.identifier.short_name)
end
def compact_namespace?(node)
/::/.match?(node.loc.name.source)
end
# First checks if the :nodoc: comment is associated with the
# class/module. Unless the element is tagged with :nodoc:, the search
# proceeds to check its ancestors for :nodoc: all.
# Note: How end-of-line comments are associated with code changed in
# parser-2.2.0.4.
def nodoc_comment?(node, require_all: false)
return false unless node&.children&.first
nodoc = nodoc(node)
return true if same_line?(nodoc, node) && nodoc?(nodoc, require_all: require_all)
nodoc_comment?(node.parent, require_all: true)
end
def nodoc?(comment, require_all: false)
/^#\s*:nodoc:#{"\s+all\s*$" if require_all}/.match?(comment.text)
end
def nodoc(node)
processed_source.ast_with_comments[node.children.first].first
end
def allowed_constants
@allowed_constants ||= cop_config.fetch('AllowedConstants', []).map(&:intern)
end
def identifier(node)
# Get the fully qualified identifier for a class/module
nodes = [node, *node.each_ancestor(:class, :module)]
nodes.reverse_each.flat_map { |n| qualify_const(n.identifier) }.join('::')
end
def qualify_const(node)
return if node.nil?
[qualify_const(node.namespace), node.short_name].compact
end
end
end
end
end