From 7d92cd55aa36b9d94ca0b73784eac03f038c0b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Fri, 1 Nov 2024 14:52:37 +0100 Subject: [PATCH 01/11] chore: duplicated no-goto-without-base into no-navigation-without-base --- README.md | 1 + docs/rules.md | 7 +- docs/rules/no-navigation-without-base.md | 60 ++++++++ .../eslint-plugin-svelte/src/rule-types.ts | 5 + .../src/rules/no-navigation-without-base.ts | 134 ++++++++++++++++++ .../eslint-plugin-svelte/src/utils/rules.ts | 2 + .../invalid/aliased-goto01-errors.yaml | 4 + .../invalid/aliased-goto01-input.svelte | 5 + .../invalid/base-not-prefixed01-errors.yaml | 8 ++ .../invalid/base-not-prefixed01-input.svelte | 7 + .../invalid/no-base01-errors.yaml | 4 + .../invalid/no-base01-input.svelte | 5 + .../valid/absolute-uri01-input.svelte | 6 + .../valid/base-aliased01-input.svelte | 8 ++ .../valid/base-prefixed01-input.svelte | 8 ++ .../src/rules/no-navigation-without-base.ts | 12 ++ 16 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 docs/rules/no-navigation-without-base.md create mode 100644 packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-aliased01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-prefixed01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/src/rules/no-navigation-without-base.ts diff --git a/README.md b/README.md index a959eda16..166567a37 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,7 @@ These rules relate to SvelteKit and its best Practices. | Rule ID | Description | | |:--------|:------------|:---| | [svelte/no-goto-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-goto-without-base/) | disallow using goto() without the base path | | +| [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | disallow using goto() without the base path | | ## Experimental diff --git a/docs/rules.md b/docs/rules.md index 29701ff7e..8379af92c 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -109,9 +109,10 @@ These rules extend the rules provided by ESLint itself, or other plugins to work These rules relate to SvelteKit and its best Practices. -| Rule ID | Description | | -| :------------------------------------------------------------- | :------------------------------------------ | :-- | -| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | disallow using goto() without the base path | | +| Rule ID | Description | | +| :------------------------------------------------------------------------- | :------------------------------------------ | :-- | +| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | disallow using goto() without the base path | | +| [svelte/no-navigation-without-base](./rules/no-navigation-without-base.md) | disallow using goto() without the base path | | ## Experimental diff --git a/docs/rules/no-navigation-without-base.md b/docs/rules/no-navigation-without-base.md new file mode 100644 index 000000000..bb8534597 --- /dev/null +++ b/docs/rules/no-navigation-without-base.md @@ -0,0 +1,60 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/no-navigation-without-base' +description: 'disallow using goto() without the base path' +since: 'v2.36.0-next.9' +--- + +# svelte/no-navigation-without-base + +> disallow using goto() without the base path + +## :book: Rule Details + +This rule reports navigation using SvelteKit's `goto()` function without prefixing a relative URL with the base path. If a non-prefixed relative URL is used for navigation, the `goto` function navigates away from the base path, which is usually not what you wanted to do (for external URLs, `window.location = url` should be used instead). + + + +```svelte + +``` + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [`goto()` documentation](https://kit.svelte.dev/docs/modules#$app-navigation-goto) +- [`base` documentation](https://kit.svelte.dev/docs/modules#$app-paths-base) + +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v2.36.0-next.9 + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-navigation-without-base.ts) diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 8b7cbe80c..c77aba124 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -179,6 +179,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inspect/ */ 'svelte/no-inspect'?: Linter.RuleEntry<[]> + /** + * disallow using goto() without the base path + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/ + */ + 'svelte/no-navigation-without-base'?: Linter.RuleEntry<[]> /** * disallow use of not function in event handler * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-not-function-handler/ 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 new file mode 100644 index 000000000..aae8f1275 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts @@ -0,0 +1,134 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { createRule } from '../utils/index.js'; +import { ReferenceTracker } from '@eslint-community/eslint-utils'; +import { getSourceCode } from '../utils/compat.js'; +import { findVariable } from '../utils/ast-utils.js'; +import type { RuleContext } from '../types.js'; + +export default createRule('no-navigation-without-base', { + meta: { + docs: { + description: 'disallow using goto() without the base path', + category: 'SvelteKit', + recommended: false + }, + schema: [], + messages: { + isNotPrefixedWithBasePath: + "Found a goto() call with a url that isn't prefixed with the base path." + }, + type: 'suggestion' + }, + create(context) { + return { + Program() { + const referenceTracker = new ReferenceTracker( + getSourceCode(context).scopeManager.globalScope! + ); + const basePathNames = extractBasePathReferences(referenceTracker, context); + for (const gotoCall of extractGotoReferences(referenceTracker)) { + if (gotoCall.arguments.length < 1) { + continue; + } + const path = gotoCall.arguments[0]; + switch (path.type) { + case 'BinaryExpression': + checkBinaryExpression(context, path, basePathNames); + break; + case 'Literal': + checkLiteral(context, path); + break; + case 'TemplateLiteral': + checkTemplateLiteral(context, path, basePathNames); + break; + default: + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + } + } + } + }; + } +}); + +function checkBinaryExpression( + context: RuleContext, + path: TSESTree.BinaryExpression, + basePathNames: Set +): void { + if (path.left.type !== 'Identifier' || !basePathNames.has(path.left)) { + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + } +} + +function checkTemplateLiteral( + context: RuleContext, + path: TSESTree.TemplateLiteral, + basePathNames: Set +): void { + const startingIdentifier = extractStartingIdentifier(path); + if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) { + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + } +} + +function checkLiteral(context: RuleContext, path: TSESTree.Literal): void { + const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; + if (!absolutePathRegex.test(path.value?.toString() ?? '')) { + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + } +} + +function extractStartingIdentifier( + templateLiteral: TSESTree.TemplateLiteral +): TSESTree.Identifier | undefined { + const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts) { + if (part.type === 'TemplateElement' && part.value.raw === '') { + // Skip empty quasi in the begining + continue; + } + if (part.type === 'Identifier') { + return part; + } + return undefined; + } + return undefined; +} + +function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.CallExpression[] { + return Array.from( + referenceTracker.iterateEsmReferences({ + '$app/navigation': { + [ReferenceTracker.ESM]: true, + goto: { + [ReferenceTracker.CALL]: true + } + } + }), + ({ node }) => node + ); +} + +function extractBasePathReferences( + referenceTracker: ReferenceTracker, + context: RuleContext +): Set { + const set = new Set(); + for (const { node } of referenceTracker.iterateEsmReferences({ + '$app/paths': { + [ReferenceTracker.ESM]: true, + base: { + [ReferenceTracker.READ]: true + } + } + })) { + const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local); + if (!variable) continue; + for (const reference of variable.references) { + if (reference.identifier.type === 'Identifier') set.add(reference.identifier); + } + } + return set; +} diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 296a7700d..284ad2373 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -35,6 +35,7 @@ import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statem import noInlineStyles from '../rules/no-inline-styles.js'; import noInnerDeclarations from '../rules/no-inner-declarations.js'; import noInspect from '../rules/no-inspect.js'; +import noNavigationWithoutBase from '../rules/no-navigation-without-base.js'; import noNotFunctionHandler from '../rules/no-not-function-handler.js'; import noObjectInTextMustaches from '../rules/no-object-in-text-mustaches.js'; import noReactiveFunctions from '../rules/no-reactive-functions.js'; @@ -103,6 +104,7 @@ export const rules = [ noInlineStyles, noInnerDeclarations, noInspect, + noNavigationWithoutBase, noNotFunctionHandler, noObjectInTextMustaches, noReactiveFunctions, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-errors.yaml new file mode 100644 index 000000000..cc35de50a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 4 + column: 8 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-input.svelte new file mode 100644 index 000000000..00f19edec --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-errors.yaml new file mode 100644 index 000000000..f0a24ef44 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-errors.yaml @@ -0,0 +1,8 @@ +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 5 + column: 7 + suggestions: null +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 6 + column: 7 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-input.svelte new file mode 100644 index 000000000..be68b2e4a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-errors.yaml new file mode 100644 index 000000000..658fcb47d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 4 + column: 7 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-input.svelte new file mode 100644 index 000000000..6f011fe2d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte new file mode 100644 index 000000000..87ee8abf3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-aliased01-input.svelte new file mode 100644 index 000000000..b11e55f92 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-aliased01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-prefixed01-input.svelte new file mode 100644 index 000000000..cd7177deb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-prefixed01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-navigation-without-base.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-navigation-without-base.ts new file mode 100644 index 000000000..6b00e4d6a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/no-navigation-without-base.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat'; +import rule from '../../../src/rules/no-navigation-without-base'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('no-navigation-without-base', rule as any, loadTestCases('no-navigation-without-base')); From 394acc5e386ed6b8b278a9f4f002ded10cc3c103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Fri, 1 Nov 2024 15:08:53 +0100 Subject: [PATCH 02/11] chore: deprecated the no-goto-without-base rule --- README.md | 2 +- docs/rules.md | 8 ++++---- docs/rules/no-goto-without-base.md | 2 ++ packages/eslint-plugin-svelte/src/rule-types.ts | 1 + .../src/rules/no-goto-without-base.ts | 2 ++ 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 166567a37..bbef759a8 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,6 @@ These rules relate to SvelteKit and its best Practices. | Rule ID | Description | | |:--------|:------------|:---| -| [svelte/no-goto-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-goto-without-base/) | disallow using goto() without the base path | | | [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | disallow using goto() without the base path | | ## Experimental @@ -438,6 +437,7 @@ These rules relate to this plugin works: | Rule ID | Replaced by | |:--------|:------------| | [svelte/@typescript-eslint/no-unnecessary-condition](https://sveltejs.github.io/eslint-plugin-svelte/rules/@typescript-eslint/no-unnecessary-condition/) | This rule is no longer needed when using svelte-eslint-parser>=v0.19.0. | +| [svelte/no-goto-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-goto-without-base/) | [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | diff --git a/docs/rules.md b/docs/rules.md index 8379af92c..0329a4aa2 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -111,7 +111,6 @@ These rules relate to SvelteKit and its best Practices. | Rule ID | Description | | | :------------------------------------------------------------------------- | :------------------------------------------ | :-- | -| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | disallow using goto() without the base path | | | [svelte/no-navigation-without-base](./rules/no-navigation-without-base.md) | disallow using goto() without the base path | | ## Experimental @@ -137,6 +136,7 @@ These rules relate to this plugin works: - :warning: We're going to remove deprecated rules in the next major release. Please migrate to successor/new rules. - :innocent: We don't fix bugs which are in deprecated rules since we don't have enough resources. -| Rule ID | Replaced by | -| :----------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | -| [svelte/@typescript-eslint/no-unnecessary-condition](./rules/@typescript-eslint/no-unnecessary-condition.md) | This rule is no longer needed when using svelte-eslint-parser>=v0.19.0. | +| Rule ID | Replaced by | +| :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | +| [svelte/@typescript-eslint/no-unnecessary-condition](./rules/@typescript-eslint/no-unnecessary-condition.md) | This rule is no longer needed when using svelte-eslint-parser>=v0.19.0. | +| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | [svelte/no-navigation-without-base](./rules/no-navigation-without-base.md) | diff --git a/docs/rules/no-goto-without-base.md b/docs/rules/no-goto-without-base.md index 757745120..727b4dc02 100644 --- a/docs/rules/no-goto-without-base.md +++ b/docs/rules/no-goto-without-base.md @@ -10,6 +10,8 @@ since: 'v2.36.0-next.9' > disallow using goto() without the base path +- :warning: This rule was **deprecated** and replaced by [svelte/no-navigation-without-base](no-navigation-without-base.md) rule. + ## :book: Rule Details This rule reports navigation using SvelteKit's `goto()` function without prefixing a relative URL with the base path. If a non-prefixed relative URL is used for navigation, the `goto` function navigates away from the base path, which is usually not what you wanted to do (for external URLs, `window.location = url` should be used instead). diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index c77aba124..22871ccd9 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -152,6 +152,7 @@ export interface RuleOptions { /** * disallow using goto() without the base path * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-goto-without-base/ + * @deprecated */ 'svelte/no-goto-without-base'?: Linter.RuleEntry<[]> /** 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 5145a7082..2dde04327 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 @@ -7,6 +7,8 @@ import type { RuleContext } from '../types.js'; export default createRule('no-goto-without-base', { meta: { + deprecated: true, + replacedBy: ['no-navigation-without-base'], docs: { description: 'disallow using goto() without the base path', category: 'SvelteKit', From 2a03e8860ad94245557265f19cd87e76ce53e2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Fri, 1 Nov 2024 19:02:08 +0100 Subject: [PATCH 03/11] test(no-navigation-without-base): added test for pushState, replaceState and links --- README.md | 2 +- docs/rules.md | 6 +++--- docs/rules/no-navigation-without-base.md | 4 ++-- packages/eslint-plugin-svelte/src/rule-types.ts | 2 +- ...oto01-errors.yaml => goto-aliased01-errors.yaml} | 0 ...o01-input.svelte => goto-aliased01-input.svelte} | 0 ...s.yaml => goto-base-not-as-prefix01-errors.yaml} | 0 ...velte => goto-base-not-as-prefix01-input.svelte} | 0 ...ase01-errors.yaml => goto-no-base01-errors.yaml} | 0 ...e01-input.svelte => goto-no-base01-input.svelte} | 0 .../invalid/link-base-not-as-prefix01-errors.yaml | 8 ++++++++ .../invalid/link-base-not-as-prefix01-input.svelte | 6 ++++++ .../invalid/link-no-base01-errors.yaml | 12 ++++++++++++ .../invalid/link-no-base01-input.svelte | 3 +++ .../invalid/pushState-aliased01-errors.yaml | 4 ++++ .../invalid/pushState-aliased01-input.svelte | 5 +++++ .../pushState-base-not-as-prefix01-errors.yaml | 8 ++++++++ .../pushState-base-not-as-prefix01-input.svelte | 7 +++++++ .../invalid/pushState-no-base01-errors.yaml | 4 ++++ .../invalid/pushState-no-base01-input.svelte | 5 +++++ .../invalid/replaceState-aliased01-errors.yaml | 5 +++++ .../invalid/replaceState-aliased01-input.svelte | 5 +++++ .../replaceState-base-not-as-prefix01-errors.yaml | 10 ++++++++++ .../replaceState-base-not-as-prefix01-input.svelte | 7 +++++++ .../invalid/replaceState-no-base01-errors.yaml | 5 +++++ .../invalid/replaceState-no-base01-input.svelte | 5 +++++ .../valid/absolute-uri01-input.svelte | 6 ------ ...nput.svelte => goto-base-aliased01-input.svelte} | 0 ...put.svelte => goto-base-prefixed01-input.svelte} | 0 .../valid/link-absolute-url01-input.svelte | 13 +++++++++++++ .../valid/link-base-aliased01-input.svelte | 6 ++++++ .../valid/link-base-prefixed01-input.svelte | 6 ++++++ .../valid/pushState-base-aliased01-input.svelte | 8 ++++++++ .../valid/pushState-base-prefixed01-input.svelte | 8 ++++++++ .../valid/pushState-empty-url01-input.svelte | 6 ++++++ .../valid/replaceState-base-aliased01-input.svelte | 8 ++++++++ .../valid/replaceState-base-prefixed01-input.svelte | 8 ++++++++ .../valid/replaceState-empty-url01-input.svelte | 6 ++++++ 38 files changed, 175 insertions(+), 13 deletions(-) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/{aliased-goto01-errors.yaml => goto-aliased01-errors.yaml} (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/{aliased-goto01-input.svelte => goto-aliased01-input.svelte} (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/{base-not-prefixed01-errors.yaml => goto-base-not-as-prefix01-errors.yaml} (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/{base-not-prefixed01-input.svelte => goto-base-not-as-prefix01-input.svelte} (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/{no-base01-errors.yaml => goto-no-base01-errors.yaml} (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/{no-base01-input.svelte => goto-no-base01-input.svelte} (100%) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/{base-aliased01-input.svelte => goto-base-aliased01-input.svelte} (100%) rename packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/{base-prefixed01-input.svelte => goto-base-prefixed01-input.svelte} (100%) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-absolute-url01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-aliased01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-aliased01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-empty-url01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-aliased01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-empty-url01-input.svelte diff --git a/README.md b/README.md index bbef759a8..469471962 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,7 @@ These rules relate to SvelteKit and its best Practices. | Rule ID | Description | | |:--------|:------------|:---| -| [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | disallow using goto() without the base path | | +| [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | disallow using navigation (links, goto, pushState, replaceState) without the base path | | ## Experimental diff --git a/docs/rules.md b/docs/rules.md index 0329a4aa2..37a2fb9f2 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -109,9 +109,9 @@ These rules extend the rules provided by ESLint itself, or other plugins to work These rules relate to SvelteKit and its best Practices. -| Rule ID | Description | | -| :------------------------------------------------------------------------- | :------------------------------------------ | :-- | -| [svelte/no-navigation-without-base](./rules/no-navigation-without-base.md) | disallow using goto() without the base path | | +| Rule ID | Description | | +| :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :-- | +| [svelte/no-navigation-without-base](./rules/no-navigation-without-base.md) | disallow using navigation (links, goto, pushState, replaceState) without the base path | | ## Experimental diff --git a/docs/rules/no-navigation-without-base.md b/docs/rules/no-navigation-without-base.md index bb8534597..8418f7ca2 100644 --- a/docs/rules/no-navigation-without-base.md +++ b/docs/rules/no-navigation-without-base.md @@ -2,13 +2,13 @@ pageClass: 'rule-details' sidebarDepth: 0 title: 'svelte/no-navigation-without-base' -description: 'disallow using goto() without the base path' +description: 'disallow using navigation (links, goto, pushState, replaceState) without the base path' since: 'v2.36.0-next.9' --- # svelte/no-navigation-without-base -> disallow using goto() without the base path +> disallow using navigation (links, goto, pushState, replaceState) without the base path ## :book: Rule Details diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 22871ccd9..ee5e8c62f 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -181,7 +181,7 @@ export interface RuleOptions { */ 'svelte/no-inspect'?: Linter.RuleEntry<[]> /** - * disallow using goto() without the base path + * disallow using navigation (links, goto, pushState, replaceState) without the base path * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/ */ 'svelte/no-navigation-without-base'?: Linter.RuleEntry<[]> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-aliased01-errors.yaml similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-errors.yaml rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-aliased01-errors.yaml diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-aliased01-input.svelte similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/aliased-goto01-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-aliased01-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-base-not-as-prefix01-errors.yaml similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-errors.yaml rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-base-not-as-prefix01-errors.yaml diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-base-not-as-prefix01-input.svelte similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/base-not-prefixed01-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-base-not-as-prefix01-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-errors.yaml similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-errors.yaml rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-errors.yaml diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-input.svelte similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/no-base01-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-errors.yaml new file mode 100644 index 000000000..087b29b87 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-errors.yaml @@ -0,0 +1,8 @@ +- message: Found a link with a url that isn't prefixed with the base path. + line: 5 + column: 9 + suggestions: null +- message: Found a link with a url that isn't prefixed with the base path. + line: 6 + column: 9 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-input.svelte new file mode 100644 index 000000000..6244c012c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-base-not-as-prefix01-input.svelte @@ -0,0 +1,6 @@ + + +Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml new file mode 100644 index 000000000..6b8f7a36e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml @@ -0,0 +1,12 @@ +- message: Found a link with a url that isn't prefixed with the base path. + line: 1 + column: 10 + suggestions: null +- message: Found a link with a url that isn't prefixed with the base path. + line: 2 + column: 9 + suggestions: null +- message: Found a link with a url that isn't prefixed with the base path. + line: 3 + column: 9 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte new file mode 100644 index 000000000..546ecda29 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte @@ -0,0 +1,3 @@ +Click me! +Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-errors.yaml new file mode 100644 index 000000000..52bd762b5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a pushState() call with a url that isn't prefixed with the base path. + line: 4 + column: 8 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-input.svelte new file mode 100644 index 000000000..066397bf9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-aliased01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-errors.yaml new file mode 100644 index 000000000..5fc5eff37 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-errors.yaml @@ -0,0 +1,8 @@ +- message: Found a pushState() call with a url that isn't prefixed with the base path. + line: 5 + column: 12 + suggestions: null +- message: Found a pushState() call with a url that isn't prefixed with the base path. + line: 6 + column: 12 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-input.svelte new file mode 100644 index 000000000..cf7aacafa --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-base-not-as-prefix01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml new file mode 100644 index 000000000..a814d0c45 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a pushState() call with a url that isn't prefixed with the base path. + line: 4 + column: 12 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte new file mode 100644 index 000000000..37ea2b65d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-errors.yaml new file mode 100644 index 000000000..5e60d7000 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-errors.yaml @@ -0,0 +1,5 @@ +- message: Found a replaceState() call with a url that isn't prefixed with the + base path. + line: 4 + column: 8 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-input.svelte new file mode 100644 index 000000000..de4169926 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-aliased01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-errors.yaml new file mode 100644 index 000000000..b8fbf0d99 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-errors.yaml @@ -0,0 +1,10 @@ +- message: Found a replaceState() call with a url that isn't prefixed with the + base path. + line: 5 + column: 15 + suggestions: null +- message: Found a replaceState() call with a url that isn't prefixed with the + base path. + line: 6 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-input.svelte new file mode 100644 index 000000000..0dd57d44a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-base-not-as-prefix01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml new file mode 100644 index 000000000..fdd0c4620 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml @@ -0,0 +1,5 @@ +- message: Found a replaceState() call with a url that isn't prefixed with the + base path. + line: 4 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte new file mode 100644 index 000000000..5826d76b0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte deleted file mode 100644 index 87ee8abf3..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/absolute-uri01-input.svelte +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-aliased01-input.svelte similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-aliased01-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-aliased01-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/base-prefixed01-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-absolute-url01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-absolute-url01-input.svelte new file mode 100644 index 000000000..b7896c265 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-absolute-url01-input.svelte @@ -0,0 +1,13 @@ + + +Click me! +Click me! +Click me! +Click me! +Click me! +Click me! +Click me! +Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-aliased01-input.svelte new file mode 100644 index 000000000..fe192e12f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-aliased01-input.svelte @@ -0,0 +1,6 @@ + + +Click me!; +Click me!; diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte new file mode 100644 index 000000000..3bdaf021c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte @@ -0,0 +1,6 @@ + + +Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-aliased01-input.svelte new file mode 100644 index 000000000..ca77f79a4 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-aliased01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte new file mode 100644 index 000000000..66854e812 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-empty-url01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-empty-url01-input.svelte new file mode 100644 index 000000000..4a8db79d9 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-empty-url01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-aliased01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-aliased01-input.svelte new file mode 100644 index 000000000..ad91c1585 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-aliased01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte new file mode 100644 index 000000000..35b7082c2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-empty-url01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-empty-url01-input.svelte new file mode 100644 index 000000000..96dae8e4d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-empty-url01-input.svelte @@ -0,0 +1,6 @@ + From df96f9972ef45dcbad07e45689411d4098cb5923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sat, 2 Nov 2024 00:41:29 +0100 Subject: [PATCH 04/11] feat(no-navigation-without-base): added support for pushState and replaceState --- .../src/rules/no-navigation-without-base.ts | 214 ++++++++++++------ 1 file changed, 148 insertions(+), 66 deletions(-) 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 aae8f1275..13abb5aec 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 @@ -8,14 +8,19 @@ import type { RuleContext } from '../types.js'; export default createRule('no-navigation-without-base', { meta: { docs: { - description: 'disallow using goto() without the base path', + description: + 'disallow using navigation (links, goto, pushState, replaceState) without the base path', category: 'SvelteKit', recommended: false }, schema: [], messages: { - isNotPrefixedWithBasePath: - "Found a goto() call with a url that isn't prefixed with the base path." + gotoNotPrefixed: "Found a goto() call with a url that isn't prefixed with the base path.", + linkNotPrefixed: "Found a link with a url that isn't prefixed with the base path.", + pushStateNotPrefixed: + "Found a pushState() call with a url that isn't prefixed with the base path.", + replaceStateNotPrefixed: + "Found a replaceState() call with a url that isn't prefixed with the base path." }, type: 'suggestion' }, @@ -26,59 +31,153 @@ export default createRule('no-navigation-without-base', { getSourceCode(context).scopeManager.globalScope! ); const basePathNames = extractBasePathReferences(referenceTracker, context); - for (const gotoCall of extractGotoReferences(referenceTracker)) { - if (gotoCall.arguments.length < 1) { - continue; - } - const path = gotoCall.arguments[0]; - switch (path.type) { - case 'BinaryExpression': - checkBinaryExpression(context, path, basePathNames); - break; - case 'Literal': - checkLiteral(context, path); - break; - case 'TemplateLiteral': - checkTemplateLiteral(context, path, basePathNames); - break; - default: - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); - } + const { + goto: gotoCalls, + pushState: pushStateCalls, + replaceState: replaceStateCalls + } = extractFunctionCallReferences(referenceTracker); + for (const gotoCall of gotoCalls) { + checkGotoCall(context, gotoCall, basePathNames); + } + for (const pushStateCall of pushStateCalls) { + checkShallowNavigationCall(context, pushStateCall, basePathNames, 'pushStateNotPrefixed'); + } + for (const replaceStateCall of replaceStateCalls) { + checkShallowNavigationCall( + context, + replaceStateCall, + basePathNames, + 'replaceStateNotPrefixed' + ); } } }; } }); -function checkBinaryExpression( +// Extract all imports of the base path + +function extractBasePathReferences( + referenceTracker: ReferenceTracker, + context: RuleContext +): Set { + const set = new Set(); + for (const { node } of referenceTracker.iterateEsmReferences({ + '$app/paths': { + [ReferenceTracker.ESM]: true, + base: { + [ReferenceTracker.READ]: true + } + } + })) { + const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local); + if (!variable) continue; + for (const reference of variable.references) { + if (reference.identifier.type === 'Identifier') set.add(reference.identifier); + } + } + return set; +} + +// Extract all references to goto, pushState and replaceState + +function extractFunctionCallReferences(referenceTracker: ReferenceTracker): { + goto: TSESTree.CallExpression[]; + pushState: TSESTree.CallExpression[]; + replaceState: TSESTree.CallExpression[]; +} { + const rawReferences = Array.from( + referenceTracker.iterateEsmReferences({ + '$app/navigation': { + [ReferenceTracker.ESM]: true, + goto: { + [ReferenceTracker.CALL]: true + }, + pushState: { + [ReferenceTracker.CALL]: true + }, + replaceState: { + [ReferenceTracker.CALL]: true + } + } + }) + ); + return { + goto: rawReferences + .filter(({ path }) => path[path.length - 1] === 'goto') + .map(({ node }) => node), + pushState: rawReferences + .filter(({ path }) => path[path.length - 1] === 'pushState') + .map(({ node }) => node), + replaceState: rawReferences + .filter(({ path }) => path[path.length - 1] === 'replaceState') + .map(({ node }) => node) + }; +} + +// Actual function checking + +function checkGotoCall( context: RuleContext, - path: TSESTree.BinaryExpression, + call: TSESTree.CallExpression, basePathNames: Set ): void { - if (path.left.type !== 'Identifier' || !basePathNames.has(path.left)) { - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + if (call.arguments.length < 1) { + return; + } + const url = call.arguments[0]; + if (!urlStartsWithBase(url, basePathNames)) { + context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' }); } } -function checkTemplateLiteral( +function checkShallowNavigationCall( context: RuleContext, - path: TSESTree.TemplateLiteral, - basePathNames: Set + call: TSESTree.CallExpression, + basePathNames: Set, + messageId: string ): void { - const startingIdentifier = extractStartingIdentifier(path); - if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) { - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + if (call.arguments.length < 1) { + return; + } + const url = call.arguments[0]; + if (!urlIsEmpty(url) && !urlStartsWithBase(url, basePathNames)) { + context.report({ loc: url.loc, messageId }); } } -function checkLiteral(context: RuleContext, path: TSESTree.Literal): void { - const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; - if (!absolutePathRegex.test(path.value?.toString() ?? '')) { - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); +// Helper functions + +function urlStartsWithBase( + url: TSESTree.CallExpressionArgument, + basePathNames: Set +): boolean { + switch (url.type) { + case 'BinaryExpression': + return binaryExpressionStartsWithBase(url, basePathNames); + case 'TemplateLiteral': + return templateLiteralStartsWithBase(url, basePathNames); + default: + return false; } } -function extractStartingIdentifier( +function binaryExpressionStartsWithBase( + url: TSESTree.BinaryExpression, + basePathNames: Set +): boolean { + return url.left.type === 'Identifier' && basePathNames.has(url.left); +} + +function templateLiteralStartsWithBase( + url: TSESTree.TemplateLiteral, + basePathNames: Set +): boolean { + const startingIdentifier = extractLiteralStartingIdentifier(url); + return startingIdentifier !== undefined && basePathNames.has(startingIdentifier); +} + +function extractLiteralStartingIdentifier( templateLiteral: TSESTree.TemplateLiteral ): TSESTree.Identifier | undefined { const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) => @@ -97,38 +196,21 @@ function extractStartingIdentifier( return undefined; } -function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.CallExpression[] { - return Array.from( - referenceTracker.iterateEsmReferences({ - '$app/navigation': { - [ReferenceTracker.ESM]: true, - goto: { - [ReferenceTracker.CALL]: true - } - } - }), - ({ node }) => node +function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean { + return ( + (url.type === 'Literal' && url.value === '') || + (url.type === 'TemplateLiteral' && + url.expressions.length === 0 && + url.quasis.length === 1 && + url.quasis[0].value.raw === '') ); } -function extractBasePathReferences( - referenceTracker: ReferenceTracker, - context: RuleContext -): Set { - const set = new Set(); - for (const { node } of referenceTracker.iterateEsmReferences({ - '$app/paths': { - [ReferenceTracker.ESM]: true, - base: { - [ReferenceTracker.READ]: true - } - } - })) { - const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local); - if (!variable) continue; - for (const reference of variable.references) { - if (reference.identifier.type === 'Identifier') set.add(reference.identifier); - } +/* +function checkLiteral(context: RuleContext, url: TSESTree.Literal): void { + const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; + if (!absolutePathRegex.test(url.value?.toString() ?? '')) { + context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' }); } - return set; } +*/ From 67e9884cc7df53a43738c1e02b60d119ca6c01c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 3 Nov 2024 01:36:49 +0100 Subject: [PATCH 05/11] feat(no-navigation-without-base): added support for links --- .../src/rules/no-navigation-without-base.ts | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) 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 13abb5aec..bc8a0dec7 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 @@ -4,6 +4,7 @@ import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { getSourceCode } from '../utils/compat.js'; import { findVariable } from '../utils/ast-utils.js'; import type { RuleContext } from '../types.js'; +import type { SvelteLiteral } from 'svelte-eslint-parser/lib/ast'; export default createRule('no-navigation-without-base', { meta: { @@ -25,12 +26,13 @@ export default createRule('no-navigation-without-base', { type: 'suggestion' }, create(context) { + let basePathNames: Set = new Set(); return { Program() { const referenceTracker = new ReferenceTracker( getSourceCode(context).scopeManager.globalScope! ); - const basePathNames = extractBasePathReferences(referenceTracker, context); + basePathNames = extractBasePathReferences(referenceTracker, context); const { goto: gotoCalls, pushState: pushStateCalls, @@ -50,6 +52,30 @@ export default createRule('no-navigation-without-base', { 'replaceStateNotPrefixed' ); } + }, + SvelteAttribute(node) { + if ( + node.parent.parent.type !== 'SvelteElement' || + node.parent.parent.kind !== 'html' || + node.parent.parent.name.type !== 'SvelteName' || + node.parent.parent.name.name !== 'a' || + node.key.name !== 'href' + ) { + return; + } + const hrefValue = node.value[0]; + if (hrefValue.type === 'SvelteLiteral') { + if (!urlIsAbsolute(hrefValue)) { + context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' }); + } + return; + } + if ( + !urlStartsWithBase(hrefValue.expression, basePathNames) && + !urlIsAbsolute(hrefValue.expression) + ) { + context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' }); + } } }; } @@ -206,11 +232,34 @@ function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean { ); } -/* -function checkLiteral(context: RuleContext, url: TSESTree.Literal): void { - const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; - if (!absolutePathRegex.test(url.value?.toString() ?? '')) { - context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' }); +function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean { + switch (url.type) { + case 'BinaryExpression': + return binaryExpressionIsAbsolute(url); + case 'Literal': + return typeof url.value === 'string' && urlValueIsAbsolute(url.value); + case 'SvelteLiteral': + return urlValueIsAbsolute(url.value); + case 'TemplateLiteral': + return templateLiteralIsAbsolute(url); + default: + return false; } } -*/ + +function binaryExpressionIsAbsolute(url: TSESTree.BinaryExpression): boolean { + return ( + (url.left.type !== 'PrivateIdentifier' && urlIsAbsolute(url.left)) || urlIsAbsolute(url.right) + ); +} + +function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean { + return ( + url.expressions.some(urlIsAbsolute) || + url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw)) + ); +} + +function urlValueIsAbsolute(url: string): boolean { + return url.includes('://'); +} From d14664a077f24accee25736ff6a3c34d6fcee22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 3 Nov 2024 18:47:20 +0100 Subject: [PATCH 06/11] feat(no-navigation-without-base): added configuration options --- .../eslint-plugin-svelte/src/rule-types.ts | 9 ++- .../src/rules/no-navigation-without-base.ts | 55 +++++++++++++++---- .../valid/ignoreGoto/_config.json | 7 +++ .../ignoreGoto/goto-ignored01-input.svelte | 5 ++ .../valid/ignoreLinks/_config.json | 7 +++ .../ignoreLinks/link-ignored01-input.svelte | 3 + .../valid/ignorePushState/_config.json | 7 +++ .../pushState-ignored01-input.svelte | 5 ++ .../valid/ignoreReplaceState/_config.json | 7 +++ .../replaceState-ignored01-input.svelte | 5 ++ 10 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/goto-ignored01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/link-ignored01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/pushState-ignored01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/replaceState-ignored01-input.svelte diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index ee5e8c62f..2c4abeec1 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -184,7 +184,7 @@ export interface RuleOptions { * disallow using navigation (links, goto, pushState, replaceState) without the base path * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/ */ - 'svelte/no-navigation-without-base'?: Linter.RuleEntry<[]> + 'svelte/no-navigation-without-base'?: Linter.RuleEntry /** * disallow use of not function in event handler * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-not-function-handler/ @@ -448,6 +448,13 @@ type SvelteNoInlineStyles = []|[{ type SvelteNoInnerDeclarations = []|[("functions" | "both")]|[("functions" | "both"), { blockScopedFunctions?: ("allow" | "disallow") }] +// ----- svelte/no-navigation-without-base ----- +type SvelteNoNavigationWithoutBase = []|[{ + ignoreGoto?: boolean + ignoreLinks?: boolean + ignorePushState?: boolean + ignoreReplaceState?: boolean +}] // ----- svelte/no-reactive-reassign ----- type SvelteNoReactiveReassign = []|[{ props?: boolean 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 bc8a0dec7..3f1034d47 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 @@ -14,7 +14,26 @@ export default createRule('no-navigation-without-base', { category: 'SvelteKit', recommended: false }, - schema: [], + schema: [ + { + type: 'object', + properties: { + ignoreGoto: { + type: 'boolean' + }, + ignoreLinks: { + type: 'boolean' + }, + ignorePushState: { + type: 'boolean' + }, + ignoreReplaceState: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], messages: { gotoNotPrefixed: "Found a goto() call with a url that isn't prefixed with the base path.", linkNotPrefixed: "Found a link with a url that isn't prefixed with the base path.", @@ -38,23 +57,35 @@ export default createRule('no-navigation-without-base', { pushState: pushStateCalls, replaceState: replaceStateCalls } = extractFunctionCallReferences(referenceTracker); - for (const gotoCall of gotoCalls) { - checkGotoCall(context, gotoCall, basePathNames); + if (context.options[0]?.ignoreGoto !== true) { + for (const gotoCall of gotoCalls) { + checkGotoCall(context, gotoCall, basePathNames); + } } - for (const pushStateCall of pushStateCalls) { - checkShallowNavigationCall(context, pushStateCall, basePathNames, 'pushStateNotPrefixed'); + if (context.options[0]?.ignorePushState !== true) { + for (const pushStateCall of pushStateCalls) { + checkShallowNavigationCall( + context, + pushStateCall, + basePathNames, + 'pushStateNotPrefixed' + ); + } } - for (const replaceStateCall of replaceStateCalls) { - checkShallowNavigationCall( - context, - replaceStateCall, - basePathNames, - 'replaceStateNotPrefixed' - ); + if (context.options[0]?.ignoreReplaceState !== true) { + for (const replaceStateCall of replaceStateCalls) { + checkShallowNavigationCall( + context, + replaceStateCall, + basePathNames, + 'replaceStateNotPrefixed' + ); + } } }, SvelteAttribute(node) { if ( + context.options[0]?.ignoreLinks === true || node.parent.parent.type !== 'SvelteElement' || node.parent.parent.kind !== 'html' || node.parent.parent.name.type !== 'SvelteName' || diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/_config.json new file mode 100644 index 000000000..f21168640 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "ignoreGoto": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/goto-ignored01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/goto-ignored01-input.svelte new file mode 100644 index 000000000..6f011fe2d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreGoto/goto-ignored01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/_config.json new file mode 100644 index 000000000..fd03211ab --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "ignoreLinks": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/link-ignored01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/link-ignored01-input.svelte new file mode 100644 index 000000000..546ecda29 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreLinks/link-ignored01-input.svelte @@ -0,0 +1,3 @@ +Click me! +Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/_config.json new file mode 100644 index 000000000..7fbdd8b77 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "ignorePushState": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/pushState-ignored01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/pushState-ignored01-input.svelte new file mode 100644 index 000000000..37ea2b65d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignorePushState/pushState-ignored01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/_config.json new file mode 100644 index 000000000..a678a15a8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "ignoreReplaceState": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/replaceState-ignored01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/replaceState-ignored01-input.svelte new file mode 100644 index 000000000..5826d76b0 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/ignoreReplaceState/replaceState-ignored01-input.svelte @@ -0,0 +1,5 @@ + From 8e587e156799cdba8c8e7ee6ea2c15b851176c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 3 Nov 2024 19:49:04 +0100 Subject: [PATCH 07/11] docs(no-navigation-without-base): documented the new rule --- docs/rules/no-navigation-without-base.md | 56 +++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/docs/rules/no-navigation-without-base.md b/docs/rules/no-navigation-without-base.md index 8418f7ca2..436a6e509 100644 --- a/docs/rules/no-navigation-without-base.md +++ b/docs/rules/no-navigation-without-base.md @@ -12,7 +12,9 @@ since: 'v2.36.0-next.9' ## :book: Rule Details -This rule reports navigation using SvelteKit's `goto()` function without prefixing a relative URL with the base path. If a non-prefixed relative URL is used for navigation, the `goto` function navigates away from the base path, which is usually not what you wanted to do (for external URLs, `window.location = url` should be used instead). +This rule reports navigation using HTML `` tags, SvelteKit's `goto()`, `pushState()` and `replaceState()` functions without prefixing a relative URL with the base path. All four of these may be used for navigation, with `goto()`, `pushState()` and `replaceState()` being intended solely for iternal navigation (i.e. not leaving the site), while `` tags may be used for both internal and external navigation. When using any way of internal navigation, the base path must be prepended, otherwise the site may break. For programmatic navigation to external URLs, using `window.location` is advised. + +This rule checks all 4 navigation options for the presence of the base path, with an exception for `` links to absolute URLs, which are assumed to be used for external navigation and so do not require the base path, and for shallow outing functions with an empty string as the path, which keeps the current URL. @@ -20,35 +22,67 @@ This rule reports navigation using SvelteKit's `goto()` function without prefixi + + +Click me! +Click me! +Click me! + + +Click me! +Click me! ``` ## :wrench: Options -Nothing. +```json +{ + "svelte/no-navigation-without-base": [ + "error", + { + "ignoreGoto": false, + "ignoreLinks": false, + "ignorePushState": false, + "ignoreReplaceState": false + } + ] +} +``` + +- `ignoreGoto` ... Whether to ignore all `goto()` calls. Default `false`. +- `ignoreLinks` ... Whether to ignore all `` tags. Default `false`. +- `ignorePushState` ... Whether to ignore all `pushState()` calls. Default `false`. +- `ignoreReplaceState` ... Whether to ignore all `replaceState()` calls. Default `false`. ## :books: Further Reading -- [`goto()` documentation](https://kit.svelte.dev/docs/modules#$app-navigation-goto) -- [`base` documentation](https://kit.svelte.dev/docs/modules#$app-paths-base) +- [`base` documentation](https://svelte.dev/docs/kit/$app-paths#base) +- [Shallow routing](https://svelte.dev/docs/kit/shallow-routing) +- [`goto()` documentation](https://svelte.dev/docs/kit/$app-navigation#goto) +- [`pushState()` documentation](https://svelte.dev/docs/kit/$app-navigation#pushState) +- [`replaceState()` documentation](https://svelte.dev/docs/kit/$app-navigation#replaceState) ## :rocket: Version From 84c133a016da7ecb454d7f93e35831daa3b87615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 3 Nov 2024 22:15:40 +0100 Subject: [PATCH 08/11] feat(no-navigation-without-base): added support for urls defined in variables --- .../src/rules/no-navigation-without-base.ts | 73 ++++++++++++++----- .../invalid/goto-no-base01-errors.yaml | 6 +- .../invalid/goto-no-base01-input.svelte | 3 + .../invalid/link-no-base01-errors.yaml | 10 ++- .../invalid/link-no-base01-input.svelte | 4 + .../invalid/pushState-no-base01-errors.yaml | 6 +- .../invalid/pushState-no-base01-input.svelte | 3 + .../replaceState-no-base01-errors.yaml | 7 +- .../replaceState-no-base01-input.svelte | 3 + .../valid/goto-base-prefixed01-input.svelte | 5 ++ .../valid/link-base-prefixed01-input.svelte | 5 ++ .../pushState-base-prefixed01-input.svelte | 5 ++ .../replaceState-base-prefixed01-input.svelte | 5 ++ 13 files changed, 110 insertions(+), 25 deletions(-) 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 3f1034d47..4c9d3ada8 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 @@ -96,14 +96,14 @@ export default createRule('no-navigation-without-base', { } const hrefValue = node.value[0]; if (hrefValue.type === 'SvelteLiteral') { - if (!urlIsAbsolute(hrefValue)) { + if (!expressionIsAbsolute(hrefValue)) { context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' }); } return; } if ( - !urlStartsWithBase(hrefValue.expression, basePathNames) && - !urlIsAbsolute(hrefValue.expression) + !expressionStartsWithBase(context, hrefValue.expression, basePathNames) && + !expressionIsAbsolute(hrefValue.expression) ) { context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' }); } @@ -183,7 +183,7 @@ function checkGotoCall( return; } const url = call.arguments[0]; - if (!urlStartsWithBase(url, basePathNames)) { + if (url.type === 'SpreadElement' || !expressionStartsWithBase(context, url, basePathNames)) { context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' }); } } @@ -198,45 +198,79 @@ function checkShallowNavigationCall( return; } const url = call.arguments[0]; - if (!urlIsEmpty(url) && !urlStartsWithBase(url, basePathNames)) { + if ( + url.type === 'SpreadElement' || + (!expressionIsEmpty(url) && !expressionStartsWithBase(context, url, basePathNames)) + ) { context.report({ loc: url.loc, messageId }); } } // Helper functions -function urlStartsWithBase( - url: TSESTree.CallExpressionArgument, +function expressionStartsWithBase( + context: RuleContext, + url: TSESTree.Expression, basePathNames: Set ): boolean { switch (url.type) { case 'BinaryExpression': - return binaryExpressionStartsWithBase(url, basePathNames); + return binaryExpressionStartsWithBase(context, url, basePathNames); + case 'Identifier': + return variableStartsWithBase(context, url, basePathNames); case 'TemplateLiteral': - return templateLiteralStartsWithBase(url, basePathNames); + return templateLiteralStartsWithBase(context, url, basePathNames); default: return false; } } function binaryExpressionStartsWithBase( + context: RuleContext, url: TSESTree.BinaryExpression, basePathNames: Set ): boolean { - return url.left.type === 'Identifier' && basePathNames.has(url.left); + return ( + url.left.type !== 'PrivateIdentifier' && + expressionStartsWithBase(context, url.left, basePathNames) + ); +} + +function variableStartsWithBase( + context: RuleContext, + url: TSESTree.Identifier, + basePathNames: Set +): boolean { + if (basePathNames.has(url)) { + return true; + } + const variable = findVariable(context, url); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return false; + } + return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames); } function templateLiteralStartsWithBase( + context: RuleContext, url: TSESTree.TemplateLiteral, basePathNames: Set ): boolean { - const startingIdentifier = extractLiteralStartingIdentifier(url); - return startingIdentifier !== undefined && basePathNames.has(startingIdentifier); + const startingIdentifier = extractLiteralStartingExpression(url); + return ( + startingIdentifier !== undefined && + expressionStartsWithBase(context, startingIdentifier, basePathNames) + ); } -function extractLiteralStartingIdentifier( +function extractLiteralStartingExpression( templateLiteral: TSESTree.TemplateLiteral -): TSESTree.Identifier | undefined { +): TSESTree.Expression | undefined { const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) => a.range[0] < b.range[0] ? -1 : 1 ); @@ -245,7 +279,7 @@ function extractLiteralStartingIdentifier( // Skip empty quasi in the begining continue; } - if (part.type === 'Identifier') { + if (part.type !== 'TemplateElement') { return part; } return undefined; @@ -253,7 +287,7 @@ function extractLiteralStartingIdentifier( return undefined; } -function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean { +function expressionIsEmpty(url: TSESTree.Expression): boolean { return ( (url.type === 'Literal' && url.value === '') || (url.type === 'TemplateLiteral' && @@ -263,7 +297,7 @@ function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean { ); } -function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean { +function expressionIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsAbsolute(url); @@ -280,13 +314,14 @@ function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean { function binaryExpressionIsAbsolute(url: TSESTree.BinaryExpression): boolean { return ( - (url.left.type !== 'PrivateIdentifier' && urlIsAbsolute(url.left)) || urlIsAbsolute(url.right) + (url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(url.left)) || + expressionIsAbsolute(url.right) ); } function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean { return ( - url.expressions.some(urlIsAbsolute) || + url.expressions.some(expressionIsAbsolute) || url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw)) ); } diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-errors.yaml index 658fcb47d..ed8b9578e 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-errors.yaml @@ -1,4 +1,8 @@ - message: Found a goto() call with a url that isn't prefixed with the base path. - line: 4 + line: 6 + column: 7 + suggestions: null +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 7 column: 7 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-input.svelte index 6f011fe2d..7cb58ea20 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-no-base01-input.svelte @@ -1,5 +1,8 @@ diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml index 6b8f7a36e..e006d4d27 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-errors.yaml @@ -1,12 +1,16 @@ - message: Found a link with a url that isn't prefixed with the base path. - line: 1 + line: 4 column: 10 suggestions: null - message: Found a link with a url that isn't prefixed with the base path. - line: 2 + line: 5 column: 9 suggestions: null - message: Found a link with a url that isn't prefixed with the base path. - line: 3 + line: 6 + column: 9 + suggestions: null +- message: Found a link with a url that isn't prefixed with the base path. + line: 7 column: 9 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte index 546ecda29..0e0f6df25 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-no-base01-input.svelte @@ -1,3 +1,7 @@ + Click me! Click me! Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml index a814d0c45..e70710ad0 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-errors.yaml @@ -1,4 +1,8 @@ - message: Found a pushState() call with a url that isn't prefixed with the base path. - line: 4 + line: 6 + column: 12 + suggestions: null +- message: Found a pushState() call with a url that isn't prefixed with the base path. + line: 7 column: 12 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte index 37ea2b65d..19fe76cf3 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-no-base01-input.svelte @@ -1,5 +1,8 @@ diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml index fdd0c4620..dd4d0d177 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-errors.yaml @@ -1,5 +1,10 @@ - message: Found a replaceState() call with a url that isn't prefixed with the base path. - line: 4 + line: 6 + column: 15 + suggestions: null +- message: Found a replaceState() call with a url that isn't prefixed with the + base path. + line: 7 column: 15 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte index 5826d76b0..4738c2ccb 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-no-base01-input.svelte @@ -1,5 +1,8 @@ diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte index cd7177deb..22602dfc8 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte @@ -2,7 +2,12 @@ import { base } from '$app/paths'; import { goto } from '$app/navigation'; + const value1 = base + '/foo/'; + const value2 = `${base}/foo/`; + // eslint-disable-next-line prefer-template -- Testing both variants goto(base + '/foo/'); goto(`${base}/foo/`); + goto(value1); + goto(value2); diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte index 3bdaf021c..b86295824 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-prefixed01-input.svelte @@ -1,6 +1,11 @@ Click me! Click me! +Click me! +Click me! diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte index 66854e812..468201793 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte @@ -2,7 +2,12 @@ import { base } from '$app/paths'; import { pushState } from '$app/navigation'; + const value1 = base + '/foo/'; + const value2 = `${base}/foo/`; + // eslint-disable-next-line prefer-template -- Testing both variants pushState(base + '/foo/'); pushState(`${base}/foo/`); + pushState(value1); + pushState(value2); diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte index 35b7082c2..7df67a5d5 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte @@ -2,7 +2,12 @@ import { base } from '$app/paths'; import { replaceState } from '$app/navigation'; + const value1 = base + '/foo/'; + const value2 = `${base}/foo/`; + // eslint-disable-next-line prefer-template -- Testing both variants replaceState(base + '/foo/'); replaceState(`${base}/foo/`); + replaceState(value1); + replaceState(value2); From 6aa1bb71b99a61161d8462856b36db6006b84b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 4 Nov 2024 01:13:10 +0100 Subject: [PATCH 09/11] chore(no-navigation-without-base): added changesets --- .changeset/cold-starfishes-doubt.md | 5 +++++ .changeset/olive-melons-explain.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/cold-starfishes-doubt.md create mode 100644 .changeset/olive-melons-explain.md diff --git a/.changeset/cold-starfishes-doubt.md b/.changeset/cold-starfishes-doubt.md new file mode 100644 index 000000000..f85c6b39a --- /dev/null +++ b/.changeset/cold-starfishes-doubt.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat: added the no-navigation-without-base rule diff --git a/.changeset/olive-melons-explain.md b/.changeset/olive-melons-explain.md new file mode 100644 index 000000000..6571a5bbf --- /dev/null +++ b/.changeset/olive-melons-explain.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': major +--- + +chore: deprecated the no-goto-without-base rule From ea788db2d6499d214e0f5c7d5dfb0aef43406b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Fri, 29 Nov 2024 21:52:32 +0100 Subject: [PATCH 10/11] test(no-navigation-without-base): added namespace import tests --- .../invalid/goto-namespace-import01-errors.yaml | 4 ++++ .../invalid/goto-namespace-import01-input.svelte | 5 +++++ .../invalid/pushState-namespace-import01-errors.yaml | 4 ++++ .../invalid/pushState-namespace-import01-input.svelte | 5 +++++ .../invalid/replaceState-namespace-import01-errors.yaml | 5 +++++ .../invalid/replaceState-namespace-import01-input.svelte | 5 +++++ .../valid/goto-base-namespace-import01-input.svelte | 8 ++++++++ .../valid/link-base-namespace-import01-input.svelte | 6 ++++++ .../valid/pushState-base-namespace-import01-input.svelte | 8 ++++++++ .../replaceState-base-namespace-import01-input.svelte | 8 ++++++++ 10 files changed, 58 insertions(+) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-namespace-import01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-namespace-import01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-namespace-import01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-namespace-import01-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-errors.yaml new file mode 100644 index 000000000..8fe7d69b8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 4 + column: 18 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-input.svelte new file mode 100644 index 000000000..9e9868428 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/goto-namespace-import01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-errors.yaml new file mode 100644 index 000000000..2de0e9fc5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a pushState() call with a url that isn't prefixed with the base path. + line: 4 + column: 23 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-input.svelte new file mode 100644 index 000000000..a3c883949 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/pushState-namespace-import01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-errors.yaml new file mode 100644 index 000000000..39e0e1c85 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-errors.yaml @@ -0,0 +1,5 @@ +- message: Found a replaceState() call with a url that isn't prefixed with the + base path. + line: 4 + column: 26 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-input.svelte new file mode 100644 index 000000000..47db8ff31 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/replaceState-namespace-import01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-namespace-import01-input.svelte new file mode 100644 index 000000000..150ad1f06 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-namespace-import01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-namespace-import01-input.svelte new file mode 100644 index 000000000..1d0630115 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-base-namespace-import01-input.svelte @@ -0,0 +1,6 @@ + + +Click me!; +Click me!; diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-namespace-import01-input.svelte new file mode 100644 index 000000000..408416c2e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-namespace-import01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-namespace-import01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-namespace-import01-input.svelte new file mode 100644 index 000000000..5b955153c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-namespace-import01-input.svelte @@ -0,0 +1,8 @@ + From add6e9b174894e15f53da90582f68c4ece702840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sat, 30 Nov 2024 10:27:14 +0100 Subject: [PATCH 11/11] fix(no-navigation-without-base): fixed namespace import detections --- .../src/rules/no-navigation-without-base.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) 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 4c9d3ada8..6f2a20994 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 @@ -127,10 +127,20 @@ function extractBasePathReferences( } } })) { - const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local); - if (!variable) continue; - for (const reference of variable.references) { - if (reference.identifier.type === 'Identifier') set.add(reference.identifier); + if (node.type === 'ImportSpecifier') { + const variable = findVariable(context, node.local); + if (variable === null) { + continue; + } + for (const reference of variable.references) { + if (reference.identifier.type === 'Identifier') set.add(reference.identifier); + } + } else if ( + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + node.property.name === 'base' + ) { + set.add(node.property); } } return set; @@ -218,6 +228,8 @@ function expressionStartsWithBase( return binaryExpressionStartsWithBase(context, url, basePathNames); case 'Identifier': return variableStartsWithBase(context, url, basePathNames); + case 'MemberExpression': + return memberExpressionStartsWithBase(url, basePathNames); case 'TemplateLiteral': return templateLiteralStartsWithBase(context, url, basePathNames); default: @@ -236,6 +248,13 @@ function binaryExpressionStartsWithBase( ); } +function memberExpressionStartsWithBase( + url: TSESTree.MemberExpression, + basePathNames: Set +): boolean { + return url.property.type === 'Identifier' && basePathNames.has(url.property); +} + function variableStartsWithBase( context: RuleContext, url: TSESTree.Identifier,