Skip to content

Commit a2b951b

Browse files
committed
feat: improve require-store-reactive-access
1 parent a31f2e6 commit a2b951b

File tree

72 files changed

+1209
-75
lines changed

Some content is hidden

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

72 files changed

+1209
-75
lines changed

.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

docs/rules/require-store-reactive-access.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ You should access the store value using the `$` prefix or the `get` function.
2525
<script>
2626
/* eslint svelte/require-store-reactive-access: "error" */
2727
import { writable, get } from "svelte/store"
28-
const storeValue = writable("hello")
28+
const storeValue = writable("world")
2929
const color = writable("red")
30+
31+
/* ✓ GOOD */
32+
$: message = `Hello ${$storeValue}`
33+
34+
/* ✗ BAD */
35+
$: message = `Hello ${storeValue}`
3036
</script>
3137
3238
<!-- ✓ GOOD -->

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.7,
178+
"atLeast": 98.71,
178179
"cache": true,
179180
"detail": true,
180181
"ignoreAsAssertion": true,
+83-50
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type * as ESTree from "estree"
2+
import type { TSESTree } from "@typescript-eslint/types"
23
import { ReferenceTracker } from "eslint-utils"
34
import type { Variable } from "eslint-scope"
45
import type { RuleContext } from "../../types"
@@ -35,27 +36,37 @@ export function* extractStoreReferences(
3536
}
3637
}
3738

39+
export type StoreChecker = (
40+
node: ESTree.Expression | TSESTree.Expression,
41+
options?: { consistent?: boolean },
42+
) => boolean
43+
type StoreCheckerWithOptions = (
44+
node: ESTree.Expression,
45+
options: { consistent: boolean },
46+
) => boolean
47+
3848
/**
3949
* Creates a function that checks whether the given expression node is a store instance or not.
4050
*/
41-
export function createStoreChecker(
42-
context: RuleContext,
43-
): (node: ESTree.Expression) => boolean {
51+
export function createStoreChecker(context: RuleContext): StoreChecker {
4452
const tools = getTypeScriptTools(context)
45-
if (tools) {
46-
return createStoreCheckerForTS(tools)
47-
}
53+
const checker = tools
54+
? createStoreCheckerForTS(tools)
55+
: createStoreCheckerForES(context)
4856

49-
return createStoreCheckerForES(context)
57+
return (node, options) =>
58+
checker(node as ESTree.Expression, {
59+
consistent: options?.consistent ?? false,
60+
})
5061
}
5162

5263
/**
5364
* Creates a function that checks whether the given expression node is a store instance or not, for EcmaScript.
5465
*/
5566
function createStoreCheckerForES(
5667
context: RuleContext,
57-
): (node: ESTree.Expression) => boolean {
58-
const variables = new Set<Variable>()
68+
): StoreCheckerWithOptions {
69+
const storeVariables = new Map<Variable, { const: boolean }>()
5970
for (const { node } of extractStoreReferences(context)) {
6071
const parent = getParent(node)
6172
if (
@@ -65,36 +76,42 @@ function createStoreCheckerForES(
6576
) {
6677
continue
6778
}
79+
const decl = getParent(parent)
80+
if (!decl || decl.type !== "VariableDeclaration") {
81+
continue
82+
}
6883

6984
const variable = findVariable(context, parent.id)
7085
if (variable) {
71-
variables.add(variable)
86+
storeVariables.set(variable, { const: decl.kind === "const" })
7287
}
7388
}
7489

75-
return (node) => {
90+
return (node, options) => {
7691
if (node.type !== "Identifier" || node.name.startsWith("$")) {
7792
return false
7893
}
7994
const variable = findVariable(context, node)
8095
if (!variable) {
8196
return false
8297
}
83-
return variables.has(variable)
98+
const info = storeVariables.get(variable)
99+
if (!info) {
100+
return false
101+
}
102+
return options.consistent ? info.const : true
84103
}
85104
}
86105

87106
/**
88107
* Creates a function that checks whether the given expression node is a store instance or not, for TypeScript.
89108
*/
90-
function createStoreCheckerForTS(
91-
tools: TSTools,
92-
): (node: ESTree.Expression) => boolean {
109+
function createStoreCheckerForTS(tools: TSTools): StoreCheckerWithOptions {
93110
const { service } = tools
94111
const checker = service.program.getTypeChecker()
95112
const tsNodeMap = service.esTreeNodeToTSNodeMap
96113

97-
return (node) => {
114+
return (node, options) => {
98115
const tsNode = tsNodeMap.get(node)
99116
if (!tsNode) {
100117
return false
@@ -107,57 +124,73 @@ function createStoreCheckerForTS(
107124
* Checks whether the given type is a store or not
108125
*/
109126
function isStoreType(type: TS.Type): boolean {
110-
if (type.isUnion()) {
111-
return type.types.some(isStoreType)
112-
}
113-
const subscribe = type.getProperty("subscribe")
114-
if (subscribe === undefined) {
115-
return false
116-
}
117-
const subscribeType = checker.getTypeOfSymbolAtLocation(
118-
subscribe,
119-
tsNode!,
120-
)
121-
return isStoreSubscribeSignatureType(subscribeType)
127+
return eachTypeCheck(type, options, (type) => {
128+
const subscribe = type.getProperty("subscribe")
129+
if (!subscribe) {
130+
return false
131+
}
132+
const subscribeType = checker.getTypeOfSymbolAtLocation(
133+
subscribe,
134+
tsNode!,
135+
)
136+
return isStoreSubscribeSignatureType(subscribeType)
137+
})
122138
}
123139

124140
/**
125141
* Checks whether the given type is a store's subscribe or not
126142
*/
127143
function isStoreSubscribeSignatureType(type: TS.Type): boolean {
128-
if (type.isUnion()) {
129-
return type.types.some(isStoreSubscribeSignatureType)
130-
}
131-
for (const signature of type.getCallSignatures()) {
132-
if (
133-
signature.parameters.length >= 2 &&
134-
isFunctionSymbol(signature.parameters[0]) &&
135-
isFunctionSymbol(signature.parameters[1])
136-
) {
137-
return true
144+
return eachTypeCheck(type, options, (type) => {
145+
for (const signature of type.getCallSignatures()) {
146+
if (
147+
signature.parameters.length >= 2 &&
148+
maybeFunctionSymbol(signature.parameters[0]) &&
149+
maybeFunctionSymbol(signature.parameters[1])
150+
) {
151+
return true
152+
}
138153
}
139-
}
140-
return false
154+
return false
155+
})
141156
}
142157

143158
/**
144-
* Checks whether the given symbol is a function param or not
159+
* Checks whether the given symbol maybe function param or not
145160
*/
146-
function isFunctionSymbol(param: TS.Symbol): boolean {
161+
function maybeFunctionSymbol(param: TS.Symbol): boolean {
147162
const type: TS.Type | undefined = checker.getApparentType(
148163
checker.getTypeOfSymbolAtLocation(param, tsNode!),
149164
)
150-
return isFunctionType(type)
165+
return maybeFunctionType(type)
166+
}
167+
168+
/**
169+
* Checks whether the given type is maybe function param or not
170+
*/
171+
function maybeFunctionType(type: TS.Type): boolean {
172+
return eachTypeCheck(type, { consistent: false }, (type) => {
173+
return type.getCallSignatures().length > 0
174+
})
151175
}
152176
}
177+
}
153178

154-
/**
155-
* Checks whether the given symbol is a function param or not
156-
*/
157-
function isFunctionType(type: TS.Type): boolean {
158-
if (type.isUnion()) {
159-
return type.types.some(isFunctionType)
179+
/**
180+
* Check the given type with the given check function.
181+
* For union types, `options.consistent: true` requires all types to pass the check function.
182+
* `options.consistent: false` considers a match if any type passes the check function.
183+
*/
184+
function eachTypeCheck(
185+
type: TS.Type,
186+
options: { consistent: boolean },
187+
check: (t: TS.Type) => boolean,
188+
): boolean {
189+
if (type.isUnion()) {
190+
if (options.consistent) {
191+
return type.types.every((t) => eachTypeCheck(t, options, check))
160192
}
161-
return type.getCallSignatures().length > 0
193+
return type.types.some((t) => eachTypeCheck(t, options, check))
162194
}
195+
return check(type)
163196
}

0 commit comments

Comments
 (0)