diff --git a/.changeset/serious-mayflies-yell.md b/.changeset/serious-mayflies-yell.md new file mode 100644 index 000000000..a03e61934 --- /dev/null +++ b/.changeset/serious-mayflies-yell.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +feat: Add `svelte/valid-context-access` rule diff --git a/README.md b/README.md index a5661af1c..17599fee5 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 SvelteKit page components. | | ## Security Vulnerability diff --git a/docs/rules.md b/docs/rules.md index ef85cb467..968875980 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 SvelteKit 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..365b4b23e --- /dev/null +++ b/docs/rules/valid-context-access.md @@ -0,0 +1,79 @@ +--- +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: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge> + +## :book: Rule Details + +This rule reports where context API is called except during component initialization. + +<ESLintCodeBlock> + +<!--eslint-skip--> + +```svelte +<script> + /* eslint svelte/valid-context-access: "error" */ + import { setContext, onMount } from "svelte" + + /** ✓ GOOD */ + setContext("answer", 42) + ;(() => { + setContext("answer", 42) + })() + + const init = () => { + setContext("answer", 42) + } + + init() + + /** ✗ BAD */ + const update = () => { + setContext("answer", 42) + } + + onMount(() => { + update() + setContext("answer", 42) + }) + + const update2 = async () => { + await Promise.resolve() + setContext("answer", 42) + } + + ;(async () => { + await Promise.resolve() + setContext("answer", 42) + })() +</script> +``` + +</ESLintCodeBlock> + +- :warning: This rule only inspects Svelte files, not JS / TS files. + +## :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/infinite-reactive-loop.ts b/src/rules/infinite-reactive-loop.ts index 13ea8741a..fe0075e16 100644 --- a/src/rules/infinite-reactive-loop.ts +++ b/src/rules/infinite-reactive-loop.ts @@ -1,57 +1,11 @@ import type { TSESTree } from "@typescript-eslint/types" import type { AST } from "svelte-eslint-parser" -import { ReferenceTracker } from "@eslint-community/eslint-utils" import { createRule } from "../utils" import type { RuleContext } from "../types" import { findVariable } from "../utils/ast-utils" import { traverseNodes } from "svelte-eslint-parser" - -/** - * Get usage of `tick` - */ -function extractTickReferences( - context: RuleContext, -): { node: TSESTree.CallExpression; name: string }[] { - const referenceTracker = new ReferenceTracker( - context.getSourceCode().scopeManager.globalScope!, - ) - const a = referenceTracker.iterateEsmReferences({ - svelte: { - [ReferenceTracker.ESM]: true, - tick: { - [ReferenceTracker.CALL]: true, - }, - }, - }) - return Array.from(a).map(({ node, path }) => { - return { - node: node as TSESTree.CallExpression, - name: path[path.length - 1], - } - }) -} - -/** - * Get usage of `setTimeout`, `setInterval`, `queueMicrotask` - */ -function extractTaskReferences( - context: RuleContext, -): { node: TSESTree.CallExpression; name: string }[] { - const referenceTracker = new ReferenceTracker( - context.getSourceCode().scopeManager.globalScope!, - ) - const a = referenceTracker.iterateGlobalReferences({ - setTimeout: { [ReferenceTracker.CALL]: true }, - setInterval: { [ReferenceTracker.CALL]: true }, - queueMicrotask: { [ReferenceTracker.CALL]: true }, - }) - return Array.from(a).map(({ node, path }) => { - return { - node: node as TSESTree.CallExpression, - name: path[path.length - 1], - } - }) -} +import { extractSvelteLifeCycleReferences } from "./reference-helpers/svelte-lifecycle" +import { extractTaskReferences } from "./reference-helpers/microtask" /** * If `node` is inside of `maybeAncestorNode`, return true. @@ -400,12 +354,14 @@ export default createRule("infinite-reactive-loop", { type: "suggestion", }, create(context) { + const tickCallExpressions = Array.from( + extractSvelteLifeCycleReferences(context, ["tick"]), + ) + const taskReferences = Array.from(extractTaskReferences(context)) + const reactiveVariableReferences = getReactiveVariableReferences(context) + return { ["SvelteReactiveStatement"]: (ast: AST.SvelteReactiveStatement) => { - const tickCallExpressions = extractTickReferences(context) - const taskReferences = extractTaskReferences(context) - const reactiveVariableReferences = - getReactiveVariableReferences(context) const trackedVariableNodes = getTrackedVariableNodes( reactiveVariableReferences, ast, diff --git a/src/rules/reference-helpers/microtask.ts b/src/rules/reference-helpers/microtask.ts new file mode 100644 index 000000000..073b968a2 --- /dev/null +++ b/src/rules/reference-helpers/microtask.ts @@ -0,0 +1,37 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { ReferenceTracker } from "@eslint-community/eslint-utils" +import type { RuleContext } from "../../types" + +type FunctionName = "setTimeout" | "setInterval" | "queueMicrotask" + +/** + * Get usage of `setTimeout`, `setInterval`, `queueMicrotask` + */ +export function* extractTaskReferences( + context: RuleContext, + functionNames: FunctionName[] = [ + "setTimeout", + "setInterval", + "queueMicrotask", + ], +): Generator<{ node: TSESTree.CallExpression; name: string }, void> { + const referenceTracker = new ReferenceTracker( + context.getSourceCode().scopeManager.globalScope!, + ) + for (const { node, path } of referenceTracker.iterateGlobalReferences({ + setTimeout: { + [ReferenceTracker.CALL]: functionNames.includes("setTimeout"), + }, + setInterval: { + [ReferenceTracker.CALL]: functionNames.includes("setInterval"), + }, + queueMicrotask: { + [ReferenceTracker.CALL]: functionNames.includes("queueMicrotask"), + }, + })) { + yield { + node: node as TSESTree.CallExpression, + name: path[path.length - 1], + } + } +} 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/reference-helpers/svelte-lifecycle.ts b/src/rules/reference-helpers/svelte-lifecycle.ts new file mode 100644 index 000000000..2b05069b7 --- /dev/null +++ b/src/rules/reference-helpers/svelte-lifecycle.ts @@ -0,0 +1,53 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { ReferenceTracker } from "@eslint-community/eslint-utils" +import type { RuleContext } from "../../types" + +type LifeCycleName = + | "onMount" + | "beforeUpdate" + | "afterUpdate" + | "onDestroy" + | "tick" + +/** + * Get usage of Svelte life cycle functions. + */ +export function* extractSvelteLifeCycleReferences( + context: RuleContext, + fuctionName: LifeCycleName[] = [ + "onMount", + "beforeUpdate", + "afterUpdate", + "onDestroy", + "tick", + ], +): 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, + onMount: { + [ReferenceTracker.CALL]: fuctionName.includes("onMount"), + }, + beforeUpdate: { + [ReferenceTracker.CALL]: fuctionName.includes("beforeUpdate"), + }, + afterUpdate: { + [ReferenceTracker.CALL]: fuctionName.includes("afterUpdate"), + }, + onDestroy: { + [ReferenceTracker.CALL]: fuctionName.includes("onDestroy"), + }, + tick: { + [ReferenceTracker.CALL]: fuctionName.includes("tick"), + }, + }, + })) { + 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..cd8490f1e --- /dev/null +++ b/src/rules/valid-context-access.ts @@ -0,0 +1,223 @@ +import { createRule } from "../utils" +import { extractContextReferences } from "./reference-helpers/svelte-context" +import { extractSvelteLifeCycleReferences } from "./reference-helpers/svelte-lifecycle" +import { extractTaskReferences } from "./reference-helpers/microtask" +import { isInsideOfPromiseThenOrCatch } from "../utils/promise" +import type { AST } from "svelte-eslint-parser" +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", + recommended: false, + }, + schema: [], + messages: { + unexpected: + "Do not call {{function}} except during component initialization.", + }, + type: "problem", + }, + create(context) { + // // This rule doesn't check other than Svelte files. + if (!context.parserServices.isSvelte) { + return {} + } + + const sourceCode = context.getSourceCode() + const lifeCycleReferences = Array.from( + extractSvelteLifeCycleReferences(context), + ).map((r) => r.node) + const taskReferences = Array.from(extractTaskReferences(context)) + + // Extract <script> blocks that is not module=context. + const scriptNotModuleElements: AST.SvelteScriptElement[] = + sourceCode.ast.body.filter((b) => { + if (b.type !== "SvelteScriptElement") return false + const isModule = b.startTag.attributes.some((a) => { + return ( + a.type === "SvelteAttribute" && + a.key.name === "context" && + a.value.some( + (v) => v.type === "SvelteLiteral" && v.value === "module", + ) + ) + }) + return !isModule + }) as AST.SvelteScriptElement[] + + const scopeManager = sourceCode.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 [] + } + + /** Return true if tnodeB is inside of nodeA. */ + function isInsideOf( + nodeA: TSESTree.Node | AST.SvelteScriptElement, + nodeB: TSESTree.Node, + ) { + return ( + nodeA.range[0] <= nodeB.range[0] && nodeB.range[1] <= nodeA.range[1] + ) + } + + /** Return true if the node is there inside of <script> block that is not module=context. */ + function isInsideOfSvelteScriptElement(node: TSESTree.Node) { + for (const script of scriptNotModuleElements) { + if (isInsideOf(script, node)) { + return true + } + } + return false + } + + const awaitExpressions: { + belongingFunction: + | TSESTree.FunctionDeclaration + | TSESTree.VariableDeclaration + | TSESTree.ArrowFunctionExpression + node: TSESTree.Node + }[] = [] + + /** Return true if the given node is later than the await expression. */ + function isAfterAwait(node: TSESTree.CallExpression) { + for (const awaitExpression of awaitExpressions) { + const { belongingFunction, node: awaitNode } = awaitExpression + if (isInsideOf(node, belongingFunction)) { + continue + } + return awaitNode.range[0] <= node.range[0] + } + return false + } + + /** Return true if node is inside of task function */ + function isInsideTaskReference(node: TSESTree.CallExpression) { + for (const taskReference of taskReferences) { + if (isInsideOf(taskReference.node, node)) { + return true + } + } + return false + } + + /** Let's lint! */ + function doLint( + visitedCallExpressions: TSESTree.CallExpression[], + contextCallExpression: TSESTree.CallExpression, + currentNode: TSESTree.CallExpression, + ) { + // Report if context function is called outside of <script> block. + if (!isInsideOfSvelteScriptElement(currentNode)) { + report(contextCallExpression) + return + } + + if (isAfterAwait(currentNode)) { + report(contextCallExpression) + return + } + + if (isInsideTaskReference(currentNode)) { + report(contextCallExpression) + return + } + + if (isInsideOfPromiseThenOrCatch(currentNode)) { + report(contextCallExpression) + return + } + + let { parent } = currentNode + 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 (lifeCycleReferences.includes(parent.expression)) { + report(contextCallExpression) + } + } + } + } + + return { + "Program:exit"() { + for (const { node } of extractContextReferences(context)) { + const visitedCallExpressions: TSESTree.CallExpression[] = [] + doLint(visitedCallExpressions, node, node) + } + }, + AwaitExpression(node) { + let parent: TSESTree.Node | undefined = node.parent + while (parent) { + if ( + parent.type === "FunctionDeclaration" || + parent.type === "VariableDeclaration" || + parent.type === "ArrowFunctionExpression" + ) { + awaitExpressions.push({ belongingFunction: parent, node }) + } + parent = parent.parent + } + }, + } + }, +}) diff --git a/src/utils/promise.ts b/src/utils/promise.ts new file mode 100644 index 000000000..0e97e1ee9 --- /dev/null +++ b/src/utils/promise.ts @@ -0,0 +1,31 @@ +import type { TSESTree } from "@typescript-eslint/types" + +/** + * Return true if `node` is inside of `then` or `catch`. + */ +export function isInsideOfPromiseThenOrCatch(node: TSESTree.Node): boolean { + let parent: TSESTree.Node | undefined = node.parent + while (parent) { + parent = parent.parent + if (parent?.type !== "ExpressionStatement") { + continue + } + const expression = parent?.expression + if (expression == null || expression?.type !== "CallExpression") { + return false + } + + const callee = expression.callee + if (callee.type !== "MemberExpression") { + return false + } + + const property = callee.property + return ( + property.type === "Identifier" && + ["then", "catch"].includes(property.name) + ) + } + + return false +} 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 @@ +<script> + import { + setContext, + getContext, + hasContext, + getAllContexts, + onMount, + } from "svelte" + + const update1 = () => { + setContext("answer", 42) + getContext("answer") + hasContext("answer") + getAllContexts() + } + + const update2 = function () { + setContext("answer", 42) + getContext("answer") + hasContext("answer") + getAllContexts() + } + + function update3() { + setContext("answer", 42) + getContext("answer") + hasContext("answer") + getAllContexts() + } + + onMount(() => { + update1() + update2() + update3() + }) +</script> 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 @@ +<script> + import { setContext, onMount } from "svelte" + + onMount(() => { + setContext("answer", 42) + }) +</script> 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..2df88bc18 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case03-input.svelte @@ -0,0 +1,8 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + setContext("answer", 42) + } +</script> + +<button on:click={() => something()}>Click Me</button> 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 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + setContext("answer", 42) + } + + something() +</script> + +{#if true} + {@const foo = something()} + <button>Click Me</button> +{/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 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + setContext("answer", 42) + } + + something() +</script> + +{#if something()} + <button>Click Me</button> +{/if} diff --git a/tests/fixtures/rules/valid-context-access/invalid/case06-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case06-errors.yaml new file mode 100644 index 000000000..9d0ec949e --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case06-errors.yaml @@ -0,0 +1,4 @@ +- message: Do not call setContext except during component initialization. + line: 5 + column: 7 + suggestions: null diff --git a/tests/fixtures/rules/valid-context-access/invalid/case06-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case06-input.svelte new file mode 100644 index 000000000..310840bb8 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case06-input.svelte @@ -0,0 +1,15 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + const inner = () => { + setContext("answer", 42) + } + inner() + } + + something() +</script> + +{#if something()} + <button>Click Me</button> +{/if} diff --git a/tests/fixtures/rules/valid-context-access/invalid/case07-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case07-errors.yaml new file mode 100644 index 000000000..5702a7bed --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case07-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call hasContext except during component initialization. + line: 5 + column: 9 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case07-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case07-input.svelte new file mode 100644 index 000000000..5484f2dc9 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case07-input.svelte @@ -0,0 +1,9 @@ +<script> + import { hasContext, onMount } from "svelte" + + onMount(() => { + if (hasContext("answer")) { + console.log("The answer exist") + } + }) +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case08-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case08-errors.yaml new file mode 100644 index 000000000..6e01eaa01 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case08-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call setContext except during component initialization. + line: 6 + column: 5 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case08-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case08-input.svelte new file mode 100644 index 000000000..574c3b3ab --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case08-input.svelte @@ -0,0 +1,9 @@ +<script> + import { setContext } from "svelte" + + const something = async () => { + await Promise.resolve() + setContext("answer", 42) + } + something() +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case09-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case09-errors.yaml new file mode 100644 index 000000000..8a807194f --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case09-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call setContext except during component initialization. + line: 3 + column: 3 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case09-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case09-input.svelte new file mode 100644 index 000000000..0017cf865 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case09-input.svelte @@ -0,0 +1,4 @@ +<script context="module"> + import { setContext } from "svelte" + setContext("answer", 42) +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case10-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case10-errors.yaml new file mode 100644 index 000000000..f33732b66 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case10-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/case10-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case10-input.svelte new file mode 100644 index 000000000..0d504f5ff --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case10-input.svelte @@ -0,0 +1,7 @@ +<script> + import { setContext } from "svelte" + + setTimeout(() => { + setContext("answer", 42) + }) +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case11-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case11-errors.yaml new file mode 100644 index 000000000..0897a75a9 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case11-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call setContext except during component initialization. + line: 6 + column: 7 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case11-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case11-input.svelte new file mode 100644 index 000000000..dc9f3571b --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case11-input.svelte @@ -0,0 +1,11 @@ +<script> + import { setContext } from "svelte" + + const doSomething = () => { + setTimeout(() => { + setContext("answer", 42) + }) + } + + doSomething() +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case12-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case12-errors.yaml new file mode 100644 index 000000000..28b8f23b8 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case12-errors.yaml @@ -0,0 +1,12 @@ +- message: Do not call setContext except during component initialization. + line: 5 + column: 5 +- message: Do not call setContext except during component initialization. + line: 9 + column: 5 +- message: Do not call setContext except during component initialization. + line: 13 + column: 5 +- message: Do not call setContext except during component initialization. + line: 17 + column: 5 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case12-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case12-input.svelte new file mode 100644 index 000000000..c170ecc49 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case12-input.svelte @@ -0,0 +1,19 @@ +<script> + import { setContext } from "svelte" + + Promise.resolve().then(() => { + setContext("answer", 42) + }) + + Promise.resolve().then(function () { + setContext("answer", 42) + }) + + Promise.resolve().catch(() => { + setContext("answer", 42) + }) + + Promise.resolve().catch(function () { + setContext("answer", 42) + }) +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case13-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case13-errors.yaml new file mode 100644 index 000000000..f33732b66 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case13-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/case13-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case13-input.svelte new file mode 100644 index 000000000..80ddb16c4 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case13-input.svelte @@ -0,0 +1,7 @@ +<script> + import { setContext } from "svelte" + ;(async () => { + await Promise.resolve() + setContext("answer", 42) + })() +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case14-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case14-errors.yaml new file mode 100644 index 000000000..4055b4a9a --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case14-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call setContext except during component initialization. + line: 4 + column: 5 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case14-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case14-input.svelte new file mode 100644 index 000000000..e8a9b151e --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case14-input.svelte @@ -0,0 +1,10 @@ +<script> + import { setContext, onMount } from "svelte" + const doSomething = () => { + setContext("answer", 42) + } + + onMount(() => { + doSomething() + }) +</script> diff --git a/tests/fixtures/rules/valid-context-access/invalid/case15-errors.yaml b/tests/fixtures/rules/valid-context-access/invalid/case15-errors.yaml new file mode 100644 index 000000000..4055b4a9a --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case15-errors.yaml @@ -0,0 +1,3 @@ +- message: Do not call setContext except during component initialization. + line: 4 + column: 5 diff --git a/tests/fixtures/rules/valid-context-access/invalid/case15-input.svelte b/tests/fixtures/rules/valid-context-access/invalid/case15-input.svelte new file mode 100644 index 000000000..f0df9c344 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/invalid/case15-input.svelte @@ -0,0 +1,11 @@ +<script> + import { setContext, onMount } from "svelte" + const doSomething = () => { + setContext("answer", 42) + } + + ;(async () => { + await Promise.resolve() + doSomething() + })() +</script> 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 @@ +<script> + import { setContext, getContext, hasContext, getAllContexts } from "svelte" + + setContext("answer", 42) + getContext("answer") + hasContext("answer") + getAllContexts() +</script> 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 @@ +<script> + import { setContext, onMount } from "svelte" + ;(() => { + setContext("answer", 42) + })() +</script> 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..461a5f1f3 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case03-input.svelte @@ -0,0 +1,8 @@ +<script> + import { setContext } from "svelte" + const something = () => { + setContext("answer", 42) + } + + something() +</script> 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 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + setContext("answer", 42) + } + + const something2 = () => { + something() + } + + ;(() => { + something() + something2() + })() +</script> 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 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + setContext("answer", 42) + } +</script> 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 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + something2() + setContext("answer", 42) + } + + const something2 = () => { + something() + setContext("answer", 42) + } + + something() + something2() +</script> 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 @@ +<script> + import { setContext, onMount } from "svelte" + const something = () => { + setContext("answer", 42) + } + + something() +</script> + +{#if something} + <button>Click Me</button> +{/if} diff --git a/tests/fixtures/rules/valid-context-access/valid/case08-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case08-input.svelte new file mode 100644 index 000000000..c0b2b1d8b --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case08-input.svelte @@ -0,0 +1,22 @@ +<script> + import { setContext, onMount } from "svelte" + + if (setContext("answer", 42)) { + console.log("setContext") + } else if (setContext("answer", 43)) { + console.log("setContext") + } + + switch (setContext("answer", 42)) { + case 1: + console.log("setContext") + break + case 2: + console.log("setContext") + break + default: + console.log("setContext") + } + + const foo = setContext("answer", 42) ? "bar" : "baz" +</script> diff --git a/tests/fixtures/rules/valid-context-access/valid/case09-input.js b/tests/fixtures/rules/valid-context-access/valid/case09-input.js new file mode 100644 index 000000000..4d3f8fe4c --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case09-input.js @@ -0,0 +1,17 @@ +import { setContext } from "svelte" + +const something = () => { + setContext("answer", 42) +} + +const something2 = async () => { + await Promise.resolve() + setContext("answer", 42) +} + +const aaa = (fn) => { + fn() +} + +aaa(() => something()) +something2() diff --git a/tests/fixtures/rules/valid-context-access/valid/case10-input.svelte b/tests/fixtures/rules/valid-context-access/valid/case10-input.svelte new file mode 100644 index 000000000..fac4aafc3 --- /dev/null +++ b/tests/fixtures/rules/valid-context-access/valid/case10-input.svelte @@ -0,0 +1,6 @@ +<script> + import { setContext, onMount } from "svelte" + + const something = (fn) => fn() + something(() => setContext("answer", 42)) +</script> diff --git a/tests/src/rules/valid-context-access.ts b/tests/src/rules/valid-context-access.ts new file mode 100644 index 000000000..f40dafd6d --- /dev/null +++ b/tests/src/rules/valid-context-access.ts @@ -0,0 +1,20 @@ +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", + }, + env: { + browser: true, + es2017: true, + }, +}) + +tester.run( + "valid-context-access", + rule as any, + loadTestCases("valid-context-access"), +)