immosquare-cleaner 1.2.4 → 1.3.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/lib/immosquare-cleaner/version.rb +1 -1
- data/linters/rubocop/cop/custom_cops/style/align_routes.rb +308 -0
- data/linters/rubocop.yml +4 -0
- data/package.json +2 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7caffdb6ce093eae93ca5f22d1e25676d935bbd8f034805307d53bd20e69c201
|
|
4
|
+
data.tar.gz: 3fa0fc3dc8f247f866f89108a13752aee5761b9e0fcb1f62071bb2de3eb35add
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e94943fb8aedca8ff9d71bd0dcc4a9a4360392806b814b8e72dbed789d37b3bdfe0952c95bf7c19bc3fc26038920de831577d44894f97fa032a5f9e2f6c8bb7c
|
|
7
|
+
data.tar.gz: 1d27e92164cf050d08421c100af8d181a696ba8b3c0c25648d669ef269cff5b2b724fccf4fe8d40da5785949aa7fc99640dec8797e788640fcf37eda9cadf346
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module CustomCops
|
|
4
|
+
module Style
|
|
5
|
+
##============================================================##
|
|
6
|
+
## This cop aligns the blocks of Rails routes in
|
|
7
|
+
## `config/routes.rb` and `config/routes/*.rb`: inside a
|
|
8
|
+
## block of consecutive route lines, the verb, the path,
|
|
9
|
+
## the target and every option (`:as`, `:via`,
|
|
10
|
+
## `:constraints`...) start in the same column. Options are
|
|
11
|
+
## aligned by name: a route without `:via` leaves that
|
|
12
|
+
## column empty and its `:as` lands under the `:as` of its
|
|
13
|
+
## neighbours.
|
|
14
|
+
##
|
|
15
|
+
## Only whitespace changes: paths, targets, options and
|
|
16
|
+
## comments come out identical, in the same order. The
|
|
17
|
+
## corrections only rewrite the gaps between tokens, never
|
|
18
|
+
## a token itself, so they cannot clash with the
|
|
19
|
+
## corrections of other cops.
|
|
20
|
+
##
|
|
21
|
+
## The option columns follow an order compatible with the
|
|
22
|
+
## order of every route in the block (`:via` before `:as`
|
|
23
|
+
## when a route carries them in that order, even when a
|
|
24
|
+
## neighbour without `:via` meets `:as` first). When two
|
|
25
|
+
## routes impose opposite orders, the block is left alone:
|
|
26
|
+
## no alignment can satisfy both without reordering.
|
|
27
|
+
##
|
|
28
|
+
## Recognized shapes: `get "a" => "c#a", :as => ...`, the
|
|
29
|
+
## same call with parentheses (`match("a" => "c#a", ...)`),
|
|
30
|
+
## and the shape without a positional target
|
|
31
|
+
## (`match "404", :to => ...`).
|
|
32
|
+
##
|
|
33
|
+
## @example
|
|
34
|
+
## bad
|
|
35
|
+
## get "users" => "users#index", :as => :users
|
|
36
|
+
## match "users/:id" => "users#update", :via => [:patch, :put], :as => :user
|
|
37
|
+
##
|
|
38
|
+
## good
|
|
39
|
+
## get "users" => "users#index", :as => :users
|
|
40
|
+
## match "users/:id" => "users#update", :via => [:patch, :put], :as => :user
|
|
41
|
+
##============================================================##
|
|
42
|
+
class AlignRoutes < RuboCop::Cop::Base
|
|
43
|
+
|
|
44
|
+
extend AutoCorrector
|
|
45
|
+
|
|
46
|
+
MSG = "Align the columns of consecutive routes.".freeze
|
|
47
|
+
|
|
48
|
+
VERBS = [:get, :post, :put, :patch, :delete, :match].freeze
|
|
49
|
+
|
|
50
|
+
##============================================================##
|
|
51
|
+
## Only the files Rails loads its routes from. A `get`
|
|
52
|
+
## call anywhere else is not a route.
|
|
53
|
+
##============================================================##
|
|
54
|
+
ROUTES_FILE = %r{config/routes(?:/[^/]+\.rb|\.rb)\z}
|
|
55
|
+
|
|
56
|
+
def on_new_investigation
|
|
57
|
+
@routes = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def on_send(node)
|
|
61
|
+
return unless routes_file?
|
|
62
|
+
return unless VERBS.include?(node.method_name)
|
|
63
|
+
return unless node.receiver.nil?
|
|
64
|
+
|
|
65
|
+
route = parse_route(node)
|
|
66
|
+
@routes << route if route
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_investigation_end
|
|
70
|
+
groups.each {|group| check_group(group) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def routes_file?
|
|
76
|
+
path = processed_source.file_path
|
|
77
|
+
path.nil? || ROUTES_FILE.match?(path.to_s)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##============================================================##
|
|
81
|
+
## A route line cut into segments: verb, path, target when
|
|
82
|
+
## there is one, then its options under their name (`:as`,
|
|
83
|
+
## `:via`...), in the order the line carries them. Every
|
|
84
|
+
## segment keeps its source range, so the corrections can
|
|
85
|
+
## address the gaps between segments. Nil when the call
|
|
86
|
+
## is not a single-line route starting its line.
|
|
87
|
+
##============================================================##
|
|
88
|
+
def parse_route(node)
|
|
89
|
+
range = node.source_range
|
|
90
|
+
return if range.first_line != range.last_line
|
|
91
|
+
|
|
92
|
+
line = processed_source.lines[range.first_line - 1]
|
|
93
|
+
return if line.nil? || !line.strip.start_with?(node.source.strip)
|
|
94
|
+
|
|
95
|
+
first = node.arguments.first
|
|
96
|
+
return if first.nil?
|
|
97
|
+
|
|
98
|
+
if first.hash_type?
|
|
99
|
+
return if first.braces? || first.pairs.empty?
|
|
100
|
+
|
|
101
|
+
pair = first.pairs.first
|
|
102
|
+
return unless pair.key.str_type?
|
|
103
|
+
|
|
104
|
+
target_pair = pair
|
|
105
|
+
path_range = pair.key.source_range
|
|
106
|
+
rest_pairs = first.pairs.drop(1)
|
|
107
|
+
elsif first.str_type?
|
|
108
|
+
target_pair = nil
|
|
109
|
+
path_range = first.source_range
|
|
110
|
+
rest_pairs = []
|
|
111
|
+
if node.arguments.size > 1
|
|
112
|
+
hash = node.arguments[1]
|
|
113
|
+
return unless hash.hash_type? && !hash.braces?
|
|
114
|
+
|
|
115
|
+
rest_pairs = hash.pairs
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
segments = [{:kind => :path, :range => path_range}]
|
|
122
|
+
segments << {:kind => :target, :range => target_pair.value.source_range, :pair => target_pair} if target_pair
|
|
123
|
+
rest_pairs.each_with_index do |pair, index|
|
|
124
|
+
key = pair.key.sym_type? ? ":#{pair.key.value}" : "__raw#{index}"
|
|
125
|
+
segments << {:kind => :option, :key => key, :range => pair.source_range}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
{:node => node, :line => range.first_line, :segments => segments}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##============================================================##
|
|
132
|
+
## Blocks of routes on adjacent lines. Any other line
|
|
133
|
+
## (comment, `scope`, `end`, a route this cop does not
|
|
134
|
+
## recognize...) puts at least one line between two
|
|
135
|
+
## routes and so closes the block. Two routes sharing a
|
|
136
|
+
## line are dropped: aligning them would rewrite the
|
|
137
|
+
## same line twice.
|
|
138
|
+
##============================================================##
|
|
139
|
+
def groups
|
|
140
|
+
sorted = @routes.sort_by {|route| route[:node].source_range.begin_pos }
|
|
141
|
+
counts = sorted.map {|route| route[:line] }.tally
|
|
142
|
+
sorted = sorted.reject {|route| counts[route[:line]] > 1 }
|
|
143
|
+
|
|
144
|
+
sorted.chunk_while {|a, b| b[:line] == a[:line] + 1 }.to_a
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
##============================================================##
|
|
148
|
+
## Check one block: compute the column of every segment,
|
|
149
|
+
## then flag each route whose gaps differ, correcting
|
|
150
|
+
## gap by gap.
|
|
151
|
+
##============================================================##
|
|
152
|
+
def check_group(group)
|
|
153
|
+
return if group.size < 2
|
|
154
|
+
|
|
155
|
+
keys = ordered_keys(group)
|
|
156
|
+
return if keys.nil?
|
|
157
|
+
|
|
158
|
+
place_segments(group, keys)
|
|
159
|
+
|
|
160
|
+
group.each do |route|
|
|
161
|
+
corrections = corrections_for(route)
|
|
162
|
+
next if corrections.empty?
|
|
163
|
+
|
|
164
|
+
add_offense(route[:node], :message => MSG) do |corrector|
|
|
165
|
+
corrections.each {|range, text| corrector.replace(range, text) }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
##============================================================##
|
|
171
|
+
## The column of every segment of every route of the
|
|
172
|
+
## block: first `=>` in one column, then one column per
|
|
173
|
+
## option name, in the order computed for the block. The
|
|
174
|
+
## comma stays glued to the value before it, the spaces
|
|
175
|
+
## follow it.
|
|
176
|
+
##============================================================##
|
|
177
|
+
def place_segments(group, keys)
|
|
178
|
+
verb_width = group.map {|route| route[:node].method_name.length }.max
|
|
179
|
+
path_width = group.map {|route| segment(route, :path)[:range].size }.max
|
|
180
|
+
|
|
181
|
+
group.each do |route|
|
|
182
|
+
node = route[:node]
|
|
183
|
+
path_start = node.source_range.column + verb_width + 1
|
|
184
|
+
segment(route, :path)[:start] = path_start
|
|
185
|
+
|
|
186
|
+
target = segment(route, :target)
|
|
187
|
+
head_end =
|
|
188
|
+
if target
|
|
189
|
+
target[:start] = path_start + path_width + 4
|
|
190
|
+
target[:start] + target[:range].size
|
|
191
|
+
else
|
|
192
|
+
path_start + path_width
|
|
193
|
+
end
|
|
194
|
+
route[:head_end] = head_end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
starts = {}
|
|
198
|
+
column = group.map {|route| route[:head_end] }.max + 2
|
|
199
|
+
keys.each do |key|
|
|
200
|
+
starts[key] = column
|
|
201
|
+
followers = keys.drop(keys.index(key) + 1)
|
|
202
|
+
ends = group.filter_map do |route|
|
|
203
|
+
option = route[:segments].find {|seg| seg[:key] == key }
|
|
204
|
+
column + option[:range].size if option && (option_keys(route) & followers).any?
|
|
205
|
+
end
|
|
206
|
+
column = ends.max + 2 if ends.any?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
group.each do |route|
|
|
210
|
+
route[:segments].each do |seg|
|
|
211
|
+
seg[:start] = starts[seg[:key]] if seg[:kind] == :option
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
##============================================================##
|
|
217
|
+
## The gaps of one route that do not match the computed
|
|
218
|
+
## columns, as [range, expected text] pairs. Ranges only
|
|
219
|
+
## ever span whitespace and the commas separating the
|
|
220
|
+
## segments, never a segment itself.
|
|
221
|
+
##============================================================##
|
|
222
|
+
def corrections_for(route)
|
|
223
|
+
node = route[:node]
|
|
224
|
+
selector = node.loc.selector
|
|
225
|
+
path = segment(route, :path)
|
|
226
|
+
corrections = []
|
|
227
|
+
|
|
228
|
+
if node.loc.begin
|
|
229
|
+
replace_gap(corrections, selector.end_pos, node.loc.begin.begin_pos, "")
|
|
230
|
+
replace_gap(corrections, node.loc.begin.end_pos, path[:range].begin_pos, "")
|
|
231
|
+
else
|
|
232
|
+
replace_gap(corrections, selector.end_pos, path[:range].begin_pos,
|
|
233
|
+
" " * (path[:start] - selector.column - selector.size))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
previous = path
|
|
237
|
+
if (target = segment(route, :target))
|
|
238
|
+
operator = target[:pair].loc.operator
|
|
239
|
+
##============================================================##
|
|
240
|
+
## The `=>` sits 3 columns before the target (its 2
|
|
241
|
+
## chars plus the space after it); the path ends, in
|
|
242
|
+
## the aligned line, at its start plus its length.
|
|
243
|
+
##============================================================##
|
|
244
|
+
replace_gap(corrections, path[:range].end_pos, operator.begin_pos,
|
|
245
|
+
" " * ((target[:start] - 3) - (path[:start] + path[:range].size)))
|
|
246
|
+
replace_gap(corrections, operator.end_pos, target[:range].begin_pos, " ")
|
|
247
|
+
previous = target
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
route[:segments].select {|seg| seg[:kind] == :option }.each do |option|
|
|
251
|
+
prev_end = previous[:start] + previous[:range].size
|
|
252
|
+
replace_gap(corrections, previous[:range].end_pos, option[:range].begin_pos,
|
|
253
|
+
",#{" " * (option[:start] - prev_end - 1)}")
|
|
254
|
+
previous = option
|
|
255
|
+
end
|
|
256
|
+
corrections
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
##============================================================##
|
|
260
|
+
## Record a gap correction when the current text differs.
|
|
261
|
+
##============================================================##
|
|
262
|
+
def replace_gap(corrections, begin_pos, end_pos, expected)
|
|
263
|
+
range = Parser::Source::Range.new(processed_source.buffer, begin_pos, end_pos)
|
|
264
|
+
return if range.source == expected
|
|
265
|
+
|
|
266
|
+
corrections << [range, expected]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def segment(route, kind)
|
|
270
|
+
route[:segments].find {|seg| seg[:kind] == kind }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def option_keys(route)
|
|
274
|
+
route[:segments].filter_map {|seg| seg[:key] }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
##============================================================##
|
|
278
|
+
## The order of the option columns: the first one
|
|
279
|
+
## compatible with the order every route gives its
|
|
280
|
+
## options, keeping first-encounter order on ties. Nil
|
|
281
|
+
## when two routes impose opposite orders.
|
|
282
|
+
##============================================================##
|
|
283
|
+
def ordered_keys(group)
|
|
284
|
+
keys = group.flat_map {|route| option_keys(route) }.uniq
|
|
285
|
+
predecessors = Hash.new {|hash, key| hash[key] = [] }
|
|
286
|
+
group.each do |route|
|
|
287
|
+
option_keys(route).each_cons(2) do |before, after|
|
|
288
|
+
predecessors[after] << before unless predecessors[after].include?(before)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
sorted = []
|
|
293
|
+
remaining = keys.dup
|
|
294
|
+
while remaining.any?
|
|
295
|
+
ready = remaining.select {|key| (predecessors[key] - sorted).empty? }
|
|
296
|
+
return if ready.empty?
|
|
297
|
+
|
|
298
|
+
sorted << ready.first
|
|
299
|
+
remaining.delete(ready.first)
|
|
300
|
+
end
|
|
301
|
+
sorted
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
data/linters/rubocop.yml
CHANGED
|
@@ -5,6 +5,7 @@ require:
|
|
|
5
5
|
- rubocop/cop/custom_cops/style/align_assignments
|
|
6
6
|
- rubocop/cop/custom_cops/style/inline_multiline_calls
|
|
7
7
|
- rubocop/cop/custom_cops/style/kwarg_priority_order
|
|
8
|
+
- rubocop/cop/custom_cops/style/align_routes
|
|
8
9
|
|
|
9
10
|
# Enable all RuboCop methods and turn caching on for large files
|
|
10
11
|
AllCops:
|
|
@@ -261,6 +262,9 @@ CustomCops/Style/KwargPriorityOrder:
|
|
|
261
262
|
- remote
|
|
262
263
|
- method
|
|
263
264
|
|
|
265
|
+
CustomCops/Style/AlignRoutes:
|
|
266
|
+
Enabled: true # Align the columns of consecutive routes in config/routes.rb and config/routes/*.rb
|
|
267
|
+
|
|
264
268
|
#################### GEM ###########################
|
|
265
269
|
Gemspec/RequireMFA:
|
|
266
270
|
Enabled: false # Don't require MFA to push a gem
|
data/package.json
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@babel/parser": "^8.0.7",
|
|
11
11
|
"@eslint/js": "^10.0.1",
|
|
12
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
13
|
-
"@typescript-eslint/parser": "^8.
|
|
12
|
+
"@typescript-eslint/eslint-plugin": "^8.72.0",
|
|
13
|
+
"@typescript-eslint/parser": "^8.72.0",
|
|
14
14
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
15
15
|
"eslint": "^10.13.0",
|
|
16
16
|
"eslint-plugin-align-import": "^1.0.0",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: immosquare-cleaner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- immosquare
|
|
@@ -195,6 +195,7 @@ files:
|
|
|
195
195
|
- linters/prettier.yml
|
|
196
196
|
- linters/rubocop.yml
|
|
197
197
|
- linters/rubocop/cop/custom_cops/style/align_assignments.rb
|
|
198
|
+
- linters/rubocop/cop/custom_cops/style/align_routes.rb
|
|
198
199
|
- linters/rubocop/cop/custom_cops/style/comment_normalization.rb
|
|
199
200
|
- linters/rubocop/cop/custom_cops/style/font_awesome_normalization.rb
|
|
200
201
|
- linters/rubocop/cop/custom_cops/style/inline_multiline_calls.rb
|