forked from rubocop/rubocop
/
semicolon.rb
130 lines (108 loc) · 3.93 KB
/
semicolon.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
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for multiple expressions placed on the same line.
# It also checks for lines terminated with a semicolon.
#
# This cop has `AllowAsExpressionSeparator` configuration option.
# It allows `;` to separate several expressions on the same line.
#
# @example
# # bad
# foo = 1; bar = 2;
# baz = 3;
#
# # good
# foo = 1
# bar = 2
# baz = 3
#
# @example AllowAsExpressionSeparator: false (default)
# # bad
# foo = 1; bar = 2
#
# @example AllowAsExpressionSeparator: true
# # good
# foo = 1; bar = 2
class Semicolon < Base
include RangeHelp
extend AutoCorrector
MSG = 'Do not use semicolons to terminate expressions.'
REGEXP_DOTS = %i[tDOT2 tDOT3].freeze
def self.autocorrect_incompatible_with
[Style::SingleLineMethods]
end
def on_new_investigation
return if processed_source.blank?
ast = processed_source.ast
@range_nodes = ast.range_type? ? [ast] : []
@range_nodes.concat(ast.each_descendant(:irange, :erange).to_a)
check_for_line_terminator_or_opener
end
def on_begin(node)
return if cop_config['AllowAsExpressionSeparator']
exprs = node.children
return if exprs.size < 2
expressions_per_line(exprs).each do |line, expr_on_line|
# Every line with more than one expression on it is a
# potential offense
next unless expr_on_line.size > 1
find_semicolon_positions(line) { |pos| register_semicolon(line, pos, true) }
end
end
private
def check_for_line_terminator_or_opener
# Make the obvious check first
return unless processed_source.raw_source.include?(';')
each_semicolon do |line, column, token_before_semicolon|
register_semicolon(line, column, false, token_before_semicolon)
end
end
def each_semicolon
tokens_for_lines.each do |line, tokens|
yield line, tokens.last.column, tokens[-2] if tokens.last.semicolon?
yield line, tokens.first.column if tokens.first.semicolon?
end
end
def tokens_for_lines
processed_source.tokens.group_by(&:line)
end
def register_semicolon(line, column, after_expression, token_before_semicolon = nil)
range = source_range(processed_source.buffer, line, column)
add_offense(range) do |corrector|
if after_expression
corrector.replace(range, "\n")
else
# Prevents becoming one range instance with subsequent line when endless range
# without parentheses.
# See: https://github.com/rubocop/rubocop/issues/10791
if REGEXP_DOTS.include?(token_before_semicolon&.type)
range_node = find_range_node(token_before_semicolon)
corrector.wrap(range_node, '(', ')') if range_node
end
corrector.remove(range)
end
end
end
def expressions_per_line(exprs)
# create a map matching lines to the number of expressions on them
exprs_lines = exprs.map(&:first_line)
exprs_lines.group_by(&:itself)
end
def find_semicolon_positions(line)
# Scan for all the semicolons on the line
semicolons = processed_source[line - 1].enum_for(:scan, ';')
semicolons.each do
yield Regexp.last_match.begin(0)
end
end
def find_range_node(token_before_semicolon)
@range_nodes.detect do |range_node|
range_node.source_range.contains?(token_before_semicolon.pos)
end
end
end
end
end
end