immosquare-cleaner 1.0.0 → 1.0.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: fe48e81e3af7200a3967f640a3f3a34f7632a32d71323b2829841516ad5f8cb8
4
- data.tar.gz: 571422d60f872ce01a3e3896260b4fee7d0e2282f16978fe7cfd18e2530a1a23
3
+ metadata.gz: 7665690ab0d77f314f9feaa5a957d405e6180b886bc6ec0f8489984e4bb32b96
4
+ data.tar.gz: f8879a8bdbf932fd97f984ecac4a22b36e7ba223b6db7ef1f0f213375c34d785
5
5
  SHA512:
6
- metadata.gz: 9f9ec67de3055d93376abc0df67abcc4368e661a4f46a0bd702c9d679407407119364f3b4a68817e39b8ec8ae888ae3365eeb3e692b453317396e21abd0ab875
7
- data.tar.gz: e32bd136ee29441e48f17af89cac59741e11e931b7d335da8a8ccf8be3914deb4f4c558b75a99f85c108c66f18189ad23f3bca93cd7bfa354a25679f1af3a084
6
+ metadata.gz: 952cac84543b75a8bf42cd2adf738bd702e2d0c424b069103aedc9145b522f10ad8aa5883e03b9532b6232ff605213f3e4c1bc781420fc35b5b21da693723cb0
7
+ data.tar.gz: 761039c215cf757e82f24b02e302dac4195f22a2233d336e94940d71ca2345eaea72c305bd3a8ace4658441f7bf3e9615bef7da2a1614c8050661b3ea989607a
@@ -66,6 +66,17 @@ module ImmosquareCleaner
66
66
 
67
67
  attr_reader :root
68
68
 
69
+ ##============================================================##
70
+ ## Everything under `generators/**/templates/` is raw template
71
+ ## content, usually ERB behind the target file's extension: any
72
+ ## formatter would mangle its tags. Shared with
73
+ ## ImmosquareCleaner.clean so a single-file clean skips it too
74
+ ##============================================================##
75
+ def self.generator_template?(folders)
76
+ generators = folders.index("generators")
77
+ !generators.nil? && folders.drop(generators + 1).include?("templates")
78
+ end
79
+
69
80
  def initialize(root)
70
81
  @root = File.expand_path(root)
71
82
  end
@@ -139,6 +150,7 @@ module ImmosquareCleaner
139
150
  segments = path.split("/")
140
151
  folders = directory ? segments : segments[0..-2]
141
152
  return true if folders.intersect?(EXCLUDED_DIRS) || "#{path}/".start_with?(*EXCLUDED_PATHS.map {|excluded| "#{excluded}/" })
153
+ return true if self.class.generator_template?(folders)
142
154
 
143
155
  !directory && (EXCLUDED_FILES.include?(segments.last) || segments.last.end_with?(*EXCLUDED_SUFFIXES))
144
156
  end
@@ -3,6 +3,7 @@ require "json"
3
3
  module ImmosquareCleaner
4
4
  module Processors
5
5
  class Json < Base
6
+
6
7
  def self.match?(file_path)
7
8
  file_path.end_with?(".json")
8
9
  end
@@ -14,6 +15,7 @@ module ImmosquareCleaner
14
15
  File.write(file_path, formatted)
15
16
  normalize_last_line
16
17
  end
18
+
17
19
  end
18
20
  end
19
21
  end
@@ -1,6 +1,7 @@
1
1
  module ImmosquareCleaner
2
2
  module Processors
3
3
  class Markdown < Base
4
+
4
5
  def self.match?(file_path)
5
6
  file_path.end_with?(".md", ".md.erb")
6
7
  end
@@ -10,6 +11,7 @@ module ImmosquareCleaner
10
11
  File.write(file_path, formatted_md)
11
12
  normalize_last_line
12
13
  end
14
+
13
15
  end
14
16
  end
15
17
  end
@@ -1,3 +1,3 @@
1
1
  module ImmosquareCleaner
2
- VERSION = "1.0.0".freeze
2
+ VERSION = "1.0.1".freeze
3
3
  end
@@ -70,10 +70,13 @@ module ImmosquareCleaner
70
70
  setup_linter_configs!
71
71
 
72
72
  ##============================================================##
73
- ## Return if the file is in the exclusion list
73
+ ## Return if the file is in the exclusion list or is a generator
74
+ ## template. With `-p`, file_path is a /tmp copy: the template
75
+ ## check runs on origin_path
74
76
  ##============================================================##
75
77
  exclude_files = configuration.exclude_files.map {|file| File.join(Dir.pwd, file) }
76
78
  return if exclude_files.include?(file_path)
79
+ return if DirectoryCleaner.generator_template?(File.dirname(origin_path || file_path).split("/"))
77
80
 
78
81
  begin
79
82
  processor = processor_for(file_path)
@@ -96,8 +99,6 @@ module ImmosquareCleaner
96
99
  File.expand_path("..", __dir__)
97
100
  end
98
101
 
99
- private
100
-
101
102
  ##============================================================##
102
103
  ## Processor registry — order matters.
103
104
  ##
@@ -125,6 +126,8 @@ module ImmosquareCleaner
125
126
  Processors::Toml
126
127
  ].freeze
127
128
 
129
+ private
130
+
128
131
  def processor_for(file_path)
129
132
  PROCESSORS.find {|p| p.match?(file_path) } || Processors::Prettier
130
133
  end
@@ -286,7 +286,9 @@ module ERBLint
286
286
  ##============================================================##
287
287
  def extract_erb_code(erb_node)
288
288
  code_node = erb_node.children.find {|c| c&.type == :code }
289
- code_node&.loc&.source&.strip
289
+ return nil unless code_node
290
+
291
+ code_node.loc&.source&.strip
290
292
  end
291
293
 
292
294
  ##============================================================##
@@ -0,0 +1,188 @@
1
+ //============================================================//
2
+ // Local fork of eslint-plugin-align-assignments (v1.1.2, by
3
+ // Lucas Florio, MIT). Upstream is unmaintained since 2019.
4
+ //
5
+ // Differences with upstream:
6
+ // - reads `context.sourceCode` (ESLint 10 removed
7
+ // `context.getSourceCode()`);
8
+ // - knows the ES2021 logical assignment operators `||=`, `&&=`
9
+ // and `??=`. Without them the rule locates the `=` one column
10
+ // off, reports the group forever and its autofix never settles
11
+ // ("Circular fixes detected").
12
+ //============================================================//
13
+
14
+ //============================================================//
15
+ // MIT License
16
+ //
17
+ // Copyright (c) Lucas Florio <lucasefe@gmail.com>
18
+ //
19
+ // Permission is hereby granted, free of charge, to any person
20
+ // obtaining a copy of this software and associated
21
+ // documentation files (the "Software"), to deal in the
22
+ // Software without restriction, including without limitation
23
+ // the rights to use, copy, modify, merge, publish, distribute,
24
+ // sublicense, and/or sell copies of the Software, and to
25
+ // permit persons to whom the Software is furnished to do so,
26
+ // subject to the following conditions:
27
+ //
28
+ // The above copyright notice and this permission notice shall
29
+ // be included in all copies or substantial portions of the
30
+ // Software.
31
+ //
32
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
33
+ // KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
34
+ // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
35
+ // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
36
+ // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
37
+ // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
38
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
39
+ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40
+ //============================================================//
41
+ const hasRequire = /require\(/
42
+ const spaceMatcher = /(\s*)((?:\|\||&&|\?\?|\+|-|\*|\/|%|&|\^|\||<<|>>|\*\*|>>>)?=)/
43
+
44
+ const assignmentOperators = ["=", "+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=", "<<=", ">>=", "**=", ">>>=", "||=", "&&=", "??="]
45
+
46
+ const getLast = (ary) => ary[ary.length - 1]
47
+
48
+ const isAssignmentExpression = (node) => node.type === "AssignmentExpression"
49
+
50
+ const isMultiline = (firstToken, assignmentToken) => firstToken.loc.start.line !== assignmentToken.loc.start.line
51
+
52
+ const rule = {
53
+ meta: {
54
+ fixable: "code",
55
+ schema: [
56
+ {
57
+ type: "object",
58
+ properties: { requiresOnly: { type: "boolean" } },
59
+ additionalProperties: false
60
+ }
61
+ ]
62
+ },
63
+
64
+ create: (context) => {
65
+ const { options } = context
66
+ const requiresOnly = options && options.length > 0 && options[0].requiresOnly
67
+ const { sourceCode } = context
68
+ const groups = []
69
+ let previousNode
70
+
71
+ const shouldStartNewGroup = (node) => {
72
+ //============================================================//
73
+ // First line of all
74
+ //============================================================//
75
+ if (!previousNode) return true
76
+
77
+ //============================================================//
78
+ // Switching parent nodes
79
+ //============================================================//
80
+ if (node.parent !== previousNode.parent) return true
81
+
82
+ //============================================================//
83
+ // If previous node was a for and included the declarations,
84
+ // new group
85
+ //============================================================//
86
+ if (previousNode.parent.type === "ForStatement" && previousNode.declarations) return true
87
+
88
+ //============================================================//
89
+ // Previous line was blank
90
+ //============================================================//
91
+ const lineOfNode = sourceCode.getFirstToken(node).loc.start.line
92
+ const lineOfPrev = sourceCode.getLastToken(previousNode).loc.start.line
93
+ return lineOfNode - lineOfPrev !== 1
94
+ }
95
+
96
+ const addNode = (groupNode, node) => {
97
+ if (shouldStartNewGroup(groupNode)) groups.push([node])
98
+ else getLast(groups).push(node)
99
+
100
+ previousNode = groupNode
101
+ }
102
+
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
106
+ }
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
113
+ }
114
+
115
+ 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("=")
121
+ }
122
+
123
+ const areAligned = (maxPos, nodes) => nodes
124
+ .filter(assignmentOnFirstLine)
125
+ .map((node) => sourceCode.getText(node))
126
+ .every((source) => source.charAt(maxPos) === "=")
127
+
128
+ const getMaxPos = (nodes) => nodes
129
+ .filter(assignmentOnFirstLine)
130
+ .map(findAssigment)
131
+ .reduce((last, current) => Math.max(last, current), [])
132
+
133
+ const check = (group) => {
134
+ const maxPos = getMaxPos(group)
135
+
136
+ if (areAligned(maxPos, group)) return
137
+
138
+ context.report({
139
+ loc: {
140
+ start: group[0].loc.start,
141
+ end: getLast(group).loc.end
142
+ },
143
+ 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)
152
+
153
+ //============================================================//
154
+ // Source line may include spaces, we need to accomodate
155
+ // for that.
156
+ //============================================================//
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}`)
165
+ })
166
+ })
167
+ }
168
+
169
+ return {
170
+ VariableDeclaration: (node) => {
171
+ const source = sourceCode.getText(node)
172
+ if (requiresOnly && !hasRequire.test(source)) return
173
+
174
+ addNode(node, node)
175
+ },
176
+
177
+ ExpressionStatement: (node) => {
178
+ if (node.expression.type !== "AssignmentExpression") return
179
+
180
+ addNode(node, node.expression)
181
+ },
182
+
183
+ "Program:exit": () => groups.forEach(check)
184
+ }
185
+ }
186
+ }
187
+
188
+ export default rule
@@ -1,27 +1,26 @@
1
1
  //============================================================//
2
2
  // ESLint 10 compatibility shim for unmaintained plugins.
3
3
  //
4
- // Context: we rely on two npm plugins to enforce our JS style
5
- // (see rules/javascript.md — alignment of assignments and
6
- // imports is mandatory):
7
- // - eslint-plugin-align-assignments (last release: 2019)
8
- // - eslint-plugin-align-import (last release: 2020)
4
+ // Context: alignment of imports is mandatory in our JS style
5
+ // (see rules/javascript.md) and relies on
6
+ // eslint-plugin-align-import (last release: 2020). Assignment
7
+ // alignment uses the local fork in ./align-assignments.mjs.
9
8
  //
10
- // Both plugins call `context.getSourceCode()` inside their
11
- // rule `create()`. ESLint 10 removed that method: the source
12
- // code is now exposed as the `context.sourceCode` property.
13
- // Loading either plugin as-is throws:
9
+ // The plugin calls `context.getSourceCode()` inside its rules'
10
+ // `create()`. ESLint 10 removed that method: the source code
11
+ // is now exposed as the `context.sourceCode` property.
12
+ // Loading it as-is throws:
14
13
  // TypeError: context.getSourceCode is not a function
15
14
  //
16
- // Since neither plugin is maintained and no drop-in
17
- // replacement exists (Prettier/Biome don't align `=`), we wrap
18
- // each rule with a Proxy that re-injects `getSourceCode()` on
19
- // the context before delegating to the original `create()`.
20
- // The upstream packages stay untouched — when a maintained
15
+ // Since it is not maintained and no drop-in replacement
16
+ // exists, we wrap each rule with a Proxy that re-injects
17
+ // `getSourceCode()` on the context before delegating to the
18
+ // original `create()`.
19
+ // The upstream package stays untouched — when a maintained
21
20
  // alternative surfaces, delete this file and import directly.
22
21
  //============================================================//
23
- import alignAssignmentsPlugin from "eslint-plugin-align-assignments"
24
- import alignImportPlugin from "eslint-plugin-align-import"
22
+ import alignAssignmentsRule from "./align-assignments.mjs"
23
+ import alignImportPlugin from "eslint-plugin-align-import"
25
24
 
26
25
 
27
26
  const withLegacyContext = (rule) => ({
@@ -35,7 +34,7 @@ const withLegacyContext = (rule) => ({
35
34
 
36
35
  export const alignAssignments = {
37
36
  rules: {
38
- "align-assignments": withLegacyContext(alignAssignmentsPlugin.rules["align-assignments"])
37
+ "align-assignments": alignAssignmentsRule
39
38
  }
40
39
  }
41
40
 
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, writeFileSync } from "fs"
2
- import * as babelParser from "@babel/parser"
2
+ import * as babelParser from "@babel/parser"
3
3
 
4
4
  //============================================================//
5
5
  // Constants
@@ -36,7 +36,7 @@ const normalizeComments = (filePath) => {
36
36
  // Process blocks in reverse order to avoid line number shifts
37
37
  //============================================================//
38
38
  const blocksReversed = [...commentBlocks].reverse()
39
- let hasChanges = false
39
+ let hasChanges = false
40
40
 
41
41
  blocksReversed.forEach((block) => {
42
42
  const normalized = normalizeCommentBlock(block, lines)
@@ -111,7 +111,7 @@ const groupConsecutiveComments = (comments, originalLines) => {
111
111
  // Check if this is a standalone comment (line starts with //)
112
112
  // Skip end-of-line comments (e.g., const x = 1 // comment)
113
113
  //============================================================//
114
- const lineContent = originalLines[comment.loc.start.line - 1]
114
+ const lineContent = originalLines[comment.loc.start.line - 1]
115
115
  const isStandalone = lineContent.trim().startsWith("//")
116
116
 
117
117
  if (!isStandalone) {
@@ -32,7 +32,7 @@ module RuboCop
32
32
 
33
33
  extend AutoCorrector
34
34
 
35
- MSG = "Collapse multi-line `%<name>s` call into a single line.".freeze
35
+ MSG = "Collapse multi-line `%{name}` call into a single line.".freeze
36
36
 
37
37
  DEFAULT_METHODS = ["link_to"].freeze
38
38
 
@@ -24,7 +24,7 @@ module RuboCop
24
24
 
25
25
  extend AutoCorrector
26
26
 
27
- MSG = "Move priority kwargs (%<keys>s) to the front of the call.".freeze
27
+ MSG = "Move priority kwargs (%{keys}) to the front of the call.".freeze
28
28
 
29
29
  DEFAULT_METHODS = ["link_to"].freeze
30
30
  DEFAULT_PRIORITY_KEYS = ["remote", "method"].freeze
@@ -39,14 +39,14 @@ module RuboCop
39
39
  ##============================================================##
40
40
  jbuilder_block_node = node.each_ancestor(:block).find do |ancestor_node|
41
41
  block_method_name = ancestor_node.children.first.method_name
42
- (block_method_name == :encode || block_method_name == :new) && ancestor_node.children.first.receiver&.const_name == "Jbuilder"
42
+ [:encode, :new].include?(block_method_name) && ancestor_node.children.first.receiver&.const_name == "Jbuilder"
43
43
  end
44
44
 
45
45
  ##============================================================##
46
46
  ## Source Name is the name of the method called. Here in our
47
47
  ## block it will be "json" but it can be any value put in the
48
48
  ## Jbuilder.encode do |name|
49
- ## ----
49
+ ## ---------
50
50
  ## In .json.jbuilder files, it will always be "json"
51
51
  ##============================================================##
52
52
  source_name = receiver.loc.expression.source
@@ -217,3 +217,5 @@ CustomCops/Style/KwargPriorityOrder:
217
217
  - method
218
218
  Gemspec/RequireMFA:
219
219
  Enabled: false
220
+ Gemspec/RequiredRubyVersion:
221
+ Enabled: false
data/linters/rubocop.yml CHANGED
@@ -247,3 +247,5 @@ CustomCops/Style/KwargPriorityOrder:
247
247
  #################### GEM ###########################
248
248
  Gemspec/RequireMFA:
249
249
  Enabled: false # Don't require MFA to push a gem
250
+ Gemspec/RequiredRubyVersion:
251
+ Enabled: false # TargetRubyVersion is always set to the Ruby running the cleaner, so it never matches the minimum a gemspec declares (`>= 3.2.6` vs 4.0.x): the cop would flag every gemspec.
data/package.json CHANGED
@@ -13,7 +13,6 @@
13
13
  "@typescript-eslint/parser": "^8.71.0",
14
14
  "@typescript/native": "npm:typescript@^7.0.2",
15
15
  "eslint": "^10.11.0",
16
- "eslint-plugin-align-assignments": "^1.1.2",
17
16
  "eslint-plugin-align-import": "^1.0.0",
18
17
  "eslint-plugin-erb": "^2.1.1",
19
18
  "eslint-plugin-prefer-arrow": "^1.2.3",
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.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - immosquare
@@ -183,6 +183,7 @@ files:
183
183
  - linters/erb_lint/custom_align_consecutive_calls.rb
184
184
  - linters/erb_lint/custom_html_to_content_tag.rb
185
185
  - linters/erb_lint/custom_single_line_if_modifier.rb
186
+ - linters/eslint-plugins/align-assignments.mjs
186
187
  - linters/eslint-plugins/eslint10-compat.mjs
187
188
  - linters/eslint.config.mjs
188
189
  - linters/js-erb-lint-4.0.6.yml