Skip to content

Commit 2895f16

Browse files
authored
feat: add svelte/require-store-reactive-access rule (#289)
* feat: add `svelte/require-store-reactive-access` rule * Create warm-eagles-sing.md * fix: add testcases and fix bugs * fix: error message * feat: improves demo * fix: docs * fix: shim path * fix: docs and message * feat: improve require-store-reactive-access
1 parent ed6e347 commit 2895f16

File tree

116 files changed

+2359
-13
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+2359
-13
lines changed

.changeset/warm-eagles-sing.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": patch
3+
---
4+
5+
feat: add `svelte/require-store-reactive-access` rule

.stylelintignore

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ LICENSE
1313
# should we ignore markdown files?
1414
*.md
1515
/docs-svelte-kit/
16+
/coverage

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
308308
| [svelte/no-store-async](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-store-async/) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | |
309309
| [svelte/no-unknown-style-directive-property](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: |
310310
| [svelte/require-store-callbacks-use-set-param](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | |
311+
| [svelte/require-store-reactive-access](https://ota-meshi.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: |
311312
| [svelte/valid-compile](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | :star: |
312313
| [svelte/valid-prop-names-in-kit-pages](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-prop-names-in-kit-pages/) | disallow props other than data or errors in Svelte Kit page components. | |
313314

docs-svelte-kit/shim/path.mjs

+36-4
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,44 @@ function isAbsolute() {
2121
}
2222

2323
function join(...args) {
24-
return args.join("/")
24+
return args.length ? normalize(args.join("/")) : "."
2525
}
2626

27-
const sep = "/"
27+
function normalize(path) {
28+
let result = []
29+
for (const part of path.replace(/\/+/gu, "/").split("/")) {
30+
if (part === "..") {
31+
if (result[0] && result[0] !== ".." && result[0] !== ".") result.shift()
32+
} else if (part === "." && result.length) {
33+
// noop
34+
} else {
35+
result.unshift(part)
36+
}
37+
}
38+
return result.reverse().join("/")
39+
}
2840

29-
const posix = { dirname, extname, resolve, relative, sep, isAbsolute, join }
41+
const sep = "/"
42+
const posix = {
43+
dirname,
44+
extname,
45+
resolve,
46+
relative,
47+
sep,
48+
isAbsolute,
49+
join,
50+
normalize,
51+
}
3052
posix.posix = posix
31-
export { dirname, extname, posix, resolve, relative, sep, isAbsolute, join }
53+
export {
54+
dirname,
55+
extname,
56+
posix,
57+
resolve,
58+
relative,
59+
sep,
60+
isAbsolute,
61+
join,
62+
normalize,
63+
}
3264
export default posix

docs-svelte-kit/src/lib/eslint/scripts/ts-create-program.mts

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type typescript from "typescript"
22
import type tsvfs from "@typescript/vfs"
3+
import path from "path"
34
type TS = typeof typescript
45
type TSVFS = typeof tsvfs
56

@@ -68,10 +69,53 @@ export async function createVirtualCompilerHost(
6869
true,
6970
ts,
7071
)
72+
73+
// Setup svelte type definition modules
74+
for (const [key, get] of Object.entries(
75+
// @ts-expect-error -- ignore
76+
import.meta.glob("../../../../../node_modules/svelte/**/*.d.ts", {
77+
as: "raw",
78+
}),
79+
)) {
80+
const modulePath = key.slice("../../../../..".length)
81+
82+
fsMap.set(
83+
modulePath,
84+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
85+
await (get as any)(),
86+
)
87+
}
88+
7189
const system = tsvfs.createSystem(fsMap)
7290
const host = tsvfs.createVirtualCompilerHost(system, compilerOptions, ts)
73-
// eslint-disable-next-line @typescript-eslint/unbound-method -- backup original
74-
const original = { getSourceFile: host.compilerHost.getSourceFile }
91+
const original = {
92+
// eslint-disable-next-line @typescript-eslint/unbound-method -- backup original
93+
getSourceFile: host.compilerHost.getSourceFile,
94+
}
95+
host.compilerHost.resolveModuleNames = function (
96+
moduleNames,
97+
containingFile,
98+
) {
99+
return moduleNames.map((m) => {
100+
const targetPaths: string[] = []
101+
if (m.startsWith(".")) {
102+
targetPaths.push(path.join(path.dirname(containingFile), m))
103+
} else {
104+
targetPaths.push(`/node_modules/${m}`)
105+
}
106+
for (const modulePath of targetPaths.flatMap((m) => [
107+
`${m}.d.ts`,
108+
`${m}.ts`,
109+
`${m}/index.d.ts`,
110+
`${m}/index.ts`,
111+
])) {
112+
if (fsMap.has(modulePath)) {
113+
return { resolvedFileName: modulePath }
114+
}
115+
}
116+
return undefined
117+
})
118+
}
75119
host.compilerHost.getSourceFile = function (
76120
fileName,
77121
languageVersionOrOptions,

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
2626
| [svelte/no-store-async](./rules/no-store-async.md) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | |
2727
| [svelte/no-unknown-style-directive-property](./rules/no-unknown-style-directive-property.md) | disallow unknown `style:property` | :star: |
2828
| [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | |
29+
| [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: |
2930
| [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | :star: |
3031
| [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. | |
3132

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/require-store-reactive-access"
5+
description: "disallow to use of the store itself as an operand. Need to use $ prefix or get function."
6+
---
7+
8+
# svelte/require-store-reactive-access
9+
10+
> disallow to use of the store itself as an operand. Need to use $ prefix or get function.
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+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14+
15+
## :book: Rule Details
16+
17+
This rule disallow to use of the store itself as an operand.
18+
You should access the store value using the `$` prefix or the `get` function.
19+
20+
<ESLintCodeBlock fix>
21+
22+
<!--eslint-skip-->
23+
24+
```svelte
25+
<script>
26+
/* eslint svelte/require-store-reactive-access: "error" */
27+
import { writable, get } from "svelte/store"
28+
const storeValue = writable("world")
29+
const color = writable("red")
30+
31+
/* ✓ GOOD */
32+
$: message = `Hello ${$storeValue}`
33+
34+
/* ✗ BAD */
35+
$: message = `Hello ${storeValue}`
36+
</script>
37+
38+
<!-- ✓ GOOD -->
39+
<p>{$storeValue}</p>
40+
<p>{get(storeValue)}</p>
41+
42+
<p class={$storeValue} />
43+
<p style:color={$color} />
44+
45+
<MyComponent prop="Hello {$storeValue}" />
46+
<MyComponent bind:this={$storeValue} />
47+
<MyComponent --style-props={$storeValue} />
48+
<MyComponent {...$storeValue} />
49+
50+
<!-- ✗ BAD -->
51+
<p>{storeValue}</p>
52+
53+
<p class={storeValue} />
54+
<p style:color />
55+
56+
<MyComponent prop="Hello {storeValue}" />
57+
<MyComponent bind:this={storeValue} />
58+
<MyComponent --style-props={storeValue} />
59+
<MyComponent {...storeValue} />
60+
```
61+
62+
</ESLintCodeBlock>
63+
64+
This rule checks the usage of store variables only if the store can be determined within a single file.
65+
However, when using `@typescript-eslint/parser` and full type information, this rule uses the type information to determine if the expression is a store.
66+
67+
<!--eslint-skip-->
68+
69+
```ts
70+
// fileName: my-stores.ts
71+
import { writable } from "svelte/store"
72+
export const storeValue = writable("hello")
73+
```
74+
75+
<!--eslint-skip-->
76+
77+
```svelte
78+
<script lang="ts">
79+
/* eslint svelte/require-store-reactive-access: "error" */
80+
import { storeValue } from "./my-stores"
81+
</script>
82+
83+
<!-- ✓ GOOD -->
84+
<p>{$storeValue}</p>
85+
86+
<!-- ✗ BAD -->
87+
<p>{storeValue}</p>
88+
```
89+
90+
## :wrench: Options
91+
92+
Nothing.
93+
94+
## :mag: Implementation
95+
96+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/require-store-reactive-access.ts)
97+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/require-store-reactive-access.ts)

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
"stylus": "^0.59.0",
164164
"svelte": "^3.46.1",
165165
"svelte-adapter-ghpages": "0.0.2",
166+
"svelte-i18n": "^3.4.0",
166167
"type-coverage": "^2.22.0",
167168
"typescript": "^4.5.2",
168169
"vite": "^3.1.0-0",
@@ -174,7 +175,7 @@
174175
"access": "public"
175176
},
176177
"typeCoverage": {
177-
"atLeast": 98.69,
178+
"atLeast": 98.72,
178179
"cache": true,
179180
"detail": true,
180181
"ignoreAsAssertion": true,

src/rules/indent-helpers/es.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
isSemicolonToken,
1818
} from "eslint-utils"
1919
import type { ESNodeListener } from "../../types-for-node"
20+
import { getParent } from "../../utils/ast-utils"
2021

2122
type NodeListener = ESNodeListener
2223

@@ -1097,12 +1098,6 @@ export function defineVisitor(context: IndentContext): NodeListener {
10971098
}
10981099
}
10991100

1100-
/** Get the parent node from the given node */
1101-
function getParent(node: ESTree.Node): ESTree.Node | null {
1102-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
1103-
return (node as any).parent || null
1104-
}
1105-
11061101
/**
11071102
* Checks whether given text is known button type
11081103
*/

0 commit comments

Comments
 (0)