immosquare-cleaner 1.2.4 → 1.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f62bc1ef91591ce4508aab72115cb5acfe1f5e67cf2ca33aff2c4adaf50666c0
4
- data.tar.gz: 63a788a487d902e7ca031dbe0df8586f58f44894534a3bcc2262f04a4b76b9e6
3
+ metadata.gz: 06af1c0a31ef811c54efdd4d3f845317c81f02db83112947da04390fd5c69e6c
4
+ data.tar.gz: d3c8617e585affa872699e5378ac6c90818d1fe0d6a3fe6f1bbcf62d727c7451
5
5
  SHA512:
6
- metadata.gz: 8574bb58dc4ffc54d776a0345cf2028f7de7640ff5314ac0701c5e79a968111a7cd2809207c235c2b3b74f5fe438bf6325c6f8a3d77d4e0ee217525b7e1cffc6
7
- data.tar.gz: a00acfbfdf8e625c2c43abc74fa2a2d12e931dc710d1c54809c5dbd9c723f74ebf8001a88e8d62a6d8a856f65e331bb8d7ee9709b8cdadc9a496f1bf8e6e4891
6
+ metadata.gz: 354c8df2a5913b65613909c4d000161aafb2fcb7ab66b09e1dd909f0df6d6b74c982f44a65c7f10bba1525ceb1a42203f1aa8df37b70eaf2a8bc327efd7b4808
7
+ data.tar.gz: b0df5b076458f940f997f92bd76643fa472d850c480f88cc9508eccf1fb7ab0cfd8b99429e80ce0d18c9bc85976c52423ae955e48983cfc7d58725d82fc5f641
@@ -1,3 +1,3 @@
1
1
  module ImmosquareCleaner
2
- VERSION = "1.2.4".freeze
2
+ VERSION = "1.3.1".freeze
3
3
  end
@@ -38,8 +38,6 @@
38
38
  // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
39
39
  // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40
40
  //============================================================//
41
- const hasRequire = /require\(/
42
- const spaceMatcher = /(\s*)((?:\|\||&&|\?\?|\+|-|\*|\/|%|&|\^|\||<<|>>|\*\*|>>>)?=)/
43
41
 
44
42
  const assignmentOperators = ["=", "+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=", "<<=", ">>=", "**=", ">>>=", "||=", "&&=", "??="]
45
43
 
@@ -100,35 +98,31 @@ const rule = {
100
98
  previousNode = groupNode
101
99
  }
102
100
 
103
- const getPrefix = (node) => {
104
- const nodeBefore = isAssignmentExpression(node) ? node.left : node.declarations.find((dcl) => dcl.type === "VariableDeclarator").id
105
- return nodeBefore.loc.end.column - nodeBefore.loc.start.column
101
+ const assignmentTokenOf = (node) => {
102
+ const left = isAssignmentExpression(node) ? node.left : node.declarations[0]?.id
103
+ if (!left) return null
104
+ const token = sourceCode.getTokenAfter(left)
105
+ return token && assignmentOperators.includes(token.value) ? token : null
106
106
  }
107
107
 
108
- const findAssigment = (node) => {
109
- const prefix = getPrefix(node)
110
- const source = sourceCode.getText(node)
111
- const match = source.slice(prefix).match(spaceMatcher)
112
- return match ? match.index + prefix + match[2].length : null
108
+ const findAssignment = (node) => {
109
+ const token = assignmentTokenOf(node)
110
+ return token ? token.loc.end.column - sourceCode.getFirstToken(node).loc.start.column - 1 : null
113
111
  }
114
112
 
115
113
  const assignmentOnFirstLine = (node) => {
116
- if (isAssignmentExpression(node)) return node.left.loc.start.line === node.right.loc.start.line
117
-
118
- const source = sourceCode.getText(node)
119
- const lines = source.split("\n")
120
- return lines[0].includes("=")
114
+ const token = assignmentTokenOf(node)
115
+ return token && !isMultiline(sourceCode.getFirstToken(node), token)
121
116
  }
122
117
 
123
118
  const areAligned = (maxPos, nodes) => nodes
124
119
  .filter(assignmentOnFirstLine)
125
- .map((node) => sourceCode.getText(node))
126
- .every((source) => source.charAt(maxPos) === "=")
120
+ .every((node) => findAssignment(node) === maxPos)
127
121
 
128
122
  const getMaxPos = (nodes) => nodes
129
123
  .filter(assignmentOnFirstLine)
130
- .map(findAssigment)
131
- .reduce((last, current) => Math.max(last, current), [])
124
+ .map(findAssignment)
125
+ .reduce((last, current) => Math.max(last, current), 0)
132
126
 
133
127
  const check = (group) => {
134
128
  const maxPos = getMaxPos(group)
@@ -141,27 +135,21 @@ const rule = {
141
135
  end: getLast(group).loc.end
142
136
  },
143
137
  message: "This group of assignments is not aligned",
144
- fix: (fixer) => group.map((node) => {
145
- const tokens = sourceCode.getTokens(node)
146
- const firstToken = tokens[0]
147
- const assignmentToken = tokens.find((token) => assignmentOperators.includes(token.value))
148
- const line = sourceCode.getText(node)
149
- const lineIsAligned = line.charAt(maxPos) === "="
150
-
151
- if (lineIsAligned || !assignmentToken || isMultiline(firstToken, assignmentToken)) return fixer.replaceText(node, line)
138
+ fix: (fixer) => group.flatMap((node) => {
139
+ const firstToken = sourceCode.getFirstToken(node)
140
+ const assignmentToken = assignmentTokenOf(node)
141
+ if (!assignmentToken || isMultiline(firstToken, assignmentToken) || findAssignment(node) === maxPos) return []
142
+ const previousToken = sourceCode.getTokenBefore(assignmentToken)
143
+ const gap = [previousToken.range[1], assignmentToken.range[0]]
144
+ if (sourceCode.commentsExistBetween(previousToken, assignmentToken)) return []
152
145
 
153
146
  //============================================================//
154
- // Source line may include spaces, we need to accomodate
155
- // for that.
147
+ // Only the spacing before the operator belongs to this rule. Replacing a declaration
148
+ // can cut a template processor's placeholder at an AST boundary inside that marker.
156
149
  //============================================================//
157
- const spacePrefix = firstToken.loc.start.column
158
- const startDelimiter = assignmentToken.loc.start.column - spacePrefix
159
- const endDelimiter = assignmentToken.loc.end.column - spacePrefix
160
- const start = line.slice(0, startDelimiter).replace(/\s+$/m, "")
161
- const ending = line.slice(endDelimiter).replace(/^\s+/m, "")
162
- const spacesRequired = maxPos - start.length - assignmentToken.value.length + 1
163
- const spaces = " ".repeat(spacesRequired)
164
- return fixer.replaceText(node, `${start}${spaces}${assignmentToken.value} ${ending}`)
150
+ const prefixWidth = previousToken.loc.end.column - firstToken.loc.start.column
151
+ const spacesRequired = maxPos - prefixWidth - assignmentToken.value.length + 1
152
+ return [fixer.replaceTextRange(gap, " ".repeat(spacesRequired))]
165
153
  })
166
154
  })
167
155
  }
@@ -169,7 +157,7 @@ const rule = {
169
157
  return {
170
158
  VariableDeclaration: (node) => {
171
159
  const source = sourceCode.getText(node)
172
- if (requiresOnly && !hasRequire.test(source)) return
160
+ if (requiresOnly && !source.includes("require(")) return
173
161
 
174
162
  addNode(node, node)
175
163
  },
@@ -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
@@ -7,10 +7,10 @@
7
7
  ],
8
8
  "private": true,
9
9
  "dependencies": {
10
- "@babel/parser": "^8.0.7",
10
+ "@babel/parser": "^8.0.8",
11
11
  "@eslint/js": "^10.0.1",
12
- "@typescript-eslint/eslint-plugin": "^8.71.2",
13
- "@typescript-eslint/parser": "^8.71.2",
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.2.4
4
+ version: 1.3.1
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