Skip to content

Commit d093f81

Browse files
committed
implement
1 parent afc47e4 commit d093f81

24 files changed

+441
-0
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
320320
| [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 | |
321321
| [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: |
322322
| [svelte/valid-compile](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | :star: |
323+
| [svelte/valid-context-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-context-access/) | context functions must be called during component initialization. | |
323324
| [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. | |
324325

325326
## Security Vulnerability

Diff for: docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
3333
| [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | |
3434
| [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: |
3535
| [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | :star: |
36+
| [svelte/valid-context-access](./rules/valid-context-access.md) | context functions must be called during component initialization. | |
3637
| [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. | |
3738

3839
## Security Vulnerability

Diff for: docs/rules/valid-context-access.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/valid-context-access"
5+
description: "context functions must be called during component initialization."
6+
---
7+
8+
# svelte/valid-context-access
9+
10+
> context functions must be called during component initialization.
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule reports where context API is called except during component initialization.
17+
18+
<ESLintCodeBlock>
19+
20+
<!--eslint-skip-->
21+
22+
```svelte
23+
<script>
24+
/* eslint svelte/valid-context-access: "error" */
25+
import { setContext, onMount } from "svelte"
26+
27+
/** ✓ GOOD */
28+
setContext("answer", 42)
29+
;(() => {
30+
setContext("answer", 42)
31+
})()
32+
33+
const init = () => {
34+
setContext("answer", 42)
35+
}
36+
37+
init()
38+
39+
/** ✗ BAD */
40+
const update = () => {
41+
setContext("answer", 42)
42+
}
43+
44+
onMount(() => {
45+
update()
46+
setContext("answer", 42)
47+
})
48+
</script>
49+
```
50+
51+
</ESLintCodeBlock>
52+
53+
## :wrench: Options
54+
55+
Nothing.
56+
57+
## :books: Further Reading
58+
59+
- [Svelte - Docs > RUN TIME > svelte > setContext](https://svelte.dev/docs#run-time-svelte-setcontext)
60+
- [Svelte - Docs > RUN TIME > svelte > getContext](https://svelte.dev/docs#run-time-svelte-getContext)
61+
- [Svelte - Docs > RUN TIME > svelte > hasContext](https://svelte.dev/docs#run-time-svelte-hasContext)
62+
- [Svelte - Docs > RUN TIME > svelte > getAllContexts](https://svelte.dev/docs#run-time-svelte-getAllContexts)
63+
64+
## :mag: Implementation
65+
66+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/valid-context-access.ts)
67+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/valid-context-access.ts)

Diff for: src/rules/reference-helpers/svelte-context.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { TSESTree } from "@typescript-eslint/types"
2+
import { ReferenceTracker } from "@eslint-community/eslint-utils"
3+
import type { RuleContext } from "../../types"
4+
5+
type ContextName = "setContext" | "getContext" | "hasContext" | "getAllContexts"
6+
7+
/** Extract svelte's context API references */
8+
export function* extractContextReferences(
9+
context: RuleContext,
10+
contextNames: ContextName[] = [
11+
"setContext",
12+
"getContext",
13+
"hasContext",
14+
"getAllContexts",
15+
],
16+
): Generator<{ node: TSESTree.CallExpression; name: string }, void> {
17+
const referenceTracker = new ReferenceTracker(
18+
context.getSourceCode().scopeManager.globalScope!,
19+
)
20+
for (const { node, path } of referenceTracker.iterateEsmReferences({
21+
svelte: {
22+
[ReferenceTracker.ESM]: true,
23+
setContext: {
24+
[ReferenceTracker.CALL]: contextNames.includes("setContext"),
25+
},
26+
getContext: {
27+
[ReferenceTracker.CALL]: contextNames.includes("getContext"),
28+
},
29+
hasContext: {
30+
[ReferenceTracker.CALL]: contextNames.includes("hasContext"),
31+
},
32+
getAllContexts: {
33+
[ReferenceTracker.CALL]: contextNames.includes("getAllContexts"),
34+
},
35+
},
36+
})) {
37+
yield {
38+
node: node as TSESTree.CallExpression,
39+
name: path[path.length - 1],
40+
}
41+
}
42+
}

Diff for: src/rules/valid-context-access.ts

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { createRule } from "../utils"
2+
import { extractContextReferences } from "./reference-helpers/svelte-context"
3+
import type { TSESTree } from "@typescript-eslint/types"
4+
5+
export default createRule("valid-context-access", {
6+
meta: {
7+
docs: {
8+
description:
9+
"context functions must be called during component initialization.",
10+
category: "Possible Errors",
11+
// TODO Switch to recommended in the major version.
12+
recommended: false,
13+
},
14+
schema: [],
15+
messages: {
16+
unexpected:
17+
"Do not call {{function}} except during component initialization.",
18+
},
19+
type: "problem",
20+
},
21+
create(context) {
22+
const scopeManager = context.getSourceCode().scopeManager
23+
const toplevelScope =
24+
scopeManager.globalScope?.childScopes.find(
25+
(scope) => scope.type === "module",
26+
) || scopeManager.globalScope
27+
28+
/** report ESLint error */
29+
function report(node: TSESTree.CallExpression) {
30+
context.report({
31+
loc: node.loc,
32+
messageId: "unexpected",
33+
data: {
34+
function:
35+
node.callee.type === "Identifier"
36+
? node.callee.name
37+
: "context function",
38+
},
39+
})
40+
}
41+
42+
/** Get nodes where the variable is used */
43+
function getReferences(id: TSESTree.Identifier | TSESTree.BindingName) {
44+
const variable = toplevelScope?.variables.find((v) => {
45+
if (id.type === "Identifier") {
46+
return v.identifiers.includes(id)
47+
}
48+
return false
49+
})
50+
if (variable) {
51+
return variable.references.filter((r) => r.identifier !== id)
52+
}
53+
return []
54+
}
55+
56+
/** Let's lint! */
57+
function doLint(
58+
visitedCallExpressions: TSESTree.CallExpression[],
59+
contextCallExpression: TSESTree.CallExpression,
60+
currentNode: TSESTree.CallExpression,
61+
) {
62+
let { parent } = currentNode
63+
if (parent?.type !== "ExpressionStatement") {
64+
report(contextCallExpression)
65+
return
66+
}
67+
while (parent) {
68+
parent = parent.parent
69+
if (
70+
parent?.type === "VariableDeclaration" ||
71+
parent?.type === "FunctionDeclaration"
72+
) {
73+
const references =
74+
parent.type === "VariableDeclaration"
75+
? getReferences(parent.declarations[0].id)
76+
: parent.id
77+
? getReferences(parent.id)
78+
: []
79+
80+
for (const reference of references) {
81+
if (reference.identifier?.parent?.type === "CallExpression") {
82+
if (
83+
!visitedCallExpressions.includes(reference.identifier.parent)
84+
) {
85+
visitedCallExpressions.push(reference.identifier.parent)
86+
doLint(
87+
visitedCallExpressions,
88+
contextCallExpression,
89+
reference.identifier?.parent,
90+
)
91+
}
92+
}
93+
}
94+
} else if (parent?.type === "ExpressionStatement") {
95+
if (parent.expression.type !== "CallExpression") {
96+
report(contextCallExpression)
97+
} else if (parent.expression.callee.type === "Identifier") {
98+
report(contextCallExpression)
99+
}
100+
}
101+
}
102+
}
103+
104+
return {
105+
Program() {
106+
for (const { node } of extractContextReferences(context)) {
107+
const visitedCallExpressions: TSESTree.CallExpression[] = []
108+
doLint(visitedCallExpressions, node, node)
109+
}
110+
},
111+
}
112+
},
113+
})

Diff for: src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import sortAttributes from "../rules/sort-attributes"
5757
import spacedHtmlComment from "../rules/spaced-html-comment"
5858
import system from "../rules/system"
5959
import validCompile from "../rules/valid-compile"
60+
import validContextAccess from "../rules/valid-context-access"
6061
import validEachKey from "../rules/valid-each-key"
6162
import validPropNamesInKitPages from "../rules/valid-prop-names-in-kit-pages"
6263

@@ -116,6 +117,7 @@ export const rules = [
116117
spacedHtmlComment,
117118
system,
118119
validCompile,
120+
validContextAccess,
119121
validEachKey,
120122
validPropNamesInKitPages,
121123
] as RuleModule[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
- message: Do not call setContext except during component initialization.
2+
line: 11
3+
column: 5
4+
- message: Do not call getContext except during component initialization.
5+
line: 12
6+
column: 5
7+
- message: Do not call hasContext except during component initialization.
8+
line: 13
9+
column: 5
10+
- message: Do not call getAllContexts except during component initialization.
11+
line: 14
12+
column: 5
13+
- message: Do not call setContext except during component initialization.
14+
line: 18
15+
column: 5
16+
- message: Do not call getContext except during component initialization.
17+
line: 19
18+
column: 5
19+
- message: Do not call hasContext except during component initialization.
20+
line: 20
21+
column: 5
22+
- message: Do not call getAllContexts except during component initialization.
23+
line: 21
24+
column: 5
25+
- message: Do not call setContext except during component initialization.
26+
line: 25
27+
column: 5
28+
- message: Do not call getContext except during component initialization.
29+
line: 26
30+
column: 5
31+
- message: Do not call hasContext except during component initialization.
32+
line: 27
33+
column: 5
34+
- message: Do not call getAllContexts except during component initialization.
35+
line: 28
36+
column: 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script>
2+
import {
3+
setContext,
4+
getContext,
5+
hasContext,
6+
getAllContexts,
7+
onMount,
8+
} from "svelte"
9+
10+
const update1 = () => {
11+
setContext("answer", 42)
12+
getContext("answer")
13+
hasContext("answer")
14+
getAllContexts()
15+
}
16+
17+
const update2 = function () {
18+
setContext("answer", 42)
19+
getContext("answer")
20+
hasContext("answer")
21+
getAllContexts()
22+
}
23+
24+
function update3() {
25+
setContext("answer", 42)
26+
getContext("answer")
27+
hasContext("answer")
28+
getAllContexts()
29+
}
30+
31+
onMount(() => {
32+
update1()
33+
update2()
34+
update3()
35+
})
36+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- message: Do not call setContext except during component initialization.
2+
line: 5
3+
column: 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import { setContext, onMount } from "svelte"
3+
4+
onMount(() => {
5+
setContext("answer", 42)
6+
})
7+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Do not call setContext except during component initialization.
2+
line: 4
3+
column: 5
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
import { setContext, onMount } from "svelte"
3+
const something = () => {
4+
setContext("answer", 42)
5+
}
6+
7+
something()
8+
</script>
9+
10+
<button on:click={() => something()}>Click Me</button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Do not call setContext except during component initialization.
2+
line: 4
3+
column: 5
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import { setContext, onMount } from "svelte"
3+
const something = () => {
4+
setContext("answer", 42)
5+
}
6+
7+
something()
8+
</script>
9+
10+
{#if true}
11+
{@const foo = something()}
12+
<button>Click Me</button>
13+
{/if}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Do not call setContext except during component initialization.
2+
line: 4
3+
column: 5
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import { setContext, onMount } from "svelte"
3+
const something = () => {
4+
setContext("answer", 42)
5+
}
6+
7+
something()
8+
</script>
9+
10+
{#if something()}
11+
<button>Click Me</button>
12+
{/if}

0 commit comments

Comments
 (0)