eslint-plugin-svelte 3.12.4 → 3.13.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/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.12.4";
17
+ version: "3.13.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.12.4";
2
+ export declare const version: "3.13.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.12.4';
5
+ export const version = '3.13.0';
@@ -30,10 +30,10 @@ export default createRule('no-navigation-without-resolve', {
30
30
  }
31
31
  ],
32
32
  messages: {
33
- gotoWithoutResolve: "Found a goto() call with a url that isn't resolved.",
34
- linkWithoutResolve: "Found a link with a url that isn't resolved.",
35
- pushStateWithoutResolve: "Found a pushState() call with a url that isn't resolved.",
36
- replaceStateWithoutResolve: "Found a replaceState() call with a url that isn't resolved."
33
+ gotoWithoutResolve: 'Unexpected goto() call without resolve().',
34
+ linkWithoutResolve: 'Unexpected href link without resolve().',
35
+ pushStateWithoutResolve: 'Unexpected pushState() call without resolve().',
36
+ replaceStateWithoutResolve: 'Unexpected replaceState() call without resolve().'
37
37
  },
38
38
  type: 'suggestion',
39
39
  conditions: [
@@ -44,29 +44,49 @@ export default createRule('no-navigation-without-resolve', {
44
44
  },
45
45
  create(context) {
46
46
  let resolveReferences = new Set();
47
+ const ignoreGoto = context.options[0]?.ignoreGoto ?? false;
48
+ const ignorePushState = context.options[0]?.ignorePushState ?? false;
49
+ const ignoreReplaceState = context.options[0]?.ignoreReplaceState ?? false;
50
+ const ignoreLinks = context.options[0]?.ignoreLinks ?? false;
47
51
  return {
48
52
  Program() {
49
53
  const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope);
50
54
  resolveReferences = extractResolveReferences(referenceTracker, context);
51
55
  const { goto: gotoCalls, pushState: pushStateCalls, replaceState: replaceStateCalls } = extractFunctionCallReferences(referenceTracker);
52
- if (context.options[0]?.ignoreGoto !== true) {
56
+ if (!ignoreGoto) {
53
57
  for (const gotoCall of gotoCalls) {
54
58
  checkGotoCall(context, gotoCall, resolveReferences);
55
59
  }
56
60
  }
57
- if (context.options[0]?.ignorePushState !== true) {
61
+ if (!ignorePushState) {
58
62
  for (const pushStateCall of pushStateCalls) {
59
63
  checkShallowNavigationCall(context, pushStateCall, resolveReferences, 'pushStateWithoutResolve');
60
64
  }
61
65
  }
62
- if (context.options[0]?.ignoreReplaceState !== true) {
66
+ if (!ignoreReplaceState) {
63
67
  for (const replaceStateCall of replaceStateCalls) {
64
68
  checkShallowNavigationCall(context, replaceStateCall, resolveReferences, 'replaceStateWithoutResolve');
65
69
  }
66
70
  }
67
71
  },
72
+ SvelteShorthandAttribute(node) {
73
+ if (ignoreLinks ||
74
+ node.parent.parent.type !== 'SvelteElement' ||
75
+ node.parent.parent.kind !== 'html' ||
76
+ node.parent.parent.name.type !== 'SvelteName' ||
77
+ node.parent.parent.name.name !== 'a' ||
78
+ node.key.name !== 'href' ||
79
+ node.value.type !== 'Identifier') {
80
+ return;
81
+ }
82
+ if (!expressionIsAbsolute(new FindVariableContext(context), node.value) &&
83
+ !expressionIsFragment(new FindVariableContext(context), node.value) &&
84
+ !isResolveCall(new FindVariableContext(context), node.value, resolveReferences)) {
85
+ context.report({ loc: node.loc, messageId: 'linkWithoutResolve' });
86
+ }
87
+ },
68
88
  SvelteAttribute(node) {
69
- if (context.options[0]?.ignoreLinks === true ||
89
+ if (ignoreLinks ||
70
90
  node.parent.parent.type !== 'SvelteElement' ||
71
91
  node.parent.parent.kind !== 'html' ||
72
92
  node.parent.parent.name.type !== 'SvelteName' ||
@@ -75,9 +95,11 @@ export default createRule('no-navigation-without-resolve', {
75
95
  return;
76
96
  }
77
97
  if ((node.value[0].type === 'SvelteLiteral' &&
98
+ !expressionIsNullish(new FindVariableContext(context), node.value[0]) &&
78
99
  !expressionIsAbsolute(new FindVariableContext(context), node.value[0]) &&
79
100
  !expressionIsFragment(new FindVariableContext(context), node.value[0])) ||
80
101
  (node.value[0].type === 'SvelteMustacheTag' &&
102
+ !expressionIsNullish(new FindVariableContext(context), node.value[0].expression) &&
81
103
  !expressionIsAbsolute(new FindVariableContext(context), node.value[0].expression) &&
82
104
  !expressionIsFragment(new FindVariableContext(context), node.value[0].expression) &&
83
105
  !isResolveCall(new FindVariableContext(context), node.value[0].expression, resolveReferences))) {
@@ -93,6 +115,9 @@ function extractResolveReferences(referenceTracker, context) {
93
115
  for (const { node } of referenceTracker.iterateEsmReferences({
94
116
  '$app/paths': {
95
117
  [ReferenceTracker.ESM]: true,
118
+ asset: {
119
+ [ReferenceTracker.READ]: true
120
+ },
96
121
  resolve: {
97
122
  [ReferenceTracker.READ]: true
98
123
  }
@@ -192,6 +217,29 @@ function expressionIsEmpty(url) {
192
217
  url.quasis.length === 1 &&
193
218
  url.quasis[0].value.raw === ''));
194
219
  }
220
+ function expressionIsNullish(ctx, url) {
221
+ switch (url.type) {
222
+ case 'Identifier':
223
+ return identifierIsNullish(ctx, url);
224
+ case 'Literal':
225
+ return url.value === null; // Undefined is an Identifier in ESTree, null is a Literal
226
+ default:
227
+ return false;
228
+ }
229
+ }
230
+ function identifierIsNullish(ctx, url) {
231
+ if (url.name === 'undefined') {
232
+ return true;
233
+ }
234
+ const variable = ctx.findVariable(url);
235
+ if (variable === null ||
236
+ variable.identifiers.length === 0 ||
237
+ variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
238
+ variable.identifiers[0].parent.init === null) {
239
+ return false;
240
+ }
241
+ return expressionIsNullish(ctx, variable.identifiers[0].parent.init);
242
+ }
195
243
  function expressionIsAbsolute(ctx, url) {
196
244
  switch (url.type) {
197
245
  case 'BinaryExpression':
@@ -109,6 +109,7 @@ export default createRule('no-unused-props', {
109
109
  */
110
110
  function getPropertyPath(node) {
111
111
  const paths = [];
112
+ let isSpread = false;
112
113
  let currentNode = node;
113
114
  let parentNode = currentNode.parent ?? null;
114
115
  while (parentNode) {
@@ -120,14 +121,17 @@ export default createRule('no-unused-props', {
120
121
  else if (property.type === 'Literal' && typeof property.value === 'string') {
121
122
  paths.push(property.value);
122
123
  }
123
- else {
124
- break;
124
+ }
125
+ else {
126
+ if (parentNode.type === 'SpreadElement' || parentNode.type === 'SvelteSpreadAttribute') {
127
+ isSpread = true;
125
128
  }
129
+ break;
126
130
  }
127
131
  currentNode = parentNode;
128
132
  parentNode = currentNode.parent ?? null;
129
133
  }
130
- return paths;
134
+ return { paths, isSpread };
131
135
  }
132
136
  /**
133
137
  * Finds all property access paths for a given variable.
@@ -135,18 +139,24 @@ export default createRule('no-unused-props', {
135
139
  function getUsedNestedPropertyPathsArray(node) {
136
140
  const variable = findVariable(context, node);
137
141
  if (!variable)
138
- return [];
142
+ return { paths: [], spreadPaths: [] };
139
143
  const pathsArray = [];
144
+ const spreadPathsArray = [];
140
145
  for (const reference of variable.references) {
141
146
  if ('identifier' in reference &&
142
147
  reference.identifier.type === 'Identifier' &&
143
148
  (reference.identifier.range[0] !== node.range[0] ||
144
149
  reference.identifier.range[1] !== node.range[1])) {
145
- const referencePath = getPropertyPath(reference.identifier);
146
- pathsArray.push(referencePath);
150
+ const { paths, isSpread } = getPropertyPath(reference.identifier);
151
+ if (isSpread) {
152
+ spreadPathsArray.push(paths);
153
+ }
154
+ else {
155
+ pathsArray.push(paths);
156
+ }
147
157
  }
148
158
  }
149
- return pathsArray;
159
+ return { paths: pathsArray, spreadPaths: spreadPathsArray };
150
160
  }
151
161
  /**
152
162
  * Checks if a property is from TypeScript's built-in type definitions.
@@ -204,7 +214,7 @@ export default createRule('no-unused-props', {
204
214
  /**
205
215
  * Recursively checks for unused properties in a type.
206
216
  */
207
- function checkUnusedProperties({ propsType, usedPropertyPaths, declaredPropertyNames, reportNode, parentPath, checkedPropsTypes, reportedPropertyPaths }) {
217
+ function checkUnusedProperties({ propsType, usedPropertyPaths, usedSpreadPropertyPaths, declaredPropertyNames, reportNode, parentPath, checkedPropsTypes, reportedPropertyPaths }) {
208
218
  // Skip checking if the type itself is a class
209
219
  if (isClassType(propsType))
210
220
  return;
@@ -224,6 +234,7 @@ export default createRule('no-unused-props', {
224
234
  checkUnusedProperties({
225
235
  propsType: propsBaseType,
226
236
  usedPropertyPaths,
237
+ usedSpreadPropertyPaths,
227
238
  declaredPropertyNames,
228
239
  reportNode,
229
240
  parentPath,
@@ -241,11 +252,14 @@ export default createRule('no-unused-props', {
241
252
  if (shouldIgnoreProperty(propName))
242
253
  continue;
243
254
  const currentPath = [...parentPath, propName];
244
- const currentPathStr = [...parentPath, propName].join('.');
255
+ const currentPathStr = currentPath.join('.');
245
256
  if (reportedPropertyPaths.has(currentPathStr))
246
257
  continue;
247
258
  const propType = typeChecker.getTypeOfSymbol(prop);
248
- const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr);
259
+ const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr) ||
260
+ usedSpreadPropertyPaths.some((path) => {
261
+ return path === '' || path === currentPathStr || path.startsWith(`${currentPathStr}.`);
262
+ });
249
263
  const isUsedInPath = usedPropertyPaths.some((path) => {
250
264
  return path.startsWith(`${currentPathStr}.`);
251
265
  });
@@ -274,6 +288,7 @@ export default createRule('no-unused-props', {
274
288
  checkUnusedProperties({
275
289
  propsType: propType,
276
290
  usedPropertyPaths,
291
+ usedSpreadPropertyPaths,
277
292
  declaredPropertyNames,
278
293
  reportNode,
279
294
  parentPath: currentPath,
@@ -306,8 +321,6 @@ export default createRule('no-unused-props', {
306
321
  function normalizeUsedPaths(paths, allowUnusedNestedProperties) {
307
322
  const normalized = [];
308
323
  for (const path of paths.sort((a, b) => a.length - b.length)) {
309
- if (path.length === 0)
310
- continue;
311
324
  if (normalized.some((p) => p.every((part, idx) => part === path[idx]))) {
312
325
  continue;
313
326
  }
@@ -332,7 +345,8 @@ export default createRule('no-unused-props', {
332
345
  if (!tsNode || !tsNode.type)
333
346
  return;
334
347
  const propsType = typeChecker.getTypeFromTypeNode(tsNode.type);
335
- let usedPropertyPathsArray = [];
348
+ const usedPropertyPathsArray = [];
349
+ const usedSpreadPropertyPathsArray = [];
336
350
  let declaredPropertyNames = new Set();
337
351
  if (node.id.type === 'ObjectPattern') {
338
352
  declaredPropertyNames = getUsedPropertyNamesFromPattern(node.id);
@@ -351,18 +365,25 @@ export default createRule('no-unused-props', {
351
365
  }
352
366
  }
353
367
  for (const identifier of identifiers) {
354
- const paths = getUsedNestedPropertyPathsArray(identifier);
368
+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(identifier);
355
369
  usedPropertyPathsArray.push(...paths.map((path) => [identifier.name, ...path]));
370
+ usedSpreadPropertyPathsArray.push(...spreadPaths.map((path) => [identifier.name, ...path]));
356
371
  }
357
372
  }
358
373
  else if (node.id.type === 'Identifier') {
359
- usedPropertyPathsArray = getUsedNestedPropertyPathsArray(node.id);
374
+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(node.id);
375
+ usedPropertyPathsArray.push(...paths);
376
+ usedSpreadPropertyPathsArray.push(...spreadPaths);
377
+ }
378
+ function runNormalizeUsedPaths(paths) {
379
+ return normalizeUsedPaths(paths, options.allowUnusedNestedProperties).map((pathArray) => {
380
+ return pathArray.join('.');
381
+ });
360
382
  }
361
383
  checkUnusedProperties({
362
384
  propsType,
363
- usedPropertyPaths: normalizeUsedPaths(usedPropertyPathsArray, options.allowUnusedNestedProperties).map((pathArray) => {
364
- return pathArray.join('.');
365
- }),
385
+ usedPropertyPaths: runNormalizeUsedPaths(usedPropertyPathsArray),
386
+ usedSpreadPropertyPaths: runNormalizeUsedPaths(usedSpreadPropertyPathsArray),
366
387
  declaredPropertyNames,
367
388
  reportNode: node.id,
368
389
  parentPath: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-svelte",
3
- "version": "3.12.4",
3
+ "version": "3.13.0",
4
4
  "description": "ESLint plugin for Svelte using AST",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  "postcss-load-config": "^3.1.4",
46
46
  "postcss-safe-parser": "^7.0.0",
47
47
  "semver": "^7.6.3",
48
- "svelte-eslint-parser": "^1.3.0"
48
+ "svelte-eslint-parser": "^1.4.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@babel/core": "^7.28.3",
@@ -79,7 +79,7 @@
79
79
  "sass": "^1.92.0",
80
80
  "source-map-js": "^1.2.1",
81
81
  "stylus": "^0.64.0",
82
- "svelte": "^5.38.6",
82
+ "svelte": "^5.41.0",
83
83
  "svelte-i18n": "^4.0.1",
84
84
  "tsx": "^4.20.5",
85
85
  "type-coverage": "^2.29.7",
@@ -108,7 +108,7 @@
108
108
  "debug": "pnpm run mocha \"tests/src/**/*.ts\" --reporter dot --timeout 60000",
109
109
  "lint": "run-p lint:*",
110
110
  "lint-fix": "pnpm run lint:es --fix",
111
- "lint:es": "eslint --cache .",
111
+ "lint:es": "eslint --concurrency auto --cache .",
112
112
  "mocha": "pnpm run ts ./node_modules/mocha/bin/mocha.js",
113
113
  "new": "pnpm run ts ./tools/new-rule.ts",
114
114
  "prebuild": "pnpm run clean",