From 9cffd3ba86926793f3240263e38914cdb2180f0a Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Fri, 16 May 2025 09:45:31 +0900 Subject: [PATCH 01/12] feat: add `svelte/no-top-level-browser-globals` rule (#1210) --- .changeset/popular-jokes-tell.md | 5 + README.md | 1 + docs/rules.md | 1 + docs/rules/no-top-level-browser-globals.md | 62 +++ packages/eslint-plugin-svelte/package.json | 3 +- .../eslint-plugin-svelte/src/rule-types.ts | 5 + .../src/rules/no-goto-without-base.ts | 2 +- .../src/rules/no-navigation-without-base.ts | 6 +- .../src/rules/no-top-level-browser-globals.ts | 395 ++++++++++++++++++ .../@eslint-community/eslint-utils.d.ts | 37 +- .../eslint-plugin-svelte/src/utils/rules.ts | 2 + .../invalid/env01-errors.yaml | 8 + .../invalid/env01-input.svelte | 15 + .../invalid/env02-errors.yaml | 8 + .../invalid/env02-input.svelte | 15 + .../invalid/env03-errors.yaml | 4 + .../invalid/env03-input.svelte | 7 + .../invalid/guards01-errors.yaml | 44 ++ .../invalid/guards01-input.svelte | 57 +++ .../invalid/guards02-errors.yaml | 16 + .../invalid/guards02-input.svelte | 22 + .../invalid/guards03-errors.yaml | 60 +++ .../invalid/guards03-input.svelte | 59 +++ .../invalid/guards04-errors.yaml | 12 + .../invalid/guards04-input.svelte | 17 + .../invalid/guards05-errors.yaml | 8 + .../invalid/guards05-input.svelte | 5 + .../invalid/guards06-errors.yaml | 8 + .../invalid/guards06-input.svelte | 18 + .../invalid/guards07-errors.yaml | 16 + .../invalid/guards07-input.svelte | 35 ++ .../invalid/guards08-errors.yaml | 20 + .../invalid/guards08-input.svelte | 8 + .../invalid/test01-errors.yaml | 4 + .../invalid/test01-input.svelte | 4 + .../invalid/test02-errors.yaml | 4 + .../invalid/test02-input.svelte | 3 + .../invalid/test03-errors.yaml | 4 + .../invalid/test03-input.svelte | 4 + .../valid/effect01-input.svelte | 6 + .../valid/env-guards01-input.svelte | 7 + .../valid/env01-input.svelte | 8 + .../valid/env02-input.svelte | 8 + .../valid/env03-input.svelte | 7 + .../valid/guards01-input.svelte | 52 +++ .../valid/guards02-input.svelte | 22 + .../valid/guards03-input.svelte | 52 +++ .../valid/guards04-input.svelte | 17 + .../valid/guards05-input.svelte | 5 + .../valid/guards06-input.svelte | 18 + .../valid/guards07-input.svelte | 26 ++ .../valid/guards08-input.svelte | 8 + .../valid/on-mount01-input.svelte | 8 + .../src/rules/no-top-level-browser-globals.ts | 16 + .../eslint-plugin-svelte/tests/utils/utils.ts | 13 +- 55 files changed, 1251 insertions(+), 26 deletions(-) create mode 100644 .changeset/popular-jokes-tell.md create mode 100644 docs/rules/no-top-level-browser-globals.md create mode 100644 packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts diff --git a/.changeset/popular-jokes-tell.md b/.changeset/popular-jokes-tell.md new file mode 100644 index 000000000..5bea6a68a --- /dev/null +++ b/.changeset/popular-jokes-tell.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +feat: add `svelte/no-top-level-browser-globals` rule diff --git a/README.md b/README.md index 8bc75adca..9cccc33bd 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [svelte/no-reactive-reassign](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-reassign/) | disallow reassigning reactive values | :star: | | [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: | | [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: | +| [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 | | | [svelte/no-unknown-style-directive-property](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: | | [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: | | [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: | diff --git a/docs/rules.md b/docs/rules.md index 1fdd627c8..459eb2ae4 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -28,6 +28,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [svelte/no-reactive-reassign](./rules/no-reactive-reassign.md) | disallow reassigning reactive values | :star: | | [svelte/no-shorthand-style-property-overrides](./rules/no-shorthand-style-property-overrides.md) | disallow shorthand style properties that override related longhand properties | :star: | | [svelte/no-store-async](./rules/no-store-async.md) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: | +| [svelte/no-top-level-browser-globals](./rules/no-top-level-browser-globals.md) | disallow using top-level browser global variables | | | [svelte/no-unknown-style-directive-property](./rules/no-unknown-style-directive-property.md) | disallow unknown `style:property` | :star: | | [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | :bulb: | | [svelte/require-store-reactive-access](./rules/require-store-reactive-access.md) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: | diff --git a/docs/rules/no-top-level-browser-globals.md b/docs/rules/no-top-level-browser-globals.md new file mode 100644 index 000000000..a82fd583f --- /dev/null +++ b/docs/rules/no-top-level-browser-globals.md @@ -0,0 +1,62 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/no-top-level-browser-globals' +description: 'disallow using top-level browser global variables' +--- + +# svelte/no-top-level-browser-globals + +> disallow using top-level browser global variables + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule reports top-level browser global variables in Svelte components. +This rule helps prevent the use of browser global variables that can cause errors in SSR (Server Side Rendering). + + + +```svelte + +``` + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [`$app/environment` documentation > browser](https://svelte.dev/docs/kit/$app-environment#browser) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts) diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index f6845a347..32af6e3b4 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -56,9 +56,10 @@ } }, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.1", + "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", + "globals": "^16.0.0", "known-css-properties": "^0.36.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 8c56fd802..2fd9c7db2 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -251,6 +251,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-target-blank/ */ 'svelte/no-target-blank'?: Linter.RuleEntry + /** + * disallow using top-level browser global variables + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-top-level-browser-globals/ + */ + 'svelte/no-top-level-browser-globals'?: Linter.RuleEntry<[]> /** * disallow trailing whitespace at the end of lines * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/ diff --git a/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts b/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts index 021bdb5b4..78182e0d0 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-goto-without-base.ts @@ -106,7 +106,7 @@ function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.Cal } } }), - ({ node }) => node + ({ node }) => node as TSESTree.CallExpression ); } diff --git a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts index 2bebe6d16..1e19b9b99 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts @@ -171,13 +171,13 @@ function extractFunctionCallReferences(referenceTracker: ReferenceTracker): { return { goto: rawReferences .filter(({ path }) => path[path.length - 1] === 'goto') - .map(({ node }) => node), + .map(({ node }) => node as TSESTree.CallExpression), pushState: rawReferences .filter(({ path }) => path[path.length - 1] === 'pushState') - .map(({ node }) => node), + .map(({ node }) => node as TSESTree.CallExpression), replaceState: rawReferences .filter(({ path }) => path[path.length - 1] === 'replaceState') - .map(({ node }) => node) + .map(({ node }) => node as TSESTree.CallExpression) }; } diff --git a/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts new file mode 100644 index 000000000..a3a94e6bc --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts @@ -0,0 +1,395 @@ +import type { TrackedReferences } from '@eslint-community/eslint-utils'; +import { ReferenceTracker, getStaticValue } from '@eslint-community/eslint-utils'; +import { createRule } from '../utils/index.js'; +import globals from 'globals'; +import type { TSESTree } from '@typescript-eslint/types'; +import { findVariable, getScope } from '../utils/ast-utils.js'; + +export default createRule('no-top-level-browser-globals', { + meta: { + docs: { + description: 'disallow using top-level browser global variables', + category: 'Possible Errors', + recommended: false + }, + schema: [], + messages: { + unexpectedGlobal: 'Unexpected top-level browser global variable "{{name}}".' + }, + type: 'problem', + conditions: [{ svelteFileTypes: ['.svelte', '.svelte.[js|ts]'] }] + }, + create(context) { + const sourceCode = context.sourceCode; + const blowerGlobals = getBrowserGlobals(); + + const referenceTracker = new ReferenceTracker(sourceCode.scopeManager.globalScope!, { + // Specifies the global variables that are allowed to prevent `window.window` from being iterated over. + globalObjectNames: ['globalThis'] + }); + + type MaybeGuard = { + reference?: { node: TSESTree.Node; name: string }; + isAvailableLocation: (node: TSESTree.Node) => boolean; + // The guard that checks whether the browser environment is set to true. + browserEnvironment: boolean; + }; + const maybeGuards: MaybeGuard[] = []; + + const functions: TSESTree.FunctionLike[] = []; + + function enterFunction(node: TSESTree.FunctionLike) { + if (isTopLevelLocation(node)) { + functions.push(node); + } + } + + function enterMetaProperty(node: TSESTree.MetaProperty) { + if (node.meta.name !== 'import' || node.property.name !== 'meta') return; + for (const ref of referenceTracker.iteratePropertyReferences(node, { + env: { + // See https://vite.dev/guide/ssr#conditional-logic + SSR: { + [ReferenceTracker.READ]: true + } + } + })) { + if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') { + const guardChecker = getGuardChecker({ node: ref.node, not: true }); + if (guardChecker) { + maybeGuards.push({ + isAvailableLocation: guardChecker, + browserEnvironment: true + }); + } + } + } + } + + function verifyGlobalReferences() { + // Collects guarded location checkers by checking module references + // that can check the browser environment. + for (const referenceNode of iterateBrowserCheckerModuleReferences()) { + if (!isTopLevelLocation(referenceNode)) continue; + const guardChecker = getGuardChecker({ node: referenceNode }); + if (guardChecker) { + maybeGuards.push({ + isAvailableLocation: guardChecker, + browserEnvironment: true + }); + } + } + + const reportCandidates: TrackedReferences[] = []; + + // Collects references to global variables. + for (const ref of iterateBrowserGlobalReferences()) { + if (!isTopLevelLocation(ref.node)) continue; + const guardChecker = getGuardCheckerFromReference(ref.node); + if (guardChecker) { + const name = ref.path.join('.'); + maybeGuards.push({ + reference: { node: ref.node, name }, + isAvailableLocation: guardChecker, + browserEnvironment: name === 'window' || name === 'document' + }); + } else { + reportCandidates.push(ref); + } + } + + for (const ref of reportCandidates) { + const name = ref.path.join('.'); + if (isAvailableLocation({ node: ref.node, name })) { + continue; + } + context.report({ + node: ref.node, + messageId: 'unexpectedGlobal', + data: { name } + }); + } + } + + return { + ':function': enterFunction, + MetaProperty: enterMetaProperty, + 'Program:exit': verifyGlobalReferences + }; + + /** + * Checks whether the node is in a location where the expression is available or not. + * @returns `true` if the expression is available. + */ + function isAvailableLocation(ref: { node: TSESTree.Node; name: string }) { + for (const guard of maybeGuards.reverse()) { + if (guard.isAvailableLocation(ref.node)) { + if (guard.browserEnvironment || guard.reference?.name === ref.name) { + return true; + } + } + } + return false; + } + + /** + * Checks whether the node is in a top-level location. + * @returns `true` if the node is in a top-level location. + */ + function isTopLevelLocation(node: TSESTree.Node) { + for (const func of functions) { + if (func.range[0] <= node.range[0] && node.range[1] <= func.range[1]) { + return false; + } + } + return true; + } + + /** + * Iterate over the references of modules that can check the browser environment. + */ + function* iterateBrowserCheckerModuleReferences(): Iterable { + for (const ref of referenceTracker.iterateEsmReferences({ + 'esm-env': { + [ReferenceTracker.ESM]: true, + // See https://www.npmjs.com/package/esm-env + BROWSER: { + [ReferenceTracker.READ]: true + } + }, + '$app/environment': { + [ReferenceTracker.ESM]: true, + // See https://svelte.dev/docs/kit/$app-environment#browser + browser: { + [ReferenceTracker.READ]: true + } + } + })) { + if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') { + yield ref.node; + } else if (ref.node.type === 'ImportSpecifier') { + const variable = findVariable(context, ref.node.local); + if (variable) { + for (const reference of variable.references) { + if (reference.isRead() && reference.identifier.type === 'Identifier') { + yield reference.identifier; + } + } + } + } + } + } + + /** + * Iterate over the used references of global variables. + */ + function* iterateBrowserGlobalReferences(): Iterable> { + yield* referenceTracker.iterateGlobalReferences( + Object.fromEntries( + blowerGlobals.map((name) => [ + name, + { + [ReferenceTracker.READ]: true + } + ]) + ) + ); + } + + /** + * If the node is a reference used in a guard clause that checks if the node is in a browser environment, + * it returns information about the expression that checks if the browser variable is available. + * @returns The guard info. + */ + function getGuardCheckerFromReference( + node: TSESTree.Node + ): ((node: TSESTree.Node) => boolean) | null { + const parent = node.parent; + if (!parent) return null; + if (parent.type === 'BinaryExpression') { + if ( + parent.operator === 'instanceof' && + parent.left === node && + node.type === 'MemberExpression' + ) { + // e.g. if (globalThis.window instanceof X) + return getGuardChecker({ node: parent }); + } + const operand = + parent.left === node ? parent.right : parent.right === node ? parent.left : null; + if (!operand) return null; + + const staticValue = getStaticValue(operand, getScope(context, operand)); + if (!staticValue) return null; + + if (staticValue.value === undefined && node.type === 'MemberExpression') { + if (parent.operator === '!==' || parent.operator === '!=') { + // e.g. if (globalThis.window !== undefined), if (globalThis.window != undefined) + return getGuardChecker({ node: parent }); + } else if (parent.operator === '===' || parent.operator === '==') { + // e.g. if (globalThis.window === undefined), if (globalThis.window == undefined) + return getGuardChecker({ node: parent, not: true }); + } + } else if (staticValue.value === null && node.type === 'MemberExpression') { + if (parent.operator === '!=') { + // e.g. if (globalThis.window != null) + return getGuardChecker({ node: parent }); + } else if (parent.operator === '==') { + // e.g. if (globalThis.window == null) + return getGuardChecker({ node: parent, not: true }); + } + } + return null; + } + if ( + parent.type === 'UnaryExpression' && + parent.operator === 'typeof' && + parent.argument === node + ) { + const pp = parent.parent; + if (!pp || pp.type !== 'BinaryExpression') { + return null; + } + const staticValue = getStaticValue( + pp.left === parent ? pp.right : pp.left, + getScope(context, node) + ); + if (!staticValue) return null; + + if (staticValue.value !== 'undefined' && staticValue.value !== 'object') { + return null; + } + if (pp.operator === '!==' || pp.operator === '!=') { + if (staticValue.value === 'undefined') { + // e.g. if (typeof window !== "undefined"), if (typeof window != "undefined") + return getGuardChecker({ node: pp }); + } + // e.g. if (typeof window !== "object"), if (typeof window != "object") + return getGuardChecker({ node: pp, not: true }); + } else if (pp.operator === '===' || pp.operator === '==') { + if (staticValue.value === 'undefined') { + // e.g. if (typeof window === "undefined"), if (typeof window == "undefined") + return getGuardChecker({ node: pp, not: true }); + } + // e.g. if (typeof window === "object"), if (typeof window == "object") + return getGuardChecker({ node: pp }); + } + return null; + } + + if (node.type === 'MemberExpression') { + if ( + ((parent.type === 'CallExpression' && parent.callee === node) || + (parent.type === 'MemberExpression' && parent.object === node)) && + parent.optional + ) { + // e.g. globalThis.location?.href + return (n) => n === node; + } + // e.g. if (globalThis.window) + return getGuardChecker({ node }); + } + return null; + } + + /** + * If the node is a guard clause checking, + * returns a function to check if the node is available. + */ + function getGuardChecker(guardInfo: { + node: TSESTree.Expression; + not?: boolean; + }): ((node: TSESTree.Node) => boolean) | null { + const parent = guardInfo.node.parent; + if (!parent) return null; + + if (parent.type === 'ConditionalExpression') { + const block = guardInfo.not ? parent.alternate : parent.consequent; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + if (parent.type === 'UnaryExpression' && parent.operator === '!') { + return getGuardChecker({ not: !guardInfo.not, node: parent }); + } + if (parent.type === 'IfStatement' && parent.test === guardInfo.node) { + if (!guardInfo.not) { + const block = parent.consequent; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + if (parent.alternate) { + const block = parent.alternate; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + if (!hasJumpStatementInAllPath(parent.consequent)) { + return null; + } + const pp = parent.parent; + if (!pp || (pp.type !== 'BlockStatement' && pp.type !== 'Program')) { + return null; + } + const start = parent.range[1]; + const end = pp.range[1]; + + return (n) => start <= n.range[0] && n.range[1] <= end; + } + if ( + !guardInfo.not && + parent.type === 'LogicalExpression' && + parent.operator === '&&' && + parent.left === guardInfo.node + ) { + const block = parent.right; + return (n) => block.range[0] <= n.range[0] && n.range[1] <= block.range[1]; + } + return null; + } + } +}); + +/** + * Get the list of browser-specific globals. + */ +function getBrowserGlobals() { + const nodeGlobals = new Set(Object.keys(globals.node)); + return [ + 'window', + 'document', + ...Object.keys(globals.browser).filter((name) => !nodeGlobals.has(name)) + ]; +} + +/** + * Checks whether all paths of a given statement have jump statements. + * @param {Statement} statement + * @returns {boolean} + */ +function hasJumpStatementInAllPath(statement: TSESTree.Statement): boolean { + if (isJumpStatement(statement)) { + return true; + } + if (statement.type === 'BlockStatement') { + return statement.body.some(hasJumpStatementInAllPath); + } + if (statement.type === 'IfStatement') { + if (!statement.alternate) { + return false; + } + return ( + hasJumpStatementInAllPath(statement.alternate) && + hasJumpStatementInAllPath(statement.consequent) + ); + } + return false; +} + +/** + * Checks whether the given statement is a jump statement. + * @param {Statement} statement + * @returns {statement is JumpStatement} + */ +function isJumpStatement(statement: TSESTree.Statement) { + return ( + statement.type === 'ReturnStatement' || + statement.type === 'ContinueStatement' || + statement.type === 'BreakStatement' + ); +} diff --git a/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts b/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts index 474538df0..e4e6c3ab4 100644 --- a/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts +++ b/packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts @@ -1,15 +1,18 @@ +import type { + CALL, + CONSTRUCT, + ESM, + READ, + StaticValue, + TraceMap, + TrackedReferences, + ReferenceTrackerOptions +} from '../../../node_modules/@eslint-community/eslint-utils/index.mjs'; +import type { AST } from 'svelte-eslint-parser'; +import type { TSESTree } from '@typescript-eslint/types'; +import type { Scope } from '@typescript-eslint/scope-manager'; + declare module '@eslint-community/eslint-utils' { - import type { AST } from 'svelte-eslint-parser'; - import type { TSESTree } from '@typescript-eslint/types'; - import type { Scope } from '@typescript-eslint/scope-manager'; - import type { - CALL, - CONSTRUCT, - ESM, - READ, - TraceMap - } from '@eslint-community/eslint-utils/referenceTracker'; - export { ReferenceTracker, TrackedReferences } from '../../../node_modules/@types/eslint-utils'; type Token = { type: string; value: string }; export function isArrowToken(token: Token): boolean; export function isCommaToken(token: Token): boolean; @@ -87,5 +90,17 @@ declare module '@eslint-community/eslint-utils' { public iterateGlobalReferences( traceMap: TraceMap ): IterableIterator>; + + /** + * Iterate the property references for a given expression AST node. + */ + public iteratePropertyReferences( + node: TSESTree.Expression, + traceMap: TraceMap + ): IterableIterator>; } + export function getStaticValue( + node: TSESTree.Node, + initialScope?: Scope | null | undefined + ): StaticValue | null; } diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 446c49b1a..bc62cc8c6 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -49,6 +49,7 @@ import noSpacesAroundEqualSignsInAttribute from '../rules/no-spaces-around-equal import noStoreAsync from '../rules/no-store-async.js'; import noSvelteInternal from '../rules/no-svelte-internal.js'; import noTargetBlank from '../rules/no-target-blank.js'; +import noTopLevelBrowserGlobals from '../rules/no-top-level-browser-globals.js'; import noTrailingSpaces from '../rules/no-trailing-spaces.js'; import noUnknownStyleDirectiveProperty from '../rules/no-unknown-style-directive-property.js'; import noUnnecessaryStateWrap from '../rules/no-unnecessary-state-wrap.js'; @@ -127,6 +128,7 @@ export const rules = [ noStoreAsync, noSvelteInternal, noTargetBlank, + noTopLevelBrowserGlobals, noTrailingSpaces, noUnknownStyleDirectiveProperty, noUnnecessaryStateWrap, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml new file mode 100644 index 000000000..1c1b0d997 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 8 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte new file mode 100644 index 000000000..98cf30b68 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env01-input.svelte @@ -0,0 +1,15 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml new file mode 100644 index 000000000..1c1b0d997 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 8 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte new file mode 100644 index 000000000..124d00c83 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env02-input.svelte @@ -0,0 +1,15 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml new file mode 100644 index 000000000..c40513248 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte new file mode 100644 index 000000000..373f21648 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/env03-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml new file mode 100644 index 000000000..d02090e05 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-errors.yaml @@ -0,0 +1,44 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 18 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 23 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 30 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 35 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 38 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 43 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 50 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 55 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte new file mode 100644 index 000000000..7a520431c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards01-input.svelte @@ -0,0 +1,57 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml new file mode 100644 index 000000000..2d829d83c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-errors.yaml @@ -0,0 +1,16 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 18 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte new file mode 100644 index 000000000..f02de6661 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards02-input.svelte @@ -0,0 +1,22 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml new file mode 100644 index 000000000..b6c66c354 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-errors.yaml @@ -0,0 +1,60 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 18 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 25 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 28 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 35 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 38 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 42 + column: 6 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 44 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 46 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 48 + column: 6 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 50 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 52 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 57 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte new file mode 100644 index 000000000..3d39e87c9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards03-input.svelte @@ -0,0 +1,59 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml new file mode 100644 index 000000000..aee75b03c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-errors.yaml @@ -0,0 +1,12 @@ +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 10 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 15 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte new file mode 100644 index 000000000..0512877d0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards04-input.svelte @@ -0,0 +1,17 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml new file mode 100644 index 000000000..b519bfe9b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 31 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 4 + column: 25 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte new file mode 100644 index 000000000..f2ad9a9c8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards05-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml new file mode 100644 index 000000000..6f17d46f4 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-errors.yaml @@ -0,0 +1,8 @@ +- message: Unexpected top-level browser global variable "location". + line: 9 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 16 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte new file mode 100644 index 000000000..8177f7e20 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards06-input.svelte @@ -0,0 +1,18 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml new file mode 100644 index 000000000..0dfb9d5c5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-errors.yaml @@ -0,0 +1,16 @@ +- message: Unexpected top-level browser global variable "location". + line: 13 + column: 15 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 17 + column: 16 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 28 + column: 16 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 33 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte new file mode 100644 index 000000000..0b36ee3a2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards07-input.svelte @@ -0,0 +1,35 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml new file mode 100644 index 000000000..8e2a6880c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-errors.yaml @@ -0,0 +1,20 @@ +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 21 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 3 + column: 49 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 14 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 5 + column: 37 + suggestions: null +- message: Unexpected top-level browser global variable "location". + line: 7 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte new file mode 100644 index 000000000..094bc97d8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/guards08-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml new file mode 100644 index 000000000..63e4b8cff --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "window". + line: 2 + column: 12 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte new file mode 100644 index 000000000..d6b7eee7e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test01-input.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml new file mode 100644 index 000000000..247c0c4a8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "location". + line: 2 + column: 14 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte new file mode 100644 index 000000000..90b80b1bb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test02-input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml new file mode 100644 index 000000000..63d8ba42a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-errors.yaml @@ -0,0 +1,4 @@ +- message: Unexpected top-level browser global variable "localStorage". + line: 2 + column: 12 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte new file mode 100644 index 000000000..895ae2616 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/invalid/test03-input.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte new file mode 100644 index 000000000..cc4ecd464 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/effect01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte new file mode 100644 index 000000000..312b9b4fb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env-guards01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte new file mode 100644 index 000000000..9ec1e8c38 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte new file mode 100644 index 000000000..8bf76db6d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env02-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte new file mode 100644 index 000000000..513c6b096 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/env03-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte new file mode 100644 index 000000000..aaf4ad2c1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards01-input.svelte @@ -0,0 +1,52 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte new file mode 100644 index 000000000..ce58a5882 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards02-input.svelte @@ -0,0 +1,22 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte new file mode 100644 index 000000000..86d47e6eb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards03-input.svelte @@ -0,0 +1,52 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte new file mode 100644 index 000000000..e42820f31 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards04-input.svelte @@ -0,0 +1,17 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte new file mode 100644 index 000000000..6bbaabf75 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards05-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte new file mode 100644 index 000000000..c414702c7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards06-input.svelte @@ -0,0 +1,18 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte new file mode 100644 index 000000000..add9f9744 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards07-input.svelte @@ -0,0 +1,26 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte new file mode 100644 index 000000000..5afe336b0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/guards08-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte new file mode 100644 index 000000000..7065d04e9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/on-mount01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts new file mode 100644 index 000000000..c13228031 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/no-top-level-browser-globals.ts @@ -0,0 +1,16 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/no-top-level-browser-globals.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + } +}); + +tester.run( + 'no-top-level-browser-globals', + rule as any, + loadTestCases('no-top-level-browser-globals') +); diff --git a/packages/eslint-plugin-svelte/tests/utils/utils.ts b/packages/eslint-plugin-svelte/tests/utils/utils.ts index 845ea656f..cfd4d81a9 100644 --- a/packages/eslint-plugin-svelte/tests/utils/utils.ts +++ b/packages/eslint-plugin-svelte/tests/utils/utils.ts @@ -11,18 +11,11 @@ import { Linter } from 'eslint'; import * as svelteParser from 'svelte-eslint-parser'; import * as typescriptParser from '@typescript-eslint/parser'; import Module from 'module'; +import globals from 'globals'; const __dirname = path.dirname(new URL(import.meta.url).pathname); const require = Module.createRequire(import.meta.url); -const globals = { - console: 'readonly', - setTimeout: 'readonly', - setInterval: 'readonly', - queueMicrotask: 'readonly', - window: 'readonly', - globalThis: 'readonly' -}; /** * Prevents leading spaces in a multiline template literal from appearing in the resulting string */ @@ -248,7 +241,7 @@ function writeFixtures( [`svelte/${ruleName}`]: ['error', ...(options || [])] }, languageOptions: { - globals, + globals: globals.browser, ecmaVersion: 2020, sourceType: 'module', ...verifyConfig?.languageOptions, @@ -332,7 +325,7 @@ function getConfig(ruleName: string, inputFile: string) { { ...config, languageOptions: { - globals, + globals: globals.browser, ecmaVersion: 2020, sourceType: 'module', ...config?.languageOptions, From 30ac92ab3e4c7874010bdb472f7fdec0da161dc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 23:42:04 +0000 Subject: [PATCH 02/12] chore(deps): update dependency eslint to ~9.27.0 (#1225) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b4433bda..cd3fdec23 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^8.16.0", "c8": "^10.1.2", "env-cmd": "^10.1.0", - "eslint": "~9.26.0", + "eslint": "~9.27.0", "eslint-config-prettier": "^10.0.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-eslint-plugin": "^6.3.2", From 21cf410db3a284cee066c0a0e4f0037a10c7516f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 12:51:59 +0900 Subject: [PATCH 03/12] chore: release eslint-plugin-svelte (#1224) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/popular-jokes-tell.md | 5 ----- docs/rules/no-top-level-browser-globals.md | 7 +++++-- packages/eslint-plugin-svelte/CHANGELOG.md | 6 ++++++ packages/eslint-plugin-svelte/package.json | 2 +- packages/eslint-plugin-svelte/src/meta.ts | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) delete mode 100644 .changeset/popular-jokes-tell.md diff --git a/.changeset/popular-jokes-tell.md b/.changeset/popular-jokes-tell.md deleted file mode 100644 index 5bea6a68a..000000000 --- a/.changeset/popular-jokes-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"eslint-plugin-svelte": minor ---- - -feat: add `svelte/no-top-level-browser-globals` rule diff --git a/docs/rules/no-top-level-browser-globals.md b/docs/rules/no-top-level-browser-globals.md index a82fd583f..85d599ba6 100644 --- a/docs/rules/no-top-level-browser-globals.md +++ b/docs/rules/no-top-level-browser-globals.md @@ -3,14 +3,13 @@ pageClass: 'rule-details' sidebarDepth: 0 title: 'svelte/no-top-level-browser-globals' description: 'disallow using top-level browser global variables' +since: 'v3.8.0' --- # svelte/no-top-level-browser-globals > disallow using top-level browser global variables -- :exclamation: **_This rule has not been released yet._** - ## :book: Rule Details This rule reports top-level browser global variables in Svelte components. @@ -56,6 +55,10 @@ Nothing. - [`$app/environment` documentation > browser](https://svelte.dev/docs/kit/$app-environment#browser) +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v3.8.0 + ## :mag: Implementation - [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts) diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index bc95d3418..f8f449969 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # eslint-plugin-svelte +## 3.8.0 + +### Minor Changes + +- [#1210](https://github.com/sveltejs/eslint-plugin-svelte/pull/1210) [`9cffd3b`](https://github.com/sveltejs/eslint-plugin-svelte/commit/9cffd3ba86926793f3240263e38914cdb2180f0a) Thanks [@ota-meshi](https://github.com/ota-meshi)! - feat: add `svelte/no-top-level-browser-globals` rule + ## 3.7.0 ### Minor Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index 32af6e3b4..124def222 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.7.0", + "version": "3.8.0", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 2bae69384..4828f4de0 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // This file has been automatically generated, // in order to update its content execute "pnpm run update" export const name = 'eslint-plugin-svelte' as const; -export const version = '3.7.0' as const; +export const version = '3.8.0' as const; From c938185b8a413f200049bc11376db76d768f2ae3 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 17 May 2025 22:56:51 +0900 Subject: [PATCH 04/12] fix(no-top-level-browser-globals): false positives for type annotations (#1227) --- .changeset/happy-weeks-argue.md | 5 ++++ .../src/rules/no-top-level-browser-globals.ts | 23 ++++++++++++++++++- .../valid/ts01-input.svelte | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .changeset/happy-weeks-argue.md create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte diff --git a/.changeset/happy-weeks-argue.md b/.changeset/happy-weeks-argue.md new file mode 100644 index 000000000..8a798dc39 --- /dev/null +++ b/.changeset/happy-weeks-argue.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": patch +--- + +fix(no-top-level-browser-globals): false positives for type annotations diff --git a/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts index a3a94e6bc..1a997eca3 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts @@ -37,6 +37,7 @@ export default createRule('no-top-level-browser-globals', { const maybeGuards: MaybeGuard[] = []; const functions: TSESTree.FunctionLike[] = []; + const typeAnnotations: (TSESTree.TypeNode | TSESTree.TSTypeAnnotation)[] = []; function enterFunction(node: TSESTree.FunctionLike) { if (isTopLevelLocation(node)) { @@ -44,6 +45,12 @@ export default createRule('no-top-level-browser-globals', { } } + function enterTypeAnnotation(node: TSESTree.TypeNode | TSESTree.TSTypeAnnotation) { + if (!isInTypeAnnotation(node)) { + typeAnnotations.push(node); + } + } + function enterMetaProperty(node: TSESTree.MetaProperty) { if (node.meta.name !== 'import' || node.property.name !== 'meta') return; for (const ref of referenceTracker.iteratePropertyReferences(node, { @@ -84,7 +91,7 @@ export default createRule('no-top-level-browser-globals', { // Collects references to global variables. for (const ref of iterateBrowserGlobalReferences()) { - if (!isTopLevelLocation(ref.node)) continue; + if (!isTopLevelLocation(ref.node) || isInTypeAnnotation(ref.node)) continue; const guardChecker = getGuardCheckerFromReference(ref.node); if (guardChecker) { const name = ref.path.join('.'); @@ -113,6 +120,7 @@ export default createRule('no-top-level-browser-globals', { return { ':function': enterFunction, + '*.typeAnnotation': enterTypeAnnotation, MetaProperty: enterMetaProperty, 'Program:exit': verifyGlobalReferences }; @@ -145,6 +153,19 @@ export default createRule('no-top-level-browser-globals', { return true; } + /** + * Checks whether the node is in type annotation. + * @returns `true` if the node is in type annotation. + */ + function isInTypeAnnotation(node: TSESTree.Node) { + for (const typeAnnotation of typeAnnotations) { + if (typeAnnotation.range[0] <= node.range[0] && node.range[1] <= typeAnnotation.range[1]) { + return true; + } + } + return false; + } + /** * Iterate over the references of modules that can check the browser environment. */ diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte new file mode 100644 index 000000000..93b8f55b9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-top-level-browser-globals/valid/ts01-input.svelte @@ -0,0 +1,3 @@ + From 04ca0d14cbec79a84987c0cee48c62cf4ff98a8f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 23:03:59 +0900 Subject: [PATCH 05/12] chore: release eslint-plugin-svelte (#1228) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/happy-weeks-argue.md | 5 ----- packages/eslint-plugin-svelte/CHANGELOG.md | 6 ++++++ packages/eslint-plugin-svelte/package.json | 2 +- packages/eslint-plugin-svelte/src/meta.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/happy-weeks-argue.md diff --git a/.changeset/happy-weeks-argue.md b/.changeset/happy-weeks-argue.md deleted file mode 100644 index 8a798dc39..000000000 --- a/.changeset/happy-weeks-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"eslint-plugin-svelte": patch ---- - -fix(no-top-level-browser-globals): false positives for type annotations diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index f8f449969..bfb66a08d 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # eslint-plugin-svelte +## 3.8.1 + +### Patch Changes + +- [#1227](https://github.com/sveltejs/eslint-plugin-svelte/pull/1227) [`c938185`](https://github.com/sveltejs/eslint-plugin-svelte/commit/c938185b8a413f200049bc11376db76d768f2ae3) Thanks [@ota-meshi](https://github.com/ota-meshi)! - fix(no-top-level-browser-globals): false positives for type annotations + ## 3.8.0 ### Minor Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index 124def222..a1664721a 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.8.0", + "version": "3.8.1", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 4828f4de0..a2ecf1aa4 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // This file has been automatically generated, // in order to update its content execute "pnpm run update" export const name = 'eslint-plugin-svelte' as const; -export const version = '3.8.0' as const; +export const version = '3.8.1' as const; From 0681f901196cf81a87169155f8f632bf12666908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 20 May 2025 04:14:40 +0200 Subject: [PATCH 06/12] fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} (#1231) --- .changeset/fast-birds-crash.md | 5 ++ .../src/rules/consistent-selector-style.ts | 36 +++++++++--- .../src/utils/element-occurences.ts | 55 +++++++++++++++++++ .../valid/id-class-type/class01-input.svelte | 16 ++++++ .../id-class-type/svelte-5/_requirements.json | 3 + .../svelte-5/class01-input.svelte | 11 ++++ .../class-dynamic-suffix01-input.svelte | 12 ++++ 7 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 .changeset/fast-birds-crash.md create mode 100644 packages/eslint-plugin-svelte/src/utils/element-occurences.ts create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte diff --git a/.changeset/fast-birds-crash.md b/.changeset/fast-birds-crash.md new file mode 100644 index 000000000..0e61e514c --- /dev/null +++ b/.changeset/fast-birds-crash.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': patch +--- + +fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} diff --git a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts index 1f64ee0da..0864dc6a9 100644 --- a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts +++ b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts @@ -12,6 +12,7 @@ import { extractExpressionSuffixLiteral } from '../utils/expression-affixes.js'; import { createRule } from '../utils/index.js'; +import { ElementOccurenceCount, elementOccurrenceCount } from '../utils/element-occurences.js'; interface Selections { exact: Map; @@ -143,7 +144,7 @@ export default createRule('consistent-selector-style', { if (styleValue === 'class') { return; } - if (styleValue === 'id' && canUseIdSelector(selection)) { + if (styleValue === 'id' && canUseIdSelector(selection.map(([elem]) => elem))) { context.report({ messageId: 'classShouldBeId', loc: styleSelectorNodeLoc(node) as AST.SourceLocation @@ -285,11 +286,13 @@ function addToArrayMap( /** * Finds all nodes in selections that could be matched by key */ -function matchSelection(selections: Selections, key: string): AST.SvelteHTMLElement[] { - const selection = selections.exact.get(key) ?? []; +function matchSelection(selections: Selections, key: string): [AST.SvelteHTMLElement, boolean][] { + const selection = (selections.exact.get(key) ?? []).map<[AST.SvelteHTMLElement, boolean]>( + (elem) => [elem, true] + ); selections.affixes.forEach((nodes, [prefix, suffix]) => { if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) { - selection.push(...nodes); + selection.push(...nodes.map<[AST.SvelteHTMLElement, boolean]>((elem) => [elem, false])); } }); return selection; @@ -299,26 +302,43 @@ function matchSelection(selections: Selections, key: string): AST.SvelteHTMLElem * Checks whether a given selection could be obtained using an ID selector */ function canUseIdSelector(selection: AST.SvelteHTMLElement[]): boolean { - return selection.length <= 1; + return ( + selection.length === 0 || + (selection.length === 1 && + elementOccurrenceCount(selection[0]) !== ElementOccurenceCount.ZeroToInf) + ); } /** * Checks whether a given selection could be obtained using a type selector */ function canUseTypeSelector( - selection: AST.SvelteHTMLElement[], + selection: [AST.SvelteHTMLElement, boolean][], typeSelections: Map ): boolean { - const types = new Set(selection.map((node) => node.name.name)); + const types = new Set(selection.map(([node]) => node.name.name)); if (types.size > 1) { return false; } if (types.size < 1) { return true; } + if ( + selection.some( + ([elem, exact]) => !exact && elementOccurrenceCount(elem) === ElementOccurenceCount.ZeroToInf + ) + ) { + return false; + } const type = [...types][0]; const typeSelection = typeSelections.get(type); - return typeSelection !== undefined && arrayEquals(typeSelection, selection); + return ( + typeSelection !== undefined && + arrayEquals( + typeSelection, + selection.map(([elem]) => elem) + ) + ); } /** diff --git a/packages/eslint-plugin-svelte/src/utils/element-occurences.ts b/packages/eslint-plugin-svelte/src/utils/element-occurences.ts new file mode 100644 index 000000000..6bc30fa06 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/utils/element-occurences.ts @@ -0,0 +1,55 @@ +import type { AST } from 'svelte-eslint-parser'; + +export enum ElementOccurenceCount { + ZeroOrOne, + One, + ZeroToInf +} + +function multiplyCounts( + left: ElementOccurenceCount, + right: ElementOccurenceCount +): ElementOccurenceCount { + if (left === ElementOccurenceCount.One) { + return right; + } + if (right === ElementOccurenceCount.One) { + return left; + } + if (left === right) { + return left; + } + return ElementOccurenceCount.ZeroToInf; +} + +function childElementOccurenceCount(parent: AST.SvelteHTMLNode | null): ElementOccurenceCount { + if (parent === null) { + return ElementOccurenceCount.One; + } + if ( + [ + 'SvelteIfBlock', + 'SvelteElseBlock', + 'SvelteAwaitBlock', + 'SvelteAwaitPendingBlock', + 'SvelteAwaitThenBlock', + 'SvelteAwaitCatchBlock' + ].includes(parent.type) + ) { + return ElementOccurenceCount.ZeroOrOne; + } + if ( + ['SvelteEachBlock', 'SvelteSnippetBlock'].includes(parent.type) || + (parent.type === 'SvelteElement' && parent.kind === 'component') + ) { + return ElementOccurenceCount.ZeroToInf; + } + return ElementOccurenceCount.One; +} + +export function elementOccurrenceCount(element: AST.SvelteHTMLNode): ElementOccurenceCount { + const parentCount = + element.parent !== null ? elementOccurrenceCount(element.parent) : ElementOccurenceCount.One; + const parentChildCount = childElementOccurenceCount(element.parent); + return multiplyCounts(parentCount, parentChildCount); +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte index b849abc73..8109e77f1 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte @@ -10,6 +10,14 @@ Text 3 +{#each ["one", "two"] as iter} + {iter} +{/each} + + + Text 5 + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte new file mode 100644 index 000000000..a44da9098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte @@ -0,0 +1,11 @@ +Outside + +{#snippet iterated()} + Text 4 +{/snippet} + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte index 5d6b127a3..2dd97dc31 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte @@ -19,6 +19,10 @@ Click me four! +{#each ["one", "two"] as count} + Bold in each +{/each} + From 15742cb594029d6280ecfcf3cf537239b26569d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 16:18:24 +0900 Subject: [PATCH 07/12] chore: release eslint-plugin-svelte (#1232) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-birds-crash.md | 5 ----- packages/eslint-plugin-svelte/CHANGELOG.md | 6 ++++++ packages/eslint-plugin-svelte/package.json | 2 +- packages/eslint-plugin-svelte/src/meta.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fast-birds-crash.md diff --git a/.changeset/fast-birds-crash.md b/.changeset/fast-birds-crash.md deleted file mode 100644 index 0e61e514c..000000000 --- a/.changeset/fast-birds-crash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'eslint-plugin-svelte': patch ---- - -fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index bfb66a08d..3c911aac5 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # eslint-plugin-svelte +## 3.8.2 + +### Patch Changes + +- [#1231](https://github.com/sveltejs/eslint-plugin-svelte/pull/1231) [`0681f90`](https://github.com/sveltejs/eslint-plugin-svelte/commit/0681f901196cf81a87169155f8f632bf12666908) Thanks [@marekdedic](https://github.com/marekdedic)! - fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} + ## 3.8.1 ### Patch Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index a1664721a..520bd8468 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.8.1", + "version": "3.8.2", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index a2ecf1aa4..9984d3ca6 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // This file has been automatically generated, // in order to update its content execute "pnpm run update" export const name = 'eslint-plugin-svelte' as const; -export const version = '3.8.1' as const; +export const version = '3.8.2' as const; From 6e86e30cd766181dce5849ae739eedd2adfd8d8e Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 21 May 2025 10:25:56 +0100 Subject: [PATCH 08/12] feat: support comma-separated ignore comments (#1235) --- .changeset/quick-cloths-double.md | 5 ++ .../svelte-compile-warns/ignore-comment.ts | 88 +++++++++++-------- ...svelte-ignore-comma-separated-input.svelte | 5 ++ ...e-ignore-comma-separated-requirements.json | 3 + 4 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 .changeset/quick-cloths-double.md create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json diff --git a/.changeset/quick-cloths-double.md b/.changeset/quick-cloths-double.md new file mode 100644 index 000000000..bcbde127c --- /dev/null +++ b/.changeset/quick-cloths-double.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +Improve performance of ignore comment extraction and add support for comma-separated ignore codes diff --git a/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts b/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts index 2655c789e..77cf5a0c4 100644 --- a/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts +++ b/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts @@ -1,7 +1,7 @@ import type { AST } from 'svelte-eslint-parser'; import type { RuleContext } from '../../types.js'; -const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore/m; +const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore\s+/; /** * Map of legacy code -> new code @@ -37,29 +37,45 @@ export function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | Ignore const ignoreComments: (IgnoreItem | IgnoreItemWithoutCode)[] = []; for (const comment of sourceCode.getAllComments()) { - const ignores = extractSvelteIgnore(comment.value, comment.range[0] + 2, comment); - if (ignores) { - ignoreComments.push(...ignores); - } else if (hasMissingCodeIgnore(comment.value)) { + const match = SVELTE_IGNORE_PATTERN.exec(comment.value); + if (!match) { + continue; + } + const codeListStart = match.index + match[0].length; + const codeList = comment.value.slice(codeListStart); + if (hasMissingCodeIgnore(codeList)) { ignoreComments.push({ range: comment.range, code: null, token: comment }); + } else { + const ignores = extractSvelteIgnore(comment.range[0] + 2, comment, codeList, codeListStart); + if (ignores) { + ignoreComments.push(...ignores); + } } } for (const token of sourceCode.ast.tokens) { if (token.type === 'HTMLComment') { const text = token.value.slice(4, -3); - const ignores = extractSvelteIgnore(text, token.range[0] + 4, token); - if (ignores) { - ignoreComments.push(...ignores); - } else if (hasMissingCodeIgnore(text)) { + const match = SVELTE_IGNORE_PATTERN.exec(text); + if (!match) { + continue; + } + const codeListStart = match.index + match[0].length; + const codeList = text.slice(codeListStart); + if (hasMissingCodeIgnore(codeList)) { ignoreComments.push({ range: token.range, code: null, token }); + } else { + const ignores = extractSvelteIgnore(token.range[0] + 4, token, codeList, codeListStart); + if (ignores) { + ignoreComments.push(...ignores); + } } } } @@ -69,46 +85,42 @@ export function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | Ignore /** Extract svelte-ignore rule names */ function extractSvelteIgnore( - text: string, startIndex: number, - token: AST.Token | AST.Comment + token: AST.Token | AST.Comment, + codeList: string, + ignoreStart: number ): IgnoreItem[] | null { - const m1 = SVELTE_IGNORE_PATTERN.exec(text); - if (!m1) { - return null; - } - const ignoreStart = m1.index + m1[0].length; - const beforeText = text.slice(ignoreStart); - if (!/^\s/.test(beforeText) || !beforeText.trim()) { - return null; - } - let start = startIndex + ignoreStart; - + const start = startIndex + ignoreStart; const results: IgnoreItem[] = []; - for (const code of beforeText.split(/\s/)) { - const end = start + code.length; - const trimmed = code.trim(); - if (trimmed) { + const separatorPattern = /\s*[\s,]\s*/g; + const separators = codeList.matchAll(separatorPattern); + let lastSeparatorEnd = 0; + for (const separator of separators) { + const code = codeList.slice(lastSeparatorEnd, separator.index); + if (code) { results.push({ - code: trimmed, - codeForV5: V5_REPLACEMENTS[trimmed] || trimmed.replace(/-/gu, '_'), - range: [start, end], + code, + codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'), + range: [start + lastSeparatorEnd, start + separator.index], token }); } - start = end + 1; /* space */ + lastSeparatorEnd = separator.index + separator[0].length; + } + if (results.length === 0) { + const code = codeList; + results.push({ + code, + codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'), + range: [start, start + code.length], + token + }); } return results; } /** Checks whether given comment has missing code svelte-ignore */ -function hasMissingCodeIgnore(text: string) { - const m1 = SVELTE_IGNORE_PATTERN.exec(text); - if (!m1) { - return false; - } - const ignoreStart = m1.index + m1[0].length; - const beforeText = text.slice(ignoreStart); - return !beforeText.trim(); +function hasMissingCodeIgnore(codeList: string) { + return !codeList.trim(); } diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte new file mode 100644 index 000000000..7e2559f55 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte @@ -0,0 +1,5 @@ + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} From a0f5c172131200be0b9689d0001a73e88ef822eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:33:57 +0900 Subject: [PATCH 09/12] chore: release eslint-plugin-svelte (#1236) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/quick-cloths-double.md | 5 ----- packages/eslint-plugin-svelte/CHANGELOG.md | 6 ++++++ packages/eslint-plugin-svelte/package.json | 2 +- packages/eslint-plugin-svelte/src/meta.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/quick-cloths-double.md diff --git a/.changeset/quick-cloths-double.md b/.changeset/quick-cloths-double.md deleted file mode 100644 index bcbde127c..000000000 --- a/.changeset/quick-cloths-double.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'eslint-plugin-svelte': minor ---- - -Improve performance of ignore comment extraction and add support for comma-separated ignore codes diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index 3c911aac5..5e27f2038 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # eslint-plugin-svelte +## 3.9.0 + +### Minor Changes + +- [#1235](https://github.com/sveltejs/eslint-plugin-svelte/pull/1235) [`6e86e30`](https://github.com/sveltejs/eslint-plugin-svelte/commit/6e86e30cd766181dce5849ae739eedd2adfd8d8e) Thanks [@43081j](https://github.com/43081j)! - Improve performance of ignore comment extraction and add support for comma-separated ignore codes + ## 3.8.2 ### Patch Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index 520bd8468..a65f5e31d 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.8.2", + "version": "3.9.0", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 9984d3ca6..a4c93e9c9 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // This file has been automatically generated, // in order to update its content execute "pnpm run update" export const name = 'eslint-plugin-svelte' as const; -export const version = '3.8.2' as const; +export const version = '3.9.0' as const; From d8e0724019053a99e4a5b393221ae6d8d8cdc66b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 02:14:52 +0000 Subject: [PATCH 10/12] chore(deps): update dependency eslint to ~9.28.0 (#1237) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd3fdec23..36bd2fad9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^8.16.0", "c8": "^10.1.2", "env-cmd": "^10.1.0", - "eslint": "~9.27.0", + "eslint": "~9.28.0", "eslint-config-prettier": "^10.0.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-eslint-plugin": "^6.3.2", From a3d42245fbb6a6663a1b3c6a4e211dce2b6dfbbb Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Mon, 2 Jun 2025 08:10:35 +0900 Subject: [PATCH 11/12] fix(prefer-const): Use `additionalProperties` instead of `ignoreReadonly` to match the ESLint core rule option name (#1239) --- .changeset/good-carrots-notice.md | 5 +++++ docs/rules/prefer-const.md | 10 +++++----- packages/eslint-plugin-svelte/src/rule-types.ts | 1 + .../eslint-plugin-svelte/src/rules/prefer-const.ts | 3 ++- .../rules/prefer-const/valid/1238/_config.json | 8 ++++++++ .../rules/prefer-const/valid/1238/input.svelte | 1 + 6 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 .changeset/good-carrots-notice.md create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte diff --git a/.changeset/good-carrots-notice.md b/.changeset/good-carrots-notice.md new file mode 100644 index 000000000..6a7f03087 --- /dev/null +++ b/.changeset/good-carrots-notice.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': patch +--- + +fix(prefer-const): Use `additionalProperties` instead of `ignoreReadonly` to match the ESLint core rule option name. diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md index 8029ea881..f158d47b6 100644 --- a/docs/rules/prefer-const.md +++ b/docs/rules/prefer-const.md @@ -46,7 +46,7 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i "error", { "destructuring": "any", - "ignoreReadonly": true, + "additionalProperties": false, "excludedRunes": ["$props", "$derived"] } ] @@ -54,14 +54,14 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i ``` - `destructuring`: The kind of the way to address variables in destructuring. There are 2 values: - - `any` (default): if any variables in destructuring should be const, this rule warns for those variables. - - `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them. -- `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment. + - `any` (default) - If any variables in destructuring should be const, this rule warns for those variables. + - `all`: If all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them. +- `ignoreReadBeforeAssign`: This is an option to avoid conflicting with `no-use-before-define` rule (without "nofunc" option). If `true` is specified, this rule will ignore variables that are read between the declaration and the first assignment. Default is `false`. - `excludedRunes`: An array of rune names that should be ignored. Even if a rune is declared with `let`, it will still be ignored. ## :books: Further Reading -- See [ESLint `prefer-const` rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule. +- See [ESLint prefer-const rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule. ## :rocket: Version diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 2fd9c7db2..81657270d 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -573,6 +573,7 @@ type SveltePreferConst = []|[{ destructuring?: ("any" | "all") ignoreReadBeforeAssign?: boolean excludedRunes?: string[] + [k: string]: unknown | undefined }] // ----- svelte/require-event-prefix ----- type SvelteRequireEventPrefix = []|[{ diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts index 842bbc924..e28765baa 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts @@ -68,7 +68,8 @@ export default createRule('prefer-const', { } } }, - additionalProperties: false + // Allow ESLint core rule properties in case new options are added in the future. + additionalProperties: true } ] }, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json new file mode 100644 index 000000000..88200a87e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/_config.json @@ -0,0 +1,8 @@ +{ + "options": [ + { + "destructuring": "all", + "additionalProperties": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte new file mode 100644 index 000000000..190a18037 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/1238/input.svelte @@ -0,0 +1 @@ +123 From 6320a20efb0dc8eed8d97b3538a53d96b9257f68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 08:12:54 +0900 Subject: [PATCH 12/12] chore: release eslint-plugin-svelte (#1240) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/good-carrots-notice.md | 5 ----- packages/eslint-plugin-svelte/CHANGELOG.md | 6 ++++++ packages/eslint-plugin-svelte/package.json | 2 +- packages/eslint-plugin-svelte/src/meta.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/good-carrots-notice.md diff --git a/.changeset/good-carrots-notice.md b/.changeset/good-carrots-notice.md deleted file mode 100644 index 6a7f03087..000000000 --- a/.changeset/good-carrots-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'eslint-plugin-svelte': patch ---- - -fix(prefer-const): Use `additionalProperties` instead of `ignoreReadonly` to match the ESLint core rule option name. diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index 5e27f2038..0b0f3dee0 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # eslint-plugin-svelte +## 3.9.1 + +### Patch Changes + +- [#1239](https://github.com/sveltejs/eslint-plugin-svelte/pull/1239) [`a3d4224`](https://github.com/sveltejs/eslint-plugin-svelte/commit/a3d42245fbb6a6663a1b3c6a4e211dce2b6dfbbb) Thanks [@baseballyama](https://github.com/baseballyama)! - fix(prefer-const): Use `additionalProperties` instead of `ignoreReadonly` to match the ESLint core rule option name. + ## 3.9.0 ### Minor Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index a65f5e31d..ffce141a8 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.9.0", + "version": "3.9.1", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index a4c93e9c9..1ea9551ba 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // This file has been automatically generated, // in order to update its content execute "pnpm run update" export const name = 'eslint-plugin-svelte' as const; -export const version = '3.9.0' as const; +export const version = '3.9.1' as const;