rubocop-sane 0.2.0 → 1.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 +4 -4
- data/.rubocop.yml +1 -1
- data/config/default.yml +75 -10
- data/lib/rubocop/cop/sane/empty_line_before_comment.rb +11 -1
- data/lib/rubocop/cop/sane/empty_lines_around_condition_body.rb +173 -0
- data/lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb +137 -26
- data/lib/rubocop/cop/sane/empty_lines_around_multiline_call.rb +228 -0
- data/lib/rubocop/cop/sane/multiline_chain_on_separate_lines.rb +92 -0
- data/lib/rubocop/cop/sane/omit_parentheses.rb +78 -0
- data/lib/rubocop/cop/sane/outdated_comments.rb +53 -0
- data/lib/rubocop/cop/sane/prohibited_comments.rb +40 -0
- data/lib/rubocop/cop/sane/redundant_self_assignment.rb +74 -0
- data/lib/rubocop/cop/sane/variable_name_length.rb +67 -0
- data/lib/rubocop/cop/sane_cops.rb +8 -0
- data/lib/rubocop/sane/version.rb +1 -1
- metadata +14 -7
- data/CHANGELOG.md +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b17fd1e8de6294cc02871537ea6ab1392304756cfca1fe3357374457df1c05aa
|
|
4
|
+
data.tar.gz: 4d7c5d93b4a375b1b14860049227be3a253e57a7c349d8b2be2510d705f5cf71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efec9c94cadc379f66a8bcbc71794baf1436a42cc6904efe10646ebe396838974a967f5d48e45027c484ea2b8c6236bc894d0dd9e384e54be945e5ea990b9394
|
|
7
|
+
data.tar.gz: c07fddd9d2a78f16beaf02ab62dc0ea1d493bdc5fd2f901a6909b600b99311400c372a5a33011a7979c88681cdcf8a0d13e0aae7d4d751b3e68b47e857b0b36d
|
data/.rubocop.yml
CHANGED
data/config/default.yml
CHANGED
|
@@ -2,32 +2,97 @@ Sane:
|
|
|
2
2
|
Enabled: true
|
|
3
3
|
DocumentationBaseURL: https://github.com/akodkod/rubocop-sane
|
|
4
4
|
|
|
5
|
+
Sane/ProhibitedComments:
|
|
6
|
+
Description: "Detects comments starting with configurable prohibited words"
|
|
7
|
+
Enabled: true
|
|
8
|
+
VersionAdded: "1.0.0"
|
|
9
|
+
ProhibitedWords:
|
|
10
|
+
- DELETE
|
|
11
|
+
- delete
|
|
12
|
+
- REMEMBER
|
|
13
|
+
- remember
|
|
14
|
+
|
|
5
15
|
Sane/DisallowMethods:
|
|
6
|
-
Description: "Enforces method replacements and prohibitions
|
|
16
|
+
Description: "Enforces method replacements and prohibitions"
|
|
7
17
|
Enabled: true
|
|
8
|
-
VersionAdded: "
|
|
18
|
+
VersionAdded: "1.0.0"
|
|
9
19
|
SafeAutoCorrect: false
|
|
10
20
|
ReplaceMethods: {}
|
|
11
21
|
ProhibitedMethods: {}
|
|
12
22
|
|
|
13
23
|
Sane/ConditionalAssignmentAllowTernary:
|
|
14
|
-
Description: "Enforces assignment inside conditions, but allows ternary operators
|
|
24
|
+
Description: "Enforces assignment inside conditions, but allows ternary operators"
|
|
15
25
|
Enabled: true
|
|
16
|
-
VersionAdded: "
|
|
26
|
+
VersionAdded: "1.0.0"
|
|
17
27
|
|
|
18
28
|
Sane/EmptyLineBeforeComment:
|
|
19
|
-
Description: "Enforces empty line before comments, except after block starts
|
|
29
|
+
Description: "Enforces empty line before comments, except after block starts"
|
|
30
|
+
Enabled: true
|
|
31
|
+
VersionAdded: "1.0.0"
|
|
32
|
+
SafeAutoCorrect: true
|
|
33
|
+
|
|
34
|
+
Sane/EmptyLinesAroundConditionBody:
|
|
35
|
+
Description: "Disallows empty lines at the beginning and end of condition bodies"
|
|
20
36
|
Enabled: true
|
|
21
|
-
VersionAdded: "
|
|
37
|
+
VersionAdded: "1.0.0"
|
|
22
38
|
SafeAutoCorrect: true
|
|
23
39
|
|
|
24
40
|
Sane/EmptyLinesAroundMultilineBlock:
|
|
25
|
-
Description: "Enforces empty lines before and after multiline blocks
|
|
41
|
+
Description: "Enforces empty lines before and after multiline blocks"
|
|
42
|
+
Enabled: true
|
|
43
|
+
VersionAdded: "1.0.0"
|
|
44
|
+
SafeAutoCorrect: true
|
|
45
|
+
|
|
46
|
+
Sane/EmptyLinesAroundMultilineCall:
|
|
47
|
+
Description: "(Experimental) Enforces empty lines before and after multiline method calls"
|
|
48
|
+
Enabled: false
|
|
49
|
+
VersionAdded: "1.0.0"
|
|
50
|
+
SafeAutoCorrect: true
|
|
51
|
+
SkipMethods:
|
|
52
|
+
- belongs_to
|
|
53
|
+
- has_one
|
|
54
|
+
- has_many
|
|
55
|
+
|
|
56
|
+
Sane/MultilineChainOnSeparateLines:
|
|
57
|
+
Description: "(Experimental) Enforces that each method in a multiline chain starts on its own line with a leading dot"
|
|
58
|
+
Enabled: false
|
|
59
|
+
VersionAdded: "1.0.0"
|
|
60
|
+
SafeAutoCorrect: false
|
|
61
|
+
|
|
62
|
+
Sane/OmitParentheses:
|
|
63
|
+
Description: "Enforces omitting parentheses for specific methods"
|
|
26
64
|
Enabled: true
|
|
27
|
-
VersionAdded: "
|
|
65
|
+
VersionAdded: "1.0.0"
|
|
28
66
|
SafeAutoCorrect: true
|
|
67
|
+
Methods: []
|
|
29
68
|
|
|
30
69
|
Sane/NoMethodCallAfterEnd:
|
|
31
|
-
Description: "Prohibits calling methods directly after `end
|
|
70
|
+
Description: "Prohibits calling methods directly after `end`"
|
|
71
|
+
Enabled: true
|
|
72
|
+
VersionAdded: "1.0.0"
|
|
73
|
+
|
|
74
|
+
Sane/OutdatedComments:
|
|
75
|
+
Description: "Detects TODO/NOTE/FIXME comments with dates in the past"
|
|
32
76
|
Enabled: true
|
|
33
|
-
VersionAdded: "
|
|
77
|
+
VersionAdded: "1.0.0"
|
|
78
|
+
|
|
79
|
+
Sane/RedundantSelfAssignment:
|
|
80
|
+
Description: "Detects redundant assignment within `||=` to the same target"
|
|
81
|
+
Enabled: true
|
|
82
|
+
VersionAdded: "1.0.0"
|
|
83
|
+
SafeAutoCorrect: true
|
|
84
|
+
|
|
85
|
+
Sane/VariableNameLength:
|
|
86
|
+
Description: "Enforces minimum variable name length"
|
|
87
|
+
Enabled: false
|
|
88
|
+
VersionAdded: "1.0.0"
|
|
89
|
+
MinLength: 2
|
|
90
|
+
AllowedNames:
|
|
91
|
+
- i
|
|
92
|
+
- j
|
|
93
|
+
- k
|
|
94
|
+
- n
|
|
95
|
+
- e
|
|
96
|
+
- x
|
|
97
|
+
- y
|
|
98
|
+
- _
|
|
@@ -95,7 +95,7 @@ module RuboCop
|
|
|
95
95
|
prev_line_number = comment.loc.line - 1
|
|
96
96
|
return false if prev_line_number < 1
|
|
97
97
|
|
|
98
|
-
processed_source.comments.any? { |c| c.loc.line == prev_line_number }
|
|
98
|
+
processed_source.comments.any? { |c| c.loc.line == prev_line_number && !inline_comment?(c) }
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def preceded_by_block_start?(comment)
|
|
@@ -105,9 +105,19 @@ module RuboCop
|
|
|
105
105
|
prev_line = processed_source.lines[prev_line_number - 1]
|
|
106
106
|
return false unless prev_line
|
|
107
107
|
|
|
108
|
+
# Strip inline comments from the previous line before checking
|
|
109
|
+
prev_line = strip_inline_comment(prev_line, prev_line_number)
|
|
110
|
+
|
|
108
111
|
block_start_pattern?(prev_line)
|
|
109
112
|
end
|
|
110
113
|
|
|
114
|
+
def strip_inline_comment(line, line_number)
|
|
115
|
+
inline = processed_source.comments.find { |c| c.loc.line == line_number && c.loc.column > 0 }
|
|
116
|
+
return line unless inline
|
|
117
|
+
|
|
118
|
+
line[0...inline.loc.column].rstrip
|
|
119
|
+
end
|
|
120
|
+
|
|
111
121
|
def block_start_pattern?(line)
|
|
112
122
|
stripped = line.strip
|
|
113
123
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# This cop checks for empty lines at the beginning and end of
|
|
7
|
+
# condition bodies (`if`, `elsif`, `else`, `unless`).
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# if condition
|
|
12
|
+
#
|
|
13
|
+
# body
|
|
14
|
+
#
|
|
15
|
+
# else
|
|
16
|
+
#
|
|
17
|
+
# another_body
|
|
18
|
+
#
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# if condition
|
|
23
|
+
# body
|
|
24
|
+
# else
|
|
25
|
+
# another_body
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
class EmptyLinesAroundConditionBody < Base
|
|
29
|
+
extend AutoCorrector
|
|
30
|
+
|
|
31
|
+
MSG_BEGINNING = "Remove empty line at the beginning of condition body"
|
|
32
|
+
MSG_END = "Remove empty line at the end of condition body"
|
|
33
|
+
|
|
34
|
+
def on_if(node)
|
|
35
|
+
return if node.ternary?
|
|
36
|
+
return if node.modifier_form?
|
|
37
|
+
|
|
38
|
+
check_if_branch(node)
|
|
39
|
+
check_else_branch(node)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def check_if_branch(node)
|
|
45
|
+
body = node.if_branch
|
|
46
|
+
return unless body
|
|
47
|
+
|
|
48
|
+
keyword_line = node.loc.keyword.line
|
|
49
|
+
closing_line = end_line_for_if_branch(node)
|
|
50
|
+
|
|
51
|
+
check_beginning(body, keyword_line)
|
|
52
|
+
check_end(body, closing_line, closing_range_for_if_branch(node))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_else_branch(node)
|
|
56
|
+
else_branch = node.else_branch
|
|
57
|
+
return unless else_branch
|
|
58
|
+
return unless node.loc.else
|
|
59
|
+
|
|
60
|
+
# elsif is handled by its own on_if call
|
|
61
|
+
return if else_branch.if_type? && else_branch.elsif?
|
|
62
|
+
|
|
63
|
+
else_line = node.loc.else.line
|
|
64
|
+
end_node = find_end_node(node)
|
|
65
|
+
end_line = end_node.loc.end.line
|
|
66
|
+
|
|
67
|
+
check_beginning(else_branch, else_line)
|
|
68
|
+
check_end(else_branch, end_line, end_node.loc.end)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def end_line_for_if_branch(node)
|
|
72
|
+
if node.else_branch
|
|
73
|
+
if node.loc.else
|
|
74
|
+
node.loc.else.line
|
|
75
|
+
else
|
|
76
|
+
node.else_branch.loc.keyword.line
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
find_end_node(node).loc.end.line
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def closing_range_for_if_branch(node)
|
|
84
|
+
if node.else_branch
|
|
85
|
+
node.loc.else || node.else_branch.loc.keyword
|
|
86
|
+
else
|
|
87
|
+
find_end_node(node).loc.end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def find_end_node(node)
|
|
92
|
+
current = node
|
|
93
|
+
current = current.parent while current.parent&.if_type? && current.elsif?
|
|
94
|
+
current
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_beginning(body, keyword_line)
|
|
98
|
+
first_line = effective_first_line(keyword_line, body.loc.first_line)
|
|
99
|
+
return unless first_line > keyword_line + 1
|
|
100
|
+
return unless blank_lines_between?(keyword_line, first_line)
|
|
101
|
+
|
|
102
|
+
range = first_body_expression(body).source_range
|
|
103
|
+
add_offense(range, message: MSG_BEGINNING) do |corrector|
|
|
104
|
+
remove_blank_lines(corrector, keyword_line, first_line)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def check_end(body, closing_line, closing_range)
|
|
109
|
+
last_line = effective_last_line(body.loc.last_line, closing_line)
|
|
110
|
+
return unless closing_line > last_line + 1
|
|
111
|
+
return unless blank_lines_between?(last_line, closing_line)
|
|
112
|
+
|
|
113
|
+
add_offense(closing_range, message: MSG_END) do |corrector|
|
|
114
|
+
remove_blank_lines(corrector, last_line, closing_line)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def first_body_expression(body)
|
|
119
|
+
body.begin_type? ? body.children.first : body
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def effective_first_line(keyword_line, body_first_line)
|
|
123
|
+
comments = comments_between(keyword_line, body_first_line)
|
|
124
|
+
comment = comments.min_by { |c| c.loc.line }
|
|
125
|
+
comment ? comment.loc.line : body_first_line
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def effective_last_line(body_last_line, closing_line)
|
|
129
|
+
comments = comments_between(body_last_line, closing_line)
|
|
130
|
+
comment = comments.max_by { |c| c.loc.line }
|
|
131
|
+
comment ? comment.loc.line : body_last_line
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def comments_between(start_line, end_line)
|
|
135
|
+
processed_source.comments.select do |c|
|
|
136
|
+
c.loc.line > start_line && c.loc.line < end_line && standalone_comment?(c)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def standalone_comment?(comment)
|
|
141
|
+
source_line = processed_source.lines[comment.loc.line - 1]
|
|
142
|
+
source_line.strip.start_with?("#")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def blank_lines_between?(start_line, end_line)
|
|
146
|
+
((start_line + 1)...end_line).any? { |line_num| blank_line?(line_num) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def blank_line?(line_num)
|
|
150
|
+
processed_source.lines[line_num - 1].strip.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def remove_blank_lines(corrector, start_line, end_line)
|
|
154
|
+
buffer = processed_source.buffer
|
|
155
|
+
|
|
156
|
+
((start_line + 1)...end_line).each do |line_num|
|
|
157
|
+
next unless blank_line?(line_num)
|
|
158
|
+
|
|
159
|
+
line_begin = buffer.line_range(line_num).begin_pos
|
|
160
|
+
next_line_begin =
|
|
161
|
+
if line_num < processed_source.lines.size
|
|
162
|
+
buffer.line_range(line_num + 1).begin_pos
|
|
163
|
+
else
|
|
164
|
+
buffer.line_range(line_num).end_pos
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
corrector.remove(Parser::Source::Range.new(buffer, line_begin, next_line_begin))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -4,7 +4,10 @@ module RuboCop
|
|
|
4
4
|
module Cop
|
|
5
5
|
module Sane
|
|
6
6
|
# This cop enforces empty lines before and after multiline blocks
|
|
7
|
-
# such as `if/else` and `
|
|
7
|
+
# such as `if/else`, `case/when`, and `begin...rescue...end`.
|
|
8
|
+
#
|
|
9
|
+
# Sorbet `sig` blocks require a blank line before, but not after,
|
|
10
|
+
# since they should be tightly coupled to their method definitions.
|
|
8
11
|
#
|
|
9
12
|
# @example
|
|
10
13
|
# # bad
|
|
@@ -49,6 +52,33 @@ module RuboCop
|
|
|
49
52
|
# end
|
|
50
53
|
# end
|
|
51
54
|
#
|
|
55
|
+
# # good - Sorbet sig blocks don't need blank line after
|
|
56
|
+
# something
|
|
57
|
+
#
|
|
58
|
+
# sig do
|
|
59
|
+
# params(x: Integer).returns(String)
|
|
60
|
+
# end
|
|
61
|
+
# def foo(x)
|
|
62
|
+
# x.to_s
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# # bad - missing blank line before begin
|
|
66
|
+
# temp_file = Tempfile.new(["import", ".csv"])
|
|
67
|
+
# begin
|
|
68
|
+
# temp_file.write(data)
|
|
69
|
+
# rescue IOError
|
|
70
|
+
# handle_error
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# # good - blank line before begin
|
|
74
|
+
# temp_file = Tempfile.new(["import", ".csv"])
|
|
75
|
+
#
|
|
76
|
+
# begin
|
|
77
|
+
# temp_file.write(data)
|
|
78
|
+
# rescue IOError
|
|
79
|
+
# handle_error
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
52
82
|
class EmptyLinesAroundMultilineBlock < Base
|
|
53
83
|
extend AutoCorrector
|
|
54
84
|
|
|
@@ -97,13 +127,37 @@ module RuboCop
|
|
|
97
127
|
|
|
98
128
|
alias on_case_match on_case
|
|
99
129
|
|
|
130
|
+
def on_kwbegin(node)
|
|
131
|
+
return unless multiline?(node)
|
|
132
|
+
|
|
133
|
+
assignment_parent = find_assignment_parent(node)
|
|
134
|
+
if assignment_parent
|
|
135
|
+
check_empty_line_before_assignment(node, assignment_parent)
|
|
136
|
+
check_empty_line_after_assignment(node, assignment_parent)
|
|
137
|
+
else
|
|
138
|
+
check_empty_line_before(node)
|
|
139
|
+
check_empty_line_after(node)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
100
143
|
def on_block(node)
|
|
101
144
|
return unless multiline?(node)
|
|
102
|
-
return if
|
|
145
|
+
return if method_chain_block?(node) # e.g., expect { ... }.to raise_error
|
|
103
146
|
return if lambda_block?(node) # e.g., -> { ... } or -> do ... end
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
147
|
+
return if block_in_collection?(node) # e.g., [items.map do...end] or {key: items.map do...end}
|
|
148
|
+
|
|
149
|
+
assignment_parent = find_assignment_parent(node)
|
|
150
|
+
if assignment_parent
|
|
151
|
+
# Don't require blank line before assignment
|
|
152
|
+
# But do check for blank line after the assignment
|
|
153
|
+
check_empty_line_after_assignment(node, assignment_parent)
|
|
154
|
+
elsif sig_block?(node)
|
|
155
|
+
# Sorbet sig blocks: require blank line before, but not after
|
|
156
|
+
check_empty_line_before(node)
|
|
157
|
+
else
|
|
158
|
+
check_empty_line_before(node)
|
|
159
|
+
check_empty_line_after(node)
|
|
160
|
+
end
|
|
107
161
|
end
|
|
108
162
|
|
|
109
163
|
alias on_numblock on_block
|
|
@@ -112,37 +166,76 @@ module RuboCop
|
|
|
112
166
|
node.send_node&.lambda_literal?
|
|
113
167
|
end
|
|
114
168
|
|
|
115
|
-
def
|
|
169
|
+
def sig_block?(node)
|
|
170
|
+
# Sorbet signature blocks: sig { ... } or sig do ... end
|
|
171
|
+
node.send_node&.method_name == :sig && node.send_node&.receiver.nil?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def method_chain_block?(node)
|
|
116
175
|
# Skip blocks that are part of a method chain
|
|
117
176
|
# e.g., expect { ... }.to raise_error
|
|
118
177
|
# e.g., foo.map { ... }&.join (csend is safe navigation &.)
|
|
119
178
|
parent = node.parent
|
|
120
|
-
|
|
179
|
+
(parent&.send_type? || parent&.csend_type?) && parent.receiver == node
|
|
180
|
+
end
|
|
121
181
|
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
|
|
182
|
+
def block_in_collection?(node)
|
|
183
|
+
# Skip blocks that are in arrays or hashes (enclosed in a structure)
|
|
184
|
+
parent = node.parent
|
|
185
|
+
return false unless parent
|
|
125
186
|
|
|
126
|
-
|
|
187
|
+
parent.array_type? || parent.pair_type?
|
|
127
188
|
end
|
|
128
189
|
|
|
129
|
-
def
|
|
190
|
+
def find_assignment_parent(node)
|
|
130
191
|
parent = node.parent
|
|
131
|
-
return
|
|
192
|
+
return nil unless parent
|
|
132
193
|
|
|
133
194
|
# Direct assignment: foo = bar.map do...end
|
|
134
|
-
return
|
|
195
|
+
return parent if assignment_node?(parent)
|
|
135
196
|
|
|
136
197
|
# Assignment via setter: self.foo = bar.map do...end
|
|
137
|
-
return
|
|
198
|
+
return parent if parent.send_type? && parent.method_name.to_s.end_with?("=")
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
141
202
|
|
|
142
|
-
|
|
143
|
-
return
|
|
203
|
+
def check_empty_line_before_assignment(block_node, assignment_node)
|
|
204
|
+
return if first_child_of_parent?(assignment_node)
|
|
144
205
|
|
|
145
|
-
|
|
206
|
+
prev_sib = previous_sibling(assignment_node)
|
|
207
|
+
return unless prev_sib
|
|
208
|
+
return if prev_sib.is_a?(Symbol)
|
|
209
|
+
return if empty_line_between?(prev_sib, assignment_node)
|
|
210
|
+
return if comment_line_before?(assignment_node)
|
|
211
|
+
|
|
212
|
+
keyword = block_keyword(block_node)
|
|
213
|
+
|
|
214
|
+
add_offense(block_node, message: format(MSG_BEFORE, keyword: keyword)) do |corrector|
|
|
215
|
+
corrector.insert_before(
|
|
216
|
+
assignment_node.source_range.with(
|
|
217
|
+
begin_pos: assignment_node.source_range.begin_pos - assignment_node.loc.column,
|
|
218
|
+
),
|
|
219
|
+
"\n",
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def check_empty_line_after_assignment(block_node, assignment_node)
|
|
225
|
+
return if last_child_of_parent?(assignment_node)
|
|
226
|
+
return if followed_by_rescue?(assignment_node)
|
|
227
|
+
|
|
228
|
+
next_sib = next_sibling(assignment_node)
|
|
229
|
+
return unless next_sib
|
|
230
|
+
return if next_sib.is_a?(Symbol)
|
|
231
|
+
return if empty_line_between?(assignment_node, next_sib)
|
|
232
|
+
return if rubocop_directive_after?(block_node)
|
|
233
|
+
|
|
234
|
+
keyword = block_keyword(block_node)
|
|
235
|
+
|
|
236
|
+
add_offense(block_node.loc.end, message: format(MSG_AFTER, keyword: keyword)) do |corrector|
|
|
237
|
+
corrector.insert_after(block_node, "\n")
|
|
238
|
+
end
|
|
146
239
|
end
|
|
147
240
|
|
|
148
241
|
def assignment_node?(node)
|
|
@@ -196,6 +289,8 @@ module RuboCop
|
|
|
196
289
|
node.keyword
|
|
197
290
|
elsif node.case_type? || node.case_match_type?
|
|
198
291
|
"case"
|
|
292
|
+
elsif node.kwbegin_type?
|
|
293
|
+
"begin"
|
|
199
294
|
elsif node.block_type? || node.numblock_type?
|
|
200
295
|
"do...end"
|
|
201
296
|
else
|
|
@@ -234,7 +329,7 @@ module RuboCop
|
|
|
234
329
|
return unless next_sibling
|
|
235
330
|
return if next_sibling.is_a?(Symbol) # Skip non-node siblings
|
|
236
331
|
return if empty_line_between?(node, next_sibling)
|
|
237
|
-
return if
|
|
332
|
+
return if rubocop_directive_after?(node)
|
|
238
333
|
|
|
239
334
|
keyword = block_keyword(node)
|
|
240
335
|
|
|
@@ -271,6 +366,9 @@ module RuboCop
|
|
|
271
366
|
return true if parent.type == :def && parent.body == node
|
|
272
367
|
return true if parent.type == :defs && parent.body == node
|
|
273
368
|
return true if parent.type == :resbody && parent.body == node
|
|
369
|
+
return true if parent.type == :class && parent.body == node
|
|
370
|
+
return true if parent.type == :module && parent.body == node
|
|
371
|
+
return true if parent.type == :sclass && parent.body == node
|
|
274
372
|
|
|
275
373
|
# For if/unless, check if node is the only thing in the if-branch or else-branch
|
|
276
374
|
if parent.type == :if
|
|
@@ -282,6 +380,9 @@ module RuboCop
|
|
|
282
380
|
return true if parent.type == :when && parent.body == node
|
|
283
381
|
return true if parent.type == :case && parent.else_branch == node
|
|
284
382
|
|
|
383
|
+
# For ensure: child[1] is the ensure clause body
|
|
384
|
+
return true if parent.type == :ensure && parent.children[1] == node
|
|
385
|
+
|
|
285
386
|
false
|
|
286
387
|
end
|
|
287
388
|
|
|
@@ -336,20 +437,30 @@ module RuboCop
|
|
|
336
437
|
line1 = node1.loc.last_line
|
|
337
438
|
line2 = node2.loc.first_line
|
|
338
439
|
|
|
339
|
-
#
|
|
340
|
-
(
|
|
440
|
+
# Check that at least one line between the nodes is truly blank
|
|
441
|
+
((line1 + 1)...line2).any? { |line_num| processed_source.lines[line_num - 1].strip.empty? }
|
|
341
442
|
end
|
|
342
443
|
|
|
343
444
|
def comment_line_before?(node)
|
|
344
445
|
line_before = node.loc.first_line - 1
|
|
345
446
|
return false if line_before < 1
|
|
346
447
|
|
|
347
|
-
processed_source.comments.any? { |c| c.loc.line == line_before }
|
|
448
|
+
processed_source.comments.any? { |c| c.loc.line == line_before && standalone_comment?(c) }
|
|
348
449
|
end
|
|
349
450
|
|
|
350
|
-
def
|
|
451
|
+
def rubocop_directive_after?(node)
|
|
351
452
|
line_after = node.loc.last_line + 1
|
|
352
|
-
processed_source.comments.any? { |c| c.loc.line == line_after }
|
|
453
|
+
processed_source.comments.any? { |c| c.loc.line == line_after && rubocop_directive?(c) }
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def rubocop_directive?(comment)
|
|
457
|
+
comment.text.start_with?("# rubocop:")
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def standalone_comment?(comment)
|
|
461
|
+
# A standalone comment is one that occupies its own line (not inline after code)
|
|
462
|
+
source_line = processed_source.lines[comment.loc.line - 1]
|
|
463
|
+
source_line.strip.start_with?("#")
|
|
353
464
|
end
|
|
354
465
|
end
|
|
355
466
|
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Enforces empty lines before and after multiline method calls,
|
|
7
|
+
# including calls with multiline arguments and multiline method chains.
|
|
8
|
+
#
|
|
9
|
+
class EmptyLinesAroundMultilineCall < Base # rubocop:disable Metrics/ClassLength
|
|
10
|
+
extend AutoCorrector
|
|
11
|
+
|
|
12
|
+
MSG_BEFORE = "Add empty line before multiline method call."
|
|
13
|
+
MSG_AFTER = "Add empty line after multiline method call."
|
|
14
|
+
|
|
15
|
+
ASSIGNMENT_TYPES = [:lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn, :masgn, :op_asgn, :or_asgn, :and_asgn].freeze
|
|
16
|
+
BODY_PARENT_TYPES = [:block, :numblock, :def, :defs, :resbody, :class, :module, :sclass].freeze
|
|
17
|
+
CONTROL_FLOW_TYPES = [:return, :break, :next].freeze
|
|
18
|
+
|
|
19
|
+
def on_send(node)
|
|
20
|
+
check_call(node)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias on_csend on_send
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def check_call(node)
|
|
28
|
+
return unless node.loc.first_line != node.loc.last_line
|
|
29
|
+
return if excluded_call?(node)
|
|
30
|
+
|
|
31
|
+
target = effective_target(node)
|
|
32
|
+
check_empty_line_before(target)
|
|
33
|
+
check_empty_line_after(node, target)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def excluded_call?(node)
|
|
37
|
+
has_block?(node) || inner_chain_call?(node) || call_as_argument?(node) ||
|
|
38
|
+
inside_collection?(node) || setter_call?(node) || inside_control_flow?(node) || skip_method?(node)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def has_block?(node)
|
|
42
|
+
parent = node.parent
|
|
43
|
+
(parent&.block_type? || parent&.numblock_type?) && node == parent.send_node
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def inner_chain_call?(node)
|
|
47
|
+
parent = node.parent
|
|
48
|
+
(parent&.send_type? || parent&.csend_type?) && node == parent.receiver
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def call_as_argument?(node)
|
|
52
|
+
parent = node.parent
|
|
53
|
+
return false unless parent&.send_type? || parent&.csend_type?
|
|
54
|
+
|
|
55
|
+
parent.arguments.include?(node)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inside_collection?(node)
|
|
59
|
+
parent = node.parent
|
|
60
|
+
parent && (parent.array_type? || parent.pair_type?)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def setter_call?(node)
|
|
64
|
+
node.method_name.to_s.end_with?("=")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inside_control_flow?(node)
|
|
68
|
+
parent = node.parent
|
|
69
|
+
parent && CONTROL_FLOW_TYPES.include?(parent.type)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def skip_method?(node)
|
|
73
|
+
cop_config.fetch("SkipMethods", []).include?(node.method_name.to_s)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def preceded_by_sig?(node)
|
|
77
|
+
prev_sib = previous_sibling(node)
|
|
78
|
+
return false unless prev_sib.is_a?(RuboCop::AST::Node)
|
|
79
|
+
|
|
80
|
+
(prev_sib.block_type? || prev_sib.numblock_type?) &&
|
|
81
|
+
prev_sib.send_node&.method_name == :sig &&
|
|
82
|
+
prev_sib.send_node&.receiver.nil?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def effective_target(node)
|
|
86
|
+
parent = node.parent
|
|
87
|
+
parent && ASSIGNMENT_TYPES.include?(parent.type) ? parent : node
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_empty_line_before(node)
|
|
91
|
+
return if first_child_of_parent?(node)
|
|
92
|
+
return if preceded_by_sig?(node)
|
|
93
|
+
|
|
94
|
+
prev_sib = previous_sibling(node)
|
|
95
|
+
return if prev_sib.nil? || prev_sib.is_a?(Symbol)
|
|
96
|
+
return if empty_line_between?(prev_sib, node)
|
|
97
|
+
return if comment_line_before?(node)
|
|
98
|
+
|
|
99
|
+
add_offense(node, message: MSG_BEFORE) do |corrector|
|
|
100
|
+
corrector.insert_before(
|
|
101
|
+
node.source_range.with(begin_pos: node.source_range.begin_pos - node.loc.column),
|
|
102
|
+
"\n",
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def check_empty_line_after(call_node, target)
|
|
108
|
+
return if last_child_of_parent?(target) || followed_by_rescue?(target)
|
|
109
|
+
|
|
110
|
+
next_sib = next_sibling(target)
|
|
111
|
+
return if next_sib.nil? || next_sib.is_a?(Symbol)
|
|
112
|
+
return if empty_line_between?(target, next_sib)
|
|
113
|
+
return if rubocop_directive_after?(call_node)
|
|
114
|
+
|
|
115
|
+
add_offense(after_offense_loc(call_node), message: MSG_AFTER) do |corrector|
|
|
116
|
+
corrector.insert_after(target, "\n")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def after_offense_loc(node)
|
|
121
|
+
if node.loc.respond_to?(:end) && node.loc.end
|
|
122
|
+
node.loc.end
|
|
123
|
+
elsif node.loc.respond_to?(:selector) && node.loc.selector
|
|
124
|
+
node.loc.selector
|
|
125
|
+
else
|
|
126
|
+
node
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def first_child_of_parent?(node)
|
|
131
|
+
parent = node.parent
|
|
132
|
+
return true unless parent
|
|
133
|
+
return true if only_child_in_body?(node, parent)
|
|
134
|
+
|
|
135
|
+
body_siblings(parent).first == node
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def last_child_of_parent?(node)
|
|
139
|
+
parent = node.parent
|
|
140
|
+
return true unless parent
|
|
141
|
+
return true if only_child_in_body?(node, parent)
|
|
142
|
+
|
|
143
|
+
body_siblings(parent).last == node
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def only_child_in_body?(node, parent)
|
|
147
|
+
return true if BODY_PARENT_TYPES.include?(parent.type) && parent.body == node
|
|
148
|
+
|
|
149
|
+
return true if (parent.type == :if) && (parent.if_branch == node || parent.else_branch == node)
|
|
150
|
+
|
|
151
|
+
return true if parent.type == :when && parent.body == node
|
|
152
|
+
return true if parent.type == :case && parent.else_branch == node
|
|
153
|
+
|
|
154
|
+
false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def body_siblings(parent)
|
|
158
|
+
parent.children.select { |c| c.is_a?(RuboCop::AST::Node) && c.loc&.expression }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def previous_sibling(node)
|
|
162
|
+
parent = node.parent
|
|
163
|
+
return unless parent
|
|
164
|
+
|
|
165
|
+
siblings = parent.children
|
|
166
|
+
index = siblings.index(node)
|
|
167
|
+
return unless index&.positive?
|
|
168
|
+
|
|
169
|
+
(index - 1).downto(0) do |i|
|
|
170
|
+
sib = siblings[i]
|
|
171
|
+
return sib if sib.is_a?(RuboCop::AST::Node) && sib.loc&.expression
|
|
172
|
+
end
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def next_sibling(node)
|
|
177
|
+
parent = node.parent
|
|
178
|
+
return unless parent
|
|
179
|
+
|
|
180
|
+
siblings = parent.children
|
|
181
|
+
index = siblings.index(node)
|
|
182
|
+
return unless index
|
|
183
|
+
|
|
184
|
+
((index + 1)...siblings.size).each do |i|
|
|
185
|
+
sib = siblings[i]
|
|
186
|
+
return sib if sib.is_a?(RuboCop::AST::Node) && sib.loc&.expression
|
|
187
|
+
end
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def empty_line_between?(node1, node2)
|
|
192
|
+
return true unless node1.loc&.expression && node2.loc&.expression
|
|
193
|
+
|
|
194
|
+
line1 = node1.loc.last_line
|
|
195
|
+
line2 = node2.loc.first_line
|
|
196
|
+
((line1 + 1)...line2).any? { |ln| processed_source.lines[ln - 1].strip.empty? }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def comment_line_before?(node)
|
|
200
|
+
line_before = node.loc.first_line - 1
|
|
201
|
+
return false if line_before < 1
|
|
202
|
+
|
|
203
|
+
processed_source.comments.any? { |c| c.loc.line == line_before && standalone_comment?(c) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def rubocop_directive_after?(node)
|
|
207
|
+
line_after = node.loc.last_line + 1
|
|
208
|
+
processed_source.comments.any? { |c| c.loc.line == line_after && c.text.start_with?("# rubocop:") }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def standalone_comment?(comment)
|
|
212
|
+
processed_source.lines[comment.loc.line - 1].strip.start_with?("#")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def followed_by_rescue?(node)
|
|
216
|
+
parent = node.parent
|
|
217
|
+
return false unless parent&.rescue_type?
|
|
218
|
+
|
|
219
|
+
siblings = parent.children
|
|
220
|
+
index = siblings.index(node)
|
|
221
|
+
return false unless index
|
|
222
|
+
|
|
223
|
+
siblings[index + 1]&.resbody_type?
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Enforces that each method in a multiline chain starts on its own line
|
|
7
|
+
# with a leading dot.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# browser.find(
|
|
12
|
+
# "div[role='menu'] .v-list-item",
|
|
13
|
+
# text: "Confirmation Sheet",
|
|
14
|
+
# wait: 10,
|
|
15
|
+
# ).click
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# browser
|
|
19
|
+
# .find(
|
|
20
|
+
# "div[role='menu'] .v-list-item",
|
|
21
|
+
# text: "Confirmation Sheet",
|
|
22
|
+
# wait: 10,
|
|
23
|
+
# )
|
|
24
|
+
# .click
|
|
25
|
+
#
|
|
26
|
+
class MultilineChainOnSeparateLines < Base
|
|
27
|
+
extend AutoCorrector
|
|
28
|
+
|
|
29
|
+
MSG = "Place each method call in a multiline chain on a separate line with a leading dot."
|
|
30
|
+
|
|
31
|
+
def on_send(node)
|
|
32
|
+
check_chain(node)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias on_csend on_send
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def check_chain(node)
|
|
40
|
+
return unless node.loc.dot
|
|
41
|
+
return unless node.receiver
|
|
42
|
+
|
|
43
|
+
receiver = node.receiver
|
|
44
|
+
return unless chained_call?(receiver)
|
|
45
|
+
|
|
46
|
+
return unless dot_on_receiver_last_line?(node, receiver)
|
|
47
|
+
return unless multiline?(receiver) || (multiline?(node) && used_as_chain_receiver?(node))
|
|
48
|
+
|
|
49
|
+
register_offense(node)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def chained_call?(receiver)
|
|
53
|
+
receiver.send_type? || receiver.csend_type?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def multiline?(node)
|
|
57
|
+
node.loc.first_line != node.loc.last_line
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dot_on_receiver_last_line?(node, receiver)
|
|
61
|
+
node.loc.dot.line == receiver.loc.last_line
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def used_as_chain_receiver?(node)
|
|
65
|
+
parent = node.parent
|
|
66
|
+
(parent&.send_type? || parent&.csend_type?) && parent.receiver == node
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def register_offense(node)
|
|
70
|
+
add_offense(node.loc.dot) do |corrector|
|
|
71
|
+
indent = chain_root_column(node) + indentation_width
|
|
72
|
+
range = node.loc.dot.with(begin_pos: node.receiver.source_range.end_pos)
|
|
73
|
+
dot_text = node.loc.dot.source
|
|
74
|
+
corrector.replace(range, "\n#{' ' * indent}#{dot_text}")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def chain_root_column(node)
|
|
79
|
+
current = node
|
|
80
|
+
while (recv = current.receiver) && (recv.send_type? || recv.csend_type?)
|
|
81
|
+
current = recv
|
|
82
|
+
end
|
|
83
|
+
current.receiver ? current.receiver.loc.column : current.loc.column
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def indentation_width
|
|
87
|
+
config.for_cop("Layout/IndentationWidth")["Width"] || 2
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Enforces omitting parentheses for specific methods.
|
|
7
|
+
#
|
|
8
|
+
# @example Methods configuration
|
|
9
|
+
# # .rubocop.yml
|
|
10
|
+
# # Sane/OmitParentheses:
|
|
11
|
+
# # Methods:
|
|
12
|
+
# # - render_record
|
|
13
|
+
# # - Log.info
|
|
14
|
+
#
|
|
15
|
+
# # bad
|
|
16
|
+
# render_record(record)
|
|
17
|
+
# Log.info("message")
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# render_record record
|
|
21
|
+
# Log.info "message"
|
|
22
|
+
#
|
|
23
|
+
class OmitParentheses < Base
|
|
24
|
+
extend AutoCorrector
|
|
25
|
+
|
|
26
|
+
MSG = "Omit parentheses for `%<method>s`."
|
|
27
|
+
|
|
28
|
+
def on_send(node)
|
|
29
|
+
return unless node.parenthesized?
|
|
30
|
+
return if node.multiline?
|
|
31
|
+
return if modifier_condition?(node)
|
|
32
|
+
|
|
33
|
+
matched_method = find_matching_method(node)
|
|
34
|
+
return unless matched_method
|
|
35
|
+
|
|
36
|
+
message = format(MSG, method: matched_method)
|
|
37
|
+
|
|
38
|
+
add_offense(node, message: message) do |corrector|
|
|
39
|
+
if node.arguments.empty?
|
|
40
|
+
corrector.remove(node.loc.begin)
|
|
41
|
+
else
|
|
42
|
+
corrector.replace(node.loc.begin, " ")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
corrector.remove(node.loc.end)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def find_matching_method(node)
|
|
52
|
+
methods.find { |method| matches?(node, method) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def matches?(node, method_config)
|
|
56
|
+
if method_config.include?(".")
|
|
57
|
+
receiver_name, method_name = method_config.split(".", 2)
|
|
58
|
+
|
|
59
|
+
node.receiver&.const_type? &&
|
|
60
|
+
node.receiver.short_name.to_s == receiver_name &&
|
|
61
|
+
node.method_name.to_s == method_name
|
|
62
|
+
else
|
|
63
|
+
node.method_name.to_s == method_config
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def modifier_condition?(node)
|
|
68
|
+
parent = node.parent
|
|
69
|
+
parent&.if_type? && parent.modifier_form?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def methods
|
|
73
|
+
@methods ||= Array(cop_config.fetch("Methods", []))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Sane
|
|
8
|
+
# Detects TODO/NOTE/FIXME comments with dates that are in the past.
|
|
9
|
+
#
|
|
10
|
+
# This cop helps ensure that dated comments are reviewed when their
|
|
11
|
+
# target date has passed.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad (when date is in the past)
|
|
15
|
+
# # TODO[2024-01-01]: Remove this after migration
|
|
16
|
+
# # NOTE[2023-06-15]: Temporary workaround
|
|
17
|
+
# # FIXME[2024-03-01]: Fix this bug
|
|
18
|
+
#
|
|
19
|
+
# # good (when date is in the future or today)
|
|
20
|
+
# # TODO[2025-12-31]: Remove this after migration
|
|
21
|
+
#
|
|
22
|
+
class OutdatedComments < Base
|
|
23
|
+
COMMENT_PATTERN = /^#\s*(NOTE|TODO|FIXME)\[(\d{4}-\d{2}-\d{2})\]:/i
|
|
24
|
+
MSG = "Review or remove this outdated comment dated %<date>s"
|
|
25
|
+
|
|
26
|
+
def on_new_investigation
|
|
27
|
+
return unless processed_source.valid_syntax?
|
|
28
|
+
|
|
29
|
+
processed_source.comments.each do |comment|
|
|
30
|
+
check_comment(comment)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def check_comment(comment)
|
|
37
|
+
match = comment.text.match(COMMENT_PATTERN)
|
|
38
|
+
return unless match
|
|
39
|
+
|
|
40
|
+
date_str = match[2]
|
|
41
|
+
parsed_date = Date.parse(date_str)
|
|
42
|
+
|
|
43
|
+
return if parsed_date >= Date.today
|
|
44
|
+
|
|
45
|
+
message = format(MSG, date: date_str)
|
|
46
|
+
add_offense(comment.source_range, severity: :warning, message:)
|
|
47
|
+
rescue Date::Error
|
|
48
|
+
# Invalid date format, ignore
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Detects comments that start with prohibited keywords.
|
|
7
|
+
# Configure ProhibitedWords to replace the case-sensitive default list.
|
|
8
|
+
# Configured words are flagged as warnings. An empty list disables matching.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# # DELETE
|
|
13
|
+
# # DELETE this code after migration
|
|
14
|
+
# # DELETE: remove after v2
|
|
15
|
+
# # REMEMBER to update the docs
|
|
16
|
+
# # remember to notify the team
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# # Delete users
|
|
20
|
+
# # DELETED items are archived
|
|
21
|
+
# # Remember what user said
|
|
22
|
+
#
|
|
23
|
+
class ProhibitedComments < Base
|
|
24
|
+
MSG = "%<word>s comment found — review and address the comment"
|
|
25
|
+
|
|
26
|
+
def on_new_investigation
|
|
27
|
+
return unless processed_source.valid_syntax?
|
|
28
|
+
|
|
29
|
+
pattern = /^#\s*(#{Regexp.union(cop_config['ProhibitedWords'])})\b/
|
|
30
|
+
processed_source.comments.each do |comment|
|
|
31
|
+
match = comment.text.match(pattern)
|
|
32
|
+
next unless match
|
|
33
|
+
|
|
34
|
+
add_offense(comment.source_range, message: format(MSG, word: match[1]), severity: :warning)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Detects redundant `||=` patterns where the RHS repeats the
|
|
7
|
+
# assignment to the same target.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# self.browser ||= self.browser = Capybara::Session.new(:cuprite)
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# self.browser ||= Capybara::Session.new(:cuprite)
|
|
15
|
+
#
|
|
16
|
+
# # bad
|
|
17
|
+
# @foo ||= @foo = compute_value
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# @foo ||= compute_value
|
|
21
|
+
#
|
|
22
|
+
class RedundantSelfAssignment < Base
|
|
23
|
+
extend AutoCorrector
|
|
24
|
+
|
|
25
|
+
MSG = "Redundant assignment in `||=`. Use `%<lhs>s ||= %<value>s` instead."
|
|
26
|
+
|
|
27
|
+
VARIABLE_ASGN_TYPES = [:lvasgn, :ivasgn, :cvasgn, :gvasgn].freeze
|
|
28
|
+
|
|
29
|
+
def on_or_asgn(node)
|
|
30
|
+
lhs, rhs = *node
|
|
31
|
+
return unless redundant_rhs?(lhs, rhs)
|
|
32
|
+
|
|
33
|
+
value_node = rhs.children.last
|
|
34
|
+
message = format(MSG, lhs: lhs.source, value: value_node.source)
|
|
35
|
+
|
|
36
|
+
add_offense(rhs, message: message) do |corrector|
|
|
37
|
+
corrector.replace(rhs, value_node.source)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def redundant_rhs?(lhs, rhs)
|
|
44
|
+
if VARIABLE_ASGN_TYPES.include?(lhs.type)
|
|
45
|
+
redundant_variable_assignment?(lhs, rhs)
|
|
46
|
+
elsif lhs.type == :casgn
|
|
47
|
+
redundant_const_assignment?(lhs, rhs)
|
|
48
|
+
elsif lhs.send_type?
|
|
49
|
+
redundant_method_assignment?(lhs, rhs)
|
|
50
|
+
else
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def redundant_variable_assignment?(lhs, rhs)
|
|
56
|
+
rhs.type == lhs.type &&
|
|
57
|
+
rhs.children[0] == lhs.children[0]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def redundant_const_assignment?(lhs, rhs)
|
|
61
|
+
rhs.type == :casgn &&
|
|
62
|
+
rhs.children[0] == lhs.children[0] &&
|
|
63
|
+
rhs.children[1] == lhs.children[1]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def redundant_method_assignment?(lhs, rhs)
|
|
67
|
+
rhs.send_type? &&
|
|
68
|
+
rhs.receiver == lhs.receiver &&
|
|
69
|
+
rhs.method_name == :"#{lhs.method_name}="
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Enforces a minimum length for variable names.
|
|
7
|
+
#
|
|
8
|
+
# Short variable names can reduce code readability. This cop checks
|
|
9
|
+
# local variable assignments and method/block parameters against a
|
|
10
|
+
# configurable minimum length. Common names like loop counters can
|
|
11
|
+
# be allowed via the `AllowedNames` option.
|
|
12
|
+
#
|
|
13
|
+
# @example MinLength: 3 (default)
|
|
14
|
+
# # bad
|
|
15
|
+
# a = 1
|
|
16
|
+
# ab = 2
|
|
17
|
+
# def foo(a, ab); end
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# age = 1
|
|
21
|
+
# amount = 2
|
|
22
|
+
# def foo(age, amount); end
|
|
23
|
+
#
|
|
24
|
+
# @example AllowedNames: ['i', 'j', 'k'] (default includes more)
|
|
25
|
+
# # good (allowed by default)
|
|
26
|
+
# items.each_with_index { |item, i| }
|
|
27
|
+
#
|
|
28
|
+
class VariableNameLength < Base
|
|
29
|
+
MSG = "Variable name '%<name>s' is too short (minimum is %<min>s characters)."
|
|
30
|
+
|
|
31
|
+
def on_lvasgn(node)
|
|
32
|
+
name = node.name
|
|
33
|
+
check_name(name, node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias on_arg on_lvasgn
|
|
37
|
+
alias on_optarg on_lvasgn
|
|
38
|
+
alias on_restarg on_lvasgn
|
|
39
|
+
alias on_kwarg on_lvasgn
|
|
40
|
+
alias on_kwoptarg on_lvasgn
|
|
41
|
+
alias on_kwrestarg on_lvasgn
|
|
42
|
+
alias on_blockarg on_lvasgn
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def check_name(name, node)
|
|
47
|
+
return unless name
|
|
48
|
+
|
|
49
|
+
name_str = name.to_s
|
|
50
|
+
return if name_str.start_with?("_")
|
|
51
|
+
return if name_str.length >= min_length
|
|
52
|
+
return if allowed_names.include?(name_str)
|
|
53
|
+
|
|
54
|
+
add_offense(node, message: format(MSG, name: name_str, min: min_length))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def min_length
|
|
58
|
+
@min_length ||= cop_config.fetch("MinLength", 3)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def allowed_names
|
|
62
|
+
@allowed_names ||= cop_config.fetch("AllowedNames", []).map(&:to_s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "sane/prohibited_comments"
|
|
3
4
|
require_relative "sane/disallow_methods"
|
|
4
5
|
require_relative "sane/conditional_assignment_allow_ternary"
|
|
5
6
|
require_relative "sane/empty_line_before_comment"
|
|
7
|
+
require_relative "sane/empty_lines_around_condition_body"
|
|
6
8
|
require_relative "sane/empty_lines_around_multiline_block"
|
|
9
|
+
require_relative "sane/empty_lines_around_multiline_call"
|
|
10
|
+
require_relative "sane/multiline_chain_on_separate_lines"
|
|
7
11
|
require_relative "sane/no_method_call_after_end"
|
|
12
|
+
require_relative "sane/outdated_comments"
|
|
13
|
+
require_relative "sane/redundant_self_assignment"
|
|
14
|
+
require_relative "sane/omit_parentheses"
|
|
15
|
+
require_relative "sane/variable_name_length"
|
data/lib/rubocop/sane/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-sane
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kodkod
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: lint_roller
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.
|
|
18
|
+
version: '1.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '1.
|
|
25
|
+
version: '1.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rubocop
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -54,7 +54,6 @@ files:
|
|
|
54
54
|
- ".rubocop.yml"
|
|
55
55
|
- ".vscode/cspell.json"
|
|
56
56
|
- ".vscode/project-words.txt"
|
|
57
|
-
- CHANGELOG.md
|
|
58
57
|
- CLAUDE.md
|
|
59
58
|
- CODE_OF_CONDUCT.md
|
|
60
59
|
- LICENSE.txt
|
|
@@ -65,8 +64,16 @@ files:
|
|
|
65
64
|
- lib/rubocop/cop/sane/conditional_assignment_allow_ternary.rb
|
|
66
65
|
- lib/rubocop/cop/sane/disallow_methods.rb
|
|
67
66
|
- lib/rubocop/cop/sane/empty_line_before_comment.rb
|
|
67
|
+
- lib/rubocop/cop/sane/empty_lines_around_condition_body.rb
|
|
68
68
|
- lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb
|
|
69
|
+
- lib/rubocop/cop/sane/empty_lines_around_multiline_call.rb
|
|
70
|
+
- lib/rubocop/cop/sane/multiline_chain_on_separate_lines.rb
|
|
69
71
|
- lib/rubocop/cop/sane/no_method_call_after_end.rb
|
|
72
|
+
- lib/rubocop/cop/sane/omit_parentheses.rb
|
|
73
|
+
- lib/rubocop/cop/sane/outdated_comments.rb
|
|
74
|
+
- lib/rubocop/cop/sane/prohibited_comments.rb
|
|
75
|
+
- lib/rubocop/cop/sane/redundant_self_assignment.rb
|
|
76
|
+
- lib/rubocop/cop/sane/variable_name_length.rb
|
|
70
77
|
- lib/rubocop/cop/sane_cops.rb
|
|
71
78
|
- lib/rubocop/sane.rb
|
|
72
79
|
- lib/rubocop/sane/plugin.rb
|
|
@@ -95,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
95
102
|
- !ruby/object:Gem::Version
|
|
96
103
|
version: '0'
|
|
97
104
|
requirements: []
|
|
98
|
-
rubygems_version:
|
|
105
|
+
rubygems_version: 4.0.6
|
|
99
106
|
specification_version: 4
|
|
100
107
|
summary: Sane RuboCop cops for modern Ruby development
|
|
101
108
|
test_files: []
|