-
-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathno-loss-of-prop-reactivity.ts
152 lines (147 loc) · 4.54 KB
/
no-loss-of-prop-reactivity.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import type { AST } from "svelte-eslint-parser"
import { createRule } from "../utils"
import type { TSESTree } from "@typescript-eslint/types"
import { findAttribute, findVariable } from "../utils/ast-utils"
import { getStaticAttributeValue } from "../utils/ast-utils"
export default createRule("no-loss-of-prop-reactivity", {
meta: {
docs: {
description: "disallow the use of props that potentially lose reactivity",
category: "Best Practices",
recommended: false,
},
schema: [],
messages: {
potentiallyLoseReactivity:
"Referencing props that potentially lose reactivity should be avoided.",
},
type: "suggestion",
},
create(context) {
if (!context.parserServices.isSvelte) {
return {}
}
let contextModule: AST.SvelteScriptElement | null = null
let scriptNode: AST.SvelteScriptElement | null = null
const reactiveStatements: AST.SvelteReactiveStatement[] = []
const functions: TSESTree.FunctionLike[] = []
const props = new Set<TSESTree.Identifier>()
return {
SvelteScriptElement(node: AST.SvelteScriptElement) {
const contextAttr = findAttribute(node, "context")
if (contextAttr && getStaticAttributeValue(contextAttr) === "module") {
contextModule = node
return
}
scriptNode = node
},
SvelteReactiveStatement(node: AST.SvelteReactiveStatement) {
reactiveStatements.push(node)
},
":function"(node: TSESTree.FunctionLike) {
functions.push(node)
},
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration) {
if (
contextModule &&
contextModule.range[0] < node.range[0] &&
node.range[1] < contextModule.range[1]
) {
return
}
if (
node.declaration?.type === "VariableDeclaration" &&
(node.declaration.kind === "let" || node.declaration.kind === "var")
) {
for (const decl of node.declaration.declarations) {
if (decl.id.type === "Identifier") {
props.add(decl.id)
}
}
}
for (const spec of node.specifiers) {
if (spec.exportKind === "type") {
continue
}
if (isMutableProp(spec.local)) {
props.add(spec.exported)
}
}
},
"Program:exit"() {
for (const prop of props) {
const variable = findVariable(context, prop)
if (
!variable ||
// ignore multiple definitions
variable.defs.length > 1
) {
return
}
for (const reference of variable.references) {
if (reference.isWrite()) continue
const id = reference.identifier as TSESTree.Identifier
if (
variable.defs.some(
(def) =>
def.node.range[0] <= id.range[0] &&
id.range[1] <= def.node.range[1],
)
) {
// The reference is in the variable definition.
continue
}
if (isInReactivityScope(id)) continue
context.report({
node: id,
messageId: "potentiallyLoseReactivity",
})
}
}
},
}
/** Checks whether given prop id is mutable variable or not */
function isMutableProp(id: TSESTree.Identifier) {
const variable = findVariable(context, id)
if (!variable || variable.defs.length === 0) {
return false
}
return variable.defs.every((def) => {
if (def.type !== "Variable") {
return false
}
return def.parent.kind === "let" || def.parent.kind === "var"
})
}
/** Checks whether given id is in potentially reactive scope or not */
function isInReactivityScope(id: TSESTree.Identifier) {
if (
!scriptNode ||
id.range[1] <= scriptNode.range[0] ||
scriptNode.range[1] <= id.range[0]
) {
// The reference is in the template.
return true
}
if (
reactiveStatements.some(
(node) =>
node.range[0] <= id.range[0] && id.range[1] <= node.range[1],
)
) {
// The reference is in the reactive statement.
return true
}
if (
functions.some(
(node) =>
node.range[0] <= id.range[0] && id.range[1] <= node.range[1],
)
) {
// The reference is in the function.
return true
}
return false
}
},
})