Skip to content

Commit d5074b2

Browse files
committed
chore(no-units-spawn-in-render): pr comment cleanup
1 parent bf6b6ad commit d5074b2

2 files changed

Lines changed: 33 additions & 43 deletions

File tree

src/rules/no-units-spawn-in-render/no-units-spawn-in-render.ts

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getContextualType, typeMatchesSpecifier } from "@typescript-eslint/type-utils"
2-
import { AST_NODE_TYPES, ESLintUtils, type TSESTree as Node } from "@typescript-eslint/utils"
3-
import { type Program, type Type, type TypeChecker, isExpression } from "typescript"
2+
import { AST_NODE_TYPES, ESLintUtils, type TSESTree as ESNode } from "@typescript-eslint/utils"
3+
import { type Program, type Node as TSNode, type Type, type TypeChecker, isExpression } from "typescript"
44

55
import { createRule } from "@/shared/create"
66
import { isType } from "@/shared/is"
@@ -74,15 +74,15 @@ export default createRule({
7474
// Populated from `import { createStore, sample } from "effector"` (handles renames too).
7575
const effectorImports = new Map<string, "factory" | "operator">()
7676

77-
type ComponentFunction = Node.FunctionDeclaration | Node.FunctionExpression | Node.ArrowFunctionExpression
77+
type ComponentFunction = ESNode.FunctionDeclaration | ESNode.FunctionExpression | ESNode.ArrowFunctionExpression
7878

7979
const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME.core}]`
8080

8181
return {
8282
// ── Phase 1: Collect effector imports ──────────────────────────────────
8383

8484
[`${importSelector} > ImportSpecifier[imported.type="Identifier"]`]: (
85-
node: Node.ImportSpecifier & { imported: Node.Identifier },
85+
node: ESNode.ImportSpecifier & { imported: ESNode.Identifier },
8686
) => {
8787
const imported = node.imported.name
8888
const local = node.local.name
@@ -164,78 +164,68 @@ export default createRule({
164164
// by callee type against the effector package
165165
// - Anything remaining is treated as a custom factory
166166

167-
"CallExpression": (node: Node.CallExpression) => {
167+
"CallExpression": (node: ESNode.CallExpression) => {
168168
const isWithinRender = stack.render.at(-1) ?? false
169169
if (!isWithinRender) return
170170

171171
const calleeName = getCalleeName(node.callee)
172172

173173
// Tier 1: known effector import — report immediately, skip type analysis
174174
const importType = calleeName ? effectorImports.get(calleeName) : undefined
175-
if (importType === "factory") {
176-
context.report({ node, messageId: "noFactoryInRender", data: { name: calleeName } })
177-
return
178-
}
179-
if (importType === "operator") {
180-
context.report({ node, messageId: "noOperatorInRender", data: { name: calleeName } })
181-
return
175+
switch (importType) {
176+
case "factory":
177+
return context.report({ node, messageId: "noFactoryInRender", data: { name: calleeName } })
178+
case "operator":
179+
return context.report({ node, messageId: "noOperatorInRender", data: { name: calleeName } })
182180
}
183181

184182
// Tier 2: return type contains effector units — classify via callee type
185183
const returnType = services.getTypeAtLocation(node)
186-
if (!hasEffectorUnitInType(returnType, checker, services.program)) return
184+
const ctx: TraverseCtx = { node: services.esTreeNodeToTSNodeMap.get(node), checker, program: services.program }
185+
186+
if (!hasEffectorUnitInType(ctx, returnType)) return
187187

188188
const calleeType = services.getTypeAtLocation(node.callee)
189+
const displayName = calleeName ?? "<expression>"
190+
189191
if (typeMatchesSpecifier(calleeType, REACT_HOOKS_SPEC, services.program)) return
190192

191-
if (typeMatchesSpecifier(calleeType, EFFECTOR_FACTORY_SPEC, services.program)) {
192-
context.report({ node, messageId: "noFactoryInRender", data: { name: calleeName ?? "<expression>" } })
193-
return
194-
}
195-
if (typeMatchesSpecifier(calleeType, EFFECTOR_OPERATOR_SPEC, services.program)) {
196-
context.report({ node, messageId: "noOperatorInRender", data: { name: calleeName ?? "<expression>" } })
197-
return
198-
}
193+
if (typeMatchesSpecifier(calleeType, EFFECTOR_FACTORY_SPEC, services.program))
194+
return context.report({ node, messageId: "noFactoryInRender", data: { name: displayName } })
199195

200-
context.report({
201-
node,
202-
messageId: "noCustomFactoryInRender",
203-
data: { name: calleeName ?? "<expression>" },
204-
})
196+
if (typeMatchesSpecifier(calleeType, EFFECTOR_OPERATOR_SPEC, services.program))
197+
return context.report({ node, messageId: "noOperatorInRender", data: { name: displayName } })
198+
199+
context.report({ node, messageId: "noCustomFactoryInRender", data: { name: displayName } })
205200
},
206201
}
207202
},
208203
})
209204

210205
const UseRegex = /^use[A-Z0-9].*$/
211206

212-
function getCalleeName(callee: Node.CallExpression["callee"]): string | null {
207+
function getCalleeName(callee: ESNode.Expression): string | null {
213208
if (callee.type === AST_NODE_TYPES.Identifier) return callee.name
214-
if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
209+
if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier)
215210
return callee.property.name
216-
}
217-
return null
211+
else return null
218212
}
219213

214+
type TraverseCtx = { node: TSNode; checker: TypeChecker; program: Program }
215+
220216
// Walks the type structure up to `depth` levels of object nesting to find effector units.
221217
// Unions don't consume depth — they are alternative shapes at the same level.
222-
function hasEffectorUnitInType(type: Type, checker: TypeChecker, program: Program, depth = 3): boolean {
223-
if (isType.unit(type, program) || isType.domain(type, program)) return true
218+
function hasEffectorUnitInType(ctx: TraverseCtx, type: Type, depth = 3): boolean {
219+
if (isType.unit(type, ctx.program)) return true
224220
if (depth <= 0) return false
225221

226222
// For unions, getProperties() only returns common properties across all members.
227223
// We must recurse into each member to check their individual properties for userland factories.
228-
if (type.isUnion()) {
229-
return type.types.some((t) => hasEffectorUnitInType(t, checker, program, depth))
230-
}
224+
if (type.isUnion()) return type.types.some((type) => hasEffectorUnitInType(ctx, type, depth))
231225

232-
const properties = type.getProperties()
233-
for (const prop of properties) {
234-
const firstDeclaration = prop.declarations?.[0]
235-
const propType = firstDeclaration
236-
? checker.getTypeOfSymbolAtLocation(prop, firstDeclaration)
237-
: checker.getTypeOfSymbol(prop)
238-
if (hasEffectorUnitInType(propType, checker, program, depth - 1)) return true
226+
for (const property of type.getProperties()) {
227+
const type = ctx.checker.getTypeOfSymbolAtLocation(property, ctx.node)
228+
if (hasEffectorUnitInType(ctx, type, depth - 1)) return true
239229
}
240230

241231
return false

src/shared/is.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const isType = {
2424
typeMatchesSpecifier(type, { from: "package", package: "effector", name: "Effect" }, program),
2525

2626
unit: (type: Type, program: Program) => {
27-
const name = ["Store", "StoreWritable", "Event", "EventCallable", "Effect"]
27+
const name = ["Store", "StoreWritable", "Event", "EventCallable", "Effect", "Domain"]
2828
return typeMatchesSpecifier(type, { from: "package", package: "effector", name }, program)
2929
},
3030

0 commit comments

Comments
 (0)