@stll/oxlint-plugin 0.5.0 → 0.6.0

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/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
- if (!isAstNode(node)) return null;
42
- if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression") return unwrapExpression(node.expression);
43
- return node;
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() {
@@ -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)) 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
- if (!isAstNode(node)) return null;
42
- if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression") return unwrapExpression(node.expression);
43
- return node;
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, unwrapExpression as d, invokedCallee as i, resolveLiteralArraySpreads as l, getImportedName as n, isIdentifier as o, getPropertyName as r, isStringLiteral as s, filenameForContext as t, staticStringValue as u };
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
@@ -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)) 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":";AAeA,MAAa,sBAAsB,aAChC,QAAQ,YAAY,QAAQ,cAAc,KAAK,GAAA,CAAI,WAAW,MAAM,GAAG;AAE1E,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAQ,KAA2B,SAAS,YAC5C,WAAW;AAEb,MAAa,gBACX,MACA,SAC2D;CAC3D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,cACpC,OAAO;CAET,IAAI,OAAO,KAAK,SAAS,UACvB,OAAO;CAET,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C;AAEA,MAAa,mBACX,SAEA,UAAU,IAAI,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAKtE,MAAa,mBAAmB,SAAiC;CAC/D,IAAI,aAAa,IAAI,GACnB,OAAO,KAAK;CAEd,IAAI,gBAAgB,IAAI,GACtB,OAAO,KAAK;CAEd,OAAO;AACT;AAEA,MAAa,qBAAqB,SAAiC;CACjE,MAAM,aAAa,iBAAiB,IAAI;CACxC,IAAI,eAAe,MAAM,OAAO;CAChC,OAAO;CACP,IAAI,gBAAgB,IAAI,GAAG,OAAO,KAAK;CACvC,IACE,CAAC,UAAU,IAAI,KACf,KAAK,SAAS,qBACd,CAAC,MAAM,QAAQ,KAAK,WAAW,KAC/B,KAAK,YAAY,WAAW,KAC5B,CAAC,MAAM,QAAQ,KAAK,MAAM,GAE1B,OAAO;CACT,MAAM,QAAQ,KAAK,OAAO,GAAG,CAAC;CAC9B,IAAI,CAAC,UAAU,KAAK,GAAG,OAAO;CAC9B,MAAM,QAAQ,MAAM;CACpB,MAAM,SACJ,OAAO,UAAU,YAAY,UAAU,OACnC,QAAQ,IAAI,OAAO,QAAQ,IAC3B,KAAA;CACN,OAAO,OAAO,WAAW,WAAW,SAAS;AAC/C;AAEA,MAAa,sBAAsB,SAAiC;CAClE,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,oBACpC,OAAO;CAET,IAAI,KAAK,aAAa,MACpB,OAAO,gBAAgB,KAAK,QAAQ;CAEtC,OAAO,kBAAkB,KAAK,QAAQ;AACxC;AAEA,MAAa,iBAAiB,SAAkC;CAC9D,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,SAAS,kBACpC,OAAO;CAET,MAAM,SAAS,iBAAiB,KAAK,MAAM;CAC3C,IAAI,CAAC,UAAU,MAAM,GACnB,OAAO;CAET,IACE,OAAO,SAAS,wBAChB,MAAM,QAAQ,OAAO,WAAW,GAEhC,OAAO,iBAAiB,OAAO,YAAY,GAAG,EAAE,CAAC;CAEnD,IACE,OAAO,SAAS,uBACf,mBAAmB,MAAM,MAAM,UAC9B,mBAAmB,MAAM,MAAM,UAEjC,OAAO,iBAAiB,OAAO,MAAM;CAEvC,OAAO;AACT;AAiDA,MAAa,2BAA2B;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,MAAa,oBAAoB,SAAkC;CACjE,OAAO,UAAU,IAAI,GAAG;EACtB,MAAM,EAAE,SAAS;EACjB,IAAI,CAAC,yBAAyB,MAAM,YAAY,YAAY,IAAI,GAAG,OAAO;EAC1E,OAAO,KAAK;CACd;CACA,OAAO;AACT;AASA,MAAa,8BACX,SAC8B;CAC9B,MAAM,WAAsB,CAAC;CAC7B,MAAM,UAAU,aAA4B;EAC1C,IAAI,CAAC,UAAU,QAAQ,KAAK,SAAS,SAAS,iBAAiB;GAC7D,SAAS,KAAK,QAAQ;GACtB;EACF;EACA,MAAM,aAAa,iBAAiB,SAAS,QAAQ;EACrD,IACE,YAAY,SAAS,qBACrB,CAAC,MAAM,QAAQ,WAAW,QAAQ,GAClC;GACA,SAAS,KAAK,QAAQ;GACtB;EACF;EACA,KAAK,MAAM,WAAW,WAAW,UAAU,OAAO,OAAO;CAC3D;CACA,KAAK,MAAM,YAAY,MAAM,OAAO,QAAQ;CAC5C,MAAM,eAAe,SAAS,WAC3B,aAAa,UAAU,QAAQ,KAAK,SAAS,SAAS,eACzD;CACA,IAAI,iBAAiB,IAAI,OAAO;EAAE,QAAQ;EAAS,MAAM;CAAS;CAClE,MAAM,SAAS,SAAS,GAAG,YAAY;CACvC,OAAO;EACL,QAAQ;EACR,MAAM,SAAS,MAAM,GAAG,YAAY;EACpC;EACA,MAAM,SAAS,MAAM,eAAe,CAAC;CACvC;AACF;AAIA,MAAa,mBAAmB,cAAsC;CACpE,IAAI,CAAC,UAAU,SAAS,KAAK,UAAU,SAAS,mBAC9C,OAAO;CAET,MAAM,WAAW,UAAU;CAC3B,IAAI,aAAa,QAAQ,GACvB,OAAO,SAAS;CAElB,IAAI,gBAAgB,QAAQ,GAC1B,OAAO,SAAS;CAElB,OAAO;AACT;AAaA,MAAa,yBACX,SAEA,aAAa,IAAI,KAAK,MAAM,QAAQ,KAAK,KAAK;AAMhD,MAAM,mBACJ,SACA,eACoB;CACpB,IAAI,QAAsB,QAAQ,WAAW,SAAS,UAAU;CAChE,OAAO,UAAU,MAAM;EACrB,MAAM,WAAW,MAAM,IAAI,IAAI,WAAW,IAAI;EAC9C,IAAI,aAAa,KAAA,GACf,OAAO;EAET,QAAQ,MAAM;CAChB;CACA,OAAO;AACT;AAEA,MAAM,sBAAsB,aAC1B,SAAS,WAAW,OACjB,cAAc,CAAC,UAAU,QAAQ,KAAK,UAAU,IACnD;AAGF,MAAM,mBAAmB;AAEzB,MAAM,qBACJ,MACA,aAEA,MAAM,aAAa,oBAAoB,aAAa,OAChD;CAAE,QAAQ,KAAK;CAAQ,UAAU;AAAS,IAC1C;AAEN,MAAM,uBACJ,SACA,UACA,SAC2B;CAC3B,MAAM,aAAa,SAAS,KAAK,GAAG,CAAC;CACrC,IAAI,SAAS,KAAK,WAAW,KAAK,eAAe,KAAA,GAC/C,OAAO;CAET,MAAM,OAAgB,WAAW;CACjC,IAAI,WAAW,SAAS,iBAAiB;EACvC,MAAM,cAAuB,WAAW;EACxC,IACE,CAAC,UAAU,IAAI,KACf,KAAK,eAAe,UACpB,CAAC,UAAU,WAAW,KACtB,YAAY,SAAS,uBACrB,YAAY,eAAe,UAC3B,CAAC,gBAAgB,YAAY,MAAM,GAEnC,OAAO;EAET,MAAM,SAAS,YAAY,OAAO;EAClC,IAAI,KAAK,SAAS,4BAChB,OAAO;GAAE;GAAQ,UAAU;EAAiB;EAE9C,IAAI,KAAK,SAAS,0BAChB,OAAO;GAAE;GAAQ,UAAU;EAAU;EAEvC,MAAM,WAAW,gBAAgB,IAAI;EACrC,OAAO,aAAa,OAAO,OAAO;GAAE;GAAQ;EAAS;CACvD;CACA,IACE,WAAW,SAAS,cACpB,CAAC,UAAU,IAAI,KACf,KAAK,SAAS,wBACd,CAAC,mBAAmB,QAAQ,KAC5B,CAAC,aAAa,KAAK,EAAE,GAErB,OAAO;CAET,OAAO,wCAAwC,SAAS,KAAK,MAAM,IAAI;AACzE;AAIA,MAAa,2CACX,SACA,MACA,uBAAO,IAAI,IAAa,MACG;CAC3B,MAAM,aAAa,iBAAiB,IAAI;CACxC,IAAI,CAAC,UAAU,UAAU,KAAK,KAAK,IAAI,UAAU,GAC/C,OAAO;CAET,KAAK,IAAI,UAAU;CACnB,IAAI,sBAAsB,UAAU,GAAG;EACrC,MAAM,WAAW,gBAAgB,SAAS,UAAU;EACpD,OAAO,aAAa,OAChB,OACA,oBAAoB,SAAS,UAAU,IAAI;CACjD;CACA,IAAI,WAAW,SAAS,oBAAoB;EAC1C,MAAM,OAAO,wCACX,SACA,WAAW,QACX,IACF;EACA,OAAO,kBAAkB,MAAM,mBAAmB,UAAU,CAAC;CAC/D;CACA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/oxlint-plugin",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",