eslint-plugin-svelte 3.3.2 → 3.4.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.
package/README.md CHANGED
@@ -272,7 +272,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
272
272
  | [svelte/no-shorthand-style-property-overrides](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-shorthand-style-property-overrides/) | disallow shorthand style properties that override related longhand properties | :star: |
273
273
  | [svelte/no-store-async](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-store-async/) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: |
274
274
  | [svelte/no-unknown-style-directive-property](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: |
275
- | [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | |
275
+ | [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | :bulb: |
276
276
  | [svelte/require-store-reactive-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-reactive-access/) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: |
277
277
  | [svelte/valid-compile](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | |
278
278
  | [svelte/valid-style-parse](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-style-parse/) | require valid style element parsing | |
@@ -294,7 +294,7 @@ These rules relate to better ways of doing things to help you avoid problems:
294
294
  |:--------|:------------|:---|
295
295
  | [svelte/block-lang](https://sveltejs.github.io/eslint-plugin-svelte/rules/block-lang/) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
296
296
  | [svelte/button-has-type](https://sveltejs.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | |
297
- | [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: |
297
+ | [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star::bulb: |
298
298
  | [svelte/no-ignored-unsubscribe](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-ignored-unsubscribe/) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | |
299
299
  | [svelte/no-immutable-reactive-statements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-immutable-reactive-statements/) | disallow reactive statements that don't reference reactive values. | :star: |
300
300
  | [svelte/no-inline-styles](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inline-styles/) | disallow attributes and directives that produce inline styles | |
@@ -323,7 +323,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
323
323
  | Rule ID | Description | |
324
324
  |:--------|:------------|:---|
325
325
  | [svelte/consistent-selector-style](https://sveltejs.github.io/eslint-plugin-svelte/rules/consistent-selector-style/) | enforce a consistent style for CSS selectors | |
326
- | [svelte/derived-has-same-inputs-outputs](https://sveltejs.github.io/eslint-plugin-svelte/rules/derived-has-same-inputs-outputs/) | derived store should use same variable names between values and callback | |
326
+ | [svelte/derived-has-same-inputs-outputs](https://sveltejs.github.io/eslint-plugin-svelte/rules/derived-has-same-inputs-outputs/) | derived store should use same variable names between values and callback | :bulb: |
327
327
  | [svelte/first-attribute-linebreak](https://sveltejs.github.io/eslint-plugin-svelte/rules/first-attribute-linebreak/) | enforce the location of first attribute | :wrench: |
328
328
  | [svelte/html-closing-bracket-new-line](https://sveltejs.github.io/eslint-plugin-svelte/rules/html-closing-bracket-new-line/) | Require or disallow a line break before tag's closing brackets | :wrench: |
329
329
  | [svelte/html-closing-bracket-spacing](https://sveltejs.github.io/eslint-plugin-svelte/rules/html-closing-bracket-spacing/) | require or disallow a space before tag's closing brackets | :wrench: |
package/lib/main.d.ts CHANGED
@@ -14,7 +14,7 @@ export declare const configs: {
14
14
  export declare const rules: Record<string, Rule.RuleModule>;
15
15
  export declare const meta: {
16
16
  name: "eslint-plugin-svelte";
17
- version: "3.3.2";
17
+ version: "3.4.0";
18
18
  };
19
19
  export declare const processors: {
20
20
  '.svelte': typeof processor;
package/lib/meta.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export declare const name = "eslint-plugin-svelte";
2
- export declare const version = "3.3.2";
2
+ export declare const version = "3.4.0";
package/lib/meta.js CHANGED
@@ -2,4 +2,4 @@
2
2
  // This file has been automatically generated,
3
3
  // in order to update its content execute "pnpm run update"
4
4
  export const name = 'eslint-plugin-svelte';
5
- export const version = '3.3.2';
5
+ export const version = '3.4.0';
@@ -1,5 +1,16 @@
1
1
  import { createRule } from '../utils/index.js';
2
2
  import { extractStoreReferences } from './reference-helpers/svelte-store.js';
3
+ import { findVariableForReplacement } from '../utils/ast-utils.js';
4
+ function createFixer(node, variable, name) {
5
+ return function* fix(fixer) {
6
+ yield fixer.replaceText(node, name);
7
+ if (variable) {
8
+ for (const ref of variable.references) {
9
+ yield fixer.replaceText(ref.identifier, name);
10
+ }
11
+ }
12
+ };
13
+ }
3
14
  export default createRule('derived-has-same-inputs-outputs', {
4
15
  meta: {
5
16
  docs: {
@@ -8,9 +19,11 @@ export default createRule('derived-has-same-inputs-outputs', {
8
19
  recommended: false,
9
20
  conflictWithPrettier: false
10
21
  },
22
+ hasSuggestions: true,
11
23
  schema: [],
12
24
  messages: {
13
- unexpected: "The argument name should be '{{name}}'."
25
+ unexpected: "The argument name should be '{{name}}'.",
26
+ renameParam: 'Rename the parameter from {{oldName}} to {{newName}}.'
14
27
  },
15
28
  type: 'suggestion'
16
29
  },
@@ -33,11 +46,21 @@ export default createRule('derived-has-same-inputs-outputs', {
33
46
  return;
34
47
  const expectedName = `$${args.name}`;
35
48
  if (expectedName !== fnParam.name) {
49
+ const { hasConflict, variable } = findVariableForReplacement(context, fn.body, fnParam.name, expectedName);
36
50
  context.report({
37
51
  node: fn,
38
52
  loc: fnParam.loc,
39
53
  messageId: 'unexpected',
40
- data: { name: expectedName }
54
+ data: { name: expectedName },
55
+ suggest: hasConflict
56
+ ? undefined
57
+ : [
58
+ {
59
+ messageId: 'renameParam',
60
+ data: { oldName: fnParam.name, newName: expectedName },
61
+ fix: createFixer(fnParam, variable, expectedName)
62
+ }
63
+ ]
41
64
  });
42
65
  }
43
66
  }
@@ -57,11 +80,21 @@ export default createRule('derived-has-same-inputs-outputs', {
57
80
  if (element && element.type === 'Identifier' && argName) {
58
81
  const expectedName = `$${argName}`;
59
82
  if (expectedName !== element.name) {
83
+ const { hasConflict, variable } = findVariableForReplacement(context, fn.body, element.name, expectedName);
60
84
  context.report({
61
85
  node: fn,
62
86
  loc: element.loc,
63
87
  messageId: 'unexpected',
64
- data: { name: expectedName }
88
+ data: { name: expectedName },
89
+ suggest: hasConflict
90
+ ? undefined
91
+ : [
92
+ {
93
+ messageId: 'renameParam',
94
+ data: { oldName: element.name, newName: expectedName },
95
+ fix: createFixer(element, variable, expectedName)
96
+ }
97
+ ]
65
98
  });
66
99
  }
67
100
  }
@@ -7,9 +7,11 @@ export default createRule('no-at-debug-tags', {
7
7
  recommended: true,
8
8
  default: 'warn'
9
9
  },
10
+ hasSuggestions: true,
10
11
  schema: [],
11
12
  messages: {
12
- unexpected: 'Unexpected `{@debug}`.'
13
+ unexpected: 'Unexpected `{@debug}`.',
14
+ suggestRemove: 'Remove `{@debug}` from the source'
13
15
  },
14
16
  type: 'problem'
15
17
  },
@@ -18,7 +20,13 @@ export default createRule('no-at-debug-tags', {
18
20
  SvelteDebugTag(node) {
19
21
  context.report({
20
22
  node,
21
- messageId: 'unexpected'
23
+ messageId: 'unexpected',
24
+ suggest: [
25
+ {
26
+ messageId: 'suggestRemove',
27
+ fix: (fixer) => fixer.remove(node)
28
+ }
29
+ ]
22
30
  });
23
31
  }
24
32
  };
@@ -127,21 +127,21 @@ export default createRule('no-unused-props', {
127
127
  /**
128
128
  * Finds all property access paths for a given variable.
129
129
  */
130
- function getUsedNestedPropertyNames(node) {
130
+ function getUsedNestedPropertyPathsArray(node) {
131
131
  const variable = findVariable(context, node);
132
132
  if (!variable)
133
133
  return [];
134
- const paths = [];
134
+ const pathsArray = [];
135
135
  for (const reference of variable.references) {
136
136
  if ('identifier' in reference &&
137
137
  reference.identifier.type === 'Identifier' &&
138
138
  (reference.identifier.range[0] !== node.range[0] ||
139
139
  reference.identifier.range[1] !== node.range[1])) {
140
140
  const referencePath = getPropertyPath(reference.identifier);
141
- paths.push(referencePath);
141
+ pathsArray.push(referencePath);
142
142
  }
143
143
  }
144
- return paths;
144
+ return pathsArray;
145
145
  }
146
146
  /**
147
147
  * Checks if a property is from TypeScript's built-in type definitions.
@@ -157,7 +157,7 @@ export default createRule('no-unused-props', {
157
157
  return false;
158
158
  return sourceFile.fileName.includes('node_modules/typescript/lib/');
159
159
  }
160
- function getUsedPropertiesFromPattern(pattern) {
160
+ function getUsedPropertyNamesFromPattern(pattern) {
161
161
  const usedProps = new Set();
162
162
  for (const prop of pattern.properties) {
163
163
  if (prop.type === 'Property' && prop.key.type === 'Identifier') {
@@ -192,24 +192,32 @@ export default createRule('no-unused-props', {
192
192
  /**
193
193
  * Recursively checks for unused properties in a type.
194
194
  */
195
- function checkUnusedProperties(type, usedPaths, usedProps, reportNode, parentPath, checkedTypes, reportedProps) {
195
+ function checkUnusedProperties({ propsType, usedPropertyPaths, declaredPropertyNames, reportNode, parentPath, checkedPropsTypes, reportedPropertyPaths }) {
196
196
  // Skip checking if the type itself is a class
197
- if (isClassType(type))
197
+ if (isClassType(propsType))
198
198
  return;
199
- const typeStr = typeChecker.typeToString(type);
200
- if (checkedTypes.has(typeStr))
199
+ const typeStr = typeChecker.typeToString(propsType);
200
+ if (checkedPropsTypes.has(typeStr))
201
201
  return;
202
- checkedTypes.add(typeStr);
203
- if (shouldIgnoreType(type))
202
+ checkedPropsTypes.add(typeStr);
203
+ if (shouldIgnoreType(propsType))
204
204
  return;
205
- const properties = typeChecker.getPropertiesOfType(type);
206
- const baseTypes = type.getBaseTypes();
207
- if (!properties.length && (!baseTypes || baseTypes.length === 0)) {
205
+ const properties = typeChecker.getPropertiesOfType(propsType);
206
+ const propsBaseTypes = propsType.getBaseTypes();
207
+ if (!properties.length && (!propsBaseTypes || propsBaseTypes.length === 0)) {
208
208
  return;
209
209
  }
210
- if (baseTypes) {
211
- for (const baseType of baseTypes) {
212
- checkUnusedProperties(baseType, usedPaths, usedProps, reportNode, parentPath, checkedTypes, reportedProps);
210
+ if (propsBaseTypes) {
211
+ for (const propsBaseType of propsBaseTypes) {
212
+ checkUnusedProperties({
213
+ propsType: propsBaseType,
214
+ usedPropertyPaths,
215
+ declaredPropertyNames,
216
+ reportNode,
217
+ parentPath,
218
+ checkedPropsTypes,
219
+ reportedPropertyPaths
220
+ });
213
221
  }
214
222
  }
215
223
  for (const prop of properties) {
@@ -222,20 +230,19 @@ export default createRule('no-unused-props', {
222
230
  continue;
223
231
  const currentPath = [...parentPath, propName];
224
232
  const currentPathStr = [...parentPath, propName].join('.');
225
- if (reportedProps.has(currentPathStr))
233
+ if (reportedPropertyPaths.has(currentPathStr))
226
234
  continue;
227
235
  const propType = typeChecker.getTypeOfSymbol(prop);
228
- const joinedUsedPaths = usedPaths.map((path) => path.join('.'));
229
- const isUsedThisInPath = joinedUsedPaths.includes(currentPathStr);
230
- const isUsedInPath = joinedUsedPaths.some((path) => {
236
+ const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr);
237
+ const isUsedInPath = usedPropertyPaths.some((path) => {
231
238
  return path.startsWith(`${currentPathStr}.`);
232
239
  });
233
240
  if (isUsedThisInPath && !isUsedInPath) {
234
241
  continue;
235
242
  }
236
- const isUsedInProps = usedProps.has(propName);
243
+ const isUsedInProps = declaredPropertyNames.has(propName);
237
244
  if (!isUsedInPath && !isUsedInProps) {
238
- reportedProps.add(currentPathStr);
245
+ reportedPropertyPaths.add(currentPathStr);
239
246
  context.report({
240
247
  node: reportNode,
241
248
  messageId: parentPath.length ? 'unusedNestedProp' : 'unusedProp',
@@ -246,19 +253,27 @@ export default createRule('no-unused-props', {
246
253
  });
247
254
  continue;
248
255
  }
249
- const isUsedNested = joinedUsedPaths.some((path) => {
256
+ const isUsedNested = usedPropertyPaths.some((path) => {
250
257
  return path.startsWith(`${currentPathStr}.`);
251
258
  });
252
259
  if (isUsedNested || isUsedInProps) {
253
- checkUnusedProperties(propType, usedPaths, usedProps, reportNode, currentPath, checkedTypes, reportedProps);
260
+ checkUnusedProperties({
261
+ propsType: propType,
262
+ usedPropertyPaths,
263
+ declaredPropertyNames,
264
+ reportNode,
265
+ parentPath: currentPath,
266
+ checkedPropsTypes,
267
+ reportedPropertyPaths
268
+ });
254
269
  }
255
270
  }
256
271
  // Check for unused index signatures only at the root level
257
272
  if (parentPath.length === 0) {
258
- const indexType = type.getStringIndexType();
259
- const numberIndexType = type.getNumberIndexType();
273
+ const indexType = propsType.getStringIndexType();
274
+ const numberIndexType = propsType.getNumberIndexType();
260
275
  const hasIndexSignature = Boolean(indexType) || Boolean(numberIndexType);
261
- if (hasIndexSignature && !hasRestElement(usedProps)) {
276
+ if (hasIndexSignature && !hasRestElement(declaredPropertyNames)) {
262
277
  context.report({
263
278
  node: reportNode,
264
279
  messageId: 'unusedIndexSignature'
@@ -296,26 +311,44 @@ export default createRule('no-unused-props', {
296
311
  const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node);
297
312
  if (!tsNode || !tsNode.type)
298
313
  return;
299
- const propType = typeChecker.getTypeFromTypeNode(tsNode.type);
300
- let usedPaths = [];
301
- let usedProps = new Set();
314
+ const propsType = typeChecker.getTypeFromTypeNode(tsNode.type);
315
+ let usedPropertyPathsArray = [];
316
+ let declaredPropertyNames = new Set();
302
317
  if (node.id.type === 'ObjectPattern') {
303
- usedProps = getUsedPropertiesFromPattern(node.id);
304
- if (usedProps.size === 0)
318
+ declaredPropertyNames = getUsedPropertyNamesFromPattern(node.id);
319
+ if (declaredPropertyNames.size === 0)
305
320
  return;
306
- const identifiers = node.id.properties
307
- .filter((p) => p.type === 'Property')
308
- .map((p) => p.value)
309
- .filter((v) => v.type === 'Identifier');
321
+ const identifiers = [];
322
+ for (const p of node.id.properties) {
323
+ if (p.type !== 'Property') {
324
+ continue;
325
+ }
326
+ if (p.value.type === 'Identifier') {
327
+ identifiers.push(p.value);
328
+ }
329
+ else if (p.value.type === 'AssignmentPattern' && p.value.left.type === 'Identifier') {
330
+ identifiers.push(p.value.left);
331
+ }
332
+ }
310
333
  for (const identifier of identifiers) {
311
- const paths = getUsedNestedPropertyNames(identifier);
312
- usedPaths.push(...paths.map((path) => [identifier.name, ...path]));
334
+ const paths = getUsedNestedPropertyPathsArray(identifier);
335
+ usedPropertyPathsArray.push(...paths.map((path) => [identifier.name, ...path]));
313
336
  }
314
337
  }
315
338
  else if (node.id.type === 'Identifier') {
316
- usedPaths = getUsedNestedPropertyNames(node.id);
339
+ usedPropertyPathsArray = getUsedNestedPropertyPathsArray(node.id);
317
340
  }
318
- checkUnusedProperties(propType, normalizeUsedPaths(usedPaths), usedProps, node.id, [], new Set(), new Set());
341
+ checkUnusedProperties({
342
+ propsType,
343
+ usedPropertyPaths: normalizeUsedPaths(usedPropertyPathsArray).map((pathArray) => {
344
+ return pathArray.join('.');
345
+ }),
346
+ declaredPropertyNames,
347
+ reportNode: node.id,
348
+ parentPath: [],
349
+ checkedPropsTypes: new Set(),
350
+ reportedPropertyPaths: new Set()
351
+ });
319
352
  }
320
353
  };
321
354
  }
@@ -1,5 +1,7 @@
1
+ import { findVariableForReplacement } from '../utils/ast-utils.js';
1
2
  import { createRule } from '../utils/index.js';
2
3
  import { extractStoreReferences } from './reference-helpers/svelte-store.js';
4
+ import { getSourceCode } from '../utils/compat.js';
3
5
  export default createRule('require-store-callbacks-use-set-param', {
4
6
  meta: {
5
7
  docs: {
@@ -7,9 +9,12 @@ export default createRule('require-store-callbacks-use-set-param', {
7
9
  category: 'Possible Errors',
8
10
  recommended: false
9
11
  },
12
+ hasSuggestions: true,
10
13
  schema: [],
11
14
  messages: {
12
- unexpected: 'Store callbacks must use `set` param.'
15
+ unexpected: 'Store callbacks must use `set` param.',
16
+ updateParam: 'Rename parameter from {{oldName}} to `set`.',
17
+ addParam: 'Add a `set` parameter.'
13
18
  },
14
19
  type: 'suggestion'
15
20
  },
@@ -23,10 +28,43 @@ export default createRule('require-store-callbacks-use-set-param', {
23
28
  }
24
29
  const param = fn.params[0];
25
30
  if (!param || (param.type === 'Identifier' && param.name !== 'set')) {
31
+ const { hasConflict, variable } = findVariableForReplacement(context, fn.body, param ? param.name : null, 'set');
32
+ const suggest = [];
33
+ if (!hasConflict) {
34
+ if (param) {
35
+ suggest.push({
36
+ messageId: 'updateParam',
37
+ data: { oldName: param.name },
38
+ *fix(fixer) {
39
+ yield fixer.replaceText(param, 'set');
40
+ if (variable) {
41
+ for (const ref of variable.references) {
42
+ yield fixer.replaceText(ref.identifier, 'set');
43
+ }
44
+ }
45
+ }
46
+ });
47
+ }
48
+ else {
49
+ const token = getSourceCode(context).getTokenBefore(fn.body, {
50
+ filter: (token) => token.type === 'Punctuator' && token.value === '(',
51
+ includeComments: false
52
+ });
53
+ if (token) {
54
+ suggest.push({
55
+ messageId: 'addParam',
56
+ fix(fixer) {
57
+ return fixer.insertTextAfter(token, 'set');
58
+ }
59
+ });
60
+ }
61
+ }
62
+ }
26
63
  context.report({
27
64
  node: fn,
28
65
  loc: fn.loc,
29
- messageId: 'unexpected'
66
+ messageId: 'unexpected',
67
+ suggest
30
68
  });
31
69
  }
32
70
  }
@@ -113,3 +113,14 @@ export declare function isSvgElement(node: SvAST.SvelteElement): boolean;
113
113
  export declare function isMathMLElement(node: SvAST.SvelteElement): boolean;
114
114
  /** Checks whether the given identifier node is used as an expression. */
115
115
  export declare function isExpressionIdentifier(node: TSESTree.Identifier): boolean;
116
+ /**
117
+ * Finds the variable for a given name in the specified node's scope.
118
+ * Also determines if the replacement name is already in use.
119
+ *
120
+ * If the `name` is set to null, this assumes you're adding a new variable
121
+ * and reports if it is already in use.
122
+ */
123
+ export declare function findVariableForReplacement(context: RuleContext, node: TSESTree.Node, name: string | null, replacementName: string): {
124
+ hasConflict: boolean;
125
+ variable: Variable | null;
126
+ };
@@ -471,3 +471,28 @@ function getSimpleNameFromNode(node, context) {
471
471
  }
472
472
  return getSourceCode(context).getText(node);
473
473
  }
474
+ /**
475
+ * Finds the variable for a given name in the specified node's scope.
476
+ * Also determines if the replacement name is already in use.
477
+ *
478
+ * If the `name` is set to null, this assumes you're adding a new variable
479
+ * and reports if it is already in use.
480
+ */
481
+ export function findVariableForReplacement(context, node, name, replacementName) {
482
+ const scope = getScope(context, node);
483
+ let variable = null;
484
+ for (const ref of scope.references) {
485
+ if (ref.identifier.name === replacementName) {
486
+ return { hasConflict: true, variable: null };
487
+ }
488
+ }
489
+ for (const v of scope.variables) {
490
+ if (v.name === replacementName) {
491
+ return { hasConflict: true, variable: null };
492
+ }
493
+ if (v.name === name) {
494
+ variable = v;
495
+ }
496
+ }
497
+ return { hasConflict: false, variable };
498
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-svelte",
3
- "version": "3.3.2",
3
+ "version": "3.4.0",
4
4
  "description": "ESLint plugin for Svelte using AST",
5
5
  "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git",
6
6
  "homepage": "https://sveltejs.github.io/eslint-plugin-svelte",
@@ -41,7 +41,7 @@
41
41
  "postcss-load-config": "^3.1.4",
42
42
  "postcss-safe-parser": "^7.0.0",
43
43
  "semver": "^7.6.3",
44
- "svelte-eslint-parser": "^1.0.1"
44
+ "svelte-eslint-parser": "^1.1.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@babel/core": "^7.26.0",