@stll/oxlint-plugin 0.6.2 → 0.6.3

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.
Files changed (85) hide show
  1. package/dist/bun-test-hygiene.cjs +2 -2
  2. package/dist/bun-test-hygiene.cjs.map +1 -1
  3. package/dist/bun-test-hygiene.d.cts +3 -2
  4. package/dist/bun-test-hygiene.d.mts +3 -2
  5. package/dist/bun-test-hygiene.mjs +2 -2
  6. package/dist/bun-test-hygiene.mjs.map +1 -1
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/no-any-casts.cjs +2 -2
  10. package/dist/no-any-casts.cjs.map +1 -1
  11. package/dist/no-any-casts.d.cts +3 -2
  12. package/dist/no-any-casts.d.mts +3 -2
  13. package/dist/no-any-casts.mjs +2 -2
  14. package/dist/no-any-casts.mjs.map +1 -1
  15. package/dist/no-checkout-writes-in-tests.cjs +2 -2
  16. package/dist/no-checkout-writes-in-tests.cjs.map +1 -1
  17. package/dist/no-checkout-writes-in-tests.d.cts +3 -2
  18. package/dist/no-checkout-writes-in-tests.d.mts +3 -2
  19. package/dist/no-checkout-writes-in-tests.mjs +2 -2
  20. package/dist/no-checkout-writes-in-tests.mjs.map +1 -1
  21. package/dist/no-dangerous-type-assertions.cjs +2 -2
  22. package/dist/no-dangerous-type-assertions.cjs.map +1 -1
  23. package/dist/no-dangerous-type-assertions.d.cts +3 -2
  24. package/dist/no-dangerous-type-assertions.d.mts +3 -2
  25. package/dist/no-dangerous-type-assertions.mjs +2 -2
  26. package/dist/no-dangerous-type-assertions.mjs.map +1 -1
  27. package/dist/no-partial-record-satisfies.cjs +2 -2
  28. package/dist/no-partial-record-satisfies.cjs.map +1 -1
  29. package/dist/no-partial-record-satisfies.d.cts +3 -2
  30. package/dist/no-partial-record-satisfies.d.mts +3 -2
  31. package/dist/no-partial-record-satisfies.mjs +2 -2
  32. package/dist/no-partial-record-satisfies.mjs.map +1 -1
  33. package/dist/no-raw-route-query-client.cjs +2 -2
  34. package/dist/no-raw-route-query-client.cjs.map +1 -1
  35. package/dist/no-raw-route-query-client.d.cts +3 -2
  36. package/dist/no-raw-route-query-client.d.mts +3 -2
  37. package/dist/no-raw-route-query-client.mjs +2 -2
  38. package/dist/no-raw-route-query-client.mjs.map +1 -1
  39. package/dist/no-real-network-in-tests.cjs +2 -2
  40. package/dist/no-real-network-in-tests.cjs.map +1 -1
  41. package/dist/no-real-network-in-tests.d.cts +3 -2
  42. package/dist/no-real-network-in-tests.d.mts +3 -2
  43. package/dist/no-real-network-in-tests.mjs +2 -2
  44. package/dist/no-real-network-in-tests.mjs.map +1 -1
  45. package/dist/no-real-timer-waits-in-tests.cjs +2 -2
  46. package/dist/no-real-timer-waits-in-tests.cjs.map +1 -1
  47. package/dist/no-real-timer-waits-in-tests.d.cts +3 -2
  48. package/dist/no-real-timer-waits-in-tests.d.mts +3 -2
  49. package/dist/no-real-timer-waits-in-tests.mjs +2 -2
  50. package/dist/no-real-timer-waits-in-tests.mjs.map +1 -1
  51. package/dist/no-unsafe-inner-html.cjs +2 -2
  52. package/dist/no-unsafe-inner-html.cjs.map +1 -1
  53. package/dist/no-unsafe-inner-html.d.cts +3 -2
  54. package/dist/no-unsafe-inner-html.d.mts +3 -2
  55. package/dist/no-unsafe-inner-html.mjs +2 -2
  56. package/dist/no-unsafe-inner-html.mjs.map +1 -1
  57. package/dist/no-unseeded-random-in-tests.cjs +2 -2
  58. package/dist/no-unseeded-random-in-tests.cjs.map +1 -1
  59. package/dist/no-unseeded-random-in-tests.d.cts +3 -2
  60. package/dist/no-unseeded-random-in-tests.d.mts +3 -2
  61. package/dist/no-unseeded-random-in-tests.mjs +2 -2
  62. package/dist/no-unseeded-random-in-tests.mjs.map +1 -1
  63. package/dist/no-wall-clock-bound.cjs +2 -2
  64. package/dist/no-wall-clock-bound.cjs.map +1 -1
  65. package/dist/no-wall-clock-bound.d.cts +3 -2
  66. package/dist/no-wall-clock-bound.d.mts +3 -2
  67. package/dist/no-wall-clock-bound.mjs +2 -2
  68. package/dist/no-wall-clock-bound.mjs.map +1 -1
  69. package/dist/no-wall-clock-in-tests.cjs +2 -2
  70. package/dist/no-wall-clock-in-tests.cjs.map +1 -1
  71. package/dist/no-wall-clock-in-tests.d.cts +3 -2
  72. package/dist/no-wall-clock-in-tests.d.mts +3 -2
  73. package/dist/no-wall-clock-in-tests.mjs +2 -2
  74. package/dist/no-wall-clock-in-tests.mjs.map +1 -1
  75. package/dist/require-loader-prefetch.cjs +2 -2
  76. package/dist/require-loader-prefetch.cjs.map +1 -1
  77. package/dist/require-loader-prefetch.d.cts +3 -2
  78. package/dist/require-loader-prefetch.d.mts +3 -2
  79. package/dist/require-loader-prefetch.mjs +2 -2
  80. package/dist/require-loader-prefetch.mjs.map +1 -1
  81. package/dist/test-rule-utils.cjs.map +1 -1
  82. package/dist/test-rule-utils.mjs.map +1 -1
  83. package/dist/utils.cjs.map +1 -1
  84. package/dist/utils.mjs.map +1 -1
  85. package/package.json +3 -4
@@ -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 { 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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"}
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;\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 (\n !(EXPRESSION_WRAPPER_TYPES satisfies readonly ESTree.Node[\"type\"][]).some(\n (wrapper) => wrapper === type,\n )\n )\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: Set<unknown> = new Set(),\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.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Portable static-safety and route-query rules for Oxlint consumers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -175,13 +175,12 @@
175
175
  },
176
176
  "scripts": {
177
177
  "build": "tsdown",
178
- "typecheck": "bun check"
178
+ "typecheck": "bun check --no-pretty --all --project=tsconfig.json"
179
179
  },
180
180
  "devDependencies": {
181
181
  "@oxlint/plugins": "1.87.0",
182
182
  "@stll/typescript-config": "workspace:*",
183
- "@types/node": "26.6.5",
184
- "typescript": "7.0.2"
183
+ "@types/node": "26.6.5"
185
184
  },
186
185
  "peerDependencies": {
187
186
  "@oxlint/plugins": ">=1.75.0 <2",