eslint-plugin-lube 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Fixed
6
+
7
+ - `svelte-naming-convention` no longer reports or renames an ambient TypeScript declaration (`declare function`, `declare const`, `declare let`, `declare var`, `declare class`, and anything inside `declare global { }` or `declare module "m" { }`) or its usages, since the name belongs to the host that injects it.
8
+ - `svelte-naming-convention` renames every overload signature and the implementation of a function together with its calls, instead of only the calls.
9
+
10
+ ## 0.8.0
11
+
12
+ ### Breaking
13
+
14
+ - **The strict config moves inline parameter types into `@param` tags** with the new
15
+ `prefer-param-tag`: `function f(/** @type {T} */ a) {}` becomes `@param {T} a` in the JSDoc block of
16
+ the declaration. It has a fix, so run `eslint --fix` once; a destructured parameter is left to you.
17
+
18
+ ### Added
19
+
20
+ - `prefer-param-tag`, in the strict config: reports a function declaration whose parameters are typed
21
+ with inline comments, `function f(/** @type {T} */ a) {}`, and fixes it into `@param {T} a` tags in the
22
+ JSDoc block of the declaration, creating the block if there is none, each tag at the position of its
23
+ parameter among the existing `@param` tags. Arrow functions and function expressions stay, and a
24
+ destructured parameter is reported without a fix.
25
+
3
26
  ## 0.7.0
4
27
 
5
28
  ### Breaking
package/README.md CHANGED
@@ -89,6 +89,7 @@ Both configs are flat config objects with `plugins` and `rules`, and differ in h
89
89
 
90
90
  **`configs.strict`** is the house style of this repository, and several of its rules cannot be fixed. It is everything in `recommended` plus:
91
91
  - `svelte-naming-convention`, which renames declarations to snake_case.
92
+ - `prefer-param-tag`, which moves the inline `/** @type {T} */` comments of a function declaration's parameters into `@param` tags on the declaration ([prefer-param-tag](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/prefer-param-tag.md)).
92
93
  - `prefer-type-tag`, which moves the cast of a declaration's value into a `@type` tag on the declaration, in a project linted with type information ([prefer-type-tag](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/prefer-type-tag.md) shows the parser options).
93
94
  - `ascii-order` for import statements, object keys, function declarations and test cases too (see below).
94
95
  - `pretty-sequence` and `pretty-imports` at 30 characters.
@@ -111,6 +112,7 @@ Override a rule after the spread: `rules: { ...lube.configs.recommended.rules, "
111
112
  | --- | --- | --- |
112
113
  | [ascii-order](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/ascii-order.md) | ASCII order of import statements, import and export names, object keys, function declarations and test cases | names only |
113
114
  | [curly](https://eslint.org/docs/latest/rules/curly) | Braces around the bodies of `if`, `else`, `for`, `while` and `do`, as ESLint's own rule does, with a fix that keeps a comment in front of the body, such as a JSDoc cast, inside the braces | no |
115
+ | [prefer-param-tag](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/prefer-param-tag.md) | JSDoc `@param` tags on a function declaration instead of inline `/** @type {T} */` comments in its parameter list | no |
114
116
  | [prefer-type-tag](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/prefer-type-tag.md) | A JSDoc `@type` tag on a declaration instead of a cast of its value, wherever the tag would type check (needs type information) | no |
115
117
  | [pretty-imports](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/pretty-imports.md) | Import and export names on one line when they fit, or one per line | yes |
116
118
  | [pretty-jsdoc-casting](https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/pretty-jsdoc-casting.md) | JSDoc type casts written as `/** @type {T} */(value)/**/` | yes |
@@ -0,0 +1,25 @@
1
+ # prefer-param-tag
2
+ Enforces JSDoc `@param` tags on a function declaration, instead of inline `/** @type {T} */` comments inside its parameter list.
3
+
4
+ - Only function declarations are checked, `export` and `export default` ones too. Arrow functions and function expressions, such as `items.map((/** @type {string} */ x) => x)`, have no declaration to carry a tag and are left alone, and so are casts in a body.
5
+ - The fix removes the inline comments and adds one `@param {T} name` line per parameter to the JSDoc block above the declaration, in parameter order, after the existing `@param` tags and before `@returns` and the like. It creates the block when there is none, and keeps the indentation of the declaration. A `@param` tag that already names the parameter is not repeated.
6
+ - A default value, `/** @type {number} */ n = 1`, is typed by `@param {number} n` too.
7
+ - A destructured or rest parameter has no simple name to write in a tag and is reported without a fix. So is a declaration that does not start its line, where a block cannot be added.
8
+
9
+ ## options
10
+ None.
11
+
12
+ ## Valid
13
+ ```js
14
+ /**
15
+ * @param {string} tx
16
+ */
17
+ function use(tx) {}
18
+ items.map((/** @type {string} */ x) => x)
19
+ ```
20
+
21
+ ## Invalid
22
+ ```js
23
+ function use(/** @type {string} */ tx) {}
24
+ async function source(/** @type {unknown} */ n, /** @type {number} */ m = 1) {}
25
+ ```
@@ -6,6 +6,7 @@ The strict config turns it on and the recommended config leaves it off, since it
6
6
  - The message names the expected name, `Rename 'camelCase' to 'camel_case'`, or, for a name without a snake_case form such as `NO_SNAKE__CASE`, asks for snake_case, UPPER_SNAKE_CASE or PascalCase.
7
7
  - Usages such as assignments, function bodies, `extends` clauses, JSX and TypeScript expressions are only renamed along with their declaration; `fixSameNames` sets the order.
8
8
  - Usages are matched to declarations by scope, so a global, an import or an exported camelCase function of the same name is left as it is.
9
+ - An ambient TypeScript declaration, `declare function acquireVsCodeApi(): Api`, `declare const`, `declare class`, or anything inside `declare global { }` or `declare module "m" { }`, names a binding the code does not own: it is neither reported nor renamed, and neither are its usages.
9
10
  - A declaration exported with `export { camelCase }` is renamed together with the export specifier, `export { camel_case as camelCase }`.
10
11
  - The JSDoc of a renamed parameter follows it: `@param {T} [camelCase]`, `{camelCase is T}` and `{typeof camelCase}`.
11
12
  - In a `.svelte` file, the store subscriptions of a renamed store are renamed with it, `$camelCase` into `$camel_case`, in the same fix, and so are the usages in the markup: mustache tags, attributes, directives and the expression of `{#if}`, `{#each}`, `{#await}` and `{#key}`. A shorthand attribute is written out, `{camelCase}` into `camelCase={camel_case}`, so the name of the property stays. Names the markup declares, the context of `{#each}`, the value of `{#await}` and a `let:` property, are not reported.
package/package.json CHANGED
@@ -7,11 +7,6 @@
7
7
  "@stylistic/eslint-plugin": "^5.10.0"
8
8
  },
9
9
  "description": "Fixable ESLint rules that format code without Prettier",
10
- "devDependencies": {
11
- "@types/estree": "^1.0.9",
12
- "@typescript/native": "npm:typescript@^7.0.2",
13
- "eslint": "^10.10.0"
14
- },
15
10
  "engines": {
16
11
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
17
12
  },
@@ -53,10 +48,10 @@
53
48
  },
54
49
  "type": "module",
55
50
  "types": "types/index.d.ts",
56
- "version": "0.7.0",
51
+ "version": "0.8.1",
57
52
  "scripts": {
58
53
  "dts": "node ../../scripts/tsc_with_clean.js eslint-plugin-lube",
59
54
  "lint": "pnpm -w lint",
60
- "test": "pnpm -w exec vitest run --coverage test/eslint-plugin-lube"
55
+ "test": "pnpm -w exec vitest run --coverage --coverage.include=packages/eslint-plugin-lube/src/** test/eslint-plugin-lube"
61
56
  }
62
57
  }
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import ascii_order from "./rules/ascii-order.js"
2
+ import prefer_param_tag from "./rules/prefer-param-tag.js"
2
3
  import prefer_type_tag from "./rules/prefer-type-tag.js"
3
4
  import pretty_imports from "./rules/pretty-imports.js"
4
5
  import pretty_jsdoc_casting from "./rules/pretty-jsdoc-casting.js"
@@ -152,6 +153,7 @@ const strict_rules = {
152
153
  }
153
154
  ],
154
155
  "lube/curly": [ "error", "all" ],
156
+ "lube/prefer-param-tag": "error",
155
157
  "lube/prefer-type-tag": "error",
156
158
  "lube/pretty-imports": "error",
157
159
  "lube/pretty-jsdoc-casting": "error",
@@ -246,6 +248,7 @@ const plugin = {
246
248
  "curly": keep_body_comments(
247
249
  /** @type {import("eslint").Rule.RuleModule} */(builtinRules.get("curly"))/**/
248
250
  ),
251
+ "prefer-param-tag": prefer_param_tag,
249
252
  "prefer-type-tag": prefer_type_tag,
250
253
  "pretty-imports": pretty_imports,
251
254
  "pretty-jsdoc-casting": pretty_jsdoc_casting,
@@ -251,13 +251,8 @@ function create_reorder(context) {
251
251
  * @returns {string}
252
252
  */
253
253
  function get_snippet(node) {
254
- const value = node.type == "Property"
255
- ? node.computed && node.key.type != "Literal"
256
- ? node.key
257
- : node.value
258
- : node
259
254
  const snippet = source_code.getText(
260
- /** @type {import("estree").Node} */(value)/**/
255
+ /** @type {import("estree").Property} */(node)/**/.value
261
256
  ).replace(/\s+/g, " ")
262
257
  return snippet.length > 30 ? snippet.slice(0, 29) + "…" : snippet
263
258
  }
@@ -341,7 +336,7 @@ function get_name(node) {
341
336
  case "TemplateLiteral":
342
337
  return node.expressions.length
343
338
  ? undefined
344
- : /** @type {import("estree").TemplateElement} */(node.quasis[0])/**/.value.cooked ?? undefined
339
+ : /** @type {string} */(/** @type {import("estree").TemplateElement} */(node.quasis[0])/**/.value.cooked)/**/
345
340
  }
346
341
  return undefined
347
342
  }
@@ -0,0 +1,252 @@
1
+ const blank_regex = /^\s*\*?\s*$/
2
+ const directive_regex = /^\s*(?:eslint-disable-next-line\b|@ts-(?:expect-error|ignore)\b)/
3
+ const line_break_regex = /\s*(?:\r\n|[\n\r\p{Zl}\p{Zp}])\s*/gu
4
+ const line_terminator_regex = /\r\n|[\n\r\p{Zl}\p{Zp}]/u
5
+ const param_tag_regex = /^\s*\*\s*@param\b/
6
+ const space_regex = /^\s*$/
7
+ const tag_after_param_regex = /^\s*\*\s*@(?:deprecated|example|returns?|see|throws|yields?)\b/
8
+ const tag_regex = /^\s*\*\s*@/
9
+ const type_regex = /^\*\s*@type\s*(\{[^]*\})\s*$/
10
+ const white_space_regex = /\s/
11
+ /**
12
+ * @param {string} text
13
+ * @returns {string}
14
+ */
15
+ function escape_regex(text) {
16
+ return text.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&")
17
+ }
18
+ /**
19
+ * @param {import("estree").Comment} comment
20
+ * @param {string} indent
21
+ * @param {string} eol
22
+ * @param {{ index: number, tag: string }[]} tags
23
+ * @returns {string}
24
+ */
25
+ function jsdoc_with(comment, indent, eol, tags) {
26
+ let rows = `/*${comment.value}*/`.split(line_terminator_regex)
27
+ /**
28
+ * @param {number} index
29
+ * @returns {string}
30
+ */
31
+ function row_at(index) {
32
+ return /** @type {string} */(rows[index])/**/
33
+ }
34
+ if (rows.length < 2 || !/^\/\*\*\s*$/.test(row_at(0)) || !/^\s*\*\/$/.test(row_at(rows.length - 1))) {
35
+ const body = comment.value.slice(1).split(line_terminator_regex)
36
+ .map(
37
+ row => row.replace(/^\s*\*?\s?/, "").trimEnd()
38
+ )
39
+ while (body[0] === "") {
40
+ body.shift()
41
+ }
42
+ while (body.at(-1) === "") {
43
+ body.pop()
44
+ }
45
+ rows = [
46
+ "/**",
47
+ ...body.map(
48
+ row => `${indent} * ${row}`.trimEnd()
49
+ ),
50
+ `${indent} */`
51
+ ]
52
+ }
53
+ for (const { index, tag } of tags) {
54
+ const rows_end = rows.length - 1
55
+ /** @type {{ end: number, start: number }[]} */
56
+ const blocks = []
57
+ for (let i = 1; i < rows_end; i++) {
58
+ if (param_tag_regex.test(row_at(i))) {
59
+ let end = i + 1
60
+ while (end < rows_end && !tag_regex.test(row_at(end)) && !blank_regex.test(row_at(end))) {
61
+ end++
62
+ }
63
+ blocks.push({ end, start: i })
64
+ }
65
+ }
66
+ let at = rows_end
67
+ const first = blocks[0]
68
+ if (first) {
69
+ at = index == 0
70
+ ? first.start
71
+ : /** @type {{ end: number, start: number }} */(blocks[Math.min(index, blocks.length) - 1])/**/.end
72
+ } else {
73
+ const tag_at = rows.findIndex(
74
+ row => tag_after_param_regex.test(row)
75
+ )
76
+ if (tag_at >= 0) {
77
+ at = tag_at
78
+ }
79
+ }
80
+ const added = [ `${indent} * ${tag}` ]
81
+ if (at == rows_end && at > 1 && !tag_regex.test(row_at(at - 1)) && !blank_regex.test(row_at(at - 1))) {
82
+ added.unshift(`${indent} *`)
83
+ }
84
+ rows.splice(at, 0, ...added)
85
+ }
86
+ return rows.join(eol)
87
+ }
88
+ /** @type {import("eslint").Rule.RuleModule} */
89
+ export default {
90
+ create(context) {
91
+ const source_code = context.sourceCode
92
+ const text = source_code.text
93
+ /**
94
+ * @param {import("estree").Comment} comment
95
+ * @returns {import("eslint").AST.Range}
96
+ */
97
+ function comment_range(comment) {
98
+ return /** @type {import("eslint").AST.Range} */(comment.range)/**/
99
+ }
100
+ /**
101
+ * @param {number} position
102
+ * @returns {number}
103
+ */
104
+ function line_start_of(position) {
105
+ let start = position
106
+ while (start > 0 && !line_terminator_regex.test(text.charAt(start - 1))) {
107
+ start--
108
+ }
109
+ return start
110
+ }
111
+ /**
112
+ * @param {import("estree").Comment} comment
113
+ * @returns {boolean}
114
+ */
115
+ function starts_line(comment) {
116
+ const start = comment_range(comment)[0]
117
+ return space_regex.test(
118
+ text.slice(line_start_of(start), start)
119
+ )
120
+ }
121
+ return {
122
+ FunctionDeclaration(node) {
123
+ /** @type {{ comment: import("estree").Comment, index: number, name: string, type: string }[]} */
124
+ const named = []
125
+ const data = {
126
+ name: node.id?.name ?? "default"
127
+ }
128
+ for (const [ index, param ] of node.params.entries()) {
129
+ const comment = source_code.getCommentsBefore(param).findLast(
130
+ item => item.type == "Block" && type_regex.test(item.value)
131
+ )
132
+ if (!comment) {
133
+ continue
134
+ }
135
+ const target = param.type == "AssignmentPattern" ? param.left : param
136
+ if (target.type == "Identifier") {
137
+ named.push(
138
+ {
139
+ comment,
140
+ index,
141
+ name: target.name,
142
+ type: /** @type {string} */(/** @type {RegExpExecArray} */(type_regex.exec(comment.value))/**/[1])/**/.replace(line_break_regex, " ")
143
+ }
144
+ )
145
+ } else {
146
+ context.report(
147
+ {
148
+ data,
149
+ loc: /** @type {import("estree").SourceLocation} */(comment.loc)/**/,
150
+ messageId: "name"
151
+ }
152
+ )
153
+ }
154
+ }
155
+ const first = named[0]
156
+ if (!first) {
157
+ return
158
+ }
159
+ const loc = /** @type {import("estree").SourceLocation} */(first.comment.loc)/**/
160
+ const statement = node.parent.type == "ExportNamedDeclaration" || node.parent.type == "ExportDefaultDeclaration"
161
+ ? node.parent
162
+ : node
163
+ const leading = source_code.getCommentsBefore(statement)
164
+ let start = /** @type {import("eslint").AST.Range} */(statement.range)/**/[0]
165
+ let last = leading.length - 1
166
+ while (last >= 0) {
167
+ const item = /** @type {import("estree").Comment} */(leading[last])/**/
168
+ if (!starts_line(item) || !directive_regex.test(item.value)) {
169
+ break
170
+ }
171
+ start = comment_range(item)[0]
172
+ last--
173
+ }
174
+ const candidate = leading[last]
175
+ const jsdoc = candidate?.type == "Block" && candidate.value.startsWith("*")
176
+ ? candidate
177
+ : undefined
178
+ const anchor = jsdoc ? comment_range(jsdoc)[0] : start
179
+ const indent = text.slice(line_start_of(anchor), anchor)
180
+ if (!space_regex.test(indent)) {
181
+ context.report(
182
+ { data, loc, messageId: "hand" }
183
+ )
184
+ return
185
+ }
186
+ const eol = text.match(line_terminator_regex)?.[0] ?? "\n"
187
+ const tags = named.filter(
188
+ item => !jsdoc || !new RegExp(
189
+ `@param\\s+(?:\\{[^]*?\\}\\s+)?\\[?${escape_regex(item.name)}(?![\\w$])`
190
+ ).test(jsdoc.value)
191
+ ).map(
192
+ item => (
193
+ {
194
+ index: item.index,
195
+ tag: `@param ${item.type} ${item.name}`
196
+ }
197
+ )
198
+ )
199
+ context.report(
200
+ {
201
+ data,
202
+ fix(fixer) {
203
+ const fixes = named.map(
204
+ ({ comment }) => {
205
+ const [ begin, close ] = comment_range(comment)
206
+ let end = close
207
+ while (white_space_regex.test(text.charAt(end))) {
208
+ end++
209
+ }
210
+ return fixer.removeRange([ begin, end ])
211
+ }
212
+ )
213
+ if (jsdoc) {
214
+ fixes.push(
215
+ fixer.replaceTextRange(
216
+ comment_range(jsdoc),
217
+ jsdoc_with(jsdoc, indent, eol, tags)
218
+ )
219
+ )
220
+ } else {
221
+ fixes.push(
222
+ fixer.insertTextBeforeRange(
223
+ [ start, start ],
224
+ `/**${eol}${tags.map(({ tag }) => `${indent} * ${tag}${eol}`).join("")}${indent} */${eol}${indent}`
225
+ )
226
+ )
227
+ }
228
+ return fixes
229
+ },
230
+ loc,
231
+ messageId: "tag"
232
+ }
233
+ )
234
+ }
235
+ }
236
+ },
237
+ meta: {
238
+ docs: {
239
+ description: "Enforces JSDoc `@param` tags on a function declaration instead of inline `/** @type {T} */` comments inside its parameter list.",
240
+ recommended: false,
241
+ url: "https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/prefer-param-tag.md"
242
+ },
243
+ fixable: "code",
244
+ messages: {
245
+ hand: "Type the parameters of `{{name}}` with `@param` tags on the declaration instead of inline comments; it is not fixed automatically because the declaration or its JSDoc comment does not start its line, so write the tags by hand",
246
+ name: "Type this parameter of `{{name}}` with a `@param` tag on the declaration instead of an inline comment; it is not fixed automatically because the parameter has no simple name to write in the tag, so document it by hand",
247
+ tag: "Type the parameters of `{{name}}` with `@param` tags on the declaration instead of inline `@type` comments"
248
+ },
249
+ schema: [],
250
+ type: "suggestion"
251
+ }
252
+ }
@@ -52,9 +52,6 @@ export default {
52
52
  const open_index = tokens.findIndex(
53
53
  token => token.range[0] == first.range[0]
54
54
  ) - 1
55
- if (tokens[open_index]?.value != "{") {
56
- return
57
- }
58
55
  const head = tokens.slice(0, open_index)
59
56
  .map(token => token.value)
60
57
  .join(" ")
@@ -36,10 +36,7 @@ export default {
36
36
  * @returns {boolean}
37
37
  */
38
38
  function is_arguments(position) {
39
- const node = /** @type {import("../../private.js").AstNode | null} */(/** @type {unknown} */(source_code.getNodeByRangeIndex(position)))/**/
40
- if (!node) {
41
- return false
42
- }
39
+ const node = /** @type {import("../../private.js").AstNode} */(/** @type {unknown} */(source_code.getNodeByRangeIndex(position)))/**/
43
40
  const type = node.type
44
41
  if (type != "CallExpression" && type != "ImportExpression" && type != "NewExpression") {
45
42
  return false
@@ -73,7 +70,7 @@ export default {
73
70
  * @param {number} start
74
71
  * @param {number} end
75
72
  * @param {[number, number][]} removed
76
- * @returns {string | null}
73
+ * @returns {string}
77
74
  */
78
75
  function render_without(start, end, removed) {
79
76
  let position = start
@@ -82,9 +79,6 @@ export default {
82
79
  if (removed_end <= start || removed_start >= end) {
83
80
  continue
84
81
  }
85
- if (source.crosses_edit(removed_start) || source.crosses_edit(removed_end)) {
86
- return null
87
- }
88
82
  let chunk_end = removed_start
89
83
  if (text[removed_start] == ")") {
90
84
  while (chunk_end > position && is_space(text.charCodeAt(chunk_end - 1))) {
@@ -239,12 +233,10 @@ export default {
239
233
  const prefix = render_without(left, content_start, removed)
240
234
  const suffix = render_without(content_end, right, removed)
241
235
  if (
242
- prefix == null
243
- || suffix == null
244
- || line_break_regex.test(prefix)
245
- && restricted_regex.test(
246
- text.slice(Math.max(0, left - 7), left)
247
- )
236
+ line_break_regex.test(prefix)
237
+ && restricted_regex.test(
238
+ text.slice(Math.max(0, left - 7), left)
239
+ )
248
240
  ) {
249
241
  return
250
242
  }
@@ -73,12 +73,8 @@ export default {
73
73
  if (left <= start || text[left - 1] != "(") {
74
74
  break
75
75
  }
76
- const right = source.skip_right(code_end, end)
77
- if (right >= end || text[right] != ")") {
78
- break
79
- }
80
76
  code_start = left - 1
81
- code_end = right + 1
77
+ code_end = source.skip_right(code_end, end) + 1
82
78
  }
83
79
  const right = source.skip_right(code_end, end)
84
80
  let last = right
@@ -197,10 +193,10 @@ export default {
197
193
  node.properties,
198
194
  object_curly_spacing
199
195
  )
200
- case "SequenceExpression":
196
+ default:
201
197
  return {
202
198
  end: node.range[1],
203
- items: node.expressions,
199
+ items: /** @type {import("../../private.js").AstNode & import("estree").SequenceExpression} */(node)/**/.expressions,
204
200
  line_indent: source.get_line_indent(
205
201
  node,
206
202
  source.get_anchor(node.range[0]),
@@ -210,7 +206,6 @@ export default {
210
206
  start: node.range[0]
211
207
  }
212
208
  }
213
- return undefined
214
209
  }
215
210
  /**
216
211
  * @param {import("../../private.js").AstNode} node
@@ -23,26 +23,18 @@ export default {
23
23
  */
24
24
  function collect(node, depth, gaps) {
25
25
  const parts = get_parts(node)
26
- const question = source_code.getTokenAfter(
26
+ const question = /** @type {import("eslint").AST.Token} */(source_code.getTokenAfter(
27
27
  /** @type {import("estree").Node} */(/** @type {unknown} */(parts.test))/**/,
28
28
  {
29
29
  filter: token => token.value == "?"
30
30
  }
31
- )
32
- const colon = question && source_code.getTokenAfter(
31
+ ))/**/
32
+ const colon = /** @type {import("eslint").AST.Token} */(source_code.getTokenAfter(
33
33
  /** @type {import("estree").Node} */(/** @type {unknown} */(parts.consequent))/**/,
34
34
  {
35
35
  filter: token => token.value == ":"
36
36
  }
37
- )
38
- if (
39
- !question
40
- || !colon
41
- || question.range[1] > parts.consequent.range[0]
42
- || colon.range[1] > parts.alternate.range[0]
43
- ) {
44
- return false
45
- }
37
+ ))/**/
46
38
  for (const operator of [ question, colon ]) {
47
39
  const before = /** @type {import("eslint").AST.Token | import("estree").Comment} */(source_code.getTokenBefore(
48
40
  operator,
@@ -160,16 +160,14 @@ export default {
160
160
  function declaration_edits(node, resolution) {
161
161
  /** @type {import("../../private.js").RenameEdit[]} */
162
162
  const edits = [ rename_edit(node) ]
163
- const block = /** @type {import("../../private.js").AstNode | undefined} */(resolution.get(node)?.scope.block)/**/
163
+ const block = /** @type {import("../../private.js").AstNode} */(/** @type {import("eslint").Scope.Variable} */(resolution.get(node))/**/.scope.block)/**/
164
164
  const func = function_of_param(node)
165
165
  const leading = func && leading_comment(func)
166
166
  /** @type {Set<import("../../private.js").Comment>} */
167
167
  const comments = new Set(
168
- block
169
- ? /** @type {import("../../private.js").Comment[]} */(source.getAllComments())/**/.filter(
170
- comment => block.type == "Program" || comment.range[0] >= block.range[0] && comment.range[1] <= block.range[1]
171
- )
172
- : []
168
+ /** @type {import("../../private.js").Comment[]} */(source.getAllComments())/**/.filter(
169
+ comment => block.type == "Program" || comment.range[0] >= block.range[0] && comment.range[1] <= block.range[1]
170
+ )
173
171
  )
174
172
  if (leading) {
175
173
  comments.add(leading)
@@ -190,7 +188,9 @@ export default {
190
188
  * @returns {void}
191
189
  */
192
190
  function declare(node) {
193
- add(declarations, node)
191
+ if (!is_ambient(node)) {
192
+ add(declarations, node)
193
+ }
194
194
  }
195
195
  /**
196
196
  * @param {import("../../private.js").AstNode & import("estree").Identifier} node
@@ -236,12 +236,28 @@ export default {
236
236
  }
237
237
  return depth > 0
238
238
  }
239
+ /**
240
+ * @param {import("../../private.js").AstNode} node
241
+ * @returns {boolean}
242
+ */
243
+ function is_ambient(node) {
244
+ for (
245
+ let current = /** @type {import("../../private.js").AstNode | undefined} */(node)/**/;
246
+ current;
247
+ current = current.parent
248
+ ) {
249
+ if ("declare" in current && current.declare === true) {
250
+ return true
251
+ }
252
+ }
253
+ return false
254
+ }
239
255
  /**
240
256
  * @param {import("../../private.js").AstNode} node
241
257
  * @returns {boolean}
242
258
  */
243
259
  function is_declaration(node) {
244
- for (let parent = node.parent; parent; parent = parent.parent) {
260
+ for (let parent = node.parent; ; parent = parent.parent) {
245
261
  switch (parent.type) {
246
262
  case "ArrayPattern":
247
263
  case "AssignmentPattern":
@@ -257,7 +273,6 @@ export default {
257
273
  return true
258
274
  }
259
275
  }
260
- return true
261
276
  }
262
277
  /**
263
278
  * @param {import("../../private.js").AstNode} node
@@ -308,9 +323,7 @@ export default {
308
323
  anchor = parent
309
324
  break
310
325
  case "VariableDeclarator":
311
- if (parent.init == func) {
312
- anchor = parent.parent
313
- }
326
+ anchor = parent.parent
314
327
  }
315
328
  if (anchor.parent.type == "ExportDefaultDeclaration" || anchor.parent.type == "ExportNamedDeclaration") {
316
329
  anchor = anchor.parent
@@ -771,7 +784,7 @@ export default {
771
784
  const resolution = new Map()
772
785
  /** @type {Map<string, import("eslint").Scope.Variable[]>} */
773
786
  const variables_by_name = new Map()
774
- const scopes = source.scopeManager?.scopes ?? []
787
+ const scopes = /** @type {import("eslint").Scope.ScopeManager} */(source.scopeManager)/**/.scopes
775
788
  for (const scope of scopes) {
776
789
  for (const variable of scope.variables) {
777
790
  const same_names = variables_by_name.get(variable.name)
@@ -808,10 +821,9 @@ export default {
808
821
  for (const nodes of declarations.values()) {
809
822
  for (const node of nodes) {
810
823
  declared.add(node)
811
- const variable = resolution.get(node)
812
- if (variable) {
813
- renamable.add(variable)
814
- }
824
+ renamable.add(
825
+ /** @type {import("eslint").Scope.Variable} */(resolution.get(node))/**/
826
+ )
815
827
  }
816
828
  }
817
829
  /** @type {Map<string, Set<import("eslint").Scope.Scope>>} */
@@ -859,7 +871,7 @@ export default {
859
871
  if (!others.length) {
860
872
  return false
861
873
  }
862
- for (const variable of variables_by_name.get(name) ?? []) {
874
+ for (const variable of /** @type {import("eslint").Scope.Variable[]} */(variables_by_name.get(name))/**/) {
863
875
  if (!renamable.has(variable)) {
864
876
  continue
865
877
  }
@@ -895,7 +907,7 @@ export default {
895
907
  * @returns {boolean}
896
908
  */
897
909
  function has_foreign_declaration(name) {
898
- return (variables_by_name.get(name) ?? []).some(
910
+ return /** @type {import("eslint").Scope.Variable[]} */(variables_by_name.get(name))/**/.some(
899
911
  variable => renamable.has(variable) && variable.identifiers.some(
900
912
  identifier => !declared.has(
901
913
  /** @type {import("../../private.js").AstNode} */(/** @type {unknown} */(identifier))/**/
@@ -922,7 +934,7 @@ export default {
922
934
  plans.set(
923
935
  name,
924
936
  {
925
- atomic: !fix_same_names || variables_by_name.has(target) || nested_targets.has(target) || store_usages.has(name),
937
+ atomic: !fix_same_names || nodes.length > 1 || variables_by_name.has(target) || nested_targets.has(target) || store_usages.has(name),
926
938
  exported: (export_locals.get(name) ?? []).filter(belongs),
927
939
  fixable: !blocked.has(name)
928
940
  && fixable_name_regex.test(name)
@@ -936,21 +948,16 @@ export default {
936
948
  }
937
949
  /**
938
950
  * @param {import("../../private.js").NamingIdentifier} node
939
- * @param {import("../../private.js").NamingIdentifier[]} nodes
940
951
  * @param {import("../../private.js").NamingIdentifier[]} usages_held
941
952
  * @returns {import("../../private.js").RenameEdit[]}
942
953
  */
943
- function declaration_fix(node, nodes, usages_held) {
954
+ function declaration_fix(node, usages_held) {
944
955
  const variable = resolution.get(node)
945
956
  return [
946
957
  ...declaration_edits(node, resolution),
947
- ...nodes.find(
948
- other => resolution.get(other) == variable
949
- ) == node
950
- ? usages_held.filter(
951
- usage => resolution.get(usage) == variable
952
- ).map(rename_edit)
953
- : []
958
+ ...usages_held.filter(
959
+ usage => resolution.get(usage) == variable
960
+ ).map(rename_edit)
954
961
  ]
955
962
  }
956
963
  /** @type {import("../../private.js").RenameEdit[]} */
@@ -996,7 +1003,7 @@ export default {
996
1003
  for (const node of nodes) {
997
1004
  report(
998
1005
  node,
999
- fixable && !used.length ? declaration_fix(node, nodes, usages_held) : null
1006
+ fixable && !used.length ? declaration_fix(node, usages_held) : null
1000
1007
  )
1001
1008
  }
1002
1009
  } else {
@@ -13,14 +13,12 @@ function skip_hole_commas(rule) {
13
13
  {
14
14
  report: {
15
15
  value: (
16
- /** @type {import("eslint").Rule.ReportDescriptor & { node?: import("eslint").AST.Token }} */ descriptor
16
+ /** @type {import("eslint").Rule.ReportDescriptor & { node: import("eslint").AST.Token }} */ descriptor
17
17
  ) => {
18
18
  const token = descriptor.node
19
- if (token?.type == "Punctuator" && token.value == ",") {
20
- const before = source_code.getTokenBefore(token)
21
- if (before?.value == "," && before.loc.end.line < token.loc.start.line) {
22
- return
23
- }
19
+ const before = source_code.getTokenBefore(token)
20
+ if (before?.value == "," && before.loc.end.line < token.loc.start.line) {
21
+ return
24
22
  }
25
23
  context.report(descriptor)
26
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAyNA;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,MAAM,EAbF,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG;IACtC,OAAO,EAAE;QACP,WAAW,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG;YAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;YACvD,KAAK,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAA;SAC3C,CAAA;QACD,MAAM,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG;YACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;YACvD,KAAK,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAA;SAC3C,CAAA;KACF,CAAA;CA2BL,CAAA;eAmDc,MAAM;AACrB,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA2NA;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,MAAM,EAbF,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG;IACtC,OAAO,EAAE;QACP,WAAW,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG;YAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;YACvD,KAAK,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAA;SAC3C,CAAA;QACD,MAAM,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG;YACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;YACvD,KAAK,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAA;SAC3C,CAAA;KACF,CAAA;CA4BL,CAAA;eAmDc,MAAM;AACrB,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,CAAA"}