fix(javascript): exclude TypeScript this annotations from runtime parameters (#2761)
* fix(javascript): exclude TypeScript this annotations from runtime parameters * test(javascript): assert exact typed argument bindings * test(apple): keep dyld slice regression portable * fix(process): report no remaining time after a provider deadline fires Node timers can fire while performance.now() is still a fraction of a millisecond short of the monotonic deadline. ProviderStartupDeadline then reported interruption "timeout" and an aborted signal while remainingMs() returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0 (Hopper and Ghidra startup) could run past an elapsed deadline, and the real-timer test "aborts a bounded wait when its real monotonic deadline expires" failed intermittently in CI (expected 1 to be 0). Once the deadline's own timer has classified a timeout, report zero remaining time. A deterministic regression test fires the timer with the monotonic clock 0.4 ms short of the deadline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): restore descriptor and fixture validation * test(ci): retain verification coverage within lint limits * fix(test): share admitted Ghidra profile installation fixtures * fix(ci): isolate timing report fixtures from workflow state --------- Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
co-authored by
hhang
Kaoru0822
Claude Opus 5.5
morluto
parent
02987d8e8c
commit
1ae9000587
@@ -154,7 +154,9 @@ node scripts/verify-windows-native-inspection.mjs
|
||||
This artifact injects one access-denied disposition result while using a real
|
||||
NTFS directory. The verifier checks retained ownership, recovery on a later
|
||||
query, cached successful admission, and cleanup on environment teardown. It
|
||||
does not reproduce or identify the host-dependent cause reported in #1701.
|
||||
renames the retained probe and recreates its old path, checking that cleanup
|
||||
removes the owned inode while preserving the replacement directory.
|
||||
It does not reproduce or identify the host-dependent cause reported in #1701.
|
||||
The fixture is stored under `.cache/` and is excluded from the shipped addon.
|
||||
|
||||
The native lane checks DACL readback independently, source write/replacement
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readdir,
|
||||
rmdir,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
@@ -52,7 +54,7 @@ try {
|
||||
join(workspace, "dist", "windows", "WindowsNativeLoader.js"),
|
||||
).href;
|
||||
const source = `import assert from 'node:assert/strict';
|
||||
import {readdirSync,renameSync,statSync} from 'node:fs';
|
||||
import {existsSync,mkdirSync,readdirSync,renameSync,statSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
import {systemWindowsNativeAuthority} from ${JSON.stringify(loader)};
|
||||
const temp=process.env.TEMP;
|
||||
@@ -63,14 +65,19 @@ assert.match(first.reason,/Delete owned object by handle failed.*Win32 5/s);
|
||||
const [name,...extra]=readdirSync(temp);assert.ok(name);assert.equal(extra.length,0);
|
||||
const path=join(temp,name);const identity=statSync(path,{bigint:true});
|
||||
// Ownership follows the retained NTFS handle even when the directory moves.
|
||||
// A replacement at its old pathname must remain caller-owned.
|
||||
const moved=path+'-renamed';renameSync(path,moved);
|
||||
assert.equal(statSync(moved,{bigint:true}).ino,identity.ino);
|
||||
mkdirSync(path);const replacement=statSync(path,{bigint:true});
|
||||
assert.notEqual(replacement.ino,identity.ino);
|
||||
if(process.argv[2]!=='teardown'){
|
||||
const second=systemWindowsNativeAuthority();assert.equal(second.available,true);
|
||||
assert.equal(readdirSync(temp).length,0);
|
||||
assert.equal(existsSync(moved),false);
|
||||
assert.deepEqual(readdirSync(temp),[name]);
|
||||
assert.equal(statSync(path,{bigint:true}).ino,replacement.ino);
|
||||
assert.equal(systemWindowsNativeAuthority(),second);
|
||||
}
|
||||
console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString()}));
|
||||
console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString(),replacementName:name,replacementInode:replacement.ino.toString()}));
|
||||
`;
|
||||
const fixture = join(workspace, "probe.mjs");
|
||||
await writeFile(fixture, source);
|
||||
@@ -86,10 +93,17 @@ console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:
|
||||
}),
|
||||
);
|
||||
assert.equal(observed.retainedOwner, true);
|
||||
assert.deepEqual(await readdir(temp), [observed.replacementName]);
|
||||
const replacement = join(temp, observed.replacementName);
|
||||
assert.equal(
|
||||
(await stat(replacement, { bigint: true })).ino.toString(),
|
||||
observed.replacementInode,
|
||||
);
|
||||
await rmdir(replacement);
|
||||
assert.deepEqual(await readdir(temp), []);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`,
|
||||
`${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, renamedOwnerCleanup: true, replacementPreserved: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
|
||||
@@ -98,7 +98,7 @@ const textCoverage = z.strictObject({
|
||||
text: z.string(),
|
||||
reported_total_bytes: z.number().int().nonnegative().nullable(),
|
||||
});
|
||||
/** A DEX prototype: parameter descriptors followed by the return descriptor. */
|
||||
/** A DEX prototype; hex parentheses also keep the advertised class valid in v mode. */
|
||||
export const dexDescriptorSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as t from "@babel/types";
|
||||
import { runtimeFunctionParameters } from "./javascriptAstValues.js";
|
||||
import { JavaScriptSymbolicBindings } from "./javascriptSymbolicBindings.js";
|
||||
import {
|
||||
symbolicVariants,
|
||||
@@ -447,7 +448,9 @@ export const collectElectronWrappedCallPaths = (
|
||||
const entry = `${key.value}.${name}`;
|
||||
functionBody(
|
||||
fn,
|
||||
fn.node.params.map(() => symbolicUnknown("public API argument")),
|
||||
runtimeFunctionParameters(fn.node).map(() =>
|
||||
symbolicUnknown("public API argument"),
|
||||
),
|
||||
{
|
||||
...memberContext,
|
||||
entryNode: node,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import {
|
||||
propertyName,
|
||||
runtimeFunctionParameters,
|
||||
semanticStaticPropertyKey,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
@@ -38,15 +39,13 @@ export const collectJavaScriptExports = (
|
||||
bindings?: JavaScriptSemanticAnalysisState,
|
||||
): StaticExports => {
|
||||
const exports = new Set<string>();
|
||||
const parameter = t.isFunction(node)
|
||||
? runtimeFunctionParameters(node)[1]
|
||||
: undefined;
|
||||
const factoryExports =
|
||||
bindings !== undefined &&
|
||||
t.isFunction(node) &&
|
||||
t.isIdentifier(node.params[1])
|
||||
? resolveSemanticBindingState(
|
||||
bindings,
|
||||
node.params[1],
|
||||
node.params[1].name,
|
||||
)?.bindingId
|
||||
bindings !== undefined && t.isFunction(node) && t.isIdentifier(parameter)
|
||||
? resolveSemanticBindingState(bindings, parameter, parameter.name)
|
||||
?.bindingId
|
||||
: undefined;
|
||||
const isExportsAlias = (reference: t.Node | null): boolean => {
|
||||
if (!t.isIdentifier(reference)) return false;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import * as t from "@babel/types";
|
||||
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
|
||||
|
||||
/** TypeScript receiver annotations occupy no positional runtime parameter slot. */
|
||||
export const runtimeFunctionParameters = (
|
||||
node: t.Function,
|
||||
): t.Function["params"] =>
|
||||
t.isIdentifier(node.params[0], { name: "this" })
|
||||
? node.params.slice(1)
|
||||
: node.params;
|
||||
|
||||
/**
|
||||
* Remove wrappers that affect TypeScript or Flow checking but not runtime
|
||||
* expression identity. The returned depth lets bounded evaluators account for
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
runtimeFunctionParameters,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { moduleFactory } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
@@ -27,7 +30,7 @@ export const javascriptBootstrapAt = (
|
||||
(!t.isFunctionExpression(call.callee) &&
|
||||
!t.isArrowFunctionExpression(call.callee)) ||
|
||||
!t.isBlockStatement(call.callee.body) ||
|
||||
call.callee.params.length !== 0 ||
|
||||
runtimeFunctionParameters(call.callee).length !== 0 ||
|
||||
call.arguments.length !== 0
|
||||
)
|
||||
return undefined;
|
||||
@@ -63,10 +66,10 @@ export const javascriptBootstrapAt = (
|
||||
!t.isFunctionDeclaration(loader) ||
|
||||
loader.id === null ||
|
||||
loader.id === undefined ||
|
||||
loader.params.length !== 1
|
||||
runtimeFunctionParameters(loader).length !== 1
|
||||
)
|
||||
continue;
|
||||
const parameter = loader.params[0];
|
||||
const parameter = runtimeFunctionParameters(loader)[0];
|
||||
if (!t.isIdentifier(parameter)) continue;
|
||||
const loaderName = loader.id.name;
|
||||
const loaderBindingId = bindingId(state, loader.id);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
runtimeFunctionParameters,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
@@ -662,10 +663,10 @@ const identityFunction = (
|
||||
!t.isFunction(node) ||
|
||||
node.async ||
|
||||
node.generator ||
|
||||
node.params.length !== 1
|
||||
runtimeFunctionParameters(node).length !== 1
|
||||
)
|
||||
return false;
|
||||
const parameter = node.params[0];
|
||||
const parameter = runtimeFunctionParameters(node)[0];
|
||||
const result = t.isBlockStatement(node.body)
|
||||
? node.body.body.length === 1 && t.isReturnStatement(node.body.body[0])
|
||||
? node.body.body[0].argument
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
semanticObjectPatternKeys,
|
||||
runtimeFunctionParameters,
|
||||
semanticStaticPropertyKey,
|
||||
semanticWriteTargets,
|
||||
} from "./javascriptAstValues.js";
|
||||
@@ -717,7 +718,7 @@ function* collectDefinitionsSteps(
|
||||
functionBodies.push({
|
||||
body: node,
|
||||
scope: nested,
|
||||
parameters: parent.params,
|
||||
parameters: runtimeFunctionParameters(parent),
|
||||
});
|
||||
}
|
||||
const scope = currentSemanticScope(stack);
|
||||
@@ -1076,7 +1077,7 @@ const scopeKind = (
|
||||
};
|
||||
|
||||
const functionParametersHaveExpressions = (node: t.Function): boolean => {
|
||||
const pending: t.Node[] = [...node.params];
|
||||
const pending: t.Node[] = [...runtimeFunctionParameters(node)];
|
||||
while (pending.length > 0) {
|
||||
const parameter = pending.pop();
|
||||
if (t.isAssignmentPattern(parameter)) return true;
|
||||
@@ -1133,7 +1134,7 @@ const bindFunctionLocals = (
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
for (const parameter of node.params)
|
||||
for (const parameter of runtimeFunctionParameters(node))
|
||||
bindPattern({
|
||||
pattern: t.isTSParameterProperty(parameter)
|
||||
? parameter.parameter
|
||||
|
||||
@@ -6,7 +6,10 @@ import type {
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
||||
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
|
||||
import {
|
||||
unwrapJavaScriptExpression,
|
||||
runtimeFunctionParameters,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
@@ -194,14 +197,15 @@ const parameterAcceptsArgument = (
|
||||
): boolean => {
|
||||
const node = state.callableNodesById.get(callable.callableId);
|
||||
if (!t.isFunction(node)) return false;
|
||||
const parameterIndex = node.params.findIndex((parameter) =>
|
||||
const parameters = runtimeFunctionParameters(node);
|
||||
const parameterIndex = parameters.findIndex((parameter) =>
|
||||
rangeContains(range(parameter), location),
|
||||
);
|
||||
if (parameterIndex === argumentIndex) return true;
|
||||
return (
|
||||
parameterIndex >= 0 &&
|
||||
parameterIndex < argumentIndex &&
|
||||
t.isRestElement(node.params[parameterIndex])
|
||||
t.isRestElement(parameters[parameterIndex])
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import * as t from "@babel/types";
|
||||
import { runtimeFunctionParameters } from "./javascriptAstValues.js";
|
||||
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import type { JavaScriptSemanticCallAnalysis } from "./javascriptSemanticCalls.js";
|
||||
@@ -279,7 +280,7 @@ const relationShape = (
|
||||
];
|
||||
|
||||
const callableParameterArity = (node: t.Node): number =>
|
||||
t.isFunction(node) ? node.params.length : 0;
|
||||
t.isFunction(node) ? runtimeFunctionParameters(node).length : 0;
|
||||
|
||||
const isAsyncCallable = (node: t.Node): boolean =>
|
||||
t.isFunction(node) && node.async;
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
semanticObjectPatternKeys,
|
||||
runtimeFunctionParameters,
|
||||
semanticStaticPropertyKey,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
@@ -212,7 +213,8 @@ export const semanticIterationSources = (
|
||||
else if (t.isYieldExpression(node) && node.delegate && node.argument != null)
|
||||
add(sources, { node: node.argument, projection: [] }, node, {});
|
||||
else if (t.isFunction(node))
|
||||
for (const parameter of node.params) visit(context, parameter, undefined);
|
||||
for (const parameter of runtimeFunctionParameters(node))
|
||||
visit(context, parameter, undefined);
|
||||
|
||||
return sources;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as t from "@babel/types";
|
||||
import { runtimeFunctionParameters } from "./javascriptAstValues.js";
|
||||
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
@@ -304,7 +305,7 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => {
|
||||
asyncChunkKeys: [],
|
||||
unknownAsyncChunkKeys: 0,
|
||||
};
|
||||
const parameter = node.params[0];
|
||||
const parameter = runtimeFunctionParameters(node)[0];
|
||||
const requireName = t.isIdentifier(parameter) ? parameter.name : null;
|
||||
if (requireName === null)
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from "./javascriptStaticAnalysisTypes.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
runtimeFunctionParameters,
|
||||
semanticStaticPropertyKey,
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
@@ -147,7 +148,7 @@ export const modulePropertyName = (
|
||||
export const factoryRequireName = (
|
||||
factory: t.FunctionExpression | t.ArrowFunctionExpression | t.ObjectMethod,
|
||||
): string | null => {
|
||||
const parameter = factory.params[2];
|
||||
const parameter = runtimeFunctionParameters(factory)[2];
|
||||
return t.isIdentifier(parameter) ? parameter.name : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
runtimeFunctionParameters,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
|
||||
@@ -387,7 +388,7 @@ export class JavaScriptSymbolicBindings {
|
||||
invocationNode?: t.Node,
|
||||
): SymbolicEnvironment {
|
||||
const values = new Map<string, SymbolicValue>();
|
||||
fn.node.params.forEach((param, index) => {
|
||||
runtimeFunctionParameters(fn.node).forEach((param, index) => {
|
||||
const target = t.isRestElement(param) ? param.argument : param;
|
||||
if (!t.isIdentifier(target)) return;
|
||||
const binding = resolveSemanticBindingState(
|
||||
@@ -866,9 +867,7 @@ export class JavaScriptSymbolicBindings {
|
||||
? {}
|
||||
: { scopeId: this.state.scopeByNode.get(factory)!.scopeId }),
|
||||
};
|
||||
const first = factory.params[0];
|
||||
const second = factory.params[1];
|
||||
const third = factory.params[2];
|
||||
const [first, second, third] = runtimeFunctionParameters(factory);
|
||||
const binding = (n: t.Node | undefined) =>
|
||||
t.isIdentifier(n)
|
||||
? resolveSemanticBindingState(this.state, n, n.name)?.bindingId
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type GhidraSeedCommitment,
|
||||
type GhidraSeedReport,
|
||||
} from "./GhidraAnalysisSeeds.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
@@ -193,19 +193,6 @@ describe("Ghidra seed session verification", () => {
|
||||
});
|
||||
|
||||
describe("Ghidra seed profile", () => {
|
||||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
}),
|
||||
};
|
||||
const pe: BinaryTarget = {
|
||||
path: "/fixture.exe",
|
||||
sha256: "a".repeat(64),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
|
||||
/** Simulated valid installation for profile tests; never starts Ghidra or Java. */
|
||||
export const ghidraProfileInstallationHost: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
}),
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ghidraProfileLanguageOverride,
|
||||
sameGhidraLanguageOverride,
|
||||
} from "./GhidraAnalysisProfile.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import {
|
||||
admitGhidraLanguageOverride,
|
||||
parseGhidraLanguageDefinitions,
|
||||
@@ -39,21 +39,6 @@ const X86_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
</language_definitions>
|
||||
`;
|
||||
|
||||
// Fake installation inspection; the language catalog is a real directory.
|
||||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
}),
|
||||
};
|
||||
|
||||
const pe: BinaryTarget = {
|
||||
path: "/fixture.exe",
|
||||
sha256: "a".repeat(64),
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
ghidraMipsProfileParameters,
|
||||
ghidraPinnedMipsLanguage,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js";
|
||||
import {
|
||||
@@ -22,20 +22,6 @@ import {
|
||||
ghidraProfileLanguageOverride,
|
||||
} from "./GhidraAnalysisProfile.js";
|
||||
|
||||
// Fake installation inspection tests admission/profile contracts, not Ghidra.
|
||||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
}),
|
||||
};
|
||||
const parsed = parseConfig({ GHIDRA_INSTALL_DIR: "/ghidra" });
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const config = parsed.value;
|
||||
@@ -155,7 +141,13 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
reason: expect.stringContaining(reason),
|
||||
});
|
||||
const resolved = await ghidra.resolveAnalysisProfile(value);
|
||||
expect(resolved.ok).toBe(false);
|
||||
expect(resolved).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisUnsupportedTargetError",
|
||||
reason: expect.stringContaining(reason),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -180,7 +172,13 @@ describe("recordless legacy MIPS32 admission", () => {
|
||||
status: "unsupported",
|
||||
});
|
||||
const resolved = await ghidra.resolveAnalysisProfile(object);
|
||||
expect(resolved.ok).toBe(false);
|
||||
expect(resolved).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisUnsupportedTargetError",
|
||||
reason: expect.stringContaining("ET_EXEC"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ghidraProfileLanguageOverride,
|
||||
} from "./GhidraAnalysisProfile.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
|
||||
// Abbreviated from Ghidra 12.1.4 Processors/PowerPC/data/languages/ppc.ldefs.
|
||||
const PPC_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -32,20 +32,6 @@ const PPC_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
</language_definitions>
|
||||
`;
|
||||
|
||||
// Fake installation inspection tests admission/profile contracts, not Ghidra.
|
||||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
}),
|
||||
};
|
||||
const configFor = (environment: NodeJS.ProcessEnv) => {
|
||||
const parsed = parseConfig({ GHIDRA_INSTALL_DIR: "/ghidra", ...environment });
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
@@ -65,6 +51,7 @@ const target = (change: Partial<PowerPcElfMetadata> = {}): BinaryTarget => ({
|
||||
describe("bounded Ghidra PowerPC admission", () => {
|
||||
it("binds the generic language instead of the loader's e500 choice", async () => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
expect(ghidra.inspectAvailability()).toMatchObject({ status: "available" });
|
||||
expect(ghidra.inspectTargetSupport(target()).status).toBe("supported");
|
||||
const resolved = await ghidra.resolveAnalysisProfile(target());
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
@@ -152,7 +139,13 @@ describe("bounded Ghidra PowerPC admission", () => {
|
||||
reason: expect.stringContaining(reason),
|
||||
});
|
||||
const resolved = await ghidra.resolveAnalysisProfile(value);
|
||||
expect(resolved.ok).toBe(false);
|
||||
expect(resolved).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisUnsupportedTargetError",
|
||||
reason: expect.stringContaining(reason),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
|
||||
import { IdaProvider } from "../ida/IdaProvider.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import {
|
||||
ghidraPinnedLanguage,
|
||||
ghidraProfileLanguageOverride,
|
||||
@@ -49,20 +49,6 @@ const RISCV_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
</language_definitions>
|
||||
`;
|
||||
|
||||
// Fake installation inspection tests admission/profile contracts, not Ghidra.
|
||||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
}),
|
||||
};
|
||||
const configOf = (
|
||||
installDir: string,
|
||||
environment: Record<string, string> = {},
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"maps arguments to runtime TypeScript parameters through %s",
|
||||
async (surface, { cli, processes }) => {
|
||||
const root = await createTestTempDirectory("rea-this-parameters-");
|
||||
await writeFixtureFiles(root, {
|
||||
"bundle.mts": [
|
||||
"self.webpackChunkApp.push([[1], { 7: function(this: void, module: unknown, exports: Record<string, unknown>, require: (id: number) => unknown) { exports.answer = 42; } }, function(this: void, require: (id: number) => unknown) { require(7); }]);",
|
||||
].join("\n"),
|
||||
"interop.cts": [
|
||||
"function identity<T>(this: void, value: T) { return value; }",
|
||||
'const electron = identity(require("electron"));',
|
||||
'electron.ipcRenderer.send("typed-identity");',
|
||||
].join("\n"),
|
||||
"ipc-wrapper.cts": [
|
||||
'const { ipcRenderer } = require("electron");',
|
||||
"function send(this: void, channel: string) { ipcRenderer.send(channel); }",
|
||||
"function sendRest(this: void, ...channels: string[]) { ipcRenderer.send(...channels); }",
|
||||
'send("typed-channel");',
|
||||
'sendRest("typed-rest");',
|
||||
].join("\n"),
|
||||
"entry.mts": [
|
||||
"function first(this: void, value: number) { return value; }",
|
||||
"function rest(this: void, ...values: number[]) { return values; }",
|
||||
"function destructured({ left }: { left: number }, right: number) { return left + right; }",
|
||||
"function mixed(this: void, head: number, ...tail: number[]) { return [head, ...tail]; }",
|
||||
"const a = first(7);",
|
||||
"const b = rest(1, 2);",
|
||||
"const c = destructured({ left: 3 }, 4);",
|
||||
"const d = mixed(9, 10, 11);",
|
||||
"console.log(JSON.stringify([a, b, c, d, first.length, rest.length, mixed.length]));",
|
||||
].join("\n"),
|
||||
});
|
||||
const control = await processes.run(
|
||||
process.execPath,
|
||||
[join(root, "entry.mts")],
|
||||
{ timeoutMs: 15_000 },
|
||||
);
|
||||
expect(control.exitCode, control.stderr).toBe(0);
|
||||
expect(JSON.parse(control.stdout)).toEqual([
|
||||
7,
|
||||
[1, 2],
|
||||
7,
|
||||
[9, 10, 11],
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
]);
|
||||
const analysis = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
const graph = analysis.semantic_graph;
|
||||
const parameters = graph.nodes.filter(
|
||||
(node) =>
|
||||
node.kind === "parameter" && node.identity.module_path === "entry.mts",
|
||||
);
|
||||
expect(parameters.map((node) => node.label).sort()).toEqual([
|
||||
"head",
|
||||
"left",
|
||||
"right",
|
||||
"tail",
|
||||
"value",
|
||||
"values",
|
||||
]);
|
||||
const parameterIds = new Set(parameters.map((node) => node.node_id));
|
||||
const flows = graph.relations.filter(
|
||||
(relation) =>
|
||||
relation.relation === "argument-to-parameter" &&
|
||||
parameterIds.has(relation.target_node_id),
|
||||
);
|
||||
expect(
|
||||
flows
|
||||
.map((flow) => {
|
||||
const source = graph.nodes.find(
|
||||
(node) => node.node_id === flow.source_node_id,
|
||||
);
|
||||
const target = parameters.find(
|
||||
(node) => node.node_id === flow.target_node_id,
|
||||
);
|
||||
const owner = graph.nodes.find(
|
||||
(node) => node.node_id === target?.function_node_id,
|
||||
);
|
||||
return [
|
||||
source?.identity.source_range?.start.line,
|
||||
source?.label,
|
||||
owner?.label,
|
||||
target?.label,
|
||||
];
|
||||
})
|
||||
.sort(),
|
||||
).toEqual([
|
||||
[5, "argument 0", "first", "value"],
|
||||
[6, "argument 0", "rest", "values"],
|
||||
[6, "argument 1", "rest", "values"],
|
||||
[7, "argument 0", "destructured", "left"],
|
||||
[7, "argument 1", "destructured", "right"],
|
||||
[8, "argument 0", "mixed", "head"],
|
||||
[8, "argument 1", "mixed", "tail"],
|
||||
[8, "argument 2", "mixed", "tail"],
|
||||
]);
|
||||
expect(
|
||||
analysis.graph.nodes
|
||||
.flatMap((node) => node.observations)
|
||||
.some(
|
||||
(observation) =>
|
||||
Array.isArray(observation.properties.exports) &&
|
||||
observation.properties.exports.includes("answer"),
|
||||
),
|
||||
).toBe(true);
|
||||
const channels = analysis.graph.nodes
|
||||
.filter((node) => node.kind === "ipc-channel")
|
||||
.flatMap((node) =>
|
||||
node.observations.map((observation) => observation.properties.channel),
|
||||
);
|
||||
expect(channels).toEqual(
|
||||
expect.arrayContaining(["typed-channel", "typed-rest", "typed-identity"]),
|
||||
);
|
||||
for (const [name, arity] of [
|
||||
["first", 1],
|
||||
["rest", 1],
|
||||
["destructured", 2],
|
||||
["mixed", 2],
|
||||
] as const) {
|
||||
const callable = graph.nodes.find(
|
||||
(node) => node.kind === "function" && node.label === name,
|
||||
);
|
||||
expect(callable, name).toBeDefined();
|
||||
const fingerprint = graph.fingerprints.find(
|
||||
(value) => value.function_node_id === callable?.node_id,
|
||||
);
|
||||
expect(fingerprint?.components.parameter_arity, name).toBe(arity);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -17,6 +17,32 @@ import { workflowGit } from "../../support/workflowGit.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const script = new URL("../../../scripts/ci/test-report.mjs", import.meta.url);
|
||||
|
||||
// Each report invocation owns its CI state and summary destination. Inherited
|
||||
// workflow flags must not change fixture outcomes or append to the real job.
|
||||
const executeTimingReport = async (
|
||||
root: string,
|
||||
environment: NodeJS.ProcessEnv = {},
|
||||
) => {
|
||||
const summary = join(root, "summary.md");
|
||||
await writeFile(summary, "");
|
||||
const output = await execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
SHARD_RESULT: "success",
|
||||
MERGE_RESULT: "success",
|
||||
SOURCE_REQUIRED: "false",
|
||||
SOURCE_RESULT: "",
|
||||
CI_VERIFY_TEST_INVENTORY: "false",
|
||||
CI_TEST_SELECTION: "",
|
||||
...environment,
|
||||
GITHUB_STEP_SUMMARY: summary,
|
||||
},
|
||||
});
|
||||
return { ...output, summary: await readFile(summary, "utf8") };
|
||||
};
|
||||
|
||||
const stepsSchema = z.array(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
@@ -225,14 +251,8 @@ it("rejects successful shard reports with missing or duplicate portable files",
|
||||
files: names.map((name) => ({ name, durationMs: 1 })),
|
||||
}),
|
||||
);
|
||||
return execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
SHARD_RESULT: "success",
|
||||
MERGE_RESULT: "success",
|
||||
CI_VERIFY_TEST_INVENTORY: "true",
|
||||
},
|
||||
return executeTimingReport(root, {
|
||||
CI_VERIFY_TEST_INVENTORY: "true",
|
||||
});
|
||||
};
|
||||
await expect(
|
||||
@@ -308,17 +328,11 @@ it("exports portable timing history and removes it on a failed, partial, or inva
|
||||
source?: string,
|
||||
) => {
|
||||
await writeFile(join(root, "test-timings.json"), JSON.stringify(value));
|
||||
await writeFile(summary, "");
|
||||
return execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
GITHUB_STEP_SUMMARY: summary,
|
||||
SHARD_RESULT: shards,
|
||||
SOURCE_REQUIRED: String(source !== undefined),
|
||||
SOURCE_RESULT: source ?? "",
|
||||
MERGE_RESULT: merge,
|
||||
},
|
||||
return executeTimingReport(root, {
|
||||
SHARD_RESULT: shards,
|
||||
SOURCE_REQUIRED: String(source !== undefined),
|
||||
SOURCE_RESULT: source ?? "",
|
||||
MERGE_RESULT: merge,
|
||||
});
|
||||
};
|
||||
await run(report);
|
||||
@@ -458,25 +472,17 @@ it("recovers complete module costs from real blob reports without executing test
|
||||
expect(file.durationMs).toBeCloseTo(
|
||||
Object.values(file.phases).reduce((sum, value) => sum + value, 0),
|
||||
);
|
||||
const partial = await execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
SHARD_RESULT: "skipped",
|
||||
SOURCE_REQUIRED: "true",
|
||||
SOURCE_RESULT: "success",
|
||||
MERGE_RESULT: "success",
|
||||
},
|
||||
const partial = await executeTimingReport(root, {
|
||||
SHARD_RESULT: "skipped",
|
||||
SOURCE_REQUIRED: "true",
|
||||
SOURCE_RESULT: "success",
|
||||
});
|
||||
expect(partial.stdout).toContain("Partial test diagnostics");
|
||||
expect(partial.stdout).toContain("sample.test.js");
|
||||
expect(partial.summary).toContain("Partial test diagnostics");
|
||||
expect(partial.summary).toContain("sample.test.js");
|
||||
await expect(
|
||||
readFile(join(root, ".cache/ci-test-history/test-results.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: { ...process.env, SHARD_RESULT: "success", MERGE_RESULT: "success" },
|
||||
});
|
||||
await executeTimingReport(root);
|
||||
expect(
|
||||
parseTestDurations(
|
||||
await readFile(
|
||||
@@ -508,10 +514,7 @@ it("recovers complete module costs from real blob reports without executing test
|
||||
files: [{ state: "failed" }],
|
||||
});
|
||||
// Even a caller accidentally marking the merge successful cannot publish this report.
|
||||
await execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: { ...process.env, SHARD_RESULT: "success", MERGE_RESULT: "success" },
|
||||
});
|
||||
await executeTimingReport(root);
|
||||
await expect(
|
||||
readFile(join(root, ".cache/ci-test-history/test-results.json")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
@@ -543,21 +546,17 @@ it("records failed omissions from full reports without treating partial or unhan
|
||||
};
|
||||
const run = async (report: unknown, source = "failure") => {
|
||||
await writeFile(join(root, "test-timings.json"), JSON.stringify(report));
|
||||
await execute(process.execPath, [fileURLToPath(script)], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
SHARD_RESULT: "failure",
|
||||
SOURCE_REQUIRED: "true",
|
||||
SOURCE_RESULT: source,
|
||||
MERGE_RESULT: "failure",
|
||||
CI_VERIFY_TEST_INVENTORY: "true",
|
||||
CI_TEST_SELECTION: JSON.stringify(
|
||||
planTestSelection(["src/sqlite/Provider.ts"], "pull_request", "fast"),
|
||||
),
|
||||
GITHUB_SHA: "merge-sha",
|
||||
CI_PR_HEAD_SHA: "pr-head-sha",
|
||||
},
|
||||
await executeTimingReport(root, {
|
||||
SHARD_RESULT: "failure",
|
||||
SOURCE_REQUIRED: "true",
|
||||
SOURCE_RESULT: source,
|
||||
MERGE_RESULT: "failure",
|
||||
CI_VERIFY_TEST_INVENTORY: "true",
|
||||
CI_TEST_SELECTION: JSON.stringify(
|
||||
planTestSelection(["src/sqlite/Provider.ts"], "pull_request", "fast"),
|
||||
),
|
||||
GITHUB_SHA: "merge-sha",
|
||||
CI_PR_HEAD_SHA: "pr-head-sha",
|
||||
});
|
||||
return JSON.parse(
|
||||
await readFile(
|
||||
|
||||
Reference in New Issue
Block a user