From 766cd04ed325549fd7b45f2bc2b5b70927093394 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Sun, 14 May 2023 20:04:05 +0900 Subject: [PATCH] implement --- README.md | 1 + docs/rules.md | 1 + docs/rules/valid-context-access.md | 67 +++++++++++ src/rules/reference-helpers/svelte-context.ts | 42 +++++++ src/rules/valid-context-access.ts | 113 ++++++++++++++++++ src/utils/rules.ts | 2 + .../invalid/case01-errors.yaml | 36 ++++++ .../invalid/case01-input.svelte | 36 ++++++ .../invalid/case02-errors.yaml | 3 + .../invalid/case02-input.svelte | 7 ++ .../invalid/case03-errors.yaml | 4 + .../invalid/case03-input.svelte | 10 ++ .../invalid/case04-errors.yaml | 4 + .../invalid/case04-input.svelte | 13 ++ .../invalid/case05-errors.yaml | 4 + .../invalid/case05-input.svelte | 12 ++ .../valid/case01-input.svelte | 8 ++ .../valid/case02-input.svelte | 6 + .../valid/case03-input.svelte | 8 ++ .../valid/case04-input.svelte | 15 +++ .../valid/case05-input.svelte | 6 + .../valid/case06-input.svelte | 15 +++ .../valid/case07-input.svelte | 12 ++ tests/src/rules/valid-context-access.ts | 16 +++ 24 files changed, 441 insertions(+) create mode 100644 docs/rules/valid-context-access.md create mode 100644 src/rules/reference-helpers/svelte-context.ts create mode 100644 src/rules/valid-context-access.ts create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case01-errors.yaml create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case01-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case02-errors.yaml create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case02-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case03-errors.yaml create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case03-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case04-errors.yaml create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case04-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case05-errors.yaml create mode 100644 tests/fixtures/rules/valid-context-access/invalid/case05-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case01-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case02-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case03-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case04-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case05-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case06-input.svelte create mode 100644 tests/fixtures/rules/valid-context-access/valid/case07-input.svelte create mode 100644 tests/src/rules/valid-context-access.ts diff --git a/README.md b/README.md index d7ba245c5..941239f10 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | | | [svelte/require-store-reactive-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-reactive-access/) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :wrench: | | [svelte/valid-compile](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | :star: | +| [svelte/valid-context-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-context-access/) | context functions must be called during component initialization. | | | [svelte/valid-prop-names-in-kit-pages](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-prop-names-in-kit-pages/) | disallow props other than data or errors in Svelte Kit page components. | | ## Security Vulnerability diff --git a/docs/rules.md b/docs/rules.md index 276f34ff8..fbb175c43 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -33,6 +33,7 @@ These rules relate to possible syntax or logic errors in Svelte code: | [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | | | [svelte/require-store-reactive-access](./rules/require-store-reactive-access.md) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :wrench: | | [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | :star: | +| [svelte/valid-context-access](./rules/valid-context-access.md) | context functions must be called during component initialization. | | | [svelte/valid-prop-names-in-kit-pages](./rules/valid-prop-names-in-kit-pages.md) | disallow props other than data or errors in Svelte Kit page components. | | ## Security Vulnerability diff --git a/docs/rules/valid-context-access.md b/docs/rules/valid-context-access.md new file mode 100644 index 000000000..ff113111f --- /dev/null +++ b/docs/rules/valid-context-access.md @@ -0,0 +1,67 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/valid-context-access" +description: "context functions must be called during component initialization." +--- + +# svelte/valid-context-access + +> context functions must be called during component initialization. + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule reports where context API is called except during component initialization. + + + + + +```svelte + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [Svelte - Docs > RUN TIME > svelte > setContext](https://svelte.dev/docs#run-time-svelte-setcontext) +- [Svelte - Docs > RUN TIME > svelte > getContext](https://svelte.dev/docs#run-time-svelte-getContext) +- [Svelte - Docs > RUN TIME > svelte > hasContext](https://svelte.dev/docs#run-time-svelte-hasContext) +- [Svelte - Docs > RUN TIME > svelte > getAllContexts](https://svelte.dev/docs#run-time-svelte-getAllContexts) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/valid-context-access.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/valid-context-access.ts) diff --git a/src/rules/reference-helpers/svelte-context.ts b/src/rules/reference-helpers/svelte-context.ts new file mode 100644 index 000000000..ebeb8f57a --- /dev/null +++ b/src/rules/reference-helpers/svelte-context.ts @@ -0,0 +1,42 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { ReferenceTracker } from "@eslint-community/eslint-utils" +import type { RuleContext } from "../../types" + +type ContextName = "setContext" | "getContext" | "hasContext" | "getAllContexts" + +/** Extract svelte's context API references */ +export function* extractContextReferences( + context: RuleContext, + contextNames: ContextName[] = [ + "setContext", + "getContext", + "hasContext", + "getAllContexts", + ], +): Generator<{ node: TSESTree.CallExpression; name: string }, void> { + const referenceTracker = new ReferenceTracker( + context.getSourceCode().scopeManager.globalScope!, + ) + for (const { node, path } of referenceTracker.iterateEsmReferences({ + svelte: { + [ReferenceTracker.ESM]: true, + setContext: { + [ReferenceTracker.CALL]: contextNames.includes("setContext"), + }, + getContext: { + [ReferenceTracker.CALL]: contextNames.includes("getContext"), + }, + hasContext: { + [ReferenceTracker.CALL]: contextNames.includes("hasContext"), + }, + getAllContexts: { + [ReferenceTracker.CALL]: contextNames.includes("getAllContexts"), + }, + }, + })) { + yield { + node: node as TSESTree.CallExpression, + name: path[path.length - 1], + } + } +} diff --git a/src/rules/valid-context-access.ts b/src/rules/valid-context-access.ts new file mode 100644 index 000000000..bcc6ca5b3 --- /dev/null +++ b/src/rules/valid-context-access.ts @@ -0,0 +1,113 @@ +import { createRule } from "../utils" +import { extractContextReferences } from "./reference-helpers/svelte-context" +import type { TSESTree } from "@typescript-eslint/types" + +export default createRule("valid-context-access", { + meta: { + docs: { + description: + "context functions must be called during component initialization.", + category: "Possible Errors", + // TODO Switch to recommended in the major version. + recommended: false, + }, + schema: [], + messages: { + unexpected: + "Do not call {{function}} except during component initialization.", + }, + type: "problem", + }, + create(context) { + const scopeManager = context.getSourceCode().scopeManager + const toplevelScope = + scopeManager.globalScope?.childScopes.find( + (scope) => scope.type === "module", + ) || scopeManager.globalScope + + /** report ESLint error */ + function report(node: TSESTree.CallExpression) { + context.report({ + loc: node.loc, + messageId: "unexpected", + data: { + function: + node.callee.type === "Identifier" + ? node.callee.name + : "context function", + }, + }) + } + + /** Get nodes where the variable is used */ + function getReferences(id: TSESTree.Identifier | TSESTree.BindingName) { + const variable = toplevelScope?.variables.find((v) => { + if (id.type === "Identifier") { + return v.identifiers.includes(id) + } + return false + }) + if (variable) { + return variable.references.filter((r) => r.identifier !== id) + } + return [] + } + + /** Let's lint! */ + function doLint( + visitedCallExpressions: TSESTree.CallExpression[], + contextCallExpression: TSESTree.CallExpression, + currentNode: TSESTree.CallExpression, + ) { + let { parent } = currentNode + if (parent?.type !== "ExpressionStatement") { + report(contextCallExpression) + return + } + while (parent) { + parent = parent.parent + if ( + parent?.type === "VariableDeclaration" || + parent?.type === "FunctionDeclaration" + ) { + const references = + parent.type === "VariableDeclaration" + ? getReferences(parent.declarations[0].id) + : parent.id + ? getReferences(parent.id) + : [] + + for (const reference of references) { + if (reference.identifier?.parent?.type === "CallExpression") { + if ( + !visitedCallExpressions.includes(reference.identifier.parent) + ) { + visitedCallExpressions.push(reference.identifier.parent) + doLint( + visitedCallExpressions, + contextCallExpression, + reference.identifier?.parent, + ) + } + } + } + } else if (parent?.type === "ExpressionStatement") { + if (parent.expression.type !== "CallExpression") { + report(contextCallExpression) + } else if (parent.expression.callee.type === "Identifier") { + report(contextCallExpression) + } + } + } + } + + return { + Program() { + for (const { node } of extractContextReferences(context)) { + const visitedCallExpressions: TSESTree.CallExpression[] = [] + doLint(visitedCallExpressions, node, node) + } + }, + } + }, +}) diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 7dac11750..487616784 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -57,6 +57,7 @@ import sortAttributes from "../rules/sort-attributes" import spacedHtmlComment from "../rules/spaced-html-comment" import system from "../rules/system" import validCompile from "../rules/valid-compile" +import validContextAccess from "../rules/valid-context-access" import validEachKey from "../rules/valid-each-key" import validPropNamesInKitPages from "../rules/valid-prop-names-in-kit-pages" @@ -116,6 +117,7 @@ export const rules = [ spacedHtmlComment, system, validCompile, + validContextAccess, validEachKey, validPropNamesInKitPages, ] as RuleModule[] diff --git a/tests/fixtures/rules/valid-context-access/invalid/case01-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case01-errors.yaml new file mode 100644 index 000000000..22f67eb10 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case01-errors.yaml @@ -0,0 +1,36 @@ +- message: Do not call setContext except during component initialization. + line: 11 + column: 5 +- message: Do not call getContext except during component initialization. + line: 12 + column: 5 +- message: Do not call hasContext except during component initialization. + line: 13 + column: 5 +- message: Do not call getAllContexts except during component initialization. + line: 14 + column: 5 +- message: Do not call setContext except during component initialization. + line: 18 + column: 5 +- message: Do not call getContext except during component initialization. + line: 19 + column: 5 +- message: Do not call hasContext except during component initialization. + line: 20 + column: 5 +- message: Do not call getAllContexts except during component initialization. + line: 21 + column: 5 +- message: Do not call setContext except during component initialization. + line: 25 + column: 5 +- message: Do not call getContext except during component initialization. + line: 26 + column: 5 +- message: Do not call hasContext except during component initialization. + line: 27 + column: 5 +- message: Do not call getAllContexts except during component initialization. + line: 28 + column: 5 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case01-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case01-input.svelte new file mode 100644 index 000000000..859baf1bb --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case01-input.svelte @@ -0,0 +1,36 @@ + diff --git a/tests/fixtures/rules/valid-context-access/invalid/case02-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case02-errors.yaml new file mode 100644 index 000000000..f33732b66 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case02-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call setContext except during component initialization. + line: 5 + column: 5 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case02-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case02-input.svelte new file mode 100644 index 000000000..b0af74395 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case02-input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/rules/valid-context-access/invalid/case03-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case03-errors.yaml new file mode 100644 index 000000000..96c6f1b93 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case03-errors.yaml @@ -0,0 +1,4 @@ +- message: Do not call setContext except during component initialization. + line: 4 + column: 5 + suggestions: null diff --git a/tests/fixtures/rules/valid-context-access/invalid/case03-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case03-input.svelte new file mode 100644 index 000000000..c88a5a69c --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case03-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/tests/fixtures/rules/valid-context-access/invalid/case04-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case04-errors.yaml new file mode 100644 index 000000000..96c6f1b93 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case04-errors.yaml @@ -0,0 +1,4 @@ +- message: Do not call setContext except during component initialization. + line: 4 + column: 5 + suggestions: null diff --git a/tests/fixtures/rules/valid-context-access/invalid/case04-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case04-input.svelte new file mode 100644 index 000000000..4b62a8d4e --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case04-input.svelte @@ -0,0 +1,13 @@ + + +{#if true} + {@const foo = something()} + +{/if} diff --git a/tests/fixtures/rules/valid-context-access/invalid/case05-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case05-errors.yaml new file mode 100644 index 000000000..96c6f1b93 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case05-errors.yaml @@ -0,0 +1,4 @@ +- message: Do not call setContext except during component initialization. + line: 4 + column: 5 + suggestions: null diff --git a/tests/fixtures/rules/valid-context-access/invalid/case05-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case05-input.svelte new file mode 100644 index 000000000..b21f3b9b8 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case05-input.svelte @@ -0,0 +1,12 @@ + + +{#if something()} + +{/if} diff --git a/tests/fixtures/rules/valid-context-access/valid/case01-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case01-input.svelte new file mode 100644 index 000000000..3ec7fa441 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case01-input.svelte @@ -0,0 +1,8 @@ + diff --git a/tests/fixtures/rules/valid-context-access/valid/case02-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case02-input.svelte new file mode 100644 index 000000000..bab6b283a --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case02-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/valid-context-access/valid/case03-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case03-input.svelte new file mode 100644 index 000000000..9bb8171c7 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case03-input.svelte @@ -0,0 +1,8 @@ + diff --git a/tests/fixtures/rules/valid-context-access/valid/case04-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case04-input.svelte new file mode 100644 index 000000000..852518424 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case04-input.svelte @@ -0,0 +1,15 @@ + diff --git a/tests/fixtures/rules/valid-context-access/valid/case05-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case05-input.svelte new file mode 100644 index 000000000..49cce3bc7 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case05-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/valid-context-access/valid/case06-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case06-input.svelte new file mode 100644 index 000000000..17e2ae7e2 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case06-input.svelte @@ -0,0 +1,15 @@ + diff --git a/tests/fixtures/rules/valid-context-access/valid/case07-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case07-input.svelte new file mode 100644 index 000000000..24c5ae0a4 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case07-input.svelte @@ -0,0 +1,12 @@ + + +{#if something} + +{/if} diff --git a/tests/src/rules/valid-context-access.ts b/tests/src/rules/valid-context-access.ts new file mode 100644 index 000000000..815950c1d --- /dev/null +++ b/tests/src/rules/valid-context-access.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "eslint" +import rule from "../../../src/rules/valid-context-access" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "valid-context-access", + rule as any, + loadTestCases("valid-context-access"), +)