mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* fix(javascript): emit no fs acquisition for a rejected literal mode
`fs.open`, `fs.openSync` and `fs/promises.open` validate the `mode` in argument 2 before
touching the filesystem, so a mode Node rejects throws synchronously and no descriptor
exists. The guard read only the flag, so a rejected mode kept its acquisition and any
linked release.
Measured on Node 24.22.1:
fs.openSync(p, "r", -1) -> THREW ERR_OUT_OF_RANGE
fs.openSync(p, "r", 1.5) -> THREW ERR_OUT_OF_RANGE
fs.openSync(p, "r", NaN) -> THREW ERR_OUT_OF_RANGE
fs.openSync(p, "r", 1e21) -> THREW ERR_OUT_OF_RANGE
fs.openSync(p, "r", true) -> THREW ERR_INVALID_ARG_TYPE
fs.openSync(p, "r", 0n) -> THREW ERR_INVALID_ARG_TYPE
fs.openSync(p, "r", {}) -> THREW ERR_INVALID_ARG_TYPE
fs.openSync(p, "r", "8") -> THREW ERR_INVALID_ARG_VALUE
fs.openSync(p, "r", "") -> THREW ERR_INVALID_ARG_VALUE
fs.openSync(p, "r", "0o644") -> THREW ERR_INVALID_ARG_VALUE
--- accepted ---
fs.openSync(p, "r", 0o644) -> ok
fs.openSync(p, "r", 2147483647) -> ok
fs.openSync(p, "r", -0) -> ok
fs.openSync(p, "r", "0644") -> ok
fs.openSync(p, "r", null) -> ok
fs.openSync(p, "r", void 0) -> ok
The guard decides only exactly-readable literals. A numeric mode is validated as an
unsigned 32-bit integer; a string mode must be a canonical octal digit string, padded
leading zeros allowed, and its value must stay inside uint32; `null`, `undefined` and
`void <expr>` default the mode and are kept, as is a computed value.
Three boundaries are deliberate:
- Values in `[2^31, 2^32-1]` **abort the Node process** rather than throwing
(`Assertion failed: args[2]->IsInt32()`), so they stay undecided instead of being
claimed as a synchronous throw.
- A spread before the mode slot shifts the runtime position.
- A unary chain is resolved through its innermost readable operand, and a nested
boolean or string result is left undecided rather than applied as a number.
Regressions cover both directions. Full suite here: 5466 passed | 7 skipped.
* fix(javascript): remove the superseded string mode check
Review finding. `rejectsOpenModeLiteral` builds `octal` from a string literal or a
no-expression template and returns inside that branch for every value it matched. A
`StringLiteral` therefore never reaches the later `isStringLiteral` check, which cannot
run and is a second copy of the canonical-octal rule to maintain.
* test(javascript): cover the spread and boundary paths of the mode guard
Review feedback from @greptile-apps: the new mode tests skipped both paths the
guard deliberately leaves undecided. Two spread cases are added:
- `openSync(...args, "r", "0600")`, where a spread in the flag slot shifts the
runtime position of the mode;
- `openSync(p, "r", ...modes)`, where a spread occupies the mode slot itself.
Both must keep the acquisition, because neither establishes the mode value. The
boundary cases were already present: `2147483647` and `2147483648` are keep
cases (`2147483648` aborts the process rather than throwing) and `4294967296` is
the reject case, alongside the octal `37777777777` keep and `40000000000` reject
pair.
127 cases pass in this file.
531 lines
20 KiB
TypeScript
531 lines
20 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import {
|
|
BUILTIN_RESOURCE_SPECIFIERS,
|
|
BUILTIN_RESOURCE_METHODS,
|
|
BUILTIN_RESOURCE_RELEASE_METHODS,
|
|
} from "./javascriptBuiltinResourceMethods.js";
|
|
import type {
|
|
JavaScriptSemanticCallable,
|
|
JavaScriptSemanticResourceOperation,
|
|
} from "./javascriptSemanticIr.js";
|
|
import {
|
|
semanticStaticPropertyName,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticBindingState,
|
|
} from "./javascriptSemanticState.js";
|
|
import {
|
|
builtinDataEffectMethod,
|
|
dataEffectMemberCallee,
|
|
outerDataEffectBinding,
|
|
traverseDataEffects,
|
|
type DataEffectTraversalContext,
|
|
} from "./javascriptSemanticDataEffectHelpers.js";
|
|
import { FS_OPEN_FLAGS, range } from "./javascriptStaticAnalysisHelpers.js";
|
|
|
|
const RELEASE_METHODS = ["close", "destroy", "end"] as const;
|
|
|
|
/**
|
|
* Release methods the value from this acquisition actually has. An unlisted
|
|
* specifier/method pair admits none, so a call to it emits no release fact.
|
|
*/
|
|
const releaseMethodsFor = (
|
|
acquisition: ResourceCandidate,
|
|
): readonly ("close" | "destroy" | "end")[] => {
|
|
const key = `${acquisition.specifier}:${acquisition.method}`;
|
|
const table: Readonly<
|
|
Record<string, readonly ("close" | "destroy" | "end")[]>
|
|
> = BUILTIN_RESOURCE_RELEASE_METHODS;
|
|
return table[key] ?? [];
|
|
};
|
|
|
|
interface ResourceCandidate {
|
|
readonly node: t.CallExpression | t.OptionalCallExpression;
|
|
readonly specifier: string;
|
|
readonly method: JavaScriptSemanticResourceOperation["method"];
|
|
readonly ownerCallableId: string | null;
|
|
readonly resultBindingId: string | null;
|
|
}
|
|
|
|
/** Recover built-in filesystem/network acquisition and local release handles. */
|
|
export const collectJavaScriptSemanticResources = (
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
): JavaScriptSemanticResourceOperation[] => {
|
|
const context: DataEffectTraversalContext = {
|
|
state,
|
|
callableIds: new Set(callables.map(({ callableId }) => callableId)),
|
|
callableStack: [],
|
|
ancestors: [],
|
|
};
|
|
const acquisitions = collectAcquisitions(program, context).filter(
|
|
(candidate) =>
|
|
!socketPortInFirstArgument(candidate) ||
|
|
!rejectsSocketPortLiteral(candidate.node, context),
|
|
);
|
|
return [
|
|
...acquisitions.map(immutableAcquisition),
|
|
...collectReleases(program, context, acquisitions),
|
|
];
|
|
};
|
|
|
|
/**
|
|
* `fs.open`/`fs.openSync` validate the flag before touching the filesystem, so
|
|
* a flag Node rejects at the call site throws synchronously and no descriptor
|
|
* exists. Node accepts only a known flag string, a nullish value (which defaults
|
|
* to `"r"`), or a bitmask integer; everything else throws. A computed value may
|
|
* still be valid, so only an exactly readable literal is decided.
|
|
*/
|
|
const rejectsOpenFlagLiteral = (
|
|
node: t.CallExpression | t.OptionalCallExpression,
|
|
context: DataEffectTraversalContext,
|
|
): boolean => {
|
|
// A spread before the flag slot shifts the runtime position, so argument 1 is
|
|
// not provably the flag. `openSync(...args, "0600")` with
|
|
// `args = [path, "w"]` opens with mode `"0600"`, not with an invalid flag.
|
|
if (t.isSpreadElement(node.arguments[0])) return false;
|
|
const flag = node.arguments[1];
|
|
if (flag === undefined || t.isSpreadElement(flag)) return false;
|
|
const value = unwrapJavaScriptExpression(flag).node;
|
|
// A no-expression template literal is exactly readable, so it is the same flag
|
|
// string Node receives.
|
|
const flagText = t.isStringLiteral(value)
|
|
? value.value
|
|
: t.isTemplateLiteral(value) && value.expressions.length === 0
|
|
? value.quasis[0]?.value.cooked
|
|
: undefined;
|
|
if (flagText !== undefined && flagText !== null)
|
|
return !FS_OPEN_FLAGS.has(flagText);
|
|
// A non-integer or out-of-int32 value is not a usable bitmask. The sign alone
|
|
// does not decide: Node accepts some signed bitmasks such as `-2147483648` and
|
|
// rejects others such as `-1`, so an in-range negative is left undecided.
|
|
const bits = staticFlagBits(value, context);
|
|
if (bits !== undefined)
|
|
return (
|
|
bits !== null &&
|
|
(!Number.isInteger(bits) || bits > 2_147_483_647 || bits < -2_147_483_648)
|
|
);
|
|
// `NaN` and `Infinity` are unshadowed globals, and neither is a bitmask; a unary
|
|
// operator over one is the same non-finite number.
|
|
if (isNonFiniteNumber(value, context)) return true;
|
|
// BigInt, boolean, object and array literals all fail Node's flag check. A
|
|
// unary operator over a BigInt literal is a BigInt too.
|
|
return (
|
|
t.isBigIntLiteral(value) ||
|
|
t.isBooleanLiteral(value) ||
|
|
t.isRegExpLiteral(value) ||
|
|
t.isObjectExpression(value) ||
|
|
t.isArrayExpression(value) ||
|
|
(t.isUnaryExpression(value) &&
|
|
t.isBigIntLiteral(unwrapJavaScriptExpression(value.argument).node))
|
|
);
|
|
};
|
|
|
|
/**
|
|
* The number a literal flag argument evaluates to, or `null` when it evaluates to
|
|
* `undefined`. Returns `undefined` when the value is not exactly readable.
|
|
*
|
|
* `void <expr>` is `undefined`, which Node accepts as the default read flag, so
|
|
* it must never be judged by its operand. `!`, `typeof` and `delete` yield a
|
|
* boolean or a string rather than a bitmask.
|
|
*/
|
|
const staticFlagBits = (
|
|
value: t.Node,
|
|
context: DataEffectTraversalContext,
|
|
depth = 0,
|
|
): number | null | undefined => {
|
|
if (t.isNumericLiteral(value)) return value.value;
|
|
if (!t.isUnaryExpression(value)) return undefined;
|
|
if (value.operator === "void") return null;
|
|
// `!` yields a boolean and `typeof` a string. Neither is a bitmask, so a caller
|
|
// that reads the flag or mode rejects it. This arm is only reachable from the
|
|
// top-level entry: `unaryOperandNumber` never returns one of these as a number.
|
|
if (
|
|
value.operator === "!" ||
|
|
value.operator === "typeof" ||
|
|
value.operator === "delete"
|
|
)
|
|
return Number.NaN;
|
|
// A unary chain resolves through its innermost readable operand, so `~-1` is `0`
|
|
// and `-(-1.5)` is `1.5`. The depth bound keeps a pathological chain from
|
|
// recursing without limit.
|
|
if (depth > 8) return undefined;
|
|
const operand = unwrapJavaScriptExpression(value.argument).node;
|
|
const number = unaryOperandNumber(operand, context, depth + 1);
|
|
if (number === undefined) return undefined;
|
|
if (value.operator === "-") return -number;
|
|
if (value.operator === "+") return number;
|
|
if (value.operator === "~") return ~number;
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* The number an operand of a unary operator evaluates to, or `undefined` when it is
|
|
* not exactly readable.
|
|
*
|
|
* This is separate from `staticFlagBits` because that function answers two different
|
|
* questions: a numeric bitmask, and "this is a boolean or a string rather than a
|
|
* bitmask". The second answer has no number, and a real `NaN` is a number, so they
|
|
* cannot share a return slot. A nested `!`, `typeof` or `delete` therefore stays
|
|
* undecided here: `+!0` is `1` at runtime, but deciding it needs the operand's
|
|
* truthiness rather than a bitmask reading.
|
|
*/
|
|
const unaryOperandNumber = (
|
|
value: t.Node,
|
|
context: DataEffectTraversalContext,
|
|
depth: number,
|
|
): number | undefined => {
|
|
if (depth > 8) return undefined;
|
|
if (t.isNumericLiteral(value)) return value.value;
|
|
if (t.isUnaryExpression(value)) {
|
|
if (
|
|
value.operator === "!" ||
|
|
value.operator === "typeof" ||
|
|
value.operator === "delete" ||
|
|
value.operator === "void"
|
|
)
|
|
return undefined;
|
|
const inner = unaryOperandNumber(
|
|
unwrapJavaScriptExpression(value.argument).node,
|
|
context,
|
|
depth + 1,
|
|
);
|
|
if (inner === undefined) return undefined;
|
|
if (value.operator === "-") return -inner;
|
|
if (value.operator === "+") return inner;
|
|
if (value.operator === "~") return ~inner;
|
|
return undefined;
|
|
}
|
|
return primitiveToNumber(value) ?? nonFiniteGlobalNumber(value, context);
|
|
};
|
|
|
|
/**
|
|
* The number an unshadowed `NaN` or `Infinity` evaluates to, or `undefined` when the
|
|
* operand names neither. A `~` over one is a finite int32, which is why the bitwise
|
|
* result must be computed rather than treated as non-finite.
|
|
*/
|
|
const nonFiniteGlobalNumber = (
|
|
value: t.Node,
|
|
context: DataEffectTraversalContext,
|
|
): number | undefined => {
|
|
if (
|
|
!t.isIdentifier(value) ||
|
|
(value.name !== "NaN" && value.name !== "Infinity") ||
|
|
!isUnmodifiedGlobalReference(value, context.state, value.name)
|
|
)
|
|
return undefined;
|
|
return value.name === "NaN" ? Number.NaN : Number.POSITIVE_INFINITY;
|
|
};
|
|
|
|
/**
|
|
* The number JavaScript's unary coercion produces for an exactly readable
|
|
* non-numeric primitive, or `undefined` when the operand is not one. `-true` is
|
|
* `-1` and `+"bad"` is `NaN`, so neither is a socket port or an fs flag.
|
|
*/
|
|
const primitiveToNumber = (value: t.Node): number | undefined => {
|
|
if (t.isStringLiteral(value)) return Number(value.value);
|
|
if (t.isBooleanLiteral(value)) return Number(value.value);
|
|
if (t.isNullLiteral(value)) return 0;
|
|
if (t.isTemplateLiteral(value) && value.expressions.length === 0) {
|
|
const cooked = value.quasis[0]?.value.cooked;
|
|
return cooked === undefined || cooked === null ? undefined : Number(cooked);
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* Whether an exactly readable expression is a non-finite number: an unshadowed
|
|
* `NaN` or `Infinity`, or a unary operator applied to one. `void` is excluded
|
|
* because it yields `undefined`, which both slots accept as the default.
|
|
*/
|
|
const isNonFiniteNumber = (
|
|
value: t.Node,
|
|
context: DataEffectTraversalContext,
|
|
): boolean => {
|
|
const unshadowed = (node: t.Node): boolean =>
|
|
t.isIdentifier(node) &&
|
|
(node.name === "NaN" || node.name === "Infinity") &&
|
|
isUnmodifiedGlobalReference(node, context.state, node.name);
|
|
if (unshadowed(value)) return true;
|
|
return (
|
|
t.isUnaryExpression(value) &&
|
|
value.operator !== "void" &&
|
|
unshadowed(unwrapJavaScriptExpression(value.argument).node)
|
|
);
|
|
};
|
|
|
|
/** `fs.open`, `fs.openSync` and `fs/promises.open` take the mode in slot 2. */
|
|
const rejectsOpenModeLiteral = (
|
|
node: t.CallExpression | t.OptionalCallExpression,
|
|
context: DataEffectTraversalContext,
|
|
): boolean => {
|
|
// A spread before the mode slot shifts the runtime position.
|
|
if (
|
|
t.isSpreadElement(node.arguments[0]) ||
|
|
t.isSpreadElement(node.arguments[1])
|
|
)
|
|
return false;
|
|
const mode = node.arguments[2];
|
|
if (mode === undefined || t.isSpreadElement(mode)) return false;
|
|
const value = unwrapJavaScriptExpression(mode).node;
|
|
// `null`, `undefined` and `void <expr>` all default the mode.
|
|
if (t.isNullLiteral(value)) return false;
|
|
// A no-expression template literal is exactly readable, so it is the same
|
|
// string Node would receive.
|
|
const octal = t.isStringLiteral(value)
|
|
? value.value
|
|
: t.isTemplateLiteral(value) && value.expressions.length === 0
|
|
? value.quasis[0]?.value.cooked
|
|
: undefined;
|
|
if (octal !== undefined && octal !== null) {
|
|
if (!/^[0-7]+$/u.test(octal)) return true;
|
|
// Leading zeros pad the text without changing the value, so the bound reads the
|
|
// significant digits. Eleven octal digits cover the whole uint32 range, so a
|
|
// longer significant run cannot fit and an eleven-digit one is exact.
|
|
const digits = octal.replace(/^0+/u, "");
|
|
return digits.length > 11 || Number.parseInt(digits, 8) > 4_294_967_295;
|
|
}
|
|
// A numeric mode is validated as an unsigned 32-bit integer.
|
|
const bits = staticFlagBits(value, context);
|
|
if (bits !== undefined) {
|
|
if (bits === null) return false;
|
|
// Values in `[2^31, 2^32-1]` abort the process rather than throwing, so
|
|
// they stay undecided instead of being claimed as a synchronous throw.
|
|
return !Number.isInteger(bits) || bits < 0 || bits > 4_294_967_295;
|
|
}
|
|
// `NaN` and `Infinity` are unshadowed globals, and neither is a mode; a unary
|
|
// operator over one is the same non-finite number.
|
|
if (isNonFiniteNumber(value, context)) return true;
|
|
// BigInt, boolean, object and array literals all fail Node's mode check. A
|
|
// unary operator over a BigInt literal is a BigInt too, and the range helpers
|
|
// above only price numeric operands.
|
|
return (
|
|
t.isBigIntLiteral(value) ||
|
|
t.isBooleanLiteral(value) ||
|
|
t.isRegExpLiteral(value) ||
|
|
t.isObjectExpression(value) ||
|
|
t.isArrayExpression(value) ||
|
|
(t.isUnaryExpression(value) &&
|
|
t.isBigIntLiteral(unwrapJavaScriptExpression(value.argument).node))
|
|
);
|
|
};
|
|
|
|
/** `fs.open`, `fs.openSync` and `fs/promises.open` take the flag in slot 1. */
|
|
const openFlagInFirstArgument = (reference: AcquisitionReference): boolean =>
|
|
reference.method === "open" || reference.method === "openSync"
|
|
? reference.specifier === "fs" || reference.specifier === "fs/promises"
|
|
: false;
|
|
|
|
const collectAcquisitions = (
|
|
program: t.Program,
|
|
context: DataEffectTraversalContext,
|
|
): ResourceCandidate[] => {
|
|
const output: ResourceCandidate[] = [];
|
|
traverseDataEffects(program, context, (node) => {
|
|
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
|
|
const reference = acquisitionMethod(node.callee, context.state);
|
|
if (reference === null) return;
|
|
if (openFlagInFirstArgument(reference)) {
|
|
if (rejectsOpenFlagLiteral(node, context)) return;
|
|
if (rejectsOpenModeLiteral(node, context)) return;
|
|
}
|
|
output.push({
|
|
node,
|
|
specifier: reference.specifier,
|
|
method: reference.method,
|
|
ownerCallableId: context.callableStack.at(-1) ?? null,
|
|
resultBindingId: outerDataEffectBinding(node, context),
|
|
});
|
|
});
|
|
return output;
|
|
};
|
|
|
|
const collectReleases = (
|
|
program: t.Program,
|
|
context: DataEffectTraversalContext,
|
|
acquisitions: readonly ResourceCandidate[],
|
|
): JavaScriptSemanticResourceOperation[] => {
|
|
const byBinding = new Map<string, ResourceCandidate[]>();
|
|
for (const acquisition of acquisitions) {
|
|
if (acquisition.resultBindingId === null) continue;
|
|
const existing = byBinding.get(acquisition.resultBindingId) ?? [];
|
|
existing.push(acquisition);
|
|
byBinding.set(acquisition.resultBindingId, existing);
|
|
}
|
|
const output: JavaScriptSemanticResourceOperation[] = [];
|
|
traverseDataEffects(program, context, (node) => {
|
|
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
|
|
const member = dataEffectMemberCallee(node);
|
|
if (member === null || !t.isIdentifier(member.object)) return;
|
|
const method = RELEASE_METHODS.find(
|
|
(candidate) =>
|
|
candidate ===
|
|
semanticStaticPropertyName(member.property, member.computed),
|
|
);
|
|
if (method === undefined) return;
|
|
const binding = resolveSemanticBindingState(
|
|
context.state,
|
|
member.object,
|
|
member.object.name,
|
|
);
|
|
const candidates =
|
|
binding === undefined ? [] : (byBinding.get(binding.bindingId) ?? []);
|
|
if (binding === undefined || candidates.length === 0) return;
|
|
const linked = candidates.filter((acquisition) =>
|
|
releaseMethodsFor(acquisition).includes(method),
|
|
);
|
|
// No linked acquisition exposes this release method, so the call is not a
|
|
// release of it at all: Node throws at the call site instead.
|
|
if (linked.length === 0) return;
|
|
output.push({
|
|
resourceId: `resource:release:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
|
|
kind: "release",
|
|
method,
|
|
location: range(node),
|
|
ownerCallableId: context.callableStack.at(-1) ?? null,
|
|
resultBindingId: binding.bindingId,
|
|
linkedResourceIds: linked.map(resourceCandidateId).sort(),
|
|
resolution:
|
|
linked.length === 1 &&
|
|
holdsOnlyAcquisitions(binding, linked, context.state)
|
|
? "complete"
|
|
: "partial",
|
|
});
|
|
});
|
|
return output;
|
|
};
|
|
|
|
/**
|
|
* A lexical handle identifies its acquisition only while every value written
|
|
* to the binding is that acquisition itself. Any replacement, even a
|
|
* conditional or later one, leaves the released resource a candidate. A
|
|
* nullish placeholder, as in `let h = null; try { h = open() } finally {
|
|
* h?.close() }`, releases nothing else: the call is skipped or throws.
|
|
*/
|
|
const holdsOnlyAcquisitions = (
|
|
binding: JavaScriptSemanticBindingState,
|
|
linked: readonly ResourceCandidate[],
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
[...binding.initializers, ...binding.referenceInitializers].every(
|
|
({ node, projection }) => {
|
|
if (projection.length > 0) return false;
|
|
let value = unwrapJavaScriptExpression(node).node;
|
|
if (t.isAwaitExpression(value))
|
|
value = unwrapJavaScriptExpression(value.argument).node;
|
|
return (
|
|
isNullishPlaceholder(value, state) ||
|
|
linked.some((acquisition) => acquisition.node === value)
|
|
);
|
|
},
|
|
);
|
|
|
|
const isNullishPlaceholder = (
|
|
value: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean =>
|
|
t.isNullLiteral(value) ||
|
|
t.isUnaryExpression(value, { operator: "void" }) ||
|
|
isUnmodifiedGlobalReference(value, state, "undefined");
|
|
|
|
const immutableAcquisition = (
|
|
candidate: ResourceCandidate,
|
|
): JavaScriptSemanticResourceOperation => ({
|
|
resourceId: resourceCandidateId(candidate),
|
|
kind: "acquire",
|
|
method: candidate.method,
|
|
location: range(candidate.node),
|
|
ownerCallableId: candidate.ownerCallableId,
|
|
resultBindingId: candidate.resultBindingId,
|
|
linkedResourceIds: [],
|
|
resolution: "complete",
|
|
});
|
|
|
|
interface AcquisitionReference {
|
|
readonly specifier: string;
|
|
readonly method: JavaScriptSemanticResourceOperation["method"];
|
|
}
|
|
|
|
const acquisitionMethod = (
|
|
callee: t.Node,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): AcquisitionReference | null => {
|
|
for (const specifier of BUILTIN_RESOURCE_SPECIFIERS) {
|
|
const method = builtinDataEffectMethod(callee, state, [specifier]);
|
|
if (
|
|
method !== null &&
|
|
(
|
|
BUILTIN_RESOURCE_METHODS[
|
|
specifier as keyof typeof BUILTIN_RESOURCE_METHODS
|
|
] as readonly string[]
|
|
).includes(method)
|
|
)
|
|
return {
|
|
specifier,
|
|
method: method as JavaScriptSemanticResourceOperation["method"],
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
const resourceCandidateId = (candidate: ResourceCandidate): string =>
|
|
`resource:acquire:${String(candidate.node.start ?? -1)}:${String(candidate.node.end ?? -1)}`;
|
|
|
|
/**
|
|
* `net` and `tls` validate a numeric port before creating a socket, so a
|
|
* literal port outside `[0, 65535]`, a fractional, non-finite, BigInt, boolean
|
|
* or `null` first argument throws synchronously and no socket exists. A string
|
|
* is a Unix socket path and an object may carry a path, so only an exactly
|
|
* readable non-port literal is decided. `""` is a path with no length, which
|
|
* Node also rejects.
|
|
*
|
|
* `undefined` and an empty object throw `ERR_MISSING_ARGS`, but those are a
|
|
* missing option rather than a bad port, so they stay undecided here.
|
|
*/
|
|
const rejectsSocketPortLiteral = (
|
|
node: t.CallExpression | t.OptionalCallExpression,
|
|
context: DataEffectTraversalContext,
|
|
): boolean => {
|
|
const port = node.arguments[0];
|
|
if (port === undefined || t.isSpreadElement(port)) return false;
|
|
const value = unwrapJavaScriptExpression(port).node;
|
|
if (t.isStringLiteral(value)) return value.value.length === 0;
|
|
if (
|
|
t.isNullLiteral(value) ||
|
|
t.isBigIntLiteral(value) ||
|
|
t.isBooleanLiteral(value)
|
|
)
|
|
return true;
|
|
if (t.isNumericLiteral(value))
|
|
return (
|
|
!Number.isInteger(value.value) || value.value < 0 || value.value > 65535
|
|
);
|
|
// `NaN` and `Infinity` are unshadowed globals, and neither is a port.
|
|
if (
|
|
t.isIdentifier(value) &&
|
|
(value.name === "NaN" || value.name === "Infinity") &&
|
|
isUnmodifiedGlobalReference(value, context.state, value.name)
|
|
)
|
|
return true;
|
|
// `-1` and `-1.5` are unary expressions over numeric literals: always out of
|
|
// range because a port is never negative.
|
|
if (t.isUnaryExpression(value)) {
|
|
const operand = unwrapJavaScriptExpression(value.argument).node;
|
|
return (
|
|
t.isNumericLiteral(operand) &&
|
|
(value.operator === "-" || !Number.isInteger(operand.value))
|
|
);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/** Only `net`/`tls` sockets take a port in argument 0. */
|
|
const socketPortInFirstArgument = (
|
|
candidate: Pick<AcquisitionReference, "specifier">,
|
|
): boolean => candidate.specifier === "net" || candidate.specifier === "tls";
|