eslint-plugin-svelte 3.6.0 → 3.8.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
@@ -271,6 +271,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
271
271
  | [svelte/no-reactive-reassign](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-reassign/) | disallow reassigning reactive values | :star: |
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
+ | [svelte/no-top-level-browser-globals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-top-level-browser-globals/) | disallow using top-level browser global variables | |
274
275
  | [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
276
  | [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
277
  | [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: |
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.6.0";
17
+ version: "3.8.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.6.0";
2
+ export declare const version: "3.8.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.6.0';
5
+ export const version = '3.8.0';
@@ -244,6 +244,11 @@ export interface RuleOptions {
244
244
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-target-blank/
245
245
  */
246
246
  'svelte/no-target-blank'?: Linter.RuleEntry<SvelteNoTargetBlank>;
247
+ /**
248
+ * disallow using top-level browser global variables
249
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-top-level-browser-globals/
250
+ */
251
+ 'svelte/no-top-level-browser-globals'?: Linter.RuleEntry<[]>;
247
252
  /**
248
253
  * disallow trailing whitespace at the end of lines
249
254
  * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../types.js").RuleModule;
2
+ export default _default;
@@ -0,0 +1,350 @@
1
+ import { ReferenceTracker, getStaticValue } from '@eslint-community/eslint-utils';
2
+ import { createRule } from '../utils/index.js';
3
+ import globals from 'globals';
4
+ import { findVariable, getScope } from '../utils/ast-utils.js';
5
+ export default createRule('no-top-level-browser-globals', {
6
+ meta: {
7
+ docs: {
8
+ description: 'disallow using top-level browser global variables',
9
+ category: 'Possible Errors',
10
+ recommended: false
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ unexpectedGlobal: 'Unexpected top-level browser global variable "{{name}}".'
15
+ },
16
+ type: 'problem',
17
+ conditions: [{ svelteFileTypes: ['.svelte', '.svelte.[js|ts]'] }]
18
+ },
19
+ create(context) {
20
+ const sourceCode = context.sourceCode;
21
+ const blowerGlobals = getBrowserGlobals();
22
+ const referenceTracker = new ReferenceTracker(sourceCode.scopeManager.globalScope, {
23
+ // Specifies the global variables that are allowed to prevent `window.window` from being iterated over.
24
+ globalObjectNames: ['globalThis']
25
+ });
26
+ const maybeGuards = [];
27
+ const functions = [];
28
+ function enterFunction(node) {
29
+ if (isTopLevelLocation(node)) {
30
+ functions.push(node);
31
+ }
32
+ }
33
+ function enterMetaProperty(node) {
34
+ if (node.meta.name !== 'import' || node.property.name !== 'meta')
35
+ return;
36
+ for (const ref of referenceTracker.iteratePropertyReferences(node, {
37
+ env: {
38
+ // See https://vite.dev/guide/ssr#conditional-logic
39
+ SSR: {
40
+ [ReferenceTracker.READ]: true
41
+ }
42
+ }
43
+ })) {
44
+ if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') {
45
+ const guardChecker = getGuardChecker({ node: ref.node, not: true });
46
+ if (guardChecker) {
47
+ maybeGuards.push({
48
+ isAvailableLocation: guardChecker,
49
+ browserEnvironment: true
50
+ });
51
+ }
52
+ }
53
+ }
54
+ }
55
+ function verifyGlobalReferences() {
56
+ // Collects guarded location checkers by checking module references
57
+ // that can check the browser environment.
58
+ for (const referenceNode of iterateBrowserCheckerModuleReferences()) {
59
+ if (!isTopLevelLocation(referenceNode))
60
+ continue;
61
+ const guardChecker = getGuardChecker({ node: referenceNode });
62
+ if (guardChecker) {
63
+ maybeGuards.push({
64
+ isAvailableLocation: guardChecker,
65
+ browserEnvironment: true
66
+ });
67
+ }
68
+ }
69
+ const reportCandidates = [];
70
+ // Collects references to global variables.
71
+ for (const ref of iterateBrowserGlobalReferences()) {
72
+ if (!isTopLevelLocation(ref.node))
73
+ continue;
74
+ const guardChecker = getGuardCheckerFromReference(ref.node);
75
+ if (guardChecker) {
76
+ const name = ref.path.join('.');
77
+ maybeGuards.push({
78
+ reference: { node: ref.node, name },
79
+ isAvailableLocation: guardChecker,
80
+ browserEnvironment: name === 'window' || name === 'document'
81
+ });
82
+ }
83
+ else {
84
+ reportCandidates.push(ref);
85
+ }
86
+ }
87
+ for (const ref of reportCandidates) {
88
+ const name = ref.path.join('.');
89
+ if (isAvailableLocation({ node: ref.node, name })) {
90
+ continue;
91
+ }
92
+ context.report({
93
+ node: ref.node,
94
+ messageId: 'unexpectedGlobal',
95
+ data: { name }
96
+ });
97
+ }
98
+ }
99
+ return {
100
+ ':function': enterFunction,
101
+ MetaProperty: enterMetaProperty,
102
+ 'Program:exit': verifyGlobalReferences
103
+ };
104
+ /**
105
+ * Checks whether the node is in a location where the expression is available or not.
106
+ * @returns `true` if the expression is available.
107
+ */
108
+ function isAvailableLocation(ref) {
109
+ for (const guard of maybeGuards.reverse()) {
110
+ if (guard.isAvailableLocation(ref.node)) {
111
+ if (guard.browserEnvironment || guard.reference?.name === ref.name) {
112
+ return true;
113
+ }
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+ /**
119
+ * Checks whether the node is in a top-level location.
120
+ * @returns `true` if the node is in a top-level location.
121
+ */
122
+ function isTopLevelLocation(node) {
123
+ for (const func of functions) {
124
+ if (func.range[0] <= node.range[0] && node.range[1] <= func.range[1]) {
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ }
130
+ /**
131
+ * Iterate over the references of modules that can check the browser environment.
132
+ */
133
+ function* iterateBrowserCheckerModuleReferences() {
134
+ for (const ref of referenceTracker.iterateEsmReferences({
135
+ 'esm-env': {
136
+ [ReferenceTracker.ESM]: true,
137
+ // See https://www.npmjs.com/package/esm-env
138
+ BROWSER: {
139
+ [ReferenceTracker.READ]: true
140
+ }
141
+ },
142
+ '$app/environment': {
143
+ [ReferenceTracker.ESM]: true,
144
+ // See https://svelte.dev/docs/kit/$app-environment#browser
145
+ browser: {
146
+ [ReferenceTracker.READ]: true
147
+ }
148
+ }
149
+ })) {
150
+ if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') {
151
+ yield ref.node;
152
+ }
153
+ else if (ref.node.type === 'ImportSpecifier') {
154
+ const variable = findVariable(context, ref.node.local);
155
+ if (variable) {
156
+ for (const reference of variable.references) {
157
+ if (reference.isRead() && reference.identifier.type === 'Identifier') {
158
+ yield reference.identifier;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Iterate over the used references of global variables.
167
+ */
168
+ function* iterateBrowserGlobalReferences() {
169
+ yield* referenceTracker.iterateGlobalReferences(Object.fromEntries(blowerGlobals.map((name) => [
170
+ name,
171
+ {
172
+ [ReferenceTracker.READ]: true
173
+ }
174
+ ])));
175
+ }
176
+ /**
177
+ * If the node is a reference used in a guard clause that checks if the node is in a browser environment,
178
+ * it returns information about the expression that checks if the browser variable is available.
179
+ * @returns The guard info.
180
+ */
181
+ function getGuardCheckerFromReference(node) {
182
+ const parent = node.parent;
183
+ if (!parent)
184
+ return null;
185
+ if (parent.type === 'BinaryExpression') {
186
+ if (parent.operator === 'instanceof' &&
187
+ parent.left === node &&
188
+ node.type === 'MemberExpression') {
189
+ // e.g. if (globalThis.window instanceof X)
190
+ return getGuardChecker({ node: parent });
191
+ }
192
+ const operand = parent.left === node ? parent.right : parent.right === node ? parent.left : null;
193
+ if (!operand)
194
+ return null;
195
+ const staticValue = getStaticValue(operand, getScope(context, operand));
196
+ if (!staticValue)
197
+ return null;
198
+ if (staticValue.value === undefined && node.type === 'MemberExpression') {
199
+ if (parent.operator === '!==' || parent.operator === '!=') {
200
+ // e.g. if (globalThis.window !== undefined), if (globalThis.window != undefined)
201
+ return getGuardChecker({ node: parent });
202
+ }
203
+ else if (parent.operator === '===' || parent.operator === '==') {
204
+ // e.g. if (globalThis.window === undefined), if (globalThis.window == undefined)
205
+ return getGuardChecker({ node: parent, not: true });
206
+ }
207
+ }
208
+ else if (staticValue.value === null && node.type === 'MemberExpression') {
209
+ if (parent.operator === '!=') {
210
+ // e.g. if (globalThis.window != null)
211
+ return getGuardChecker({ node: parent });
212
+ }
213
+ else if (parent.operator === '==') {
214
+ // e.g. if (globalThis.window == null)
215
+ return getGuardChecker({ node: parent, not: true });
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ if (parent.type === 'UnaryExpression' &&
221
+ parent.operator === 'typeof' &&
222
+ parent.argument === node) {
223
+ const pp = parent.parent;
224
+ if (!pp || pp.type !== 'BinaryExpression') {
225
+ return null;
226
+ }
227
+ const staticValue = getStaticValue(pp.left === parent ? pp.right : pp.left, getScope(context, node));
228
+ if (!staticValue)
229
+ return null;
230
+ if (staticValue.value !== 'undefined' && staticValue.value !== 'object') {
231
+ return null;
232
+ }
233
+ if (pp.operator === '!==' || pp.operator === '!=') {
234
+ if (staticValue.value === 'undefined') {
235
+ // e.g. if (typeof window !== "undefined"), if (typeof window != "undefined")
236
+ return getGuardChecker({ node: pp });
237
+ }
238
+ // e.g. if (typeof window !== "object"), if (typeof window != "object")
239
+ return getGuardChecker({ node: pp, not: true });
240
+ }
241
+ else if (pp.operator === '===' || pp.operator === '==') {
242
+ if (staticValue.value === 'undefined') {
243
+ // e.g. if (typeof window === "undefined"), if (typeof window == "undefined")
244
+ return getGuardChecker({ node: pp, not: true });
245
+ }
246
+ // e.g. if (typeof window === "object"), if (typeof window == "object")
247
+ return getGuardChecker({ node: pp });
248
+ }
249
+ return null;
250
+ }
251
+ if (node.type === 'MemberExpression') {
252
+ if (((parent.type === 'CallExpression' && parent.callee === node) ||
253
+ (parent.type === 'MemberExpression' && parent.object === node)) &&
254
+ parent.optional) {
255
+ // e.g. globalThis.location?.href
256
+ return (n) => n === node;
257
+ }
258
+ // e.g. if (globalThis.window)
259
+ return getGuardChecker({ node });
260
+ }
261
+ return null;
262
+ }
263
+ /**
264
+ * If the node is a guard clause checking,
265
+ * returns a function to check if the node is available.
266
+ */
267
+ function getGuardChecker(guardInfo) {
268
+ const parent = guardInfo.node.parent;
269
+ if (!parent)
270
+ return null;
271
+ if (parent.type === 'ConditionalExpression') {
272
+ const block = guardInfo.not ? parent.alternate : parent.consequent;
273
+ return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1];
274
+ }
275
+ if (parent.type === 'UnaryExpression' && parent.operator === '!') {
276
+ return getGuardChecker({ not: !guardInfo.not, node: parent });
277
+ }
278
+ if (parent.type === 'IfStatement' && parent.test === guardInfo.node) {
279
+ if (!guardInfo.not) {
280
+ const block = parent.consequent;
281
+ return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1];
282
+ }
283
+ if (parent.alternate) {
284
+ const block = parent.alternate;
285
+ return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1];
286
+ }
287
+ if (!hasJumpStatementInAllPath(parent.consequent)) {
288
+ return null;
289
+ }
290
+ const pp = parent.parent;
291
+ if (!pp || (pp.type !== 'BlockStatement' && pp.type !== 'Program')) {
292
+ return null;
293
+ }
294
+ const start = parent.range[1];
295
+ const end = pp.range[1];
296
+ return (n) => start <= n.range[0] && n.range[1] <= end;
297
+ }
298
+ if (!guardInfo.not &&
299
+ parent.type === 'LogicalExpression' &&
300
+ parent.operator === '&&' &&
301
+ parent.left === guardInfo.node) {
302
+ const block = parent.right;
303
+ return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1];
304
+ }
305
+ return null;
306
+ }
307
+ }
308
+ });
309
+ /**
310
+ * Get the list of browser-specific globals.
311
+ */
312
+ function getBrowserGlobals() {
313
+ const nodeGlobals = new Set(Object.keys(globals.node));
314
+ return [
315
+ 'window',
316
+ 'document',
317
+ ...Object.keys(globals.browser).filter((name) => !nodeGlobals.has(name))
318
+ ];
319
+ }
320
+ /**
321
+ * Checks whether all paths of a given statement have jump statements.
322
+ * @param {Statement} statement
323
+ * @returns {boolean}
324
+ */
325
+ function hasJumpStatementInAllPath(statement) {
326
+ if (isJumpStatement(statement)) {
327
+ return true;
328
+ }
329
+ if (statement.type === 'BlockStatement') {
330
+ return statement.body.some(hasJumpStatementInAllPath);
331
+ }
332
+ if (statement.type === 'IfStatement') {
333
+ if (!statement.alternate) {
334
+ return false;
335
+ }
336
+ return (hasJumpStatementInAllPath(statement.alternate) &&
337
+ hasJumpStatementInAllPath(statement.consequent));
338
+ }
339
+ return false;
340
+ }
341
+ /**
342
+ * Checks whether the given statement is a jump statement.
343
+ * @param {Statement} statement
344
+ * @returns {statement is JumpStatement}
345
+ */
346
+ function isJumpStatement(statement) {
347
+ return (statement.type === 'ReturnStatement' ||
348
+ statement.type === 'ContinueStatement' ||
349
+ statement.type === 'BreakStatement');
350
+ }
@@ -395,6 +395,8 @@ export type ASTNodeListener = {
395
395
  'SvelteShorthandAttribute:exit'?: (node: AST.SvelteShorthandAttribute & ASTNodeWithParent) => void;
396
396
  SvelteSpreadAttribute?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void;
397
397
  'SvelteSpreadAttribute:exit'?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void;
398
+ SvelteAttachTag?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void;
399
+ 'SvelteAttachTag:exit'?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void;
398
400
  SvelteDirective?: (node: AST.SvelteDirective & ASTNodeWithParent) => void;
399
401
  'SvelteDirective:exit'?: (node: AST.SvelteDirective & ASTNodeWithParent) => void;
400
402
  SvelteStyleDirective?: (node: AST.SvelteStyleDirective & ASTNodeWithParent) => void;
@@ -781,6 +783,8 @@ export type SvelteNodeListener = {
781
783
  'SvelteShorthandAttribute:exit'?: (node: AST.SvelteShorthandAttribute & ASTNodeWithParent) => void;
782
784
  SvelteSpreadAttribute?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void;
783
785
  'SvelteSpreadAttribute:exit'?: (node: AST.SvelteSpreadAttribute & ASTNodeWithParent) => void;
786
+ SvelteAttachTag?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void;
787
+ 'SvelteAttachTag:exit'?: (node: AST.SvelteAttachTag & ASTNodeWithParent) => void;
784
788
  SvelteDirective?: (node: AST.SvelteDirective & ASTNodeWithParent) => void;
785
789
  'SvelteDirective:exit'?: (node: AST.SvelteDirective & ASTNodeWithParent) => void;
786
790
  SvelteStyleDirective?: (node: AST.SvelteStyleDirective & ASTNodeWithParent) => void;
@@ -89,13 +89,13 @@ export declare function getMustacheTokens(node: SvAST.SvelteDirective | SvAST.Sv
89
89
  closeToken: SvAST.Token;
90
90
  } | null;
91
91
  /** Get attribute key text */
92
- export declare function getAttributeKeyText(node: SvAST.SvelteAttribute | SvAST.SvelteShorthandAttribute | SvAST.SvelteStyleDirective | SvAST.SvelteDirective | SvAST.SvelteSpecialDirective | SvAST.SvelteGenericsDirective, context: RuleContext): string;
92
+ export declare function getAttributeKeyText(node: SvAST.SvelteAttribute | SvAST.SvelteShorthandAttribute | SvAST.SvelteStyleDirective | SvAST.SvelteDirective | SvAST.SvelteSpecialDirective | SvAST.SvelteGenericsDirective | SvAST.SvelteAttachTag, context: RuleContext): string;
93
93
  /** Get directive name */
94
94
  export declare function getDirectiveName(node: SvAST.SvelteDirective): string;
95
95
  /**
96
96
  * Extract all class names used in a HTML element attribute.
97
97
  */
98
- export declare function findClassesInAttribute(attribute: SvAST.SvelteAttribute | SvAST.SvelteShorthandAttribute | SvAST.SvelteSpreadAttribute | SvAST.SvelteDirective | SvAST.SvelteStyleDirective | SvAST.SvelteSpecialDirective | SvAST.SvelteGenericsDirective): string[];
98
+ export declare function findClassesInAttribute(attribute: SvAST.SvelteAttribute | SvAST.SvelteShorthandAttribute | SvAST.SvelteSpreadAttribute | SvAST.SvelteDirective | SvAST.SvelteStyleDirective | SvAST.SvelteSpecialDirective | SvAST.SvelteGenericsDirective | SvAST.SvelteAttachTag): string[];
99
99
  /**
100
100
  * Returns name of SvelteElement
101
101
  */
@@ -331,6 +331,8 @@ export function getAttributeKeyText(node, context) {
331
331
  const dir = getDirectiveName(node);
332
332
  return `${dir}:${getSimpleNameFromNode(node.key.name, context)}${node.key.modifiers.length ? `|${node.key.modifiers.join('|')}` : ''}`;
333
333
  }
334
+ case 'SvelteAttachTag':
335
+ return '@attach';
334
336
  default:
335
337
  throw new Error(`Unknown node type: ${
336
338
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- error
@@ -45,6 +45,7 @@ import noSpacesAroundEqualSignsInAttribute from '../rules/no-spaces-around-equal
45
45
  import noStoreAsync from '../rules/no-store-async.js';
46
46
  import noSvelteInternal from '../rules/no-svelte-internal.js';
47
47
  import noTargetBlank from '../rules/no-target-blank.js';
48
+ import noTopLevelBrowserGlobals from '../rules/no-top-level-browser-globals.js';
48
49
  import noTrailingSpaces from '../rules/no-trailing-spaces.js';
49
50
  import noUnknownStyleDirectiveProperty from '../rules/no-unknown-style-directive-property.js';
50
51
  import noUnnecessaryStateWrap from '../rules/no-unnecessary-state-wrap.js';
@@ -122,6 +123,7 @@ export const rules = [
122
123
  noStoreAsync,
123
124
  noSvelteInternal,
124
125
  noTargetBlank,
126
+ noTopLevelBrowserGlobals,
125
127
  noTrailingSpaces,
126
128
  noUnknownStyleDirectiveProperty,
127
129
  noUnnecessaryStateWrap,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-svelte",
3
- "version": "3.6.0",
3
+ "version": "3.8.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",
@@ -32,15 +32,16 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "@eslint-community/eslint-utils": "^4.4.1",
35
+ "@eslint-community/eslint-utils": "^4.6.1",
36
36
  "@jridgewell/sourcemap-codec": "^1.5.0",
37
37
  "esutils": "^2.0.3",
38
+ "globals": "^16.0.0",
38
39
  "known-css-properties": "^0.36.0",
39
40
  "postcss": "^8.4.49",
40
41
  "postcss-load-config": "^3.1.4",
41
42
  "postcss-safe-parser": "^7.0.0",
42
43
  "semver": "^7.6.3",
43
- "svelte-eslint-parser": "^1.1.1"
44
+ "svelte-eslint-parser": "^1.2.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@babel/core": "^7.26.0",
@@ -71,7 +72,7 @@
71
72
  "sass": "^1.81.0",
72
73
  "source-map-js": "^1.2.1",
73
74
  "stylus": "^0.64.0",
74
- "svelte": "^5.2.9",
75
+ "svelte": "^5.30.1",
75
76
  "svelte-i18n": "^4.0.1",
76
77
  "tsx": "^4.19.2",
77
78
  "type-coverage": "^2.29.7",