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..85d599ba6
--- /dev/null
+++ b/docs/rules/no-top-level-browser-globals.md
@@ -0,0 +1,65 @@
+---
+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
+
+## :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)
+
+## :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)
+- [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/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",
diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md
index bc95d3418..5e27f2038 100644
--- a/packages/eslint-plugin-svelte/CHANGELOG.md
+++ b/packages/eslint-plugin-svelte/CHANGELOG.md
@@ -1,5 +1,29 @@
# 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
+
+- [#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
+
+- [#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
+
+- [#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 f6845a347..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.7.0",
+ "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",
@@ -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/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts
index 2bae69384..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.7.0' as const;
+export const version = '3.9.0' as const;
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/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/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..1a997eca3
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts
@@ -0,0 +1,416 @@
+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[] = [];
+ const typeAnnotations: (TSESTree.TypeNode | TSESTree.TSTypeAnnotation)[] = [];
+
+ function enterFunction(node: TSESTree.FunctionLike) {
+ if (isTopLevelLocation(node)) {
+ functions.push(node);
+ }
+ }
+
+ 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, {
+ 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) || isInTypeAnnotation(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,
+ '*.typeAnnotation': enterTypeAnnotation,
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ 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/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/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/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/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/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}
+
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/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 @@
+
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"
+}
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,