From 90cb8a95510f7e08023be6d172a243c0139aff0b Mon Sep 17 00:00:00 2001 From: catloversg <152669316+catloversg@users.noreply.github.com> Date: Sun, 10 Nov 2024 06:34:46 +0700 Subject: [PATCH] MISC: Add proper type check to AST walking code (#1757) --- src/NetscriptJSEvaluator.ts | 38 ++++++++++++++------ src/NetscriptWorker.ts | 31 ++++++++++------ src/Script/RamCalculations.ts | 67 ++++++++++++++++++++++++----------- 3 files changed, 93 insertions(+), 43 deletions(-) diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index f478d77a7..702682d8b 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -4,15 +4,13 @@ */ import * as walk from "acorn-walk"; import { parse } from "acorn"; +import type * as acorn from "acorn"; import { LoadedModule, type ScriptURL, type ScriptModule } from "./Script/LoadedModule"; import type { Script } from "./Script/Script"; import type { ScriptFilePath } from "./Paths/ScriptFilePath"; import { FileType, getFileType, getModuleScript, transformScript } from "./utils/ScriptTransformer"; -// Acorn type def is straight up incomplete so we have to fill with our own. -export type Node = any; - // Makes a blob that contains the code of a given script. function makeScriptBlob(code: string): Blob { return new Blob([code], { type: "text/javascript" }); @@ -103,33 +101,51 @@ function generateLoadedModule(script: Script, scripts: Map try { // Sent a resolver function as an extra arg. See createAsyncFunction JSInterpreter.js:3209 const callback = args.pop() as (value: unknown) => void; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- NS1 is deprecated. const result = await entry(...args.map((arg) => int.pseudoToNative(arg))); return callback(int.nativeToPseudo(result)); } catch (e: unknown) { @@ -105,6 +107,7 @@ async function startNetscript1Script(workerScript: WorkerScript): Promise } else { // new object layer, e.g. bladeburner int.setProperty(intLayer, name, int.nativeToPseudo({})); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- NS1 is deprecated. wrapNS1Layer(int, (intLayer as BasicObject).properties[name], nsLayer[name]); } } @@ -139,7 +142,7 @@ async function startNetscript1Script(workerScript: WorkerScript): Promise */ function processNetscript1Imports(code: string, workerScript: WorkerScript): { code: string; lineOffset: number } { //allowReserved prevents 'import' from throwing error in ES5 - const ast: Node = parse(code, { + const ast = parse(code, { ecmaVersion: 9, allowReserved: true, sourceType: "module", @@ -159,9 +162,9 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c // Walk over the tree and process ImportDeclaration nodes walksimple(ast, { - ImportDeclaration: (node: Node) => { + ImportDeclaration: (node: acorn.ImportDeclaration) => { hasImports = true; - const scriptName = resolveScriptFilePath(node.source.value, root, legacyScriptExtension); + const scriptName = resolveScriptFilePath(node.source.value as string, root, legacyScriptExtension); if (!scriptName) throw new Error("'Import' failed due to invalid path: " + scriptName); const script = getScript(scriptName); if (!script) throw new Error("'Import' failed due to script not found: " + scriptName); @@ -175,9 +178,12 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c // import * as namespace from script const namespace = node.specifiers[0].local.name; const fnNames: string[] = []; //Names only - const fnDeclarations: Node[] = []; //FunctionDeclaration Node objects + const fnDeclarations: acorn.Node[] = []; //FunctionDeclaration Node objects walksimple(scriptAst, { - FunctionDeclaration: (node: Node) => { + FunctionDeclaration: (node: acorn.FunctionDeclaration | acorn.AnonymousFunctionDeclaration) => { + if (!node.id) { + return; + } fnNames.push(node.id.name); fnDeclarations.push(node); }, @@ -187,7 +193,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c generatedCode += `var ${namespace};\n(function (namespace) {\n`; //Add the function declarations - fnDeclarations.forEach((fn: Node) => { + fnDeclarations.forEach((fn) => { generatedCode += generate(fn); generatedCode += "\n"; }); @@ -205,14 +211,17 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c //Get array of all fns to import const fnsToImport: string[] = []; - node.specifiers.forEach((e: Node) => { + node.specifiers.forEach((e) => { fnsToImport.push(e.local.name); }); //Walk through script and get FunctionDeclaration code for all specified fns - const fnDeclarations: Node[] = []; + const fnDeclarations: acorn.Node[] = []; walksimple(scriptAst, { - FunctionDeclaration: (node: Node) => { + FunctionDeclaration: (node: acorn.FunctionDeclaration | acorn.AnonymousFunctionDeclaration) => { + if (!node.id) { + return; + } if (fnsToImport.includes(node.id.name)) { fnDeclarations.push(node); } @@ -220,7 +229,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c }); //Convert FunctionDeclarations into code - fnDeclarations.forEach((fn: Node) => { + fnDeclarations.forEach((fn) => { generatedCode += generate(fn); generatedCode += "\n"; }); diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index be6b46a9c..8dced94a9 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -12,7 +12,6 @@ import { RamCalculationErrorCode } from "./RamCalculationErrorCodes"; import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; import type { Script } from "./Script"; -import type { Node } from "../NetscriptJSEvaluator"; import type { ScriptFilePath } from "../Paths/ScriptFilePath"; import type { ServerName } from "../Types/strings"; import { roundToTwo } from "../utils/helpers/roundToTwo"; @@ -288,7 +287,7 @@ export function checkInfiniteLoop(ast: AST, code: string): number[] { ast as acorn.Node, // Pretend that ast is an acorn node {}, { - WhileStatement: (node: Node, st: unknown, walkDeeper: walk.WalkerCallback) => { + WhileStatement: (node: acorn.WhileStatement, st: unknown, walkDeeper: walk.WalkerCallback) => { const previousLines = code.slice(0, node.start).trimEnd().split("\n"); const lineNumber = previousLines.length + 1; if (previousLines[previousLines.length - 1].match(/^\s*\/\/\s*@ignore-infinite/)) { @@ -354,7 +353,7 @@ function parseOnlyCalculateDeps( key: string; } - function checkRamOverride(node: Node) { + function checkRamOverride(node: acorn.BlockStatement) { // To trigger a syntactic RAM override, the first statement must be a call // to ns.ramOverride() (or something that looks similar). if (!node.body || !node.body.length) return; @@ -364,7 +363,12 @@ function parseOnlyCalculateDeps( if (expr.type !== "CallExpression") return; if (!expr.arguments || expr.arguments.length !== 1) return; - function findIdentifier(node: Node): Node { + /** + * This function is called with expr.callee. expr.callee can be Expression or Super. In its implementation, the + * "node" parameter can be reassigned to node.property if "node" is MemberExpression. node.property may be + * PrivateIdentifier, so we need to add that type to the type list of "node". + */ + function findIdentifier(node: acorn.Expression | acorn.Super | acorn.PrivateIdentifier) { for (;;) { // Find the identifier node attached to the call switch (node.type) { @@ -404,36 +408,36 @@ function parseOnlyCalculateDeps( // walkDeeper is for doing recursive walks of expressions in composites that we handle. function commonVisitors(): walk.RecursiveVisitors { return { - Identifier: (node: Node, st: State) => { + Identifier: (node: acorn.Identifier, st: State) => { if (objectPrototypeProperties.includes(node.name)) { return; } addRef(st.key, node.name); }, - WhileStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback) => { + WhileStatement: (node: acorn.WhileStatement, st: State, walkDeeper: walk.WalkerCallback) => { addRef(st.key, specialReferenceWHILE); node.test && walkDeeper(node.test, st); node.body && walkDeeper(node.body, st); }, - DoWhileStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback) => { + DoWhileStatement: (node: acorn.DoWhileStatement, st: State, walkDeeper: walk.WalkerCallback) => { addRef(st.key, specialReferenceWHILE); node.test && walkDeeper(node.test, st); node.body && walkDeeper(node.body, st); }, - ForStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback) => { + ForStatement: (node: acorn.ForStatement, st: State, walkDeeper: walk.WalkerCallback) => { addRef(st.key, specialReferenceFOR); node.init && walkDeeper(node.init, st); node.test && walkDeeper(node.test, st); node.update && walkDeeper(node.update, st); node.body && walkDeeper(node.body, st); }, - IfStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback) => { + IfStatement: (node: acorn.IfStatement, st: State, walkDeeper: walk.WalkerCallback) => { addRef(st.key, specialReferenceIF); node.test && walkDeeper(node.test, st); node.consequent && walkDeeper(node.consequent, st); node.alternate && walkDeeper(node.alternate, st); }, - MemberExpression: (node: Node, st: State, walkDeeper: walk.WalkerCallback) => { + MemberExpression: (node: acorn.MemberExpression, st: State, walkDeeper: walk.WalkerCallback) => { node.object && walkDeeper(node.object, st); node.property && walkDeeper(node.property, st); }, @@ -445,8 +449,12 @@ function parseOnlyCalculateDeps( { key: globalKey }, Object.assign( { - ImportDeclaration: (node: Node, st: State) => { + ImportDeclaration: (node: acorn.ImportDeclaration, st: State) => { const rawImportModuleName = node.source.value; + if (typeof rawImportModuleName !== "string") { + console.error("Invalid node when walking ImportDeclaration in parseOnlyCalculateDeps. node:", node); + return; + } // Skip these modules. They are popular path aliases of NetscriptDefinitions.d.ts. if (fileTypeFeature.isTypeScript && (rawImportModuleName === "@nsdefs" || rawImportModuleName === "@ns")) { return; @@ -462,7 +470,11 @@ function parseOnlyCalculateDeps( for (let i = 0; i < node.specifiers.length; ++i) { const spec = node.specifiers[i]; - if (spec.imported !== undefined && spec.local !== undefined) { + /** + * spec can be ImportSpecifier, ImportDefaultSpecifier, or ImportNamespaceSpecifier. "imported" only exists + * in ImportSpecifier. "imported" can be Identifier or Literal. imported.name only exists in Identifier. + */ + if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.local !== undefined) { // We depend on specific things. internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name; } else { @@ -473,7 +485,7 @@ function parseOnlyCalculateDeps( } } }, - FunctionDeclaration: (node: Node) => { + FunctionDeclaration: (node: acorn.FunctionDeclaration) => { if (node.id?.name === "main") { checkRamOverride(node.body); } @@ -481,22 +493,35 @@ function parseOnlyCalculateDeps( const key = currentModule + "." + (node.id === null ? "__SPECIAL_DEFAULT_EXPORT__" : node.id.name); walk.recursive(node, { key: key }, commonVisitors()); }, - ExportNamedDeclaration: (node: Node, st: State, walkDeeper: walk.WalkerCallback) => { - if (node.declaration !== null) { + ExportNamedDeclaration: ( + node: acorn.ExportNamedDeclaration, + st: State, + walkDeeper: walk.WalkerCallback, + ) => { + if (node.declaration != null) { // if this is true, the statement is not a named export, but rather a exported function/variable walkDeeper(node.declaration, st); return; } for (const specifier of node.specifiers) { + /** + * specifier.exported can be Identifier or Literal. specifier.exported.name only exists in Identifier. + */ + if (specifier.exported.type === "Literal") { + continue; + } const exportedDepName = currentModule + "." + specifier.exported.name; - - if (node.source !== null) { + /** + * We need to use specifier.local.name and node.source.value. Before doing that, we need to check if they + * exist. local.name only exists in Identifier. + */ + if (node.source != null && typeof node.source.value === "string" && specifier.local.type === "Identifier") { // if this is true, we are re-exporting something - addRef(exportedDepName, specifier.local.name, node.source.value); - additionalModules.push(node.source.value); - } else if (specifier.exported.name !== specifier.local.name) { - // this makes sure we are not refering to ourselves + addRef(exportedDepName, specifier.local.name, node.source.value as ScriptFilePath); + additionalModules.push(node.source.value as ScriptFilePath); + } else if (specifier.local.type === "Identifier" && specifier.exported.name !== specifier.local.name) { + // this makes sure we are not referring to ourselves // if this is not true, we don't need to add anything addRef(exportedDepName, specifier.local.name); }