mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(javascript): preserve lexical open and exact template semantics
This commit is contained in:
@@ -166,6 +166,12 @@ Selected bounded text is then parsed as inert data to recover:
|
||||
- utility-process entrypoints and native `.node` binding requests without
|
||||
parsing or executing the add-on.
|
||||
|
||||
Overloaded `.open` calls use lexical receiver facts: ambient browser window and
|
||||
document receivers are treated as browsing-context or document operations,
|
||||
while locally shadowed receivers can still contribute network endpoint
|
||||
candidates. A template recovered with a parser error and no cooked value stays
|
||||
dynamic; its raw spelling is not treated as a valid JavaScript string.
|
||||
|
||||
Each recovered bundle module retains the exact factory-source digest. A complete
|
||||
bounded AST also receives a `babel-ast-v1` structural fingerprint that ignores
|
||||
ordinary identifier names while retaining syntax, literals, operators, object
|
||||
|
||||
@@ -23,6 +23,13 @@ it("classifies overloaded open calls from lexical receiver facts", async () => {
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(method, "/api");
|
||||
parent.open("GET", "/parent-target");
|
||||
self.open("POST", "/self-target");
|
||||
document.open("GET", "/document-target");
|
||||
{
|
||||
const parent = new XMLHttpRequest();
|
||||
parent.open(method, "/shadowed-parent");
|
||||
}
|
||||
indexedDB.open("records", "2");
|
||||
`,
|
||||
);
|
||||
@@ -46,6 +53,6 @@ it("classifies overloaded open calls from lexical receiver facts", async () => {
|
||||
.flatMap(({ observations }) =>
|
||||
observations.map(({ properties }) => properties.storage_kind),
|
||||
);
|
||||
expect(endpoints).toEqual(["/api", "/dav"]);
|
||||
expect(endpoints).toEqual(["/api", "/dav", "/shadowed-parent"]);
|
||||
expect(storage).toEqual(["indexed-db"]);
|
||||
});
|
||||
|
||||
@@ -44,4 +44,24 @@ describe("Electron option evidence", () => {
|
||||
service_name: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps recovered templates with invalid escapes dynamic", () => {
|
||||
const source = [
|
||||
"new BrowserWindow({ webPreferences: { sandbox: `",
|
||||
String.raw`\unicode`,
|
||||
"` } });",
|
||||
].join("");
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
|
||||
expect(analysis.parse_status).toBe("partial");
|
||||
expect(analysis.parse_error_count).toBe(1);
|
||||
expect(
|
||||
analysis.electron.browser_windows[0]?.web_preferences,
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "sandbox",
|
||||
value: expect.objectContaining({ status: "dynamic", value: null }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,11 +148,6 @@ const literalValue = (
|
||||
if (node === null || node === undefined) return { found: false };
|
||||
const literal = readExactJavaScriptLiteral(node);
|
||||
if (literal.found) return literal;
|
||||
if (t.isTemplateLiteral(node) && node.expressions.length === 0)
|
||||
return {
|
||||
found: true,
|
||||
value: node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw ?? "",
|
||||
};
|
||||
if (
|
||||
t.isUnaryExpression(node, { operator: "-" }) &&
|
||||
t.isNumericLiteral(node.argument)
|
||||
|
||||
@@ -179,6 +179,10 @@ export const classifyParsedJavaScriptOpenReceivers = (
|
||||
if (fact !== undefined) facts.set(node.start ?? -1, fact);
|
||||
return;
|
||||
}
|
||||
// A lexically bound receiver overrides ambient browser globals. Mark an
|
||||
// otherwise unresolved local as local so name-based window.open
|
||||
// heuristics do not hide its XHR-like overloads.
|
||||
facts.set(node.start ?? -1, "local");
|
||||
if (receiver === "indexedDB" || receiver === "caches") {
|
||||
facts.set(
|
||||
node.start ?? -1,
|
||||
@@ -235,7 +239,17 @@ export const classifyParsedJavaScriptOpenReceivers = (
|
||||
const openGlobalFact = (
|
||||
name: string,
|
||||
): JavaScriptOpenReceiverFact | undefined => {
|
||||
if (name === "window") return "window";
|
||||
if (
|
||||
name === "window" ||
|
||||
name === "self" ||
|
||||
name === "globalThis" ||
|
||||
name === "top" ||
|
||||
name === "parent" ||
|
||||
name === "opener" ||
|
||||
name === "frames" ||
|
||||
name === "document"
|
||||
)
|
||||
return "window";
|
||||
if (name === "indexedDB") return "indexed-db";
|
||||
if (name === "caches") return "cache-storage";
|
||||
return undefined;
|
||||
|
||||
@@ -11,7 +11,10 @@ import type {
|
||||
JavaScriptStaticPathContext,
|
||||
JavaScriptStaticStorage,
|
||||
} from "./javascriptStaticAnalysisTypes.js";
|
||||
import { semanticStaticPropertyName } from "./javascriptAstValues.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyName,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { compareCodePoints } from "../canonicalOrdering.js";
|
||||
|
||||
export {
|
||||
@@ -179,10 +182,8 @@ export const staticPathResolutionContext = (
|
||||
|
||||
const staticPathAt = (node: t.Node): string | undefined => {
|
||||
if (t.isStringLiteral(node)) return node.value;
|
||||
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
||||
const value = node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;
|
||||
return value;
|
||||
}
|
||||
if (t.isTemplateLiteral(node) && node.expressions.length === 0)
|
||||
return stringValue(node);
|
||||
if (t.isBinaryExpression(node, { operator: "+" })) {
|
||||
const left = staticPathAt(node.left);
|
||||
const right = staticPathAt(node.right);
|
||||
@@ -447,6 +448,9 @@ const xhrOpenUrl = (
|
||||
): string | undefined => {
|
||||
const url = stringValue(urlNode);
|
||||
if (url === undefined) return undefined;
|
||||
// A lexical fact is stronger than the receiver's spelling. In particular,
|
||||
// browser Window and Document globals overload `open` with URL/content APIs.
|
||||
if (receiverFact === "window") return undefined;
|
||||
const method = stringValue(methodNode);
|
||||
// IndexedDB and Cache Storage `open` take a name and a version, so a
|
||||
// receiver spelled as storage needs a standard HTTP method literal.
|
||||
@@ -458,8 +462,7 @@ const xhrOpenUrl = (
|
||||
storageKind(name) !== undefined;
|
||||
if (method === undefined)
|
||||
return storage ||
|
||||
receiverFact === "window" ||
|
||||
WINDOW_OPEN_CALL.test(name) ||
|
||||
(receiverFact === undefined && WINDOW_OPEN_CALL.test(name)) ||
|
||||
FS_OPEN_FLAGS.has(url) ||
|
||||
BROWSING_CONTEXT_KEYWORDS.has(url.toLowerCase())
|
||||
? undefined
|
||||
@@ -600,10 +603,11 @@ const memberPropertyName = (
|
||||
export const stringValue = (
|
||||
node: t.Node | null | undefined,
|
||||
): string | undefined => {
|
||||
if (t.isStringLiteral(node)) return node.value;
|
||||
if (t.isTemplateLiteral(node) && node.expressions.length === 0)
|
||||
return node.quasis[0]?.value.cooked ?? undefined;
|
||||
return undefined;
|
||||
if (node === null || node === undefined) return undefined;
|
||||
const literal = readExactJavaScriptLiteral(node);
|
||||
return literal.found && typeof literal.value === "string"
|
||||
? literal.value
|
||||
: undefined;
|
||||
};
|
||||
|
||||
/** Return a string or numeric argument literal. */
|
||||
|
||||
@@ -167,6 +167,34 @@ describe("inert JavaScript path resolution", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps recovered templates with invalid escapes unknown as paths", () => {
|
||||
const expression = ["`", String.raw`\unicode`, "`"].join("");
|
||||
const { analysis } = analyzePathConsumers(expression);
|
||||
|
||||
expect(analysis.parse_status).toBe("partial");
|
||||
expect(analysis.parse_error_count).toBe(3);
|
||||
expect(analysis.role_paths).toEqual([]);
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{
|
||||
preload_path: null,
|
||||
preload_resolution_context: null,
|
||||
web_preferences: [
|
||||
{
|
||||
name: "preload",
|
||||
value: { status: "dynamic", value: null, expression },
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(analysis.electron.utility_processes).toEqual([
|
||||
expect.objectContaining({
|
||||
module_path: null,
|
||||
module_resolution_context: null,
|
||||
module_expression: expression,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"path.join()",
|
||||
"path.join(__dirname)",
|
||||
|
||||
@@ -191,6 +191,17 @@ describe("web bundle static-analysis parity", () => {
|
||||
document.open("text/html", "/replace");
|
||||
parent.open("GET", "/parent-xhr");
|
||||
self.open("post", "/self-xhr");
|
||||
top.open("GET", "/top-target");
|
||||
opener.open("GET", "/opener-target");
|
||||
frames.open("GET", "/frames-target");
|
||||
globalThis.open("GET", "/global-target");
|
||||
document.open("GET", "/document-target");
|
||||
const parentWindow = parent;
|
||||
parentWindow.open("GET", "/alias-target");
|
||||
{
|
||||
const parent = new XMLHttpRequest();
|
||||
parent.open(method, "/shadowed-parent-xhr");
|
||||
}
|
||||
xhr.open(method, "api/relative");
|
||||
fs.open(path, "w+", done);
|
||||
popup.open(url, "_TOP");
|
||||
@@ -201,14 +212,15 @@ describe("web bundle static-analysis parity", () => {
|
||||
).toEqual([
|
||||
"/dav/",
|
||||
"/dynamic-method",
|
||||
"/parent-xhr",
|
||||
"/self-xhr",
|
||||
"/shadowed-parent-xhr",
|
||||
"/xhr-get",
|
||||
"api/relative",
|
||||
"https://xhr.example.test/submit",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("web bundle API classification parity", () => {
|
||||
it("reads storage open versions as storage rather than endpoints in both analyzers", () => {
|
||||
const source = `
|
||||
indexedDB.open(databaseName, "2");
|
||||
|
||||
Reference in New Issue
Block a user