eslint-plugin-svelte 3.0.3 → 3.2.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
@@ -302,7 +302,9 @@ These rules relate to better ways of doing things to help you avoid problems:
302
302
  | [svelte/no-reactive-functions](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-functions/) | it's not necessary to define functions in reactive statements | :star::bulb: |
303
303
  | [svelte/no-reactive-literals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | don't assign literal values in reactive statements | :star::bulb: |
304
304
  | [svelte/no-svelte-internal](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-svelte-internal/) | svelte/internal will be removed in Svelte 6. | :star: |
305
+ | [svelte/no-unnecessary-state-wrap](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unnecessary-state-wrap/) | Disallow unnecessary $state wrapping of reactive classes | :star::bulb: |
305
306
  | [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | |
307
+ | [svelte/no-unused-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-props/) | Warns about defined Props properties that are unused | :star: |
306
308
  | [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
307
309
  | [svelte/no-useless-children-snippet](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-children-snippet/) | disallow explicit children snippet where it's not needed | :star: |
308
310
  | [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :star::wrench: |
@@ -28,6 +28,8 @@ const config = [
28
28
  'svelte/no-store-async': 'error',
29
29
  'svelte/no-svelte-internal': 'error',
30
30
  'svelte/no-unknown-style-directive-property': 'error',
31
+ 'svelte/no-unnecessary-state-wrap': 'error',
32
+ 'svelte/no-unused-props': 'error',
31
33
  'svelte/no-unused-svelte-ignore': 'error',
32
34
  'svelte/no-useless-children-snippet': 'error',
33
35
  'svelte/no-useless-mustaches': 'error',
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.0.3";
17
+ version: "3.2.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.0.3";
2
+ export declare const version = "3.2.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.0.3';
5
+ export const version = '3.2.0';
@@ -249,11 +249,21 @@ export interface RuleOptions {
249
249
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/
250
250
  */
251
251
  'svelte/no-unknown-style-directive-property'?: Linter.RuleEntry<SvelteNoUnknownStyleDirectiveProperty>;
252
+ /**
253
+ * Disallow unnecessary $state wrapping of reactive classes
254
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unnecessary-state-wrap/
255
+ */
256
+ 'svelte/no-unnecessary-state-wrap'?: Linter.RuleEntry<SvelteNoUnnecessaryStateWrap>;
252
257
  /**
253
258
  * disallow the use of a class in the template without a corresponding style
254
259
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/
255
260
  */
256
261
  'svelte/no-unused-class-name'?: Linter.RuleEntry<SvelteNoUnusedClassName>;
262
+ /**
263
+ * Warns about defined Props properties that are unused
264
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-props/
265
+ */
266
+ 'svelte/no-unused-props'?: Linter.RuleEntry<SvelteNoUnusedProps>;
257
267
  /**
258
268
  * disallow unused svelte-ignore comments
259
269
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/
@@ -521,11 +531,23 @@ type SvelteNoUnknownStyleDirectiveProperty = [] | [
521
531
  ignorePrefixed?: boolean;
522
532
  }
523
533
  ];
534
+ type SvelteNoUnnecessaryStateWrap = [] | [
535
+ {
536
+ additionalReactiveClasses?: string[];
537
+ allowReassign?: boolean;
538
+ }
539
+ ];
524
540
  type SvelteNoUnusedClassName = [] | [
525
541
  {
526
542
  allowedClassNames?: string[];
527
543
  }
528
544
  ];
545
+ type SvelteNoUnusedProps = [] | [
546
+ {
547
+ checkImportedTypes?: boolean;
548
+ ignorePatterns?: string[];
549
+ }
550
+ ];
529
551
  type SvelteNoUselessMustaches = [] | [
530
552
  {
531
553
  ignoreIncludesComment?: boolean;
@@ -541,6 +563,7 @@ type SveltePreferConst = [] | [
541
563
  {
542
564
  destructuring?: ("any" | "all");
543
565
  ignoreReadBeforeAssign?: boolean;
566
+ excludedRunes?: string[];
544
567
  }
545
568
  ];
546
569
  type SvelteShorthandAttribute = [] | [
@@ -49,6 +49,7 @@ export default createRule('consistent-selector-style', {
49
49
  const styleSelectorNodeLoc = sourceCode.parserServices.styleSelectorNodeLoc;
50
50
  const checkGlobal = context.options[0]?.checkGlobal ?? false;
51
51
  const style = context.options[0]?.style ?? ['type', 'id', 'class'];
52
+ const whitelistedClasses = [];
52
53
  const classSelections = new Map();
53
54
  const idSelections = new Map();
54
55
  const typeSelections = new Map();
@@ -89,6 +90,9 @@ export default createRule('consistent-selector-style', {
89
90
  * Checks a class selector
90
91
  */
91
92
  function checkClassSelector(node) {
93
+ if (whitelistedClasses.includes(node.value)) {
94
+ return;
95
+ }
92
96
  const selection = classSelections.get(node.value) ?? [];
93
97
  for (const styleValue of style) {
94
98
  if (styleValue === 'class') {
@@ -171,6 +175,9 @@ export default createRule('consistent-selector-style', {
171
175
  addToArrayMap(classSelections, className, node);
172
176
  }
173
177
  for (const attribute of node.startTag.attributes) {
178
+ if (attribute.type === 'SvelteDirective' && attribute.kind === 'Class') {
179
+ whitelistedClasses.push(attribute.key.name.name);
180
+ }
174
181
  if (attribute.type !== 'SvelteAttribute' || attribute.key.name !== 'id') {
175
182
  continue;
176
183
  }
@@ -71,13 +71,14 @@ export default createRule('no-navigation-without-base', {
71
71
  }
72
72
  const hrefValue = node.value[0];
73
73
  if (hrefValue.type === 'SvelteLiteral') {
74
- if (!expressionIsAbsolute(hrefValue)) {
74
+ if (!expressionIsAbsolute(hrefValue) && !expressionIsFragment(hrefValue)) {
75
75
  context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
76
76
  }
77
77
  return;
78
78
  }
79
79
  if (!expressionStartsWithBase(context, hrefValue.expression, basePathNames) &&
80
- !expressionIsAbsolute(hrefValue.expression)) {
80
+ !expressionIsAbsolute(hrefValue.expression) &&
81
+ !expressionIsFragment(hrefValue.expression)) {
81
82
  context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
82
83
  }
83
84
  }
@@ -247,3 +248,27 @@ function templateLiteralIsAbsolute(url) {
247
248
  function urlValueIsAbsolute(url) {
248
249
  return url.includes('://');
249
250
  }
251
+ function expressionIsFragment(url) {
252
+ switch (url.type) {
253
+ case 'BinaryExpression':
254
+ return binaryExpressionIsFragment(url);
255
+ case 'Literal':
256
+ return typeof url.value === 'string' && urlValueIsFragment(url.value);
257
+ case 'SvelteLiteral':
258
+ return urlValueIsFragment(url.value);
259
+ case 'TemplateLiteral':
260
+ return templateLiteralIsFragment(url);
261
+ default:
262
+ return false;
263
+ }
264
+ }
265
+ function binaryExpressionIsFragment(url) {
266
+ return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(url.left);
267
+ }
268
+ function templateLiteralIsFragment(url) {
269
+ return ((url.expressions.length >= 1 && expressionIsFragment(url.expressions[0])) ||
270
+ (url.quasis.length >= 1 && urlValueIsFragment(url.quasis[0].value.raw)));
271
+ }
272
+ function urlValueIsFragment(url) {
273
+ return url.startsWith('#');
274
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.js").RuleModule;
2
+ export default _default;
@@ -0,0 +1,133 @@
1
+ import { createRule } from '../utils/index.js';
2
+ import { getSourceCode } from '../utils/compat.js';
3
+ import { ReferenceTracker } from '@eslint-community/eslint-utils';
4
+ const REACTIVE_CLASSES = [
5
+ 'SvelteSet',
6
+ 'SvelteMap',
7
+ 'SvelteURL',
8
+ 'SvelteURLSearchParams',
9
+ 'SvelteDate',
10
+ 'MediaQuery'
11
+ ];
12
+ export default createRule('no-unnecessary-state-wrap', {
13
+ meta: {
14
+ docs: {
15
+ description: 'Disallow unnecessary $state wrapping of reactive classes',
16
+ category: 'Best Practices',
17
+ recommended: true
18
+ },
19
+ schema: [
20
+ {
21
+ type: 'object',
22
+ properties: {
23
+ additionalReactiveClasses: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'string'
27
+ },
28
+ uniqueItems: true
29
+ },
30
+ allowReassign: {
31
+ type: 'boolean'
32
+ }
33
+ },
34
+ additionalProperties: false
35
+ }
36
+ ],
37
+ messages: {
38
+ unnecessaryStateWrap: '{{className}} is already reactive, $state wrapping is unnecessary.',
39
+ suggestRemoveStateWrap: 'Remove unnecessary $state wrapping'
40
+ },
41
+ type: 'suggestion',
42
+ hasSuggestions: true,
43
+ conditions: [
44
+ {
45
+ svelteVersions: ['5'],
46
+ runes: [true, 'undetermined']
47
+ }
48
+ ]
49
+ },
50
+ create(context) {
51
+ const options = context.options[0] ?? {};
52
+ const additionalReactiveClasses = options.additionalReactiveClasses ?? [];
53
+ const allowReassign = options.allowReassign ?? false;
54
+ const referenceTracker = new ReferenceTracker(getSourceCode(context).scopeManager.globalScope);
55
+ const traceMap = {};
56
+ for (const reactiveClass of REACTIVE_CLASSES) {
57
+ traceMap[reactiveClass] = {
58
+ [ReferenceTracker.CALL]: true,
59
+ [ReferenceTracker.CONSTRUCT]: true
60
+ };
61
+ }
62
+ // Track all reactive class imports and their aliases
63
+ const references = referenceTracker.iterateEsmReferences({
64
+ 'svelte/reactivity': {
65
+ [ReferenceTracker.ESM]: true,
66
+ ...traceMap
67
+ }
68
+ });
69
+ const referenceNodeAndNames = Array.from(references).map(({ node, path }) => {
70
+ return {
71
+ node,
72
+ name: path[path.length - 1]
73
+ };
74
+ });
75
+ function isReassigned(identifier) {
76
+ const variable = getSourceCode(context).scopeManager.getDeclaredVariables(identifier.parent)[0];
77
+ return variable.references.some((ref) => {
78
+ return ref.isWrite() && ref.identifier !== identifier;
79
+ });
80
+ }
81
+ function reportUnnecessaryStateWrap(stateNode, targetNode, className, identifier) {
82
+ if (allowReassign && identifier && isReassigned(identifier)) {
83
+ return;
84
+ }
85
+ context.report({
86
+ node: targetNode,
87
+ messageId: 'unnecessaryStateWrap',
88
+ data: {
89
+ className
90
+ },
91
+ suggest: [
92
+ {
93
+ messageId: 'suggestRemoveStateWrap',
94
+ fix(fixer) {
95
+ return fixer.replaceText(stateNode, getSourceCode(context).getText(targetNode));
96
+ }
97
+ }
98
+ ]
99
+ });
100
+ }
101
+ return {
102
+ CallExpression(node) {
103
+ if (node.callee.type !== 'Identifier' || node.callee.name !== '$state') {
104
+ return;
105
+ }
106
+ for (const arg of node.arguments) {
107
+ if ((arg.type === 'NewExpression' || arg.type === 'CallExpression') &&
108
+ arg.callee.type === 'Identifier') {
109
+ const name = arg.callee.name;
110
+ if (additionalReactiveClasses.includes(name)) {
111
+ const parent = node.parent;
112
+ if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
113
+ reportUnnecessaryStateWrap(node, arg, name, parent.id);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ },
119
+ 'Program:exit': () => {
120
+ for (const { node, name } of referenceNodeAndNames) {
121
+ if (node.parent?.type === 'CallExpression' &&
122
+ node.parent.callee.type === 'Identifier' &&
123
+ node.parent.callee.name === '$state') {
124
+ const parent = node.parent.parent;
125
+ if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
126
+ reportUnnecessaryStateWrap(node.parent, node, name, parent.id);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ };
132
+ }
133
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.js").RuleModule;
2
+ export default _default;
@@ -0,0 +1,280 @@
1
+ import { createRule } from '../utils/index.js';
2
+ import { getTypeScriptTools } from '../utils/ts-utils/index.js';
3
+ import { findVariable } from '../utils/ast-utils.js';
4
+ import { toRegExp } from '../utils/regexp.js';
5
+ import { getFilename } from '../utils/compat.js';
6
+ export default createRule('no-unused-props', {
7
+ meta: {
8
+ docs: {
9
+ description: 'Warns about defined Props properties that are unused',
10
+ category: 'Best Practices',
11
+ recommended: true
12
+ },
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ checkImportedTypes: {
18
+ type: 'boolean',
19
+ default: false
20
+ },
21
+ ignorePatterns: {
22
+ type: 'array',
23
+ items: {
24
+ type: 'string'
25
+ },
26
+ default: []
27
+ }
28
+ },
29
+ additionalProperties: false
30
+ }
31
+ ],
32
+ messages: {
33
+ unusedProp: "'{{name}}' is an unused Props property.",
34
+ unusedNestedProp: "'{{name}}' in '{{parent}}' is an unused property.",
35
+ unusedIndexSignature: 'Index signature is unused. Consider using rest operator (...) to capture remaining properties.'
36
+ },
37
+ type: 'suggestion',
38
+ conditions: [
39
+ {
40
+ svelteVersions: ['5'],
41
+ runes: [true, 'undetermined']
42
+ }
43
+ ]
44
+ },
45
+ create(context) {
46
+ const fileName = getFilename(context);
47
+ const tools = getTypeScriptTools(context);
48
+ if (!tools) {
49
+ return {};
50
+ }
51
+ const typeChecker = tools.service.program.getTypeChecker();
52
+ if (!typeChecker) {
53
+ return {};
54
+ }
55
+ const options = context.options[0] ?? {};
56
+ const checkImportedTypes = options.checkImportedTypes ?? false;
57
+ const ignorePatterns = (options.ignorePatterns ?? []).map((p) => {
58
+ if (typeof p === 'string') {
59
+ return toRegExp(p);
60
+ }
61
+ return p;
62
+ });
63
+ function shouldIgnore(name) {
64
+ return ignorePatterns.some((pattern) => pattern.test(name));
65
+ }
66
+ function shouldIgnoreType(type) {
67
+ const typeStr = typeChecker.typeToString(type);
68
+ const symbol = type.getSymbol();
69
+ const symbolName = symbol?.getName();
70
+ return shouldIgnore(typeStr) || (symbolName ? shouldIgnore(symbolName) : false);
71
+ }
72
+ function isExternalType(type) {
73
+ const symbol = type.getSymbol();
74
+ if (!symbol)
75
+ return false;
76
+ const declarations = symbol.getDeclarations();
77
+ if (!declarations || declarations.length === 0)
78
+ return false;
79
+ const sourceFile = declarations[0].getSourceFile();
80
+ return sourceFile.fileName !== fileName;
81
+ }
82
+ /**
83
+ * Extracts property paths from member expressions.
84
+ */
85
+ function getPropertyPath(node) {
86
+ const paths = [];
87
+ let currentNode = node;
88
+ let parentNode = currentNode.parent ?? null;
89
+ while (parentNode) {
90
+ if (parentNode.type === 'MemberExpression' && parentNode.object === currentNode) {
91
+ const property = parentNode.property;
92
+ if (property.type === 'Identifier') {
93
+ paths.push(property.name);
94
+ }
95
+ else if (property.type === 'Literal' && typeof property.value === 'string') {
96
+ paths.push(property.value);
97
+ }
98
+ else {
99
+ break;
100
+ }
101
+ }
102
+ currentNode = parentNode;
103
+ parentNode = currentNode.parent ?? null;
104
+ }
105
+ return paths;
106
+ }
107
+ /**
108
+ * Finds all property access paths for a given variable.
109
+ */
110
+ function getUsedNestedPropertyNames(node) {
111
+ const variable = findVariable(context, node);
112
+ if (!variable)
113
+ return [];
114
+ const paths = [];
115
+ for (const reference of variable.references) {
116
+ if ('identifier' in reference && reference.identifier.type === 'Identifier') {
117
+ const referencePath = getPropertyPath(reference.identifier);
118
+ paths.push(referencePath);
119
+ }
120
+ }
121
+ return paths;
122
+ }
123
+ /**
124
+ * Checks if a property is from TypeScript's built-in type definitions.
125
+ * These properties should be ignored as they are not user-defined props.
126
+ */
127
+ function isBuiltInProperty(prop) {
128
+ const declarations = prop.getDeclarations();
129
+ if (!declarations || declarations.length === 0)
130
+ return false;
131
+ const declaration = declarations[0];
132
+ const sourceFile = declaration.getSourceFile();
133
+ if (!sourceFile)
134
+ return false;
135
+ return sourceFile.fileName.includes('node_modules/typescript/lib/');
136
+ }
137
+ function getUsedPropertiesFromPattern(pattern) {
138
+ const usedProps = new Set();
139
+ for (const prop of pattern.properties) {
140
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
141
+ usedProps.add(prop.key.name);
142
+ }
143
+ else if (prop.type === 'RestElement') {
144
+ // If there's a rest element, all properties are potentially used
145
+ return new Set();
146
+ }
147
+ }
148
+ return usedProps;
149
+ }
150
+ /**
151
+ * Check if the type is a class type (has constructor or prototype)
152
+ */
153
+ function isClassType(type) {
154
+ if (!type)
155
+ return false;
156
+ // Check if it's a class instance type
157
+ if (type.isClass())
158
+ return true;
159
+ // Check for constructor signatures
160
+ const constructorType = type.getConstructSignatures();
161
+ if (constructorType.length > 0)
162
+ return true;
163
+ // Check if it has a prototype property
164
+ const symbol = type.getSymbol();
165
+ if (symbol?.members?.has('prototype'))
166
+ return true;
167
+ return false;
168
+ }
169
+ /**
170
+ * Recursively checks for unused properties in a type.
171
+ */
172
+ function checkUnusedProperties(type, usedPaths, usedProps, reportNode, parentPath, checkedTypes, reportedProps) {
173
+ // Skip checking if the type itself is a class
174
+ if (isClassType(type))
175
+ return;
176
+ const typeStr = typeChecker.typeToString(type);
177
+ if (checkedTypes.has(typeStr))
178
+ return;
179
+ checkedTypes.add(typeStr);
180
+ if (shouldIgnoreType(type))
181
+ return;
182
+ if (!checkImportedTypes && isExternalType(type))
183
+ return;
184
+ const properties = typeChecker.getPropertiesOfType(type);
185
+ const baseTypes = type.getBaseTypes();
186
+ if (!properties.length && (!baseTypes || baseTypes.length === 0)) {
187
+ return;
188
+ }
189
+ if (baseTypes) {
190
+ for (const baseType of baseTypes) {
191
+ checkUnusedProperties(baseType, usedPaths, usedProps, reportNode, parentPath, checkedTypes, reportedProps);
192
+ }
193
+ }
194
+ for (const prop of properties) {
195
+ if (isBuiltInProperty(prop))
196
+ continue;
197
+ const propName = prop.getName();
198
+ const currentPath = [...parentPath, propName];
199
+ const currentPathStr = [...parentPath, propName].join('.');
200
+ if (reportedProps.has(currentPathStr))
201
+ continue;
202
+ const propType = typeChecker.getTypeOfSymbol(prop);
203
+ const isUsedInPath = usedPaths.some((path) => {
204
+ const usedPath = path.join('.');
205
+ return usedPath === currentPathStr || usedPath.startsWith(`${currentPathStr}.`);
206
+ });
207
+ const isUsedInProps = usedProps.has(propName);
208
+ if (!isUsedInPath && !isUsedInProps) {
209
+ reportedProps.add(currentPathStr);
210
+ context.report({
211
+ node: reportNode,
212
+ messageId: parentPath.length ? 'unusedNestedProp' : 'unusedProp',
213
+ data: {
214
+ name: propName,
215
+ parent: parentPath.join('.')
216
+ }
217
+ });
218
+ }
219
+ const isUsedNested = usedPaths.some((path) => {
220
+ return path.join('.').startsWith(`${currentPathStr}.`);
221
+ });
222
+ if (isUsedNested || isUsedInProps) {
223
+ checkUnusedProperties(propType, usedPaths, usedProps, reportNode, currentPath, checkedTypes, reportedProps);
224
+ }
225
+ }
226
+ // Check for unused index signatures only at the root level
227
+ if (parentPath.length === 0) {
228
+ const indexType = type.getStringIndexType();
229
+ const numberIndexType = type.getNumberIndexType();
230
+ const hasIndexSignature = Boolean(indexType) || Boolean(numberIndexType);
231
+ if (hasIndexSignature && !hasRestElement(usedProps)) {
232
+ context.report({
233
+ node: reportNode,
234
+ messageId: 'unusedIndexSignature'
235
+ });
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Returns true if the destructuring pattern includes a rest element,
241
+ * which means all remaining properties are potentially used.
242
+ */
243
+ function hasRestElement(usedProps) {
244
+ return usedProps.size === 0;
245
+ }
246
+ return {
247
+ 'VariableDeclaration > VariableDeclarator': (node) => {
248
+ // Only check $props declarations
249
+ if (node.init?.type !== 'CallExpression' ||
250
+ node.init.callee.type !== 'Identifier' ||
251
+ node.init.callee.name !== '$props') {
252
+ return;
253
+ }
254
+ const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node);
255
+ if (!tsNode || !tsNode.type)
256
+ return;
257
+ const propType = typeChecker.getTypeFromTypeNode(tsNode.type);
258
+ let usedPaths = [];
259
+ let usedProps = new Set();
260
+ if (node.id.type === 'ObjectPattern') {
261
+ usedProps = getUsedPropertiesFromPattern(node.id);
262
+ if (usedProps.size === 0)
263
+ return;
264
+ const identifiers = node.id.properties
265
+ .filter((p) => p.type === 'Property')
266
+ .map((p) => p.value)
267
+ .filter((v) => v.type === 'Identifier');
268
+ for (const identifier of identifiers) {
269
+ const paths = getUsedNestedPropertyNames(identifier);
270
+ usedPaths.push(...paths);
271
+ }
272
+ }
273
+ else if (node.id.type === 'Identifier') {
274
+ usedPaths = getUsedNestedPropertyNames(node.id);
275
+ }
276
+ checkUnusedProperties(propType, usedPaths, usedProps, node.id, [], new Set(), new Set());
277
+ }
278
+ };
279
+ }
280
+ });
@@ -15,7 +15,7 @@ function findDeclarationCallee(node) {
15
15
  * Determines if a declaration should be skipped in the const preference analysis.
16
16
  * Specifically checks for Svelte's state management utilities ($props, $derived).
17
17
  */
18
- function shouldSkipDeclaration(declaration) {
18
+ function shouldSkipDeclaration(declaration, excludedRunes) {
19
19
  if (!declaration) {
20
20
  return false;
21
21
  }
@@ -23,15 +23,13 @@ function shouldSkipDeclaration(declaration) {
23
23
  if (!callee) {
24
24
  return false;
25
25
  }
26
- if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) {
26
+ if (callee.type === 'Identifier' && excludedRunes.includes(callee.name)) {
27
27
  return true;
28
28
  }
29
29
  if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') {
30
30
  return false;
31
31
  }
32
- if (callee.object.name === '$derived' &&
33
- callee.property.type === 'Identifier' &&
34
- callee.property.name === 'by') {
32
+ if (excludedRunes.includes(callee.object.name)) {
35
33
  return true;
36
34
  }
37
35
  return false;
@@ -44,16 +42,34 @@ export default createRule('prefer-const', {
44
42
  category: 'Best Practices',
45
43
  recommended: false,
46
44
  extensionRule: 'prefer-const'
47
- }
45
+ },
46
+ schema: [
47
+ {
48
+ type: 'object',
49
+ properties: {
50
+ destructuring: { enum: ['any', 'all'] },
51
+ ignoreReadBeforeAssign: { type: 'boolean' },
52
+ excludedRunes: {
53
+ type: 'array',
54
+ items: {
55
+ type: 'string'
56
+ }
57
+ }
58
+ },
59
+ additionalProperties: false
60
+ }
61
+ ]
48
62
  },
49
63
  create(context) {
64
+ const config = context.options[0] ?? {};
65
+ const excludedRunes = config.excludedRunes ?? ['$props', '$derived'];
50
66
  return defineWrapperListener(coreRule, context, {
51
67
  createListenerProxy(coreListener) {
52
68
  return {
53
69
  ...coreListener,
54
70
  VariableDeclaration(node) {
55
71
  for (const decl of node.declarations) {
56
- if (shouldSkipDeclaration(decl.init)) {
72
+ if (shouldSkipDeclaration(decl.init, excludedRunes)) {
57
73
  return;
58
74
  }
59
75
  }
@@ -7,11 +7,11 @@ function checkProp(node, context, expectedPropNames) {
7
7
  return;
8
8
  for (const p of node.id.properties) {
9
9
  if (p.type === 'Property' &&
10
- p.value.type === 'Identifier' &&
11
- !expectedPropNames.includes(p.value.name)) {
10
+ p.key.type === 'Identifier' &&
11
+ !expectedPropNames.includes(p.key.name)) {
12
12
  context.report({
13
- node: p.value,
14
- loc: p.value.loc,
13
+ node: p.key,
14
+ loc: p.key.loc,
15
15
  messageId: 'unexpected'
16
16
  });
17
17
  }
@@ -46,7 +46,9 @@ import noSvelteInternal from '../rules/no-svelte-internal.js';
46
46
  import noTargetBlank from '../rules/no-target-blank.js';
47
47
  import noTrailingSpaces from '../rules/no-trailing-spaces.js';
48
48
  import noUnknownStyleDirectiveProperty from '../rules/no-unknown-style-directive-property.js';
49
+ import noUnnecessaryStateWrap from '../rules/no-unnecessary-state-wrap.js';
49
50
  import noUnusedClassName from '../rules/no-unused-class-name.js';
51
+ import noUnusedProps from '../rules/no-unused-props.js';
50
52
  import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js';
51
53
  import noUselessChildrenSnippet from '../rules/no-useless-children-snippet.js';
52
54
  import noUselessMustaches from '../rules/no-useless-mustaches.js';
@@ -118,7 +120,9 @@ export const rules = [
118
120
  noTargetBlank,
119
121
  noTrailingSpaces,
120
122
  noUnknownStyleDirectiveProperty,
123
+ noUnnecessaryStateWrap,
121
124
  noUnusedClassName,
125
+ noUnusedProps,
122
126
  noUnusedSvelteIgnore,
123
127
  noUselessChildrenSnippet,
124
128
  noUselessMustaches,
@@ -16,7 +16,7 @@ function getSvelteFileType(filePath) {
16
16
  return null;
17
17
  }
18
18
  function getSvelteKitFileTypeFromFilePath(filePath) {
19
- const fileName = filePath.split('/').pop();
19
+ const fileName = filePath.split(/[/\\]/).pop();
20
20
  switch (fileName) {
21
21
  case '+page.svelte': {
22
22
  return '+page.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-svelte",
3
- "version": "3.0.3",
3
+ "version": "3.2.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",