From e6615fdaf4bf5a5ba35a2a0dfdca7830fb17f3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 28 Jan 2024 14:49:53 +0100 Subject: [PATCH 01/14] test(no-goto-without-base): added tests for the rule --- .../invalid/base-not-prefix01-errors.yaml | 1 + .../invalid/base-not-prefix01-input.svelte | 7 +++++++ .../invalid/no-base01-errors.yaml | 1 + .../invalid/no-base01-input.svelte | 5 +++++ .../valid/absolute-uri01-input.svelte | 6 ++++++ .../valid/base-aliased01-input.svelte | 7 +++++++ .../valid/base-prefixed01-input.svelte | 7 +++++++ tests/src/rules/no-goto-without-base.ts | 12 ++++++++++++ 8 files changed, 46 insertions(+) create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte create mode 100644 tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte create mode 100644 tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte create mode 100644 tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte create mode 100644 tests/src/rules/no-goto-without-base.ts diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml @@ -0,0 +1 @@ +[] diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte new file mode 100644 index 000000000..721b4db9d --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml @@ -0,0 +1 @@ +[] diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte new file mode 100644 index 000000000..869706ec7 --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte b/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte new file mode 100644 index 000000000..aed885422 --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte b/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte new file mode 100644 index 000000000..8274f0b23 --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte b/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte new file mode 100644 index 000000000..4bdec0cb4 --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/src/rules/no-goto-without-base.ts b/tests/src/rules/no-goto-without-base.ts new file mode 100644 index 000000000..1fadf4174 --- /dev/null +++ b/tests/src/rules/no-goto-without-base.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat'; +import rule from '../../../src/rules/no-goto-without-base'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('no-goto-without-base', rule as any, loadTestCases('no-goto-without-base')); From 4e5d3d5879cacf442b0e03eb731591b5be645f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 28 Jan 2024 15:02:04 +0100 Subject: [PATCH 02/14] feat: added a rule category for SvelteKit-related rules --- README.md | 7 +++++++ docs-svelte-kit/src/lib/eslint/scripts/linter.js | 5 +++++ docs-svelte-kit/src/lib/utils.js | 1 + docs/rules.md | 7 +++++++ src/types.ts | 1 + tools/render-rules.ts | 2 ++ 6 files changed, 23 insertions(+) diff --git a/README.md b/README.md index 51fdef1d2..3495855a7 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,13 @@ These rules extend the rules provided by ESLint itself, or other plugins to work | [svelte/no-inner-declarations](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inner-declarations/) | disallow variable or `function` declarations in nested blocks | :star: | | [svelte/no-trailing-spaces](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/) | disallow trailing whitespace at the end of lines | :wrench: | +## SvelteKit + +These rules relate to SvelteKit and its best Practices. + +| Rule ID | Description | | +|:--------|:------------|:---| + ## Experimental :warning: These rules are considered experimental and may change or be removed in the future: diff --git a/docs-svelte-kit/src/lib/eslint/scripts/linter.js b/docs-svelte-kit/src/lib/eslint/scripts/linter.js index a47058017..f361a50fe 100644 --- a/docs-svelte-kit/src/lib/eslint/scripts/linter.js +++ b/docs-svelte-kit/src/lib/eslint/scripts/linter.js @@ -32,6 +32,11 @@ export const categories = [ classes: 'svelte-category', rules: [] }, + { + title: 'SvelteKit', + classes: 'svelte-category', + rules: [] + }, { title: 'Experimental', classes: 'svelte-category', diff --git a/docs-svelte-kit/src/lib/utils.js b/docs-svelte-kit/src/lib/utils.js index 8ef89992b..ce28beab4 100644 --- a/docs-svelte-kit/src/lib/utils.js +++ b/docs-svelte-kit/src/lib/utils.js @@ -18,6 +18,7 @@ const categories = [ 'Best Practices', 'Stylistic Issues', 'Extension Rules', + 'SvelteKit', 'Experimental', 'System' ]; diff --git a/docs/rules.md b/docs/rules.md index 257cb4f93..f31aefd05 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -101,6 +101,13 @@ These rules extend the rules provided by ESLint itself, or other plugins to work | [svelte/no-inner-declarations](./rules/no-inner-declarations.md) | disallow variable or `function` declarations in nested blocks | :star: | | [svelte/no-trailing-spaces](./rules/no-trailing-spaces.md) | disallow trailing whitespace at the end of lines | :wrench: | +## SvelteKit + +These rules relate to SvelteKit and its best Practices. + +| Rule ID | Description | | +| :------------------------------------------------------------- | :--------------- | :-- | + ## Experimental :warning: These rules are considered experimental and may change or be removed in the future: diff --git a/src/types.ts b/src/types.ts index 64591ae7b..ab313fbb6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export type RuleCategory = | 'Best Practices' | 'Stylistic Issues' | 'Extension Rules' + | 'SvelteKit' | 'Experimental' | 'System'; diff --git a/tools/render-rules.ts b/tools/render-rules.ts index c6209b8a9..8e8466bd7 100644 --- a/tools/render-rules.ts +++ b/tools/render-rules.ts @@ -7,6 +7,7 @@ const categories = [ 'Best Practices', 'Stylistic Issues', 'Extension Rules', + 'SvelteKit', 'Experimental', 'System' ] as const; @@ -18,6 +19,7 @@ const descriptions: Record<(typeof categories)[number], string> = { 'Stylistic Issues': 'These rules relate to style guidelines, and are therefore quite subjective:', 'Extension Rules': 'These rules extend the rules provided by ESLint itself, or other plugins to work well in Svelte:', + SvelteKit: 'These rules relate to SvelteKit and its best Practices.', Experimental: ':warning: These rules are considered experimental and may change or be removed in the future:', System: 'These rules relate to this plugin works:' From 3b2b0aa81b2ee53eb73a88bfb0caa880d4ef00d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 28 Jan 2024 21:55:54 +0100 Subject: [PATCH 03/14] chore(no-goto-without-base): formatted tests --- .../invalid/base-not-prefix01-input.svelte | 9 +++++---- .../no-goto-without-base/invalid/no-base01-input.svelte | 4 ++-- .../valid/absolute-uri01-input.svelte | 6 +++--- .../valid/base-aliased01-input.svelte | 9 +++++---- .../valid/base-prefixed01-input.svelte | 9 +++++---- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte index 721b4db9d..1aeff3aec 100644 --- a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte +++ b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte @@ -1,7 +1,8 @@ diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte index 869706ec7..6f011fe2d 100644 --- a/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte +++ b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-input.svelte @@ -1,5 +1,5 @@ diff --git a/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte b/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte index aed885422..87ee8abf3 100644 --- a/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte +++ b/tests/fixtures/rules/no-goto-without-base/valid/absolute-uri01-input.svelte @@ -1,6 +1,6 @@ diff --git a/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte b/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte index 8274f0b23..b11e55f92 100644 --- a/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte +++ b/tests/fixtures/rules/no-goto-without-base/valid/base-aliased01-input.svelte @@ -1,7 +1,8 @@ diff --git a/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte b/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte index 4bdec0cb4..cd7177deb 100644 --- a/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte +++ b/tests/fixtures/rules/no-goto-without-base/valid/base-prefixed01-input.svelte @@ -1,7 +1,8 @@ From f1f7542e2fc28192042115139a48f3a6323e9ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 28 Jan 2024 22:14:14 +0100 Subject: [PATCH 04/14] test(no-goto-without-base): added test errors for the rule --- .../invalid/base-not-prefix01-errors.yaml | 1 - .../invalid/base-not-prefixed01-errors.yaml | 8 ++++++++ ...ix01-input.svelte => base-not-prefixed01-input.svelte} | 0 .../no-goto-without-base/invalid/no-base01-errors.yaml | 5 ++++- 4 files changed, 12 insertions(+), 2 deletions(-) delete mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefixed01-errors.yaml rename tests/fixtures/rules/no-goto-without-base/invalid/{base-not-prefix01-input.svelte => base-not-prefixed01-input.svelte} (100%) diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml deleted file mode 100644 index fe51488c7..000000000 --- a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-errors.yaml +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefixed01-errors.yaml b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefixed01-errors.yaml new file mode 100644 index 000000000..ed8b9578e --- /dev/null +++ b/tests/fixtures/rules/no-goto-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: 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/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte b/tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefixed01-input.svelte similarity index 100% rename from tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefix01-input.svelte rename to tests/fixtures/rules/no-goto-without-base/invalid/base-not-prefixed01-input.svelte diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml index fe51488c7..658fcb47d 100644 --- a/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml +++ b/tests/fixtures/rules/no-goto-without-base/invalid/no-base01-errors.yaml @@ -1 +1,4 @@ -[] +- message: Found a goto() call with a url that isn't prefixed with the base path. + line: 4 + column: 7 + suggestions: null From a88c1884236fd9423af09d1eeb55802ff16e4c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 28 Jan 2024 22:16:40 +0100 Subject: [PATCH 05/14] feat(no-goto-without-base): implemented binary expression parsing --- README.md | 1 + docs/rules.md | 5 +- src/rules/no-goto-without-base.ts | 80 +++++++++++++++++++++++++++++++ src/utils/rules.ts | 2 + 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/rules/no-goto-without-base.ts diff --git a/README.md b/README.md index 3495855a7..ebd15a809 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,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 | | ## Experimental diff --git a/docs/rules.md b/docs/rules.md index f31aefd05..e932ded0a 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -105,8 +105,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 | | -| :------------------------------------------------------------- | :--------------- | :-- | +| Rule ID | Description | | +| :------------------------------------------------------------- | :------------------------------------------ | :-- | +| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | disallow using goto() without the base path | | ## Experimental diff --git a/src/rules/no-goto-without-base.ts b/src/rules/no-goto-without-base.ts new file mode 100644 index 000000000..7e27743b9 --- /dev/null +++ b/src/rules/no-goto-without-base.ts @@ -0,0 +1,80 @@ +import { createRule } from '../utils'; +import { ReferenceTracker } from '@eslint-community/eslint-utils'; +import { getSourceCode } from '../utils/compat'; + +export default createRule('no-goto-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 = extractBasePathNames(referenceTracker); + for (const gotoCall of extractGotoReferences(referenceTracker)) { + if (gotoCall.arguments.length < 1) { + continue; + } + const path = gotoCall.arguments[0]; + if (path.type === 'BinaryExpression') { + if (path.left.type === 'Identifier' && basePathNames.includes(path.left.name)) { + continue; + } + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + continue; + } + if (path.type === 'TemplateLiteral') { + continue; + // TODO Parse literals begining with base + } + if (path.type === 'Literal') { + // TODO: Absolute url + continue; + } + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + } + } + }; + } +}); + +// TODO: Return type +function extractGotoReferences(referenceTracker: ReferenceTracker) { + return Array.from( + referenceTracker.iterateEsmReferences({ + '$app/navigation': { + [ReferenceTracker.ESM]: true, + goto: { + [ReferenceTracker.CALL]: true + } + } + }), + ({ node }) => node + ); +} + +function extractBasePathNames(referenceTracker: ReferenceTracker): string[] { + return Array.from( + referenceTracker.iterateEsmReferences({ + '$app/paths': { + [ReferenceTracker.ESM]: true, + base: { + [ReferenceTracker.READ]: true + } + } + }), + ({ node }) => node.local.name + ); +} diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 7a7713e45..cb19a67a3 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -27,6 +27,7 @@ import noDupeUseDirectives from '../rules/no-dupe-use-directives'; import noDynamicSlotName from '../rules/no-dynamic-slot-name'; import noExportLoadInSvelteModuleInKitPages from '../rules/no-export-load-in-svelte-module-in-kit-pages'; import noExtraReactiveCurlies from '../rules/no-extra-reactive-curlies'; +import noGotoWithoutBase from '../rules/no-goto-without-base'; import noIgnoredUnsubscribe from '../rules/no-ignored-unsubscribe'; import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statements'; import noInlineStyles from '../rules/no-inline-styles'; @@ -90,6 +91,7 @@ export const rules = [ noDynamicSlotName, noExportLoadInSvelteModuleInKitPages, noExtraReactiveCurlies, + noGotoWithoutBase, noIgnoredUnsubscribe, noImmutableReactiveStatements, noInlineStyles, From 0729f84717be52b847e8e74aaf0a9bb39385119d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 28 Jan 2024 22:57:11 +0100 Subject: [PATCH 06/14] feat(no-goto-without-base): implemented absolute URL checking --- src/rules/no-goto-without-base.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/rules/no-goto-without-base.ts b/src/rules/no-goto-without-base.ts index 7e27743b9..49a4b1e2e 100644 --- a/src/rules/no-goto-without-base.ts +++ b/src/rules/no-goto-without-base.ts @@ -30,6 +30,7 @@ export default createRule('no-goto-without-base', { const path = gotoCall.arguments[0]; if (path.type === 'BinaryExpression') { if (path.left.type === 'Identifier' && basePathNames.includes(path.left.name)) { + // The URL is in the form `base + "/foo"`, which is OK continue; } context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); @@ -40,7 +41,12 @@ export default createRule('no-goto-without-base', { // TODO Parse literals begining with base } if (path.type === 'Literal') { - // TODO: Absolute url + const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; + if (absolutePathRegex.test(path.value)) { + // The URL is absolute, which is OK + continue; + } + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); continue; } context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); From 8e96c84b5b6a2f72ed6efd9d9b64b1b0ca00e7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 00:19:46 +0100 Subject: [PATCH 07/14] feat(no-goto-without-base): implemented template literal checking --- src/rules/no-goto-without-base.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/rules/no-goto-without-base.ts b/src/rules/no-goto-without-base.ts index 49a4b1e2e..213e68427 100644 --- a/src/rules/no-goto-without-base.ts +++ b/src/rules/no-goto-without-base.ts @@ -37,8 +37,16 @@ export default createRule('no-goto-without-base', { continue; } if (path.type === 'TemplateLiteral') { + const startingIdentifier = extractStartingIdentifier(path); + if ( + startingIdentifier !== undefined && + basePathNames.includes(startingIdentifier.name) + ) { + // The URL is in the form `${base}/foo`, which is OK + continue; + } + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); continue; - // TODO Parse literals begining with base } if (path.type === 'Literal') { const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; @@ -56,6 +64,22 @@ export default createRule('no-goto-without-base', { } }); +function extractStartingIdentifier(templateLiteral) { + const literalParts = templateLiteral.expressions + .concat(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; + } +} + // TODO: Return type function extractGotoReferences(referenceTracker: ReferenceTracker) { return Array.from( From e132cee7ebdb888ed1433e95db92ac1bf018c890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 00:33:54 +0100 Subject: [PATCH 08/14] chore(no-goto-without-base): added missing function types --- src/rules/no-goto-without-base.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/rules/no-goto-without-base.ts b/src/rules/no-goto-without-base.ts index 213e68427..26c45841f 100644 --- a/src/rules/no-goto-without-base.ts +++ b/src/rules/no-goto-without-base.ts @@ -1,3 +1,4 @@ +import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils'; import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { getSourceCode } from '../utils/compat'; @@ -50,7 +51,7 @@ export default createRule('no-goto-without-base', { } if (path.type === 'Literal') { const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; - if (absolutePathRegex.test(path.value)) { + if (absolutePathRegex.test(path.value?.toString() ?? '')) { // The URL is absolute, which is OK continue; } @@ -64,10 +65,12 @@ export default createRule('no-goto-without-base', { } }); -function extractStartingIdentifier(templateLiteral) { - const literalParts = templateLiteral.expressions - .concat(templateLiteral.quasis) - .sort((a, b) => (a.range[0] < b.range[0] ? -1 : 1)); +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 @@ -78,10 +81,10 @@ function extractStartingIdentifier(templateLiteral) { } return undefined; } + return undefined; } -// TODO: Return type -function extractGotoReferences(referenceTracker: ReferenceTracker) { +function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.CallExpression[] { return Array.from( referenceTracker.iterateEsmReferences({ '$app/navigation': { From c0427c70912f0c3bc42df16912672325bbadb439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 00:52:41 +0100 Subject: [PATCH 09/14] chore(no-goto-without-base): Split code --- src/rules/no-goto-without-base.ts | 70 ++++++++++++++++++------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/rules/no-goto-without-base.ts b/src/rules/no-goto-without-base.ts index 26c45841f..071100511 100644 --- a/src/rules/no-goto-without-base.ts +++ b/src/rules/no-goto-without-base.ts @@ -2,6 +2,7 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils'; import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { getSourceCode } from '../utils/compat'; +import type { RuleContext } from '../types'; export default createRule('no-goto-without-base', { meta: { @@ -29,42 +30,53 @@ export default createRule('no-goto-without-base', { continue; } const path = gotoCall.arguments[0]; - if (path.type === 'BinaryExpression') { - if (path.left.type === 'Identifier' && basePathNames.includes(path.left.name)) { - // The URL is in the form `base + "/foo"`, which is OK - continue; - } - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); - continue; - } - if (path.type === 'TemplateLiteral') { - const startingIdentifier = extractStartingIdentifier(path); - if ( - startingIdentifier !== undefined && - basePathNames.includes(startingIdentifier.name) - ) { - // The URL is in the form `${base}/foo`, which is OK - continue; - } - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); - continue; + 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' }); } - if (path.type === 'Literal') { - const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i; - if (absolutePathRegex.test(path.value?.toString() ?? '')) { - // The URL is absolute, which is OK - continue; - } - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); - continue; - } - context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); } } }; } }); +function checkBinaryExpression( + context: RuleContext, + path: TSESTree.BinaryExpression, + basePathNames: string[] +): void { + if (path.left.type !== 'Identifier' || !basePathNames.includes(path.left.name)) { + context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); + } +} + +function checkTemplateLiteral( + context: RuleContext, + path: TSESTree.TemplateLiteral, + basePathNames: string[] +): void { + const startingIdentifier = extractStartingIdentifier(path); + if (startingIdentifier === undefined || !basePathNames.includes(startingIdentifier.name)) { + 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 { From c68344a406c64e0bdd33d3d3d4254a85b6dee8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 00:54:34 +0100 Subject: [PATCH 10/14] chore(no-goto-without-base): Added a changeset --- .changeset/shaggy-dryers-smoke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaggy-dryers-smoke.md diff --git a/.changeset/shaggy-dryers-smoke.md b/.changeset/shaggy-dryers-smoke.md new file mode 100644 index 000000000..44312f9b8 --- /dev/null +++ b/.changeset/shaggy-dryers-smoke.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +feat: added the no-goto-without-base rule From 89d3bb2831e1b6e6eec8a61958375dd37a55f0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 15:11:31 +0100 Subject: [PATCH 11/14] docs(no-goto-without-base): documented the rule --- docs/rules/no-goto-without-base.md | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/rules/no-goto-without-base.md diff --git a/docs/rules/no-goto-without-base.md b/docs/rules/no-goto-without-base.md new file mode 100644 index 000000000..8b8b5eb2d --- /dev/null +++ b/docs/rules/no-goto-without-base.md @@ -0,0 +1,63 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/no-goto-without-base' +description: 'disallow using goto() without the base path' +--- + +# svelte/no-goto-without-base + +> disallow using goto() without the base path + +- :exclamation: **_This rule has not been released yet._** + +## :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) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-goto-without-base.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/no-goto-without-base.ts) From b3bd172231757c86298fa905f0d91a8cea480543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 15:13:18 +0100 Subject: [PATCH 12/14] test(no-goto-without-base): added a test with aliased goto --- .../no-goto-without-base/invalid/aliased-goto01-errors.yaml | 4 ++++ .../no-goto-without-base/invalid/aliased-goto01-input.svelte | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-errors.yaml create mode 100644 tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-input.svelte diff --git a/tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-errors.yaml b/tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-errors.yaml new file mode 100644 index 000000000..cc35de50a --- /dev/null +++ b/tests/fixtures/rules/no-goto-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/tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-input.svelte b/tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-input.svelte new file mode 100644 index 000000000..00f19edec --- /dev/null +++ b/tests/fixtures/rules/no-goto-without-base/invalid/aliased-goto01-input.svelte @@ -0,0 +1,5 @@ + From 3c6d843dd8c39503df84fb02d0376212989c764a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 29 Jan 2024 15:22:35 +0100 Subject: [PATCH 13/14] chore(no-goto-without-base): lint fix --- docs/rules/no-goto-without-base.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/rules/no-goto-without-base.md b/docs/rules/no-goto-without-base.md index 8b8b5eb2d..e1d0de774 100644 --- a/docs/rules/no-goto-without-base.md +++ b/docs/rules/no-goto-without-base.md @@ -42,8 +42,6 @@ This rule reports navigation using SvelteKit's `goto()` function without prefixi goto('/foo/' + base); goto(`/foo/${base}`); - - ``` From 9a07b52d5e8154d9b30f834c7c2547c510701667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 3 Mar 2024 23:40:12 +0100 Subject: [PATCH 14/14] feat(no-goto-without-base): checking references --- src/rules/no-goto-without-base.ts | 41 +++++++++++++++++++------------ 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/rules/no-goto-without-base.ts b/src/rules/no-goto-without-base.ts index 071100511..9154dc264 100644 --- a/src/rules/no-goto-without-base.ts +++ b/src/rules/no-goto-without-base.ts @@ -2,6 +2,7 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils'; import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { getSourceCode } from '../utils/compat'; +import { findVariable } from '../utils/ast-utils'; import type { RuleContext } from '../types'; export default createRule('no-goto-without-base', { @@ -24,7 +25,7 @@ export default createRule('no-goto-without-base', { const referenceTracker = new ReferenceTracker( getSourceCode(context).scopeManager.globalScope! ); - const basePathNames = extractBasePathNames(referenceTracker); + const basePathNames = extractBasePathReferences(referenceTracker, context); for (const gotoCall of extractGotoReferences(referenceTracker)) { if (gotoCall.arguments.length < 1) { continue; @@ -52,9 +53,9 @@ export default createRule('no-goto-without-base', { function checkBinaryExpression( context: RuleContext, path: TSESTree.BinaryExpression, - basePathNames: string[] + basePathNames: Set ): void { - if (path.left.type !== 'Identifier' || !basePathNames.includes(path.left.name)) { + if (path.left.type !== 'Identifier' || !basePathNames.has(path.left)) { context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); } } @@ -62,10 +63,10 @@ function checkBinaryExpression( function checkTemplateLiteral( context: RuleContext, path: TSESTree.TemplateLiteral, - basePathNames: string[] + basePathNames: Set ): void { const startingIdentifier = extractStartingIdentifier(path); - if (startingIdentifier === undefined || !basePathNames.includes(startingIdentifier.name)) { + if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) { context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' }); } } @@ -110,16 +111,24 @@ function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.Cal ); } -function extractBasePathNames(referenceTracker: ReferenceTracker): string[] { - return Array.from( - referenceTracker.iterateEsmReferences({ - '$app/paths': { - [ReferenceTracker.ESM]: true, - base: { - [ReferenceTracker.READ]: true - } +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 } - }), - ({ node }) => node.local.name - ); + } + })) { + 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; }