fix(javascript): classify direct defaults and argv elements

This commit is contained in:
morluto
2026-10-09 05:50:23 +08:00
parent 14b19a0b8a
commit 6495171812
4 changed files with 128 additions and 85 deletions
@@ -20,7 +20,6 @@ import {
} from "./javascriptSemanticState.js";
import {
builtinDataEffectMethod as builtinMethod,
containsSemanticNode as containsNode,
dataEffectArgumentBindingId as argumentBindingId,
dataEffectLiteralString as literalString,
dataEffectMemberCallee as memberCallee,
@@ -150,6 +149,7 @@ const processConfigurationMember = (
if (family !== "env" && family !== "argv") return null;
// `process.env[""]` names an exact (empty) key; only a dynamic key is unknown.
const key = semanticStaticPropertyKey(node.property, node.computed);
if (family === "argv" && key !== null && !isArrayIndexKey(key)) return null;
return {
kind: family === "env" ? "environment" : "argv",
key,
@@ -157,39 +157,47 @@ const processConfigurationMember = (
};
};
const isArrayIndexKey = (key: string): boolean => {
if (!/^(?:0|[1-9][0-9]*)$/u.test(key)) return false;
const index = Number(key);
return Number.isSafeInteger(index) && index < 2 ** 32 - 1;
};
const collectDefaults = (
program: t.Program,
context: TraversalContext,
configurations: readonly ConfigurationCandidate[],
): JavaScriptSemanticConfigurationOperation[] => {
const output: JavaScriptSemanticConfigurationOperation[] = [];
const configurationsByStart = configurations
.filter(
({ node }) =>
node.start !== null &&
node.start !== undefined &&
node.end !== null &&
node.end !== undefined,
)
.toSorted(
(left, right) => (left.node.start ?? 0) - (right.node.start ?? 0),
);
const configurationsByNode = new Map(
configurations.map((candidate) => [candidate.node, candidate]),
);
traverseWithContext(program, context, (node) => {
if (
!t.isLogicalExpression(node) ||
(node.operator !== "??" && node.operator !== "||")
)
return;
const sources = configurationsIn(node.left, configurationsByStart);
if (sources.length !== 1 || sources[0] === undefined) return;
let source = node.left;
while (
t.isParenthesizedExpression(source) ||
t.isTSAsExpression(source) ||
t.isTSTypeAssertion(source) ||
t.isTypeCastExpression(source) ||
t.isTSSatisfiesExpression(source) ||
t.isTSNonNullExpression(source)
)
source = source.expression;
const candidate = configurationsByNode.get(source);
if (candidate === undefined) return;
output.push({
configId: `config:default:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
kind: "default",
location: range(node.right),
ownerCallableId: context.callableStack.at(-1) ?? null,
resultBindingId: outerAssignedBinding(node, context),
key: sources[0].operation.key,
sourceConfigId: sources[0].operation.configId,
key: candidate.operation.key,
sourceConfigId: candidate.operation.configId,
value: primitiveValue(node.right),
resolution: primitiveValue(node.right) === null ? "partial" : "complete",
});
@@ -197,39 +205,6 @@ const collectDefaults = (
return output;
};
/** Find only configuration candidates whose source offsets can fit this left operand. */
const configurationsIn = (
expression: t.Node,
candidates: readonly ConfigurationCandidate[],
): readonly ConfigurationCandidate[] => {
if (
expression.start === null ||
expression.start === undefined ||
expression.end === null ||
expression.end === undefined
)
return [];
let low = 0;
let high = candidates.length;
while (low < high) {
const middle = low + Math.floor((high - low) / 2);
const start = candidates[middle]?.node.start;
if (start !== null && start !== undefined && start < expression.start)
low = middle + 1;
else high = middle;
}
const sources: ConfigurationCandidate[] = [];
for (let index = low; index < candidates.length; index += 1) {
const candidate = candidates[index];
if (candidate === undefined) continue;
const start = candidate.node.start;
if (start === null || start === undefined || start > expression.end) break;
if (containsNode(expression, candidate.node)) sources.push(candidate);
if (sources.length > 1) break;
}
return sources;
};
const collectRequests = (
program: t.Program,
context: TraversalContext,