@stll/oxlint-plugin 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bun-test-hygiene.cjs +401 -0
- package/dist/bun-test-hygiene.cjs.map +1 -0
- package/dist/bun-test-hygiene.d.cts +4 -0
- package/dist/bun-test-hygiene.d.mts +5 -0
- package/dist/bun-test-hygiene.mjs +397 -0
- package/dist/bun-test-hygiene.mjs.map +1 -0
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -19
- package/dist/index.d.mts +27 -19
- package/dist/index.mjs +10 -1
- package/dist/index.mjs.map +1 -1
- package/dist/no-unsafe-inner-html.cjs +210 -44
- package/dist/no-unsafe-inner-html.cjs.map +1 -1
- package/dist/no-unsafe-inner-html.d.cts +20 -3
- package/dist/no-unsafe-inner-html.d.mts +20 -3
- package/dist/no-unsafe-inner-html.mjs +209 -46
- package/dist/no-unsafe-inner-html.mjs.map +1 -1
- package/dist/no-wall-clock-bound.cjs.map +1 -1
- package/dist/no-wall-clock-bound.mjs.map +1 -1
- package/dist/require-loader-prefetch.mjs +1 -1
- package/dist/utils.cjs +76 -3
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.mjs +71 -4
- package/dist/utils.mjs.map +1 -1
- package/package.json +13 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-wall-clock-bound.cjs","names":["isNode","testOnlyPlugin","createBindingResolver","propertyName","identifierName"],"sources":["../src/no-wall-clock-bound.ts"],"sourcesContent":["import type { Scope } from \"@oxlint/plugins\";\n\nimport {\n createBindingResolver,\n identifierName,\n isNode,\n propertyName,\n testOnlyPlugin,\n type Node,\n} from \"./test-rule-utils\";\n\nconst timerModules = new Set([\n \"node:timers/promises\",\n \"timers/promises\",\n \"@stll/test-kit\",\n]);\nconst timerHelpers = new Set([\"delay\", \"setTimeout\", \"sleep\"]);\nconst testModules = new Set([\"@jest/globals\", \"bun:test\", \"vitest\"]);\nconst functionTypes = new Set([\n \"ArrowFunctionExpression\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n]);\nconst matcherModifiers = new Set([\"not\", \"rejects\", \"resolves\"]);\n\nconst isVariableReference = (identifier: Node): boolean => {\n const parent = identifier.parent;\n if (!isNode(parent)) return true;\n if (\n parent.type === \"MemberExpression\" &&\n parent.property === identifier &&\n parent.computed !== true\n )\n return false;\n if (\n (parent.type === \"Property\" ||\n parent.type === \"MethodDefinition\" ||\n parent.type === \"PropertyDefinition\") &&\n parent.key === identifier &&\n parent.computed !== true &&\n parent.value !== identifier\n )\n return false;\n if (\n (parent.type === \"LabeledStatement\" ||\n parent.type === \"BreakStatement\" ||\n parent.type === \"ContinueStatement\") &&\n parent.label === identifier\n )\n return false;\n if (\n (parent.type === \"VariableDeclarator\" ||\n parent.type === \"FunctionDeclaration\" ||\n parent.type === \"FunctionExpression\" ||\n parent.type === \"ClassDeclaration\" ||\n parent.type === \"ClassExpression\") &&\n parent.id === identifier\n )\n return false;\n return true;\n};\n\nconst enclosingFunction = (node: Node): Node | null => {\n let current = node.parent;\n while (isNode(current)) {\n if (functionTypes.has(current.type)) return current;\n current = current.parent;\n }\n return null;\n};\n\nexport default testOnlyPlugin(\n \"no-wall-clock-bound\",\n \"noWallClockBound\",\n \"A test must not assert on measured wall-clock time or race a timer, because such assertions fail on loaded runners; use a controlled clock or deferred for ordering.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n\n const isGlobalMember = (\n node: Node,\n objectName: string,\n member: string,\n ): boolean => {\n if (node.type !== \"MemberExpression\") return false;\n if (propertyName(node.property) !== member || !isNode(node.object))\n return false;\n const binding = resolveBinding(node.object);\n return binding.type === \"global\" && binding.name === objectName;\n };\n\n const isClockRead = (value: unknown): boolean => {\n if (!isNode(value) || value.type !== \"CallExpression\") return false;\n if (!isNode(value.callee)) return false;\n if (\n isGlobalMember(value.callee, \"performance\", \"now\") ||\n isGlobalMember(value.callee, \"Date\", \"now\") ||\n isGlobalMember(value.callee, \"Bun\", \"nanoseconds\")\n )\n return true;\n const callee = value.callee;\n return (\n callee.type === \"MemberExpression\" &&\n propertyName(callee.property) === \"bigint\" &&\n isNode(callee.object) &&\n isGlobalMember(callee.object, \"process\", \"hrtime\")\n );\n };\n\n const constInitializerInSameFunction = (\n identifier: Node,\n use: Node,\n ): Node | null => {\n const name = identifierName(identifier);\n if (name === null) return null;\n let scope: Scope | null = getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition?.type !== \"Variable\") return null;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init) ||\n enclosingFunction(declarator) !== enclosingFunction(use)\n )\n return null;\n return declarator.init;\n }\n return null;\n };\n\n const containsClockRead = (\n value: unknown,\n use: Node,\n followIdentifier: boolean,\n visitedIdentifiers = new Set<Node>(),\n ): boolean => {\n if (Array.isArray(value))\n return value.some((child) =>\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n if (!isNode(value)) return false;\n if (isClockRead(value)) return true;\n if (\n followIdentifier &&\n value.type === \"Identifier\" &&\n isVariableReference(value)\n ) {\n if (visitedIdentifiers.has(value)) return false;\n visitedIdentifiers.add(value);\n const initializer = constInitializerInSameFunction(value, use);\n if (\n initializer !== null &&\n containsClockRead(initializer, use, true, visitedIdentifiers)\n )\n return true;\n }\n return Object.values(value).some(\n (child) =>\n child !== value.parent &&\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n };\n\n const matcherExpectCall = (value: unknown): Node | null => {\n let current = value;\n while (\n isNode(current) &&\n current.type === \"MemberExpression\" &&\n matcherModifiers.has(propertyName(current.property) ?? \"\")\n )\n current = current.object;\n return isNode(current) && current.type === \"CallExpression\"\n ? current\n : null;\n };\n\n const containsSetTimeout = (value: unknown): boolean => {\n if (Array.isArray(value)) return value.some(containsSetTimeout);\n if (!isNode(value)) return false;\n if (value.type === \"CallExpression\" && isNode(value.callee)) {\n const binding = resolveBinding(value.callee);\n if (binding.type === \"global\" && binding.name === \"setTimeout\")\n return true;\n }\n for (const child of Object.values(value)) {\n if (child === value.parent) continue;\n if (Array.isArray(child)) {\n if (child.some(containsSetTimeout)) return true;\n } else if (containsSetTimeout(child)) return true;\n }\n return false;\n };\n\n const isTimer = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"NewExpression\") {\n if (!isNode(value.callee)) return false;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"global\" &&\n binding.name === \"Promise\" &&\n containsSetTimeout(value.arguments)\n );\n }\n if (value.type !== \"CallExpression\" || !isNode(value.callee))\n return false;\n if (isGlobalMember(value.callee, \"Bun\", \"sleep\")) return true;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"import\" &&\n timerModules.has(binding.module) &&\n timerHelpers.has(binding.imported)\n );\n };\n\n const isTimerOrBoundTimer = (value: unknown, use: Node): boolean => {\n if (isTimer(value)) return true;\n if (!isNode(value) || value.type !== \"Identifier\") return false;\n const initializer = constInitializerInSameFunction(value, use);\n return initializer !== null && isTimer(initializer);\n };\n\n return {\n CallExpression(node) {\n if (!isNode(node.callee) || node.callee.type !== \"MemberExpression\")\n return;\n const member = propertyName(node.callee.property);\n if (member === \"race\" && isNode(node.callee.object)) {\n const binding = resolveBinding(node.callee.object);\n if (binding.type === \"global\" && binding.name === \"Promise\") {\n const firstArgument = Array.isArray(node.arguments)\n ? node.arguments.at(0)\n : null;\n if (\n isNode(firstArgument) &&\n firstArgument.type === \"ArrayExpression\" &&\n Array.isArray(firstArgument.elements) &&\n firstArgument.elements.some((element) =>\n isTimerOrBoundTimer(element, node),\n )\n )\n report(node);\n return;\n }\n }\n const expectCall = matcherExpectCall(node.callee.object);\n if (expectCall === null || !isNode(expectCall.callee)) return;\n const calleeBinding = resolveBinding(expectCall.callee);\n const isExpect =\n (calleeBinding.type === \"global\" &&\n calleeBinding.name === \"expect\") ||\n (calleeBinding.type === \"import\" &&\n calleeBinding.imported === \"expect\" &&\n testModules.has(calleeBinding.module));\n if (!isExpect) return;\n const assertionValues = [expectCall.arguments, node.arguments];\n if (containsClockRead(assertionValues, node, true)) report(node);\n },\n };\n },\n);\n"],"mappings":";;;;;;AAWA,MAAM,+BAAe,IAAI,IAAI;CAC3B;CACA;CACA;AACF,CAAC;AACD,MAAM,+BAAe,IAAI,IAAI;CAAC;CAAS;CAAc;AAAO,CAAC;AAC7D,MAAM,8BAAc,IAAI,IAAI;CAAC;CAAiB;CAAY;AAAQ,CAAC;AACnE,MAAM,gCAAgB,IAAI,IAAI;CAC5B;CACA;CACA;AACF,CAAC;AACD,MAAM,mCAAmB,IAAI,IAAI;CAAC;CAAO;CAAW;AAAU,CAAC;AAE/D,MAAM,uBAAuB,eAA8B;CACzD,MAAM,SAAS,WAAW;CAC1B,IAAI,CAACA,wBAAAA,OAAO,MAAM,GAAG,OAAO;CAC5B,IACE,OAAO,SAAS,sBAChB,OAAO,aAAa,cACpB,OAAO,aAAa,MAEpB,OAAO;CACT,KACG,OAAO,SAAS,cACf,OAAO,SAAS,sBAChB,OAAO,SAAS,yBAClB,OAAO,QAAQ,cACf,OAAO,aAAa,QACpB,OAAO,UAAU,YAEjB,OAAO;CACT,KACG,OAAO,SAAS,sBACf,OAAO,SAAS,oBAChB,OAAO,SAAS,wBAClB,OAAO,UAAU,YAEjB,OAAO;CACT,KACG,OAAO,SAAS,wBACf,OAAO,SAAS,yBAChB,OAAO,SAAS,wBAChB,OAAO,SAAS,sBAChB,OAAO,SAAS,sBAClB,OAAO,OAAO,YAEd,OAAO;CACT,OAAO;AACT;AAEA,MAAM,qBAAqB,SAA4B;CACrD,IAAI,UAAU,KAAK;CACnB,OAAOA,wBAAAA,OAAO,OAAO,GAAG;EACtB,IAAI,cAAc,IAAI,QAAQ,IAAI,GAAG,OAAO;EAC5C,UAAU,QAAQ;CACpB;CACA,OAAO;AACT;AAEA,IAAA,8BAAeC,wBAAAA,eACb,uBACA,oBACA,yKACC,QAAQ,aAAa;CACpB,MAAM,iBAAiBC,wBAAAA,sBAAsB,QAAQ;CAErD,MAAM,kBACJ,MACA,YACA,WACY;EACZ,IAAI,KAAK,SAAS,oBAAoB,OAAO;EAC7C,IAAIC,wBAAAA,aAAa,KAAK,QAAQ,MAAM,UAAU,CAACH,wBAAAA,OAAO,KAAK,MAAM,GAC/D,OAAO;EACT,MAAM,UAAU,eAAe,KAAK,MAAM;EAC1C,OAAO,QAAQ,SAAS,YAAY,QAAQ,SAAS;CACvD;CAEA,MAAM,eAAe,UAA4B;EAC/C,IAAI,CAACA,wBAAAA,OAAO,KAAK,KAAK,MAAM,SAAS,kBAAkB,OAAO;EAC9D,IAAI,CAACA,wBAAAA,OAAO,MAAM,MAAM,GAAG,OAAO;EAClC,IACE,eAAe,MAAM,QAAQ,eAAe,KAAK,KACjD,eAAe,MAAM,QAAQ,QAAQ,KAAK,KAC1C,eAAe,MAAM,QAAQ,OAAO,aAAa,GAEjD,OAAO;EACT,MAAM,SAAS,MAAM;EACrB,OACE,OAAO,SAAS,sBAChBG,wBAAAA,aAAa,OAAO,QAAQ,MAAM,YAClCH,wBAAAA,OAAO,OAAO,MAAM,KACpB,eAAe,OAAO,QAAQ,WAAW,QAAQ;CAErD;CAEA,MAAM,kCACJ,YACA,QACgB;EAChB,MAAM,OAAOI,wBAAAA,eAAe,UAAU;EACtC,IAAI,SAAS,MAAM,OAAO;EAC1B,IAAI,QAAsB,SAAS,UAAU;EAC7C,OAAO,UAAU,MAAM;GACrB,MAAM,WAAW,MAAM,IAAI,IAAI,IAAI;GACnC,IAAI,aAAa,KAAA,GAAW;IAC1B,QAAQ,MAAM;IACd;GACF;GACA,MAAM,aAAa,SAAS,KAAK,GAAG,CAAC;GACrC,IAAI,YAAY,SAAS,YAAY,OAAO;GAC5C,MAAM,aAAa,WAAW;GAC9B,IACE,CAACJ,wBAAAA,OAAO,UAAU,KAClB,CAACA,wBAAAA,OAAO,WAAW,MAAM,KACzB,WAAW,OAAO,SAAS,yBAC3B,WAAW,OAAO,SAAS,WAC3B,CAACA,wBAAAA,OAAO,WAAW,IAAI,KACvB,kBAAkB,UAAU,MAAM,kBAAkB,GAAG,GAEvD,OAAO;GACT,OAAO,WAAW;EACpB;EACA,OAAO;CACT;CAEA,MAAM,qBACJ,OACA,KACA,kBACA,qCAAqB,IAAI,IAAU,MACvB;EACZ,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,UACjB,kBAAkB,OAAO,KAAK,kBAAkB,kBAAkB,CACpE;EACF,IAAI,CAACA,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,YAAY,KAAK,GAAG,OAAO;EAC/B,IACE,oBACA,MAAM,SAAS,gBACf,oBAAoB,KAAK,GACzB;GACA,IAAI,mBAAmB,IAAI,KAAK,GAAG,OAAO;GAC1C,mBAAmB,IAAI,KAAK;GAC5B,MAAM,cAAc,+BAA+B,OAAO,GAAG;GAC7D,IACE,gBAAgB,QAChB,kBAAkB,aAAa,KAAK,MAAM,kBAAkB,GAE5D,OAAO;EACX;EACA,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,MACzB,UACC,UAAU,MAAM,UAChB,kBAAkB,OAAO,KAAK,kBAAkB,kBAAkB,CACtE;CACF;CAEA,MAAM,qBAAqB,UAAgC;EACzD,IAAI,UAAU;EACd,OACEA,wBAAAA,OAAO,OAAO,KACd,QAAQ,SAAS,sBACjB,iBAAiB,IAAIG,wBAAAA,aAAa,QAAQ,QAAQ,KAAK,EAAE,GAEzD,UAAU,QAAQ;EACpB,OAAOH,wBAAAA,OAAO,OAAO,KAAK,QAAQ,SAAS,mBACvC,UACA;CACN;CAEA,MAAM,sBAAsB,UAA4B;EACtD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,kBAAkB;EAC9D,IAAI,CAACA,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,MAAM,SAAS,oBAAoBA,wBAAAA,OAAO,MAAM,MAAM,GAAG;GAC3D,MAAM,UAAU,eAAe,MAAM,MAAM;GAC3C,IAAI,QAAQ,SAAS,YAAY,QAAQ,SAAS,cAChD,OAAO;EACX;EACA,KAAK,MAAM,SAAS,OAAO,OAAO,KAAK,GAAG;GACxC,IAAI,UAAU,MAAM,QAAQ;GAC5B,IAAI,MAAM,QAAQ,KAAK,GACjB;QAAA,MAAM,KAAK,kBAAkB,GAAG,OAAO;GAAA,OACtC,IAAI,mBAAmB,KAAK,GAAG,OAAO;EAC/C;EACA,OAAO;CACT;CAEA,MAAM,WAAW,UAA4B;EAC3C,IAAI,CAACA,wBAAAA,OAAO,KAAK,GAAG,OAAO;EAC3B,IAAI,MAAM,SAAS,iBAAiB;GAClC,IAAI,CAACA,wBAAAA,OAAO,MAAM,MAAM,GAAG,OAAO;GAClC,MAAM,UAAU,eAAe,MAAM,MAAM;GAC3C,OACE,QAAQ,SAAS,YACjB,QAAQ,SAAS,aACjB,mBAAmB,MAAM,SAAS;EAEtC;EACA,IAAI,MAAM,SAAS,oBAAoB,CAACA,wBAAAA,OAAO,MAAM,MAAM,GACzD,OAAO;EACT,IAAI,eAAe,MAAM,QAAQ,OAAO,OAAO,GAAG,OAAO;EACzD,MAAM,UAAU,eAAe,MAAM,MAAM;EAC3C,OACE,QAAQ,SAAS,YACjB,aAAa,IAAI,QAAQ,MAAM,KAC/B,aAAa,IAAI,QAAQ,QAAQ;CAErC;CAEA,MAAM,uBAAuB,OAAgB,QAAuB;EAClE,IAAI,QAAQ,KAAK,GAAG,OAAO;EAC3B,IAAI,CAACA,wBAAAA,OAAO,KAAK,KAAK,MAAM,SAAS,cAAc,OAAO;EAC1D,MAAM,cAAc,+BAA+B,OAAO,GAAG;EAC7D,OAAO,gBAAgB,QAAQ,QAAQ,WAAW;CACpD;CAEA,OAAO,EACL,eAAe,MAAM;EACnB,IAAI,CAACA,wBAAAA,OAAO,KAAK,MAAM,KAAK,KAAK,OAAO,SAAS,oBAC/C;EAEF,IADeG,wBAAAA,aAAa,KAAK,OAAO,QAC/B,MAAM,UAAUH,wBAAAA,OAAO,KAAK,OAAO,MAAM,GAAG;GACnD,MAAM,UAAU,eAAe,KAAK,OAAO,MAAM;GACjD,IAAI,QAAQ,SAAS,YAAY,QAAQ,SAAS,WAAW;IAC3D,MAAM,gBAAgB,MAAM,QAAQ,KAAK,SAAS,IAC9C,KAAK,UAAU,GAAG,CAAC,IACnB;IACJ,IACEA,wBAAAA,OAAO,aAAa,KACpB,cAAc,SAAS,qBACvB,MAAM,QAAQ,cAAc,QAAQ,KACpC,cAAc,SAAS,MAAM,YAC3B,oBAAoB,SAAS,IAAI,CACnC,GAEA,OAAO,IAAI;IACb;GACF;EACF;EACA,MAAM,aAAa,kBAAkB,KAAK,OAAO,MAAM;EACvD,IAAI,eAAe,QAAQ,CAACA,wBAAAA,OAAO,WAAW,MAAM,GAAG;EACvD,MAAM,gBAAgB,eAAe,WAAW,MAAM;EAOtD,IAAI,EALD,cAAc,SAAS,YACtB,cAAc,SAAS,YACxB,cAAc,SAAS,YACtB,cAAc,aAAa,YAC3B,YAAY,IAAI,cAAc,MAAM,IACzB;EACf,MAAM,kBAAkB,CAAC,WAAW,WAAW,KAAK,SAAS;EAC7D,IAAI,kBAAkB,iBAAiB,MAAM,IAAI,GAAG,OAAO,IAAI;CACjE,EACF;AACF,CACF"}
|
|
1
|
+
{"version":3,"file":"no-wall-clock-bound.cjs","names":["isNode","testOnlyPlugin","createBindingResolver","propertyName","identifierName"],"sources":["../src/no-wall-clock-bound.ts"],"sourcesContent":["import type { Scope } from \"@oxlint/plugins\";\n\nimport {\n createBindingResolver,\n identifierName,\n isNode,\n type Node,\n propertyName,\n testOnlyPlugin,\n} from \"./test-rule-utils\";\n\nconst timerModules = new Set([\n \"node:timers/promises\",\n \"timers/promises\",\n \"@stll/test-kit\",\n]);\nconst timerHelpers = new Set([\"delay\", \"setTimeout\", \"sleep\"]);\nconst testModules = new Set([\"@jest/globals\", \"bun:test\", \"vitest\"]);\nconst functionTypes = new Set([\n \"ArrowFunctionExpression\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n]);\nconst matcherModifiers = new Set([\"not\", \"rejects\", \"resolves\"]);\n\nconst isVariableReference = (identifier: Node): boolean => {\n const parent = identifier.parent;\n if (!isNode(parent)) return true;\n if (\n parent.type === \"MemberExpression\" &&\n parent.property === identifier &&\n parent.computed !== true\n )\n return false;\n if (\n (parent.type === \"Property\" ||\n parent.type === \"MethodDefinition\" ||\n parent.type === \"PropertyDefinition\") &&\n parent.key === identifier &&\n parent.computed !== true &&\n parent.value !== identifier\n )\n return false;\n if (\n (parent.type === \"LabeledStatement\" ||\n parent.type === \"BreakStatement\" ||\n parent.type === \"ContinueStatement\") &&\n parent.label === identifier\n )\n return false;\n if (\n (parent.type === \"VariableDeclarator\" ||\n parent.type === \"FunctionDeclaration\" ||\n parent.type === \"FunctionExpression\" ||\n parent.type === \"ClassDeclaration\" ||\n parent.type === \"ClassExpression\") &&\n parent.id === identifier\n )\n return false;\n return true;\n};\n\nconst enclosingFunction = (node: Node): Node | null => {\n let current = node.parent;\n while (isNode(current)) {\n if (functionTypes.has(current.type)) return current;\n current = current.parent;\n }\n return null;\n};\n\nexport default testOnlyPlugin(\n \"no-wall-clock-bound\",\n \"noWallClockBound\",\n \"A test must not assert on measured wall-clock time or race a timer, because such assertions fail on loaded runners; use a controlled clock or deferred for ordering.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n\n const isGlobalMember = (\n node: Node,\n objectName: string,\n member: string,\n ): boolean => {\n if (node.type !== \"MemberExpression\") return false;\n if (propertyName(node.property) !== member || !isNode(node.object))\n return false;\n const binding = resolveBinding(node.object);\n return binding.type === \"global\" && binding.name === objectName;\n };\n\n const isClockRead = (value: unknown): boolean => {\n if (!isNode(value) || value.type !== \"CallExpression\") return false;\n if (!isNode(value.callee)) return false;\n if (\n isGlobalMember(value.callee, \"performance\", \"now\") ||\n isGlobalMember(value.callee, \"Date\", \"now\") ||\n isGlobalMember(value.callee, \"Bun\", \"nanoseconds\")\n )\n return true;\n const callee = value.callee;\n return (\n callee.type === \"MemberExpression\" &&\n propertyName(callee.property) === \"bigint\" &&\n isNode(callee.object) &&\n isGlobalMember(callee.object, \"process\", \"hrtime\")\n );\n };\n\n const constInitializerInSameFunction = (\n identifier: Node,\n use: Node,\n ): Node | null => {\n const name = identifierName(identifier);\n if (name === null) return null;\n let scope: Scope | null = getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition?.type !== \"Variable\") return null;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init) ||\n enclosingFunction(declarator) !== enclosingFunction(use)\n )\n return null;\n return declarator.init;\n }\n return null;\n };\n\n const containsClockRead = (\n value: unknown,\n use: Node,\n followIdentifier: boolean,\n visitedIdentifiers = new Set<Node>(),\n ): boolean => {\n if (Array.isArray(value))\n return value.some((child) =>\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n if (!isNode(value)) return false;\n if (isClockRead(value)) return true;\n if (\n followIdentifier &&\n value.type === \"Identifier\" &&\n isVariableReference(value)\n ) {\n if (visitedIdentifiers.has(value)) return false;\n visitedIdentifiers.add(value);\n const initializer = constInitializerInSameFunction(value, use);\n if (\n initializer !== null &&\n containsClockRead(initializer, use, true, visitedIdentifiers)\n )\n return true;\n }\n return Object.values(value).some(\n (child) =>\n child !== value.parent &&\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n };\n\n const matcherExpectCall = (value: unknown): Node | null => {\n let current = value;\n while (\n isNode(current) &&\n current.type === \"MemberExpression\" &&\n matcherModifiers.has(propertyName(current.property) ?? \"\")\n )\n current = current.object;\n return isNode(current) && current.type === \"CallExpression\"\n ? current\n : null;\n };\n\n const containsSetTimeout = (value: unknown): boolean => {\n if (Array.isArray(value)) return value.some(containsSetTimeout);\n if (!isNode(value)) return false;\n if (value.type === \"CallExpression\" && isNode(value.callee)) {\n const binding = resolveBinding(value.callee);\n if (binding.type === \"global\" && binding.name === \"setTimeout\")\n return true;\n }\n for (const child of Object.values(value)) {\n if (child === value.parent) continue;\n if (Array.isArray(child)) {\n if (child.some(containsSetTimeout)) return true;\n } else if (containsSetTimeout(child)) return true;\n }\n return false;\n };\n\n const isTimer = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"NewExpression\") {\n if (!isNode(value.callee)) return false;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"global\" &&\n binding.name === \"Promise\" &&\n containsSetTimeout(value.arguments)\n );\n }\n if (value.type !== \"CallExpression\" || !isNode(value.callee))\n return false;\n if (isGlobalMember(value.callee, \"Bun\", \"sleep\")) return true;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"import\" &&\n timerModules.has(binding.module) &&\n timerHelpers.has(binding.imported)\n );\n };\n\n const isTimerOrBoundTimer = (value: unknown, use: Node): boolean => {\n if (isTimer(value)) return true;\n if (!isNode(value) || value.type !== \"Identifier\") return false;\n const initializer = constInitializerInSameFunction(value, use);\n return initializer !== null && isTimer(initializer);\n };\n\n return {\n CallExpression(node) {\n if (!isNode(node.callee) || node.callee.type !== \"MemberExpression\")\n return;\n const member = propertyName(node.callee.property);\n if (member === \"race\" && isNode(node.callee.object)) {\n const binding = resolveBinding(node.callee.object);\n if (binding.type === \"global\" && binding.name === \"Promise\") {\n const firstArgument = Array.isArray(node.arguments)\n ? node.arguments.at(0)\n : null;\n if (\n isNode(firstArgument) &&\n firstArgument.type === \"ArrayExpression\" &&\n Array.isArray(firstArgument.elements) &&\n firstArgument.elements.some((element) =>\n isTimerOrBoundTimer(element, node),\n )\n )\n report(node);\n return;\n }\n }\n const expectCall = matcherExpectCall(node.callee.object);\n if (expectCall === null || !isNode(expectCall.callee)) return;\n const calleeBinding = resolveBinding(expectCall.callee);\n const isExpect =\n (calleeBinding.type === \"global\" &&\n calleeBinding.name === \"expect\") ||\n (calleeBinding.type === \"import\" &&\n calleeBinding.imported === \"expect\" &&\n testModules.has(calleeBinding.module));\n if (!isExpect) return;\n const assertionValues = [expectCall.arguments, node.arguments];\n if (containsClockRead(assertionValues, node, true)) report(node);\n },\n };\n },\n);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-wall-clock-bound.mjs","names":[],"sources":["../src/no-wall-clock-bound.ts"],"sourcesContent":["import type { Scope } from \"@oxlint/plugins\";\n\nimport {\n createBindingResolver,\n identifierName,\n isNode,\n propertyName,\n testOnlyPlugin,\n type Node,\n} from \"./test-rule-utils\";\n\nconst timerModules = new Set([\n \"node:timers/promises\",\n \"timers/promises\",\n \"@stll/test-kit\",\n]);\nconst timerHelpers = new Set([\"delay\", \"setTimeout\", \"sleep\"]);\nconst testModules = new Set([\"@jest/globals\", \"bun:test\", \"vitest\"]);\nconst functionTypes = new Set([\n \"ArrowFunctionExpression\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n]);\nconst matcherModifiers = new Set([\"not\", \"rejects\", \"resolves\"]);\n\nconst isVariableReference = (identifier: Node): boolean => {\n const parent = identifier.parent;\n if (!isNode(parent)) return true;\n if (\n parent.type === \"MemberExpression\" &&\n parent.property === identifier &&\n parent.computed !== true\n )\n return false;\n if (\n (parent.type === \"Property\" ||\n parent.type === \"MethodDefinition\" ||\n parent.type === \"PropertyDefinition\") &&\n parent.key === identifier &&\n parent.computed !== true &&\n parent.value !== identifier\n )\n return false;\n if (\n (parent.type === \"LabeledStatement\" ||\n parent.type === \"BreakStatement\" ||\n parent.type === \"ContinueStatement\") &&\n parent.label === identifier\n )\n return false;\n if (\n (parent.type === \"VariableDeclarator\" ||\n parent.type === \"FunctionDeclaration\" ||\n parent.type === \"FunctionExpression\" ||\n parent.type === \"ClassDeclaration\" ||\n parent.type === \"ClassExpression\") &&\n parent.id === identifier\n )\n return false;\n return true;\n};\n\nconst enclosingFunction = (node: Node): Node | null => {\n let current = node.parent;\n while (isNode(current)) {\n if (functionTypes.has(current.type)) return current;\n current = current.parent;\n }\n return null;\n};\n\nexport default testOnlyPlugin(\n \"no-wall-clock-bound\",\n \"noWallClockBound\",\n \"A test must not assert on measured wall-clock time or race a timer, because such assertions fail on loaded runners; use a controlled clock or deferred for ordering.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n\n const isGlobalMember = (\n node: Node,\n objectName: string,\n member: string,\n ): boolean => {\n if (node.type !== \"MemberExpression\") return false;\n if (propertyName(node.property) !== member || !isNode(node.object))\n return false;\n const binding = resolveBinding(node.object);\n return binding.type === \"global\" && binding.name === objectName;\n };\n\n const isClockRead = (value: unknown): boolean => {\n if (!isNode(value) || value.type !== \"CallExpression\") return false;\n if (!isNode(value.callee)) return false;\n if (\n isGlobalMember(value.callee, \"performance\", \"now\") ||\n isGlobalMember(value.callee, \"Date\", \"now\") ||\n isGlobalMember(value.callee, \"Bun\", \"nanoseconds\")\n )\n return true;\n const callee = value.callee;\n return (\n callee.type === \"MemberExpression\" &&\n propertyName(callee.property) === \"bigint\" &&\n isNode(callee.object) &&\n isGlobalMember(callee.object, \"process\", \"hrtime\")\n );\n };\n\n const constInitializerInSameFunction = (\n identifier: Node,\n use: Node,\n ): Node | null => {\n const name = identifierName(identifier);\n if (name === null) return null;\n let scope: Scope | null = getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition?.type !== \"Variable\") return null;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init) ||\n enclosingFunction(declarator) !== enclosingFunction(use)\n )\n return null;\n return declarator.init;\n }\n return null;\n };\n\n const containsClockRead = (\n value: unknown,\n use: Node,\n followIdentifier: boolean,\n visitedIdentifiers = new Set<Node>(),\n ): boolean => {\n if (Array.isArray(value))\n return value.some((child) =>\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n if (!isNode(value)) return false;\n if (isClockRead(value)) return true;\n if (\n followIdentifier &&\n value.type === \"Identifier\" &&\n isVariableReference(value)\n ) {\n if (visitedIdentifiers.has(value)) return false;\n visitedIdentifiers.add(value);\n const initializer = constInitializerInSameFunction(value, use);\n if (\n initializer !== null &&\n containsClockRead(initializer, use, true, visitedIdentifiers)\n )\n return true;\n }\n return Object.values(value).some(\n (child) =>\n child !== value.parent &&\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n };\n\n const matcherExpectCall = (value: unknown): Node | null => {\n let current = value;\n while (\n isNode(current) &&\n current.type === \"MemberExpression\" &&\n matcherModifiers.has(propertyName(current.property) ?? \"\")\n )\n current = current.object;\n return isNode(current) && current.type === \"CallExpression\"\n ? current\n : null;\n };\n\n const containsSetTimeout = (value: unknown): boolean => {\n if (Array.isArray(value)) return value.some(containsSetTimeout);\n if (!isNode(value)) return false;\n if (value.type === \"CallExpression\" && isNode(value.callee)) {\n const binding = resolveBinding(value.callee);\n if (binding.type === \"global\" && binding.name === \"setTimeout\")\n return true;\n }\n for (const child of Object.values(value)) {\n if (child === value.parent) continue;\n if (Array.isArray(child)) {\n if (child.some(containsSetTimeout)) return true;\n } else if (containsSetTimeout(child)) return true;\n }\n return false;\n };\n\n const isTimer = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"NewExpression\") {\n if (!isNode(value.callee)) return false;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"global\" &&\n binding.name === \"Promise\" &&\n containsSetTimeout(value.arguments)\n );\n }\n if (value.type !== \"CallExpression\" || !isNode(value.callee))\n return false;\n if (isGlobalMember(value.callee, \"Bun\", \"sleep\")) return true;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"import\" &&\n timerModules.has(binding.module) &&\n timerHelpers.has(binding.imported)\n );\n };\n\n const isTimerOrBoundTimer = (value: unknown, use: Node): boolean => {\n if (isTimer(value)) return true;\n if (!isNode(value) || value.type !== \"Identifier\") return false;\n const initializer = constInitializerInSameFunction(value, use);\n return initializer !== null && isTimer(initializer);\n };\n\n return {\n CallExpression(node) {\n if (!isNode(node.callee) || node.callee.type !== \"MemberExpression\")\n return;\n const member = propertyName(node.callee.property);\n if (member === \"race\" && isNode(node.callee.object)) {\n const binding = resolveBinding(node.callee.object);\n if (binding.type === \"global\" && binding.name === \"Promise\") {\n const firstArgument = Array.isArray(node.arguments)\n ? node.arguments.at(0)\n : null;\n if (\n isNode(firstArgument) &&\n firstArgument.type === \"ArrayExpression\" &&\n Array.isArray(firstArgument.elements) &&\n firstArgument.elements.some((element) =>\n isTimerOrBoundTimer(element, node),\n )\n )\n report(node);\n return;\n }\n }\n const expectCall = matcherExpectCall(node.callee.object);\n if (expectCall === null || !isNode(expectCall.callee)) return;\n const calleeBinding = resolveBinding(expectCall.callee);\n const isExpect =\n (calleeBinding.type === \"global\" &&\n calleeBinding.name === \"expect\") ||\n (calleeBinding.type === \"import\" &&\n calleeBinding.imported === \"expect\" &&\n testModules.has(calleeBinding.module));\n if (!isExpect) return;\n const assertionValues = [expectCall.arguments, node.arguments];\n if (containsClockRead(assertionValues, node, true)) report(node);\n },\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"no-wall-clock-bound.mjs","names":[],"sources":["../src/no-wall-clock-bound.ts"],"sourcesContent":["import type { Scope } from \"@oxlint/plugins\";\n\nimport {\n createBindingResolver,\n identifierName,\n isNode,\n type Node,\n propertyName,\n testOnlyPlugin,\n} from \"./test-rule-utils\";\n\nconst timerModules = new Set([\n \"node:timers/promises\",\n \"timers/promises\",\n \"@stll/test-kit\",\n]);\nconst timerHelpers = new Set([\"delay\", \"setTimeout\", \"sleep\"]);\nconst testModules = new Set([\"@jest/globals\", \"bun:test\", \"vitest\"]);\nconst functionTypes = new Set([\n \"ArrowFunctionExpression\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n]);\nconst matcherModifiers = new Set([\"not\", \"rejects\", \"resolves\"]);\n\nconst isVariableReference = (identifier: Node): boolean => {\n const parent = identifier.parent;\n if (!isNode(parent)) return true;\n if (\n parent.type === \"MemberExpression\" &&\n parent.property === identifier &&\n parent.computed !== true\n )\n return false;\n if (\n (parent.type === \"Property\" ||\n parent.type === \"MethodDefinition\" ||\n parent.type === \"PropertyDefinition\") &&\n parent.key === identifier &&\n parent.computed !== true &&\n parent.value !== identifier\n )\n return false;\n if (\n (parent.type === \"LabeledStatement\" ||\n parent.type === \"BreakStatement\" ||\n parent.type === \"ContinueStatement\") &&\n parent.label === identifier\n )\n return false;\n if (\n (parent.type === \"VariableDeclarator\" ||\n parent.type === \"FunctionDeclaration\" ||\n parent.type === \"FunctionExpression\" ||\n parent.type === \"ClassDeclaration\" ||\n parent.type === \"ClassExpression\") &&\n parent.id === identifier\n )\n return false;\n return true;\n};\n\nconst enclosingFunction = (node: Node): Node | null => {\n let current = node.parent;\n while (isNode(current)) {\n if (functionTypes.has(current.type)) return current;\n current = current.parent;\n }\n return null;\n};\n\nexport default testOnlyPlugin(\n \"no-wall-clock-bound\",\n \"noWallClockBound\",\n \"A test must not assert on measured wall-clock time or race a timer, because such assertions fail on loaded runners; use a controlled clock or deferred for ordering.\",\n (report, getScope) => {\n const resolveBinding = createBindingResolver(getScope);\n\n const isGlobalMember = (\n node: Node,\n objectName: string,\n member: string,\n ): boolean => {\n if (node.type !== \"MemberExpression\") return false;\n if (propertyName(node.property) !== member || !isNode(node.object))\n return false;\n const binding = resolveBinding(node.object);\n return binding.type === \"global\" && binding.name === objectName;\n };\n\n const isClockRead = (value: unknown): boolean => {\n if (!isNode(value) || value.type !== \"CallExpression\") return false;\n if (!isNode(value.callee)) return false;\n if (\n isGlobalMember(value.callee, \"performance\", \"now\") ||\n isGlobalMember(value.callee, \"Date\", \"now\") ||\n isGlobalMember(value.callee, \"Bun\", \"nanoseconds\")\n )\n return true;\n const callee = value.callee;\n return (\n callee.type === \"MemberExpression\" &&\n propertyName(callee.property) === \"bigint\" &&\n isNode(callee.object) &&\n isGlobalMember(callee.object, \"process\", \"hrtime\")\n );\n };\n\n const constInitializerInSameFunction = (\n identifier: Node,\n use: Node,\n ): Node | null => {\n const name = identifierName(identifier);\n if (name === null) return null;\n let scope: Scope | null = getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(name);\n if (variable === undefined) {\n scope = scope.upper;\n continue;\n }\n const definition = variable.defs.at(0);\n if (definition?.type !== \"Variable\") return null;\n const declarator = definition.node as unknown;\n if (\n !isNode(declarator) ||\n !isNode(declarator.parent) ||\n declarator.parent.type !== \"VariableDeclaration\" ||\n declarator.parent.kind !== \"const\" ||\n !isNode(declarator.init) ||\n enclosingFunction(declarator) !== enclosingFunction(use)\n )\n return null;\n return declarator.init;\n }\n return null;\n };\n\n const containsClockRead = (\n value: unknown,\n use: Node,\n followIdentifier: boolean,\n visitedIdentifiers = new Set<Node>(),\n ): boolean => {\n if (Array.isArray(value))\n return value.some((child) =>\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n if (!isNode(value)) return false;\n if (isClockRead(value)) return true;\n if (\n followIdentifier &&\n value.type === \"Identifier\" &&\n isVariableReference(value)\n ) {\n if (visitedIdentifiers.has(value)) return false;\n visitedIdentifiers.add(value);\n const initializer = constInitializerInSameFunction(value, use);\n if (\n initializer !== null &&\n containsClockRead(initializer, use, true, visitedIdentifiers)\n )\n return true;\n }\n return Object.values(value).some(\n (child) =>\n child !== value.parent &&\n containsClockRead(child, use, followIdentifier, visitedIdentifiers),\n );\n };\n\n const matcherExpectCall = (value: unknown): Node | null => {\n let current = value;\n while (\n isNode(current) &&\n current.type === \"MemberExpression\" &&\n matcherModifiers.has(propertyName(current.property) ?? \"\")\n )\n current = current.object;\n return isNode(current) && current.type === \"CallExpression\"\n ? current\n : null;\n };\n\n const containsSetTimeout = (value: unknown): boolean => {\n if (Array.isArray(value)) return value.some(containsSetTimeout);\n if (!isNode(value)) return false;\n if (value.type === \"CallExpression\" && isNode(value.callee)) {\n const binding = resolveBinding(value.callee);\n if (binding.type === \"global\" && binding.name === \"setTimeout\")\n return true;\n }\n for (const child of Object.values(value)) {\n if (child === value.parent) continue;\n if (Array.isArray(child)) {\n if (child.some(containsSetTimeout)) return true;\n } else if (containsSetTimeout(child)) return true;\n }\n return false;\n };\n\n const isTimer = (value: unknown): boolean => {\n if (!isNode(value)) return false;\n if (value.type === \"NewExpression\") {\n if (!isNode(value.callee)) return false;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"global\" &&\n binding.name === \"Promise\" &&\n containsSetTimeout(value.arguments)\n );\n }\n if (value.type !== \"CallExpression\" || !isNode(value.callee))\n return false;\n if (isGlobalMember(value.callee, \"Bun\", \"sleep\")) return true;\n const binding = resolveBinding(value.callee);\n return (\n binding.type === \"import\" &&\n timerModules.has(binding.module) &&\n timerHelpers.has(binding.imported)\n );\n };\n\n const isTimerOrBoundTimer = (value: unknown, use: Node): boolean => {\n if (isTimer(value)) return true;\n if (!isNode(value) || value.type !== \"Identifier\") return false;\n const initializer = constInitializerInSameFunction(value, use);\n return initializer !== null && isTimer(initializer);\n };\n\n return {\n CallExpression(node) {\n if (!isNode(node.callee) || node.callee.type !== \"MemberExpression\")\n return;\n const member = propertyName(node.callee.property);\n if (member === \"race\" && isNode(node.callee.object)) {\n const binding = resolveBinding(node.callee.object);\n if (binding.type === \"global\" && binding.name === \"Promise\") {\n const firstArgument = Array.isArray(node.arguments)\n ? node.arguments.at(0)\n : null;\n if (\n isNode(firstArgument) &&\n firstArgument.type === \"ArrayExpression\" &&\n Array.isArray(firstArgument.elements) &&\n firstArgument.elements.some((element) =>\n isTimerOrBoundTimer(element, node),\n )\n )\n report(node);\n return;\n }\n }\n const expectCall = matcherExpectCall(node.callee.object);\n if (expectCall === null || !isNode(expectCall.callee)) return;\n const calleeBinding = resolveBinding(expectCall.callee);\n const isExpect =\n (calleeBinding.type === \"global\" &&\n calleeBinding.name === \"expect\") ||\n (calleeBinding.type === \"import\" &&\n calleeBinding.imported === \"expect\" &&\n testModules.has(calleeBinding.module));\n if (!isExpect) return;\n const assertionValues = [expectCall.arguments, node.arguments];\n if (containsClockRead(assertionValues, node, true)) report(node);\n },\n };\n },\n);\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isAstNode,
|
|
1
|
+
import { a as isAstNode, f as unwrapExpression, o as isIdentifier, r as getPropertyName, s as isStringLiteral, t as filenameForContext, u as resolveLiteralArraySpreads } from "./utils.mjs";
|
|
2
2
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
package/dist/utils.cjs
CHANGED
|
@@ -37,10 +37,22 @@ const invokedCallee = (node) => {
|
|
|
37
37
|
if (callee.type === "MemberExpression" && (memberPropertyName(callee) === "call" || memberPropertyName(callee) === "apply")) return unwrapExpression(callee.object);
|
|
38
38
|
return callee;
|
|
39
39
|
};
|
|
40
|
+
const EXPRESSION_WRAPPER_TYPES = [
|
|
41
|
+
"ParenthesizedExpression",
|
|
42
|
+
"ChainExpression",
|
|
43
|
+
"TSNonNullExpression",
|
|
44
|
+
"TSAsExpression",
|
|
45
|
+
"TSSatisfiesExpression",
|
|
46
|
+
"TSTypeAssertion",
|
|
47
|
+
"TSInstantiationExpression"
|
|
48
|
+
];
|
|
40
49
|
const unwrapExpression = (node) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
while (isAstNode(node)) {
|
|
51
|
+
const { type } = node;
|
|
52
|
+
if (!EXPRESSION_WRAPPER_TYPES.some((wrapper) => wrapper === type)) return node;
|
|
53
|
+
node = node.expression;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
44
56
|
};
|
|
45
57
|
const resolveLiteralArraySpreads = (args) => {
|
|
46
58
|
const expanded = [];
|
|
@@ -77,6 +89,61 @@ const getImportedName = (specifier) => {
|
|
|
77
89
|
if (isStringLiteral(imported)) return imported.value;
|
|
78
90
|
return null;
|
|
79
91
|
};
|
|
92
|
+
const isIdentifierReference = (node) => isIdentifier(node) && Array.isArray(node.range);
|
|
93
|
+
const resolveVariable = (context, identifier) => {
|
|
94
|
+
let scope = context.sourceCode.getScope(identifier);
|
|
95
|
+
while (scope !== null) {
|
|
96
|
+
const variable = scope.set.get(identifier.name);
|
|
97
|
+
if (variable !== void 0) return variable;
|
|
98
|
+
scope = scope.upper;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
const isSingleAssignment = (variable) => variable.references.every((reference) => !reference.isWrite() || reference.init);
|
|
103
|
+
const NAMESPACE_IMPORT = "*";
|
|
104
|
+
const memberOfNamespace = (base, property) => base?.imported === NAMESPACE_IMPORT && property !== null ? {
|
|
105
|
+
source: base.source,
|
|
106
|
+
imported: property
|
|
107
|
+
} : null;
|
|
108
|
+
const bindingFromVariable = (context, variable, seen) => {
|
|
109
|
+
const definition = variable.defs.at(0);
|
|
110
|
+
if (variable.defs.length !== 1 || definition === void 0) return null;
|
|
111
|
+
const node = definition.node;
|
|
112
|
+
if (definition.type === "ImportBinding") {
|
|
113
|
+
const declaration = definition.parent;
|
|
114
|
+
if (!isAstNode(node) || node.importKind === "type" || !isAstNode(declaration) || declaration.type !== "ImportDeclaration" || declaration.importKind === "type" || !isStringLiteral(declaration.source)) return null;
|
|
115
|
+
const source = declaration.source.value;
|
|
116
|
+
if (node.type === "ImportNamespaceSpecifier") return {
|
|
117
|
+
source,
|
|
118
|
+
imported: NAMESPACE_IMPORT
|
|
119
|
+
};
|
|
120
|
+
if (node.type === "ImportDefaultSpecifier") return {
|
|
121
|
+
source,
|
|
122
|
+
imported: "default"
|
|
123
|
+
};
|
|
124
|
+
const imported = getImportedName(node);
|
|
125
|
+
return imported === null ? null : {
|
|
126
|
+
source,
|
|
127
|
+
imported
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (definition.type !== "Variable" || !isAstNode(node) || node.type !== "VariableDeclarator" || !isSingleAssignment(variable) || !isIdentifier(node.id)) return null;
|
|
131
|
+
return resolveImportedExpressionForEnforcement(context, node.init, seen);
|
|
132
|
+
};
|
|
133
|
+
const resolveImportedExpressionForEnforcement = (context, node, seen = /* @__PURE__ */ new Set()) => {
|
|
134
|
+
const expression = unwrapExpression(node);
|
|
135
|
+
if (!isAstNode(expression) || seen.has(expression)) return null;
|
|
136
|
+
seen.add(expression);
|
|
137
|
+
if (isIdentifierReference(expression)) {
|
|
138
|
+
const variable = resolveVariable(context, expression);
|
|
139
|
+
return variable === null ? null : bindingFromVariable(context, variable, seen);
|
|
140
|
+
}
|
|
141
|
+
if (expression.type === "MemberExpression") {
|
|
142
|
+
const base = resolveImportedExpressionForEnforcement(context, expression.object, seen);
|
|
143
|
+
return memberOfNamespace(base, memberPropertyName(expression));
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
80
147
|
//#endregion
|
|
81
148
|
Object.defineProperty(exports, "filenameForContext", {
|
|
82
149
|
enumerable: true,
|
|
@@ -126,6 +193,12 @@ Object.defineProperty(exports, "memberPropertyName", {
|
|
|
126
193
|
return memberPropertyName;
|
|
127
194
|
}
|
|
128
195
|
});
|
|
196
|
+
Object.defineProperty(exports, "resolveImportedExpressionForEnforcement", {
|
|
197
|
+
enumerable: true,
|
|
198
|
+
get: function() {
|
|
199
|
+
return resolveImportedExpressionForEnforcement;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
129
202
|
Object.defineProperty(exports, "resolveLiteralArraySpreads", {
|
|
130
203
|
enumerable: true,
|
|
131
204
|
get: function() {
|
package/dist/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\nexport const staticStringValue = (node: unknown): string | null => {\n const expression = unwrapExpression(node);\n if (expression === null) return null;\n node = expression;\n if (isStringLiteral(node)) return node.value;\n if (\n !isAstNode(node) ||\n node.type !== \"TemplateLiteral\" ||\n !Array.isArray(node.expressions) ||\n node.expressions.length !== 0 ||\n !Array.isArray(node.quasis)\n )\n return null;\n const quasi = node.quasis.at(0);\n if (!isAstNode(quasi)) return null;\n const value = quasi.value;\n const cooked =\n typeof value === \"object\" && value !== null\n ? Reflect.get(value, \"cooked\")\n : undefined;\n return typeof cooked === \"string\" ? cooked : null;\n};\n\nexport const memberPropertyName = (node: unknown): string | null => {\n if (!isAstNode(node) || node.type !== \"MemberExpression\") {\n return null;\n }\n if (node.computed !== true) {\n return getPropertyName(node.property);\n }\n return staticStringValue(node.property);\n};\n\nexport const invokedCallee = (node: unknown): AstNode | null => {\n if (!isAstNode(node) || node.type !== \"CallExpression\") {\n return null;\n }\n const callee = unwrapExpression(node.callee);\n if (!isAstNode(callee)) {\n return null;\n }\n if (\n callee.type === \"SequenceExpression\" &&\n Array.isArray(callee.expressions)\n ) {\n return unwrapExpression(callee.expressions.at(-1));\n }\n if (\n callee.type === \"MemberExpression\" &&\n (memberPropertyName(callee) === \"call\" ||\n memberPropertyName(callee) === \"apply\")\n ) {\n return unwrapExpression(callee.object);\n }\n return callee;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\nexport type LiteralArgumentResolution =\n | { status: \"known\"; args: unknown[] }\n | { status: \"opaque\"; args: unknown[]; spread: unknown; tail: unknown[] };\n\n// Expand literal array spreads and retain unresolved spread nodes in sequence.\n// Positional readers use only `args`, the prefix before the first barrier;\n// source inspectors can still inspect later entries in `tail`.\nexport const resolveLiteralArraySpreads = (\n args: unknown[],\n): LiteralArgumentResolution => {\n const expanded: unknown[] = [];\n const append = (argument: unknown): void => {\n if (!isAstNode(argument) || argument.type !== \"SpreadElement\") {\n expanded.push(argument);\n return;\n }\n const expression = unwrapExpression(argument.argument);\n if (\n expression?.type !== \"ArrayExpression\" ||\n !Array.isArray(expression.elements)\n ) {\n expanded.push(argument);\n return;\n }\n for (const element of expression.elements) append(element);\n };\n for (const argument of args) append(argument);\n const barrierIndex = expanded.findIndex(\n (argument) => isAstNode(argument) && argument.type === \"SpreadElement\",\n );\n if (barrierIndex === -1) return { status: \"known\", args: expanded };\n const spread = expanded.at(barrierIndex);\n return {\n status: \"opaque\",\n args: expanded.slice(0, barrierIndex),\n spread,\n tail: expanded.slice(barrierIndex + 1),\n };\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { ESTree, Ranged, Scope, Variable } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\nexport const staticStringValue = (node: unknown): string | null => {\n const expression = unwrapExpression(node);\n if (expression === null) return null;\n node = expression;\n if (isStringLiteral(node)) return node.value;\n if (\n !isAstNode(node) ||\n node.type !== \"TemplateLiteral\" ||\n !Array.isArray(node.expressions) ||\n node.expressions.length !== 0 ||\n !Array.isArray(node.quasis)\n )\n return null;\n const quasi = node.quasis.at(0);\n if (!isAstNode(quasi)) return null;\n const value = quasi.value;\n const cooked =\n typeof value === \"object\" && value !== null\n ? Reflect.get(value, \"cooked\")\n : undefined;\n return typeof cooked === \"string\" ? cooked : null;\n};\n\nexport const memberPropertyName = (node: unknown): string | null => {\n if (!isAstNode(node) || node.type !== \"MemberExpression\") {\n return null;\n }\n if (node.computed !== true) {\n return getPropertyName(node.property);\n }\n return staticStringValue(node.property);\n};\n\nexport const invokedCallee = (node: unknown): AstNode | null => {\n if (!isAstNode(node) || node.type !== \"CallExpression\") {\n return null;\n }\n const callee = unwrapExpression(node.callee);\n if (!isAstNode(callee)) {\n return null;\n }\n if (\n callee.type === \"SequenceExpression\" &&\n Array.isArray(callee.expressions)\n ) {\n return unwrapExpression(callee.expressions.at(-1));\n }\n if (\n callee.type === \"MemberExpression\" &&\n (memberPropertyName(callee) === \"call\" ||\n memberPropertyName(callee) === \"apply\")\n ) {\n return unwrapExpression(callee.object);\n }\n return callee;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Parser wrappers with no value transformation of their own.\nexport const EXPRESSION_WRAPPER_TYPES = [\n \"ParenthesizedExpression\",\n \"ChainExpression\",\n \"TSNonNullExpression\",\n \"TSAsExpression\",\n \"TSSatisfiesExpression\",\n \"TSTypeAssertion\",\n \"TSInstantiationExpression\",\n] as const satisfies readonly ESTree.Node[\"type\"][];\n\n// Keep the parser's other expression-bearing contexts intact.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n while (isAstNode(node)) {\n const { type } = node;\n if (!EXPRESSION_WRAPPER_TYPES.some((wrapper) => wrapper === type))\n return node;\n node = node.expression;\n }\n return null;\n};\n\nexport type LiteralArgumentResolution =\n | { status: \"known\"; args: unknown[] }\n | { status: \"opaque\"; args: unknown[]; spread: unknown; tail: unknown[] };\n\n// Expand literal array spreads and retain unresolved spread nodes in sequence.\n// Positional readers use only `args`, the prefix before the first barrier;\n// source inspectors can still inspect later entries in `tail`.\nexport const resolveLiteralArraySpreads = (\n args: unknown[],\n): LiteralArgumentResolution => {\n const expanded: unknown[] = [];\n const append = (argument: unknown): void => {\n if (!isAstNode(argument) || argument.type !== \"SpreadElement\") {\n expanded.push(argument);\n return;\n }\n const expression = unwrapExpression(argument.argument);\n if (\n expression?.type !== \"ArrayExpression\" ||\n !Array.isArray(expression.elements)\n ) {\n expanded.push(argument);\n return;\n }\n for (const element of expression.elements) append(element);\n };\n for (const argument of args) append(argument);\n const barrierIndex = expanded.findIndex(\n (argument) => isAstNode(argument) && argument.type === \"SpreadElement\",\n );\n if (barrierIndex === -1) return { status: \"known\", args: expanded };\n const spread = expanded.at(barrierIndex);\n return {\n status: \"opaque\",\n args: expanded.slice(0, barrierIndex),\n spread,\n tail: expanded.slice(barrierIndex + 1),\n };\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n\n// An Identifier the parser emitted as a value reference (it carries a range,\n// which synthetic name nodes do not).\nexport const isIdentifierReference = (\n node: unknown,\n): node is ESTree.IdentifierReference =>\n isIdentifier(node) && Array.isArray(node.range);\n\nexport type ScopeContext = {\n sourceCode: { getScope: (node: ESTree.Node) => Scope };\n};\n\nconst resolveVariable = (\n context: ScopeContext,\n identifier: ESTree.IdentifierReference,\n): Variable | null => {\n let scope: Scope | null = context.sourceCode.getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(identifier.name);\n if (variable !== undefined) {\n return variable;\n }\n scope = scope.upper;\n }\n return null;\n};\n\nconst isSingleAssignment = (variable: Variable): boolean =>\n variable.references.every(\n (reference) => !reference.isWrite() || reference.init,\n );\n\ntype ImportedBinding = { source: string; imported: string };\nconst NAMESPACE_IMPORT = \"*\";\n\nconst memberOfNamespace = (\n base: ImportedBinding | null,\n property: string | null,\n): ImportedBinding | null =>\n base?.imported === NAMESPACE_IMPORT && property !== null\n ? { source: base.source, imported: property }\n : null;\n\nconst bindingFromVariable = (\n context: ScopeContext,\n variable: Variable,\n seen: Set<unknown>,\n): ImportedBinding | null => {\n const definition = variable.defs.at(0);\n if (variable.defs.length !== 1 || definition === undefined) {\n return null;\n }\n const node: unknown = definition.node;\n if (definition.type === \"ImportBinding\") {\n const declaration: unknown = definition.parent;\n if (\n !isAstNode(node) ||\n node.importKind === \"type\" ||\n !isAstNode(declaration) ||\n declaration.type !== \"ImportDeclaration\" ||\n declaration.importKind === \"type\" ||\n !isStringLiteral(declaration.source)\n ) {\n return null;\n }\n const source = declaration.source.value;\n if (node.type === \"ImportNamespaceSpecifier\") {\n return { source, imported: NAMESPACE_IMPORT };\n }\n if (node.type === \"ImportDefaultSpecifier\") {\n return { source, imported: \"default\" };\n }\n const imported = getImportedName(node);\n return imported === null ? null : { source, imported };\n }\n if (\n definition.type !== \"Variable\" ||\n !isAstNode(node) ||\n node.type !== \"VariableDeclarator\" ||\n !isSingleAssignment(variable) ||\n !isIdentifier(node.id)\n ) {\n return null;\n }\n return resolveImportedExpressionForEnforcement(context, node.init, seen);\n};\n\n// The module export an expression evaluates to, or null for a local,\n// parameter, call result, or unresolved global.\nexport const resolveImportedExpressionForEnforcement = (\n context: ScopeContext,\n node: unknown,\n seen = new Set<unknown>(),\n): ImportedBinding | null => {\n const expression = unwrapExpression(node);\n if (!isAstNode(expression) || seen.has(expression)) {\n return null;\n }\n seen.add(expression);\n if (isIdentifierReference(expression)) {\n const variable = resolveVariable(context, expression);\n return variable === null\n ? null\n : bindingFromVariable(context, variable, seen);\n }\n if (expression.type === \"MemberExpression\") {\n const base = resolveImportedExpressionForEnforcement(\n context,\n expression.object,\n seen,\n );\n return memberOfNamespace(base, memberPropertyName(expression));\n }\n return null;\n};\n"],"mappings":"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"}
|
package/dist/utils.mjs
CHANGED
|
@@ -37,10 +37,22 @@ const invokedCallee = (node) => {
|
|
|
37
37
|
if (callee.type === "MemberExpression" && (memberPropertyName(callee) === "call" || memberPropertyName(callee) === "apply")) return unwrapExpression(callee.object);
|
|
38
38
|
return callee;
|
|
39
39
|
};
|
|
40
|
+
const EXPRESSION_WRAPPER_TYPES = [
|
|
41
|
+
"ParenthesizedExpression",
|
|
42
|
+
"ChainExpression",
|
|
43
|
+
"TSNonNullExpression",
|
|
44
|
+
"TSAsExpression",
|
|
45
|
+
"TSSatisfiesExpression",
|
|
46
|
+
"TSTypeAssertion",
|
|
47
|
+
"TSInstantiationExpression"
|
|
48
|
+
];
|
|
40
49
|
const unwrapExpression = (node) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
while (isAstNode(node)) {
|
|
51
|
+
const { type } = node;
|
|
52
|
+
if (!EXPRESSION_WRAPPER_TYPES.some((wrapper) => wrapper === type)) return node;
|
|
53
|
+
node = node.expression;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
44
56
|
};
|
|
45
57
|
const resolveLiteralArraySpreads = (args) => {
|
|
46
58
|
const expanded = [];
|
|
@@ -77,7 +89,62 @@ const getImportedName = (specifier) => {
|
|
|
77
89
|
if (isStringLiteral(imported)) return imported.value;
|
|
78
90
|
return null;
|
|
79
91
|
};
|
|
92
|
+
const isIdentifierReference = (node) => isIdentifier(node) && Array.isArray(node.range);
|
|
93
|
+
const resolveVariable = (context, identifier) => {
|
|
94
|
+
let scope = context.sourceCode.getScope(identifier);
|
|
95
|
+
while (scope !== null) {
|
|
96
|
+
const variable = scope.set.get(identifier.name);
|
|
97
|
+
if (variable !== void 0) return variable;
|
|
98
|
+
scope = scope.upper;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
const isSingleAssignment = (variable) => variable.references.every((reference) => !reference.isWrite() || reference.init);
|
|
103
|
+
const NAMESPACE_IMPORT = "*";
|
|
104
|
+
const memberOfNamespace = (base, property) => base?.imported === NAMESPACE_IMPORT && property !== null ? {
|
|
105
|
+
source: base.source,
|
|
106
|
+
imported: property
|
|
107
|
+
} : null;
|
|
108
|
+
const bindingFromVariable = (context, variable, seen) => {
|
|
109
|
+
const definition = variable.defs.at(0);
|
|
110
|
+
if (variable.defs.length !== 1 || definition === void 0) return null;
|
|
111
|
+
const node = definition.node;
|
|
112
|
+
if (definition.type === "ImportBinding") {
|
|
113
|
+
const declaration = definition.parent;
|
|
114
|
+
if (!isAstNode(node) || node.importKind === "type" || !isAstNode(declaration) || declaration.type !== "ImportDeclaration" || declaration.importKind === "type" || !isStringLiteral(declaration.source)) return null;
|
|
115
|
+
const source = declaration.source.value;
|
|
116
|
+
if (node.type === "ImportNamespaceSpecifier") return {
|
|
117
|
+
source,
|
|
118
|
+
imported: NAMESPACE_IMPORT
|
|
119
|
+
};
|
|
120
|
+
if (node.type === "ImportDefaultSpecifier") return {
|
|
121
|
+
source,
|
|
122
|
+
imported: "default"
|
|
123
|
+
};
|
|
124
|
+
const imported = getImportedName(node);
|
|
125
|
+
return imported === null ? null : {
|
|
126
|
+
source,
|
|
127
|
+
imported
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (definition.type !== "Variable" || !isAstNode(node) || node.type !== "VariableDeclarator" || !isSingleAssignment(variable) || !isIdentifier(node.id)) return null;
|
|
131
|
+
return resolveImportedExpressionForEnforcement(context, node.init, seen);
|
|
132
|
+
};
|
|
133
|
+
const resolveImportedExpressionForEnforcement = (context, node, seen = /* @__PURE__ */ new Set()) => {
|
|
134
|
+
const expression = unwrapExpression(node);
|
|
135
|
+
if (!isAstNode(expression) || seen.has(expression)) return null;
|
|
136
|
+
seen.add(expression);
|
|
137
|
+
if (isIdentifierReference(expression)) {
|
|
138
|
+
const variable = resolveVariable(context, expression);
|
|
139
|
+
return variable === null ? null : bindingFromVariable(context, variable, seen);
|
|
140
|
+
}
|
|
141
|
+
if (expression.type === "MemberExpression") {
|
|
142
|
+
const base = resolveImportedExpressionForEnforcement(context, expression.object, seen);
|
|
143
|
+
return memberOfNamespace(base, memberPropertyName(expression));
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
80
147
|
//#endregion
|
|
81
|
-
export { isAstNode as a, memberPropertyName as c,
|
|
148
|
+
export { isAstNode as a, memberPropertyName as c, staticStringValue as d, unwrapExpression as f, invokedCallee as i, resolveImportedExpressionForEnforcement as l, getImportedName as n, isIdentifier as o, getPropertyName as r, isStringLiteral as s, filenameForContext as t, resolveLiteralArraySpreads as u };
|
|
82
149
|
|
|
83
150
|
//# sourceMappingURL=utils.mjs.map
|
package/dist/utils.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { Ranged } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\nexport const staticStringValue = (node: unknown): string | null => {\n const expression = unwrapExpression(node);\n if (expression === null) return null;\n node = expression;\n if (isStringLiteral(node)) return node.value;\n if (\n !isAstNode(node) ||\n node.type !== \"TemplateLiteral\" ||\n !Array.isArray(node.expressions) ||\n node.expressions.length !== 0 ||\n !Array.isArray(node.quasis)\n )\n return null;\n const quasi = node.quasis.at(0);\n if (!isAstNode(quasi)) return null;\n const value = quasi.value;\n const cooked =\n typeof value === \"object\" && value !== null\n ? Reflect.get(value, \"cooked\")\n : undefined;\n return typeof cooked === \"string\" ? cooked : null;\n};\n\nexport const memberPropertyName = (node: unknown): string | null => {\n if (!isAstNode(node) || node.type !== \"MemberExpression\") {\n return null;\n }\n if (node.computed !== true) {\n return getPropertyName(node.property);\n }\n return staticStringValue(node.property);\n};\n\nexport const invokedCallee = (node: unknown): AstNode | null => {\n if (!isAstNode(node) || node.type !== \"CallExpression\") {\n return null;\n }\n const callee = unwrapExpression(node.callee);\n if (!isAstNode(callee)) {\n return null;\n }\n if (\n callee.type === \"SequenceExpression\" &&\n Array.isArray(callee.expressions)\n ) {\n return unwrapExpression(callee.expressions.at(-1));\n }\n if (\n callee.type === \"MemberExpression\" &&\n (memberPropertyName(callee) === \"call\" ||\n memberPropertyName(callee) === \"apply\")\n ) {\n return unwrapExpression(callee.object);\n }\n return callee;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Peel TS-only wrapping nodes so a shape check sees the underlying\n// expression. Returns the original node when no wrapping is present.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n if (!isAstNode(node)) {\n return null;\n }\n if (\n node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ChainExpression\"\n ) {\n return unwrapExpression(node.expression);\n }\n return node;\n};\n\nexport type LiteralArgumentResolution =\n | { status: \"known\"; args: unknown[] }\n | { status: \"opaque\"; args: unknown[]; spread: unknown; tail: unknown[] };\n\n// Expand literal array spreads and retain unresolved spread nodes in sequence.\n// Positional readers use only `args`, the prefix before the first barrier;\n// source inspectors can still inspect later entries in `tail`.\nexport const resolveLiteralArraySpreads = (\n args: unknown[],\n): LiteralArgumentResolution => {\n const expanded: unknown[] = [];\n const append = (argument: unknown): void => {\n if (!isAstNode(argument) || argument.type !== \"SpreadElement\") {\n expanded.push(argument);\n return;\n }\n const expression = unwrapExpression(argument.argument);\n if (\n expression?.type !== \"ArrayExpression\" ||\n !Array.isArray(expression.elements)\n ) {\n expanded.push(argument);\n return;\n }\n for (const element of expression.elements) append(element);\n };\n for (const argument of args) append(argument);\n const barrierIndex = expanded.findIndex(\n (argument) => isAstNode(argument) && argument.type === \"SpreadElement\",\n );\n if (barrierIndex === -1) return { status: \"known\", args: expanded };\n const spread = expanded.at(barrierIndex);\n return {\n status: \"opaque\",\n args: expanded.slice(0, barrierIndex),\n spread,\n tail: expanded.slice(barrierIndex + 1),\n };\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["// Shared AST helpers for the rules in this folder.\n//\n// Oxlint plugin AST nodes are passed in untyped. Each helper narrows from\n// `unknown` so rule files can call them without per-call type ceremony or\n// shared type-import boilerplate.\n\nimport type { ESTree, Ranged, Scope, Variable } from \"@oxlint/plugins\";\n\nexport type AstNode = Ranged & { type: string } & Record<string, unknown>;\n\ntype FilenameContext = {\n filename?: string;\n getFilename?: () => string;\n};\n\nexport const filenameForContext = (context: FilenameContext): string =>\n (context.filename ?? context.getFilename?.() ?? \"\").replaceAll(\"\\\\\", \"/\");\n\nexport const isAstNode = (node: unknown): node is AstNode =>\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n typeof (node as { type: unknown }).type === \"string\" &&\n \"range\" in node;\n\nexport const isIdentifier = (\n node: unknown,\n name?: string,\n): node is AstNode & { type: \"Identifier\"; name: string } => {\n if (!isAstNode(node) || node.type !== \"Identifier\") {\n return false;\n }\n if (typeof node.name !== \"string\") {\n return false;\n }\n return name === undefined || node.name === name;\n};\n\nexport const isStringLiteral = (\n node: unknown,\n): node is AstNode & { type: \"Literal\"; value: string } =>\n isAstNode(node) && node.type === \"Literal\" && typeof node.value === \"string\";\n\n// Resolve the static name of a Property or MemberExpression key:\n// Identifier.name or string-Literal.value. Returns null for computed keys\n// driven by a non-literal expression.\nexport const getPropertyName = (node: unknown): string | null => {\n if (isIdentifier(node)) {\n return node.name;\n }\n if (isStringLiteral(node)) {\n return node.value;\n }\n return null;\n};\n\nexport const staticStringValue = (node: unknown): string | null => {\n const expression = unwrapExpression(node);\n if (expression === null) return null;\n node = expression;\n if (isStringLiteral(node)) return node.value;\n if (\n !isAstNode(node) ||\n node.type !== \"TemplateLiteral\" ||\n !Array.isArray(node.expressions) ||\n node.expressions.length !== 0 ||\n !Array.isArray(node.quasis)\n )\n return null;\n const quasi = node.quasis.at(0);\n if (!isAstNode(quasi)) return null;\n const value = quasi.value;\n const cooked =\n typeof value === \"object\" && value !== null\n ? Reflect.get(value, \"cooked\")\n : undefined;\n return typeof cooked === \"string\" ? cooked : null;\n};\n\nexport const memberPropertyName = (node: unknown): string | null => {\n if (!isAstNode(node) || node.type !== \"MemberExpression\") {\n return null;\n }\n if (node.computed !== true) {\n return getPropertyName(node.property);\n }\n return staticStringValue(node.property);\n};\n\nexport const invokedCallee = (node: unknown): AstNode | null => {\n if (!isAstNode(node) || node.type !== \"CallExpression\") {\n return null;\n }\n const callee = unwrapExpression(node.callee);\n if (!isAstNode(callee)) {\n return null;\n }\n if (\n callee.type === \"SequenceExpression\" &&\n Array.isArray(callee.expressions)\n ) {\n return unwrapExpression(callee.expressions.at(-1));\n }\n if (\n callee.type === \"MemberExpression\" &&\n (memberPropertyName(callee) === \"call\" ||\n memberPropertyName(callee) === \"apply\")\n ) {\n return unwrapExpression(callee.object);\n }\n return callee;\n};\n\n// Match `<object>.<property>` member access where both halves are\n// Identifiers and the access is not computed.\nexport const isMemberAccess = (\n node: unknown,\n object: string,\n property: string,\n): boolean =>\n isAstNode(node) &&\n node.type === \"MemberExpression\" &&\n node.computed === false &&\n isIdentifier(node.object, object) &&\n isIdentifier(node.property, property);\n\n// Match `CallExpression` whose callee is an Identifier with the given name.\nexport const isCallTo = (node: unknown, name: string): boolean =>\n isAstNode(node) &&\n node.type === \"CallExpression\" &&\n isIdentifier(node.callee, name);\n\n// Resolve the dot-notation name of a callee: an Identifier, or a\n// non-computed MemberExpression chain (e.g. `t.String`, `Schema.is`,\n// `process.stderr.write`). Returns null when the chain is computed\n// or the property name itself can't be resolved.\n//\n// If the chain is rooted at a non-Identifier expression (e.g. `foo().bar`),\n// returns the bare property name (\"bar\") rather than null. Callers that\n// match the result against a fixed allowlist must consider whether they\n// need to distinguish `foo().createSafeHandler` from `createSafeHandler`.\nexport const getCalleeName = (callee: unknown): string | null => {\n if (isIdentifier(callee)) {\n return callee.name;\n }\n if (!isAstNode(callee) || callee.type !== \"MemberExpression\") {\n return null;\n }\n if (callee.computed !== false) {\n return null;\n }\n const objectName = getCalleeName(callee.object);\n const propertyName = getPropertyName(callee.property);\n if (propertyName === null) {\n return null;\n }\n return objectName === null ? propertyName : `${objectName}.${propertyName}`;\n};\n\n// Parser wrappers with no value transformation of their own.\nexport const EXPRESSION_WRAPPER_TYPES = [\n \"ParenthesizedExpression\",\n \"ChainExpression\",\n \"TSNonNullExpression\",\n \"TSAsExpression\",\n \"TSSatisfiesExpression\",\n \"TSTypeAssertion\",\n \"TSInstantiationExpression\",\n] as const satisfies readonly ESTree.Node[\"type\"][];\n\n// Keep the parser's other expression-bearing contexts intact.\nexport const unwrapExpression = (node: unknown): AstNode | null => {\n while (isAstNode(node)) {\n const { type } = node;\n if (!EXPRESSION_WRAPPER_TYPES.some((wrapper) => wrapper === type))\n return node;\n node = node.expression;\n }\n return null;\n};\n\nexport type LiteralArgumentResolution =\n | { status: \"known\"; args: unknown[] }\n | { status: \"opaque\"; args: unknown[]; spread: unknown; tail: unknown[] };\n\n// Expand literal array spreads and retain unresolved spread nodes in sequence.\n// Positional readers use only `args`, the prefix before the first barrier;\n// source inspectors can still inspect later entries in `tail`.\nexport const resolveLiteralArraySpreads = (\n args: unknown[],\n): LiteralArgumentResolution => {\n const expanded: unknown[] = [];\n const append = (argument: unknown): void => {\n if (!isAstNode(argument) || argument.type !== \"SpreadElement\") {\n expanded.push(argument);\n return;\n }\n const expression = unwrapExpression(argument.argument);\n if (\n expression?.type !== \"ArrayExpression\" ||\n !Array.isArray(expression.elements)\n ) {\n expanded.push(argument);\n return;\n }\n for (const element of expression.elements) append(element);\n };\n for (const argument of args) append(argument);\n const barrierIndex = expanded.findIndex(\n (argument) => isAstNode(argument) && argument.type === \"SpreadElement\",\n );\n if (barrierIndex === -1) return { status: \"known\", args: expanded };\n const spread = expanded.at(barrierIndex);\n return {\n status: \"opaque\",\n args: expanded.slice(0, barrierIndex),\n spread,\n tail: expanded.slice(barrierIndex + 1),\n };\n};\n\n// Resolve an ImportSpecifier's imported binding name (Identifier.name or\n// string-Literal.value). Returns null when the specifier shape is unexpected.\nexport const getImportedName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n const imported = specifier.imported;\n if (isIdentifier(imported)) {\n return imported.name;\n }\n if (isStringLiteral(imported)) {\n return imported.value;\n }\n return null;\n};\n\n// Resolve an ImportSpecifier's local binding name. This differs from\n// getImportedName for aliased imports such as `import { source as local }`.\nexport const getImportLocalName = (specifier: unknown): string | null => {\n if (!isAstNode(specifier) || specifier.type !== \"ImportSpecifier\") {\n return null;\n }\n return isIdentifier(specifier.local) ? specifier.local.name : null;\n};\n\n// An Identifier the parser emitted as a value reference (it carries a range,\n// which synthetic name nodes do not).\nexport const isIdentifierReference = (\n node: unknown,\n): node is ESTree.IdentifierReference =>\n isIdentifier(node) && Array.isArray(node.range);\n\nexport type ScopeContext = {\n sourceCode: { getScope: (node: ESTree.Node) => Scope };\n};\n\nconst resolveVariable = (\n context: ScopeContext,\n identifier: ESTree.IdentifierReference,\n): Variable | null => {\n let scope: Scope | null = context.sourceCode.getScope(identifier);\n while (scope !== null) {\n const variable = scope.set.get(identifier.name);\n if (variable !== undefined) {\n return variable;\n }\n scope = scope.upper;\n }\n return null;\n};\n\nconst isSingleAssignment = (variable: Variable): boolean =>\n variable.references.every(\n (reference) => !reference.isWrite() || reference.init,\n );\n\ntype ImportedBinding = { source: string; imported: string };\nconst NAMESPACE_IMPORT = \"*\";\n\nconst memberOfNamespace = (\n base: ImportedBinding | null,\n property: string | null,\n): ImportedBinding | null =>\n base?.imported === NAMESPACE_IMPORT && property !== null\n ? { source: base.source, imported: property }\n : null;\n\nconst bindingFromVariable = (\n context: ScopeContext,\n variable: Variable,\n seen: Set<unknown>,\n): ImportedBinding | null => {\n const definition = variable.defs.at(0);\n if (variable.defs.length !== 1 || definition === undefined) {\n return null;\n }\n const node: unknown = definition.node;\n if (definition.type === \"ImportBinding\") {\n const declaration: unknown = definition.parent;\n if (\n !isAstNode(node) ||\n node.importKind === \"type\" ||\n !isAstNode(declaration) ||\n declaration.type !== \"ImportDeclaration\" ||\n declaration.importKind === \"type\" ||\n !isStringLiteral(declaration.source)\n ) {\n return null;\n }\n const source = declaration.source.value;\n if (node.type === \"ImportNamespaceSpecifier\") {\n return { source, imported: NAMESPACE_IMPORT };\n }\n if (node.type === \"ImportDefaultSpecifier\") {\n return { source, imported: \"default\" };\n }\n const imported = getImportedName(node);\n return imported === null ? null : { source, imported };\n }\n if (\n definition.type !== \"Variable\" ||\n !isAstNode(node) ||\n node.type !== \"VariableDeclarator\" ||\n !isSingleAssignment(variable) ||\n !isIdentifier(node.id)\n ) {\n return null;\n }\n return resolveImportedExpressionForEnforcement(context, node.init, seen);\n};\n\n// The module export an expression evaluates to, or null for a local,\n// parameter, call result, or unresolved global.\nexport const resolveImportedExpressionForEnforcement = (\n context: ScopeContext,\n node: unknown,\n seen = new Set<unknown>(),\n): ImportedBinding | null => {\n const expression = unwrapExpression(node);\n if (!isAstNode(expression) || seen.has(expression)) {\n return null;\n }\n seen.add(expression);\n if (isIdentifierReference(expression)) {\n const variable = resolveVariable(context, expression);\n return variable === null\n ? null\n : bindingFromVariable(context, variable, seen);\n }\n if (expression.type === \"MemberExpression\") {\n const base = resolveImportedExpressionForEnforcement(\n context,\n expression.object,\n seen,\n );\n return memberOfNamespace(base, memberPropertyName(expression));\n }\n return null;\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/oxlint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Portable static-safety and route-query rules for Oxlint consumers.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -92,6 +92,17 @@
|
|
|
92
92
|
},
|
|
93
93
|
"default": "./dist/require-loader-prefetch.mjs"
|
|
94
94
|
},
|
|
95
|
+
"./bun-test-hygiene": {
|
|
96
|
+
"import": {
|
|
97
|
+
"types": "./dist/bun-test-hygiene.d.mts",
|
|
98
|
+
"default": "./dist/bun-test-hygiene.mjs"
|
|
99
|
+
},
|
|
100
|
+
"require": {
|
|
101
|
+
"types": "./dist/bun-test-hygiene.d.cts",
|
|
102
|
+
"default": "./dist/bun-test-hygiene.cjs"
|
|
103
|
+
},
|
|
104
|
+
"default": "./dist/bun-test-hygiene.mjs"
|
|
105
|
+
},
|
|
95
106
|
"./no-wall-clock-in-tests": {
|
|
96
107
|
"import": {
|
|
97
108
|
"types": "./dist/no-wall-clock-in-tests.d.mts",
|
|
@@ -164,14 +175,12 @@
|
|
|
164
175
|
},
|
|
165
176
|
"scripts": {
|
|
166
177
|
"build": "tsdown",
|
|
167
|
-
"typecheck": "bun check"
|
|
168
|
-
"format": "oxfmt ."
|
|
178
|
+
"typecheck": "bun check"
|
|
169
179
|
},
|
|
170
180
|
"devDependencies": {
|
|
171
181
|
"@oxlint/plugins": "1.87.0",
|
|
172
182
|
"@stll/typescript-config": "workspace:*",
|
|
173
183
|
"@types/node": "26.6.5",
|
|
174
|
-
"oxfmt": "0.72.0",
|
|
175
184
|
"typescript": "7.0.2"
|
|
176
185
|
},
|
|
177
186
|
"peerDependencies": {
|