forked from rrrene/credo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
space_around_operators.ex
266 lines (217 loc) · 7.5 KB
/
space_around_operators.ex
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
defmodule Credo.Check.Consistency.SpaceAroundOperators do
use Credo.Check,
id: "EX1005",
run_on_all: true,
base_priority: :high,
tags: [:formatter],
param_defaults: [ignore: [:|]],
explanations: [
check: """
Use spaces around operators like `+`, `-`, `*` and `/`. This is the
**preferred** way, although other styles are possible, as long as it is
applied consistently.
# preferred
1 + 2 * 4
# also okay
1+2*4
While this is not necessarily a concern for the correctness of your code,
you should use a consistent style throughout your codebase.
""",
params: [
ignore: "List of operators to be ignored for this check."
]
]
@collector Credo.Check.Consistency.SpaceAroundOperators.Collector
# TODO: add *ignored* operators, so you can add "|" and still write
# [head|tail] while enforcing 2 + 3 / 1 ...
# FIXME: this seems to be already implemented, but there don't seem to be
# any related test cases around.
@doc false
@impl true
def run_on_all_source_files(exec, source_files, params) do
@collector.find_and_append_issues(source_files, exec, params, &issues_for/3)
end
defp issues_for(expected, source_file, params) do
tokens = Credo.Code.to_tokens(source_file)
ast = SourceFile.ast(source_file)
issue_meta = IssueMeta.for(source_file, params)
issue_locations =
expected
|> @collector.find_locations_not_matching(source_file)
|> Enum.reject(&ignored?(&1, params))
|> Enum.filter(&create_issue?(&1, tokens, ast, issue_meta))
Enum.map(issue_locations, fn location ->
format_issue(
issue_meta,
message: message_for(expected),
line_no: location[:line_no],
column: location[:column],
trigger: location[:trigger]
)
end)
end
defp message_for(:with_space = _expected) do
"There are spaces around operators most of the time, but not here."
end
defp message_for(:without_space = _expected) do
"There are no spaces around operators most of the time, but here there are."
end
defp ignored?(location, params) do
ignored_triggers = Params.get(params, :ignore, __MODULE__)
Enum.member?(ignored_triggers, location[:trigger])
end
defp create_issue?(location, tokens, ast, issue_meta) do
line_no = location[:line_no]
trigger = location[:trigger]
column = location[:column]
line =
issue_meta
|> IssueMeta.source_file()
|> SourceFile.line_at(line_no)
create_issue?(trigger, line_no, column, line, tokens, ast)
end
defp create_issue?(trigger, line_no, column, line, tokens, ast) when trigger in [:+, :-] do
create_issue?(line, column, trigger) &&
!parameter_in_function_call?({line_no, column, trigger}, tokens, ast)
end
defp create_issue?(trigger, _line_no, column, line, _tokens, _ast) do
create_issue?(line, column, trigger)
end
# Don't create issues for `c = -1`
# TODO: Consider moving these checks inside the Collector.
defp create_issue?(line, column, trigger) when trigger in [:+, :-] do
!number_with_sign?(line, column) && !number_in_range?(line, column) &&
!(trigger == :- && minus_in_binary_size?(line, column))
end
defp create_issue?(line, column, trigger) when trigger == :-> do
!arrow_in_typespec?(line, column)
end
defp create_issue?(line, column, trigger) when trigger == :/ do
!number_in_function_capture?(line, column)
end
defp create_issue?(line, _column, trigger) when trigger == :* do
# The Elixir formatter always removes spaces around the asterisk in
# typespecs for binaries by default. Credo shouldn't conflict with the
# default Elixir formatter settings.
!typespec_binary_unit_operator_without_spaces?(line)
end
defp create_issue?(_, _, _), do: true
defp typespec_binary_unit_operator_without_spaces?(line) do
# In code this construct can only appear inside a binary typespec. It could
# also appear verbatim in a string, but it's rather unlikely...
line =~ "_::_*"
end
defp arrow_in_typespec?(line, column) do
# -2 because we need to subtract the operator
line
|> String.slice(0..(column - 2))
|> String.match?(~r/\(\s*$/)
end
defp number_with_sign?(line, column) do
line
# -2 because we need to subtract the operator
|> String.slice(0..(column - 2))
|> String.match?(~r/(\A\s+|\@[a-zA-Z0-9\_]+\.?|[\|\\\{\[\(\,\:\>\<\=\+\-\*\/])\s*$/)
end
defp number_in_range?(line, column) do
line
|> Credo.Backports.String.slice(column..-1)
|> String.match?(~r/^\d+\.\./)
end
defp number_in_function_capture?(line, column) do
line
|> String.slice(0..(column - 2))
|> String.match?(~r/[\.\&][a-z0-9_]+[\!\?]?$/)
end
# TODO: this implementation is a bit naive. improve it.
defp minus_in_binary_size?(line, column) do
# -2 because we need to subtract the operator
binary_pattern_start_before? =
line
|> String.slice(0..(column - 2))
|> String.match?(~r/\<\</)
# -2 because we need to subtract the operator
double_colon_before? =
line
|> String.slice(0..(column - 2))
|> String.match?(~r/\:\:/)
# -1 because we need to subtract the operator
binary_pattern_end_after? =
line
|> Credo.Backports.String.slice(column..-1)
|> String.match?(~r/\>\>/)
# -1 because we need to subtract the operator
typed_after? =
line
|> Credo.Backports.String.slice(column..-1)
|> String.match?(~r/^\s*(integer|native|signed|unsigned|binary|size|little|float)/)
# -2 because we need to subtract the operator
typed_before? =
line
|> String.slice(0..(column - 2))
|> String.match?(~r/(integer|native|signed|unsigned|binary|size|little|float)\s*$/)
heuristics_met_count =
[
binary_pattern_start_before?,
binary_pattern_end_after?,
double_colon_before?,
typed_after?,
typed_before?
]
|> Enum.count(& &1)
heuristics_met_count >= 2
end
defp parameter_in_function_call?(location_tuple, tokens, ast) do
case find_prev_current_next_token(tokens, location_tuple) do
{prev, _current, _next} ->
prev
|> Credo.Code.TokenAstCorrelation.find_tokens_in_ast(ast)
|> List.wrap()
|> List.first()
|> is_parameter_in_function_call()
_ ->
false
end
end
defp is_parameter_in_function_call({atom, _, arguments})
when is_atom(atom) and is_list(arguments) do
true
end
defp is_parameter_in_function_call(
{{:., _, [{:__aliases__, _, _mods}, fun_name]}, _, arguments}
)
when is_atom(fun_name) and is_list(arguments) do
true
end
defp is_parameter_in_function_call(_) do
false
end
# TOKENS
defp find_prev_current_next_token(tokens, location_tuple) do
tokens
|> traverse_prev_current_next(&matching_location(location_tuple, &1, &2, &3, &4), [])
|> List.first()
end
defp traverse_prev_current_next(tokens, callback, acc) do
tokens
|> case do
[prev | [current | [next | rest]]] ->
acc = callback.(prev, current, next, acc)
traverse_prev_current_next([current | [next | rest]], callback, acc)
_ ->
acc
end
end
defp matching_location(
{line_no, column, trigger},
prev,
{_, {line_no, column, _}, trigger} = current,
next,
acc
) do
acc ++ [{prev, current, next}]
end
defp matching_location(_, _prev, _current, _next, acc) do
acc
end
end