Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add svelte/valid-context-access rule #480

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/serious-mayflies-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-svelte": minor
---

feat: Add `svelte/valid-context-access` rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions docs/rules/valid-context-access.md
Original file line number Diff line number Diff line change
@@ -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)
60 changes: 8 additions & 52 deletions src/rules/infinite-reactive-loop.ts
Original file line number Diff line number Diff line change
@@ -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"
Comment on lines +7 to +8
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved few functions to utils dir because I want to use these in valid-context-access rule.


/**
* If `node` is inside of `maybeAncestorNode`, return true.
Expand Down Expand Up @@ -388,7 +342,7 @@
description:
"Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent.",
category: "Possible Errors",
// TODO Switch to recommended in the major version.

Check warning on line 345 in src/rules/infinite-reactive-loop.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO Switch to recommended in the major...'
recommended: false,
},
schema: [],
Expand All @@ -400,12 +354,14 @@
type: "suggestion",
},
create(context) {
const tickCallExpressions = Array.from(
extractSvelteLifeCycleReferences(context, ["tick"]),
)
const taskReferences = Array.from(extractTaskReferences(context))
const reactiveVariableReferences = getReactiveVariableReferences(context)
Comment on lines +357 to +361
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just I moved there lines for performance improvement.


return {
["SvelteReactiveStatement"]: (ast: AST.SvelteReactiveStatement) => {
const tickCallExpressions = extractTickReferences(context)
const taskReferences = extractTaskReferences(context)
const reactiveVariableReferences =
getReactiveVariableReferences(context)
const trackedVariableNodes = getTrackedVariableNodes(
reactiveVariableReferences,
ast,
Expand Down
37 changes: 37 additions & 0 deletions src/rules/reference-helpers/microtask.ts
Original file line number Diff line number Diff line change
@@ -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],
}
}
}
42 changes: 42 additions & 0 deletions src/rules/reference-helpers/svelte-context.ts
Original file line number Diff line number Diff line change
@@ -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],
}
}
}
53 changes: 53 additions & 0 deletions src/rules/reference-helpers/svelte-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -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],
}
}
}