/
gem_comment.rb
171 lines (153 loc) · 5.32 KB
/
gem_comment.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
# frozen_string_literal: true
module RuboCop
module Cop
module Bundler
# Each gem in the Gemfile should have a comment explaining
# its purpose in the project, or the reason for its version
# or source.
#
# The optional "OnlyFor" configuration array
# can be used to only register offenses when the gems
# use certain options or have version specifiers.
#
# When "version_specifiers" is included, a comment
# will be enforced if the gem has any version specifier.
#
# When "restrictive_version_specifiers" is included, a comment
# will be enforced if the gem has a version specifier that
# holds back the version of the gem.
#
# For any other value in the array, a comment will be enforced for
# a gem if an option by the same name is present.
# A useful use case is to enforce a comment when using
# options that change the source of a gem:
#
# - `bitbucket`
# - `gist`
# - `git`
# - `github`
# - `source`
#
# For a full list of options supported by bundler,
# see https://bundler.io/man/gemfile.5.html
# .
#
# @example OnlyFor: [] (default)
# # bad
#
# gem 'foo'
#
# # good
#
# # Helpers for the foo things.
# gem 'foo'
#
# @example OnlyFor: ['version_specifiers']
# # bad
#
# gem 'foo', '< 2.1'
#
# # good
#
# # Version 2.1 introduces breaking change baz
# gem 'foo', '< 2.1'
#
# @example OnlyFor: ['restrictive_version_specifiers']
# # bad
#
# gem 'foo', '< 2.1'
#
# # good
#
# gem 'foo', '>= 1.0'
#
# # Version 2.1 introduces breaking change baz
# gem 'foo', '< 2.1'
#
# @example OnlyFor: ['version_specifiers', 'github']
# # bad
#
# gem 'foo', github: 'some_account/some_fork_of_foo'
#
# gem 'bar', '< 2.1'
#
# # good
#
# # Using this fork because baz
# gem 'foo', github: 'some_account/some_fork_of_foo'
#
# # Version 2.1 introduces breaking change baz
# gem 'bar', '< 2.1'
#
class GemComment < Base
include DefNode
include GemDeclaration
MSG = 'Missing gem description comment.'
CHECKED_OPTIONS_CONFIG = 'OnlyFor'
VERSION_SPECIFIERS_OPTION = 'version_specifiers'
RESTRICTIVE_VERSION_SPECIFIERS_OPTION = 'restrictive_version_specifiers'
RESTRICTIVE_VERSION_PATTERN = /\A\s*(?:<|~>|\d|=)/.freeze
RESTRICT_ON_SEND = %i[gem].freeze
def on_send(node)
return unless gem_declaration?(node)
return if ignored_gem?(node)
return if commented_any_descendant?(node)
return if cop_config[CHECKED_OPTIONS_CONFIG].any? && !checked_options_present?(node)
add_offense(node)
end
private
def commented_any_descendant?(node)
commented?(node) || node.each_descendant.any? { |n| commented?(n) }
end
def commented?(node)
preceding_lines = preceding_lines(node)
preceding_comment?(node, preceding_lines.last)
end
# The args node1 & node2 may represent a RuboCop::AST::Node
# or a Parser::Source::Comment. Both respond to #loc.
def precede?(node1, node2)
node2.loc.line - node1.loc.line <= 1
end
def preceding_lines(node)
processed_source.ast_with_comments[node].select do |line|
line.loc.line <= node.loc.line
end
end
def preceding_comment?(node1, node2)
node1 && node2 && precede?(node2, node1) && comment_line?(node2.loc.expression.source)
end
def ignored_gem?(node)
ignored_gems = Array(cop_config['IgnoredGems'])
ignored_gems.include?(node.first_argument.value)
end
def checked_options_present?(node)
(cop_config[CHECKED_OPTIONS_CONFIG].include?(VERSION_SPECIFIERS_OPTION) &&
version_specified_gem?(node)) ||
(cop_config[CHECKED_OPTIONS_CONFIG].include?(RESTRICTIVE_VERSION_SPECIFIERS_OPTION) &&
restrictive_version_specified_gem?(node)) ||
contains_checked_options?(node)
end
# Besides the gem name, all other *positional* arguments to `gem` are version specifiers,
# as long as it has one we know there's at least one version specifier.
def version_specified_gem?(node)
# arguments[0] is the gem name
node.arguments[1]&.str_type?
end
# Version specifications that restrict all updates going forward. This excludes versions
# like ">= 1.0" or "!= 2.0.3".
def restrictive_version_specified_gem?(node)
return unless version_specified_gem?(node)
node.arguments[1..-1]
.any? { |arg| arg&.str_type? && RESTRICTIVE_VERSION_PATTERN.match?(arg.value) }
end
def contains_checked_options?(node)
(Array(cop_config[CHECKED_OPTIONS_CONFIG]) & gem_options(node).map(&:to_s)).any?
end
def gem_options(node)
return [] unless node.arguments.last&.type == :hash
node.arguments.last.keys.map(&:value)
end
end
end
end
end