diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index 22532ec2a..1f6e98b33 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -8,7 +8,6 @@ import { parse } from "acorn"; import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; import { Script } from "./Script/Script"; import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath"; -import { root } from "./Paths/Directory"; // Acorn type def is straight up incomplete so we have to fill with our own. export type Node = any; @@ -125,7 +124,7 @@ function generateLoadedModule(script: Script, scripts: Map { script.invalidateModule(); diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index 1a5222f49..a9b0abc32 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -14,7 +14,7 @@ import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; import { Script } from "./Script"; import { Node } from "../NetscriptJSEvaluator"; import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath"; -import { root } from "../Paths/Directory"; +import { ServerName } from "../Types/strings"; export interface RamUsageEntry { type: "ns" | "dom" | "fn" | "misc"; @@ -56,12 +56,21 @@ function getNumericCost(cost: number | (() => number)): number { * Parses code into an AST and walks through it recursively to calculate * RAM usage. Also accounts for imported modules. * @param otherScripts - All other scripts on the server. Used to account for imported scripts - * @param code - The code being parsed */ -function parseOnlyRamCalculate(otherScripts: Map, code: string, ns1?: boolean): RamCalculation { + * @param code - The code being parsed + * @param scriptname - The name of the script that ram needs to be added to + * @param server - Servername of the scripts for Error Message + * */ +function parseOnlyRamCalculate( + otherScripts: Map, + code: string, + scriptname: ScriptFilePath, + server: ServerName, + ns1?: boolean, +): RamCalculation { /** * Maps dependent identifiers to their dependencies. * - * The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__. + * The initial identifier is .__GLOBAL__. * It depends on all the functions declared in the module, all the global scopes * of its imports, and any identifiers referenced in this global scope. Each * function depends on all the identifiers referenced internally. @@ -74,10 +83,10 @@ function parseOnlyRamCalculate(otherScripts: Map, code: const completedParses = new Set(); // Scripts we've discovered that need to be parsed. - const parseQueue: string[] = []; + const parseQueue: ScriptFilePath[] = []; // Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap. - function parseCode(code: string, moduleName: string): void { - const result = parseOnlyCalculateDeps(code, moduleName); + function parseCode(code: string, moduleName: ScriptFilePath): void { + const result = parseOnlyCalculateDeps(code, moduleName, ns1); completedParses.add(moduleName); // Add any additional modules to the parse queue; @@ -92,7 +101,7 @@ function parseOnlyRamCalculate(otherScripts: Map, code: } // Parse the initial module, which is the "main" script that is being run - const initialModule = "__SPECIAL_INITIAL_MODULE__"; + const initialModule = scriptname; parseCode(code, initialModule); // Process additional modules, which occurs if the "main" script has any imports @@ -101,21 +110,19 @@ function parseOnlyRamCalculate(otherScripts: Map, code: if (nextModule === undefined) throw new Error("nextModule should not be undefined"); if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; - // Using root as the path base right now. Difficult to implement - const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js"); - if (!filename) { - return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `Invalid import path: "${nextModule}"` }; - } - const script = otherScripts.get(filename); + const script = otherScripts.get(nextModule); if (!script) { - return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `No such file on server: "${filename}"` }; + return { + errorCode: RamCalculationErrorCode.ImportError, + errorMessage: `File: "${nextModule}" not found on server: ${server}`, + }; } parseCode(script.code, nextModule); } // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan - // are those that start with __SPECIAL_INITIAL_MODULE__. + // are those that start with the name of the main script. let ram = RamCostConstants.Base; const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }]; const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule)); @@ -250,7 +257,7 @@ export function checkInfiniteLoop(code: string): number[] { interface ParseDepsResult { dependencyMap: Record | undefined>; - additionalModules: string[]; + additionalModules: ScriptFilePath[]; } /** @@ -259,7 +266,7 @@ interface ParseDepsResult { * for RAM usage calculations. It also returns an array of additional modules * that need to be parsed (i.e. are 'import'ed scripts). */ -function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsResult { +function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1?: boolean): ParseDepsResult { const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" }); // Everything from the global scope goes in ".". Everything else goes in ".function", where only // the outermost layer of functions counts. @@ -271,7 +278,7 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR // Filled when we import names from other modules. const internalToExternal: Record = {}; - const additionalModules: string[] = []; + const additionalModules: ScriptFilePath[] = []; // References get added pessimistically. They are added for thisModule.name, name, and for // any aliases. @@ -338,7 +345,12 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR Object.assign( { ImportDeclaration: (node: Node, st: State) => { - const importModuleName = node.source.value; + const importModuleName = resolveScriptFilePath(node.source.value, currentModule, ns1 ? ".script" : ".js"); + if (!importModuleName) + throw new Error( + `ScriptFilePath couldnt be resolved in ImportDeclaration. Value: ${node.source.value} ScriptFilePath: ${currentModule}`, + ); + additionalModules.push(importModuleName); // This module's global scope refers to that module's global scope, no matter how we @@ -397,16 +409,21 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR /** * Calculate's a scripts RAM Usage * @param {string} code - The script's code + * @param {ScriptFilePath} scriptname - The script's name. Used to resolve relative paths * @param {Script[]} otherScripts - All other scripts on the server. * Used to account for imported scripts + * @param {ServerName} server - Servername of the scripts for Error Message + * @param {boolean} ns1 - Deprecated: is the fileExtension .script or .js */ export function calculateRamUsage( code: string, + scriptname: ScriptFilePath, otherScripts: Map, + server: ServerName, ns1?: boolean, ): RamCalculation { try { - return parseOnlyRamCalculate(otherScripts, code, ns1); + return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1); } catch (e) { return { errorCode: RamCalculationErrorCode.SyntaxError, diff --git a/src/Script/Script.ts b/src/Script/Script.ts index 9e6139f0b..45384c9c6 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -73,7 +73,13 @@ export class Script implements ContentFile { * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports */ updateRamUsage(otherScripts: Map): void { - const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script")); + const ramCalc = calculateRamUsage( + this.code, + this.filename, + otherScripts, + this.server, + this.filename.endsWith(".script"), + ); if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) { this.ramUsage = roundToTwo(ramCalc.cost); this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; diff --git a/src/ScriptEditor/ui/ScriptEditorContext.tsx b/src/ScriptEditor/ui/ScriptEditorContext.tsx index e23465ca3..9453cc214 100644 --- a/src/ScriptEditor/ui/ScriptEditorContext.tsx +++ b/src/ScriptEditor/ui/ScriptEditorContext.tsx @@ -8,11 +8,13 @@ import { useBoolean } from "../../ui/React/hooks"; import { BaseServer } from "../../Server/BaseServer"; import { Options } from "./Options"; +import { FilePath } from "../../Paths/FilePath"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; export interface ScriptEditorContextShape { ram: string; ramEntries: string[][]; - updateRAM: (newCode: string | null, server: BaseServer | null) => void; + updateRAM: (newCode: string | null, filename: FilePath | null, server: BaseServer | null) => void; isUpdatingRAM: boolean; startUpdatingRAM: () => void; @@ -28,14 +30,13 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React const [ram, setRAM] = useState("RAM: ???"); const [ramEntries, setRamEntries] = useState([["???", ""]]); - const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, server) => { - if (newCode === null || server === null) { + const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, filename, server) => { + if (newCode == null || filename == null || server == null || !hasScriptExtension(filename)) { setRAM("N/A"); setRamEntries([["N/A", ""]]); return; } - - const ramUsage = calculateRamUsage(newCode, server.scripts); + const ramUsage = calculateRamUsage(newCode, filename, server.scripts, server.hostname); if (ramUsage.cost && ramUsage.cost > 0) { const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? []; const entriesDisp = []; diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 5e0cc21ad..202599703 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -143,6 +143,7 @@ function Root(props: IProps): React.ReactElement { infLoop(newCode); updateRAM( !currentScript || currentScript.isTxt ? null : newCode, + currentScript && currentScript.path, currentScript && GetServer(currentScript.hostname), ); finishUpdatingRAM(); diff --git a/test/jest/Netscript/RamCalculation.test.ts b/test/jest/Netscript/RamCalculation.test.ts index 452ae036e..e505ac233 100644 --- a/test/jest/Netscript/RamCalculation.test.ts +++ b/test/jest/Netscript/RamCalculation.test.ts @@ -9,6 +9,7 @@ import { calculateRamUsage } from "../../../src/Script/RamCalculations"; import { ns } from "../../../src/NetscriptFunctions"; import { InternalAPI } from "src/Netscript/APIWrapper"; import { Singularity } from "@nsdefs"; +import { ScriptFilePath } from "src/Paths/ScriptFilePath"; type PotentiallyAsyncFunction = (arg?: unknown) => { catch?: PotentiallyAsyncFunction }; @@ -71,13 +72,15 @@ describe("Netscript RAM Calculation/Generation Tests", function () { extraLayerCost = 0, ) { const code = `${fnPath.join(".")}();\n`.repeat(3); + const filename = "testfile.js" as ScriptFilePath; const fnName = fnPath[fnPath.length - 1]; + const server = "testserver"; //check imported getRamCost fn vs. expected ram from test expect(getRamCost(fnPath, true)).toEqual(expectedRamCost); // Static ram check - const staticCost = calculateRamUsage(code, new Map()).cost; + const staticCost = calculateRamUsage(code, filename, new Map(), server).cost; expect(staticCost).toBeCloseTo(Math.min(baseCost + expectedRamCost + extraLayerCost, maxCost)); // reset workerScript for dynamic check diff --git a/test/jest/Netscript/StaticRamParsingCalculation.test.ts b/test/jest/Netscript/StaticRamParsingCalculation.test.ts index db1d83fac..ebe3a0997 100644 --- a/test/jest/Netscript/StaticRamParsingCalculation.test.ts +++ b/test/jest/Netscript/StaticRamParsingCalculation.test.ts @@ -10,6 +10,10 @@ const GrowCost = 0.15; const SleeveGetTaskCost = 4; const HacknetCost = 4; const MaxCost = 1024; + +const filename = "testfile.js" as ScriptFilePath; +const folderFilename = "test/testfile.js" as ScriptFilePath; +const server = "testserver"; describe("Parsing NetScript code to work out static RAM costs", function () { jest.spyOn(console, "error").mockImplementation(() => {}); /** Tests numeric equality, allowing for floating point imprecision - and includes script base cost */ @@ -24,7 +28,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const code = ` export async function main(ns) { } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, 0); }); @@ -34,7 +38,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.print("Slum snakes r00l!"); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, 0); }); @@ -44,7 +48,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost); }); @@ -54,7 +58,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await X.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost); }); @@ -65,7 +69,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost); }); @@ -76,7 +80,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.grow("joesguns"); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost + GrowCost); }); @@ -89,7 +93,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost); }); @@ -104,7 +108,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { async doHacking() { await this.ns.hack("joesguns"); } } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost); }); @@ -119,7 +123,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { async doHacking() { await this.#ns.hack("joesguns"); } } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HackCost); }); }); @@ -132,7 +136,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function get() { return 0; } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, 0); }); @@ -143,7 +147,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function purchaseNode() { return 0; } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; // Works at present, because the parser checks the namespace only, not the function name expectCost(calculated, 0); }); @@ -156,7 +160,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function getTask() { return 0; } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, 0); }); }); @@ -168,7 +172,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.hacknet.purchaseNode(0); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, HacknetCost); }); @@ -178,7 +182,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.sleeve.getTask(3); } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, SleeveGetTaskCost); }); }); @@ -196,7 +200,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () { dummy(); } `; - const calculated = calculateRamUsage(code, new Map([["libTest.js" as ScriptFilePath, lib]])).cost; + const calculated = calculateRamUsage( + code, + filename, + new Map([["libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; expectCost(calculated, 0); }); @@ -212,7 +221,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await doHack(ns); } `; - const calculated = calculateRamUsage(code, new Map([["libTest.js" as ScriptFilePath, lib]])).cost; + const calculated = calculateRamUsage( + code, + filename, + new Map([["libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; expectCost(calculated, HackCost); }); @@ -229,7 +243,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await doHack(ns); } `; - const calculated = calculateRamUsage(code, new Map([["libTest.js" as ScriptFilePath, lib]])).cost; + const calculated = calculateRamUsage( + code, + filename, + new Map([["libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; expectCost(calculated, HackCost); }); @@ -246,7 +265,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await test.doHack(ns); } `; - const calculated = calculateRamUsage(code, new Map([["libTest.js" as ScriptFilePath, lib]])).cost; + const calculated = calculateRamUsage( + code, + filename, + new Map([["libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; expectCost(calculated, HackCost + GrowCost); }); @@ -267,7 +291,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ${lines.join("\n")}; } `; - const calculated = calculateRamUsage(code, new Map()).cost; + const calculated = calculateRamUsage(code, filename, new Map(), server).cost; expectCost(calculated, MaxCost); }); @@ -289,7 +313,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await test.doHack(ns); } `; - const calculated = calculateRamUsage(code, new Map([["libTest.js" as ScriptFilePath, lib]])).cost; + const calculated = calculateRamUsage( + code, + filename, + new Map([["libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; expectCost(calculated, HackCost); }); @@ -315,8 +344,119 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await growerInstance.doGrow(); } `; - const calculated = calculateRamUsage(code, new Map([["libTest.js" as ScriptFilePath, lib]])).cost; + const calculated = calculateRamUsage( + code, + filename, + new Map([["libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; expectCost(calculated, GrowCost); }); + + it("Importing with a relative path - One Layer Deep", async function () { + const libCode = ` + export async function testRelative(ns) { + await ns.hack("n00dles") + } + `; + const lib = new Script("test/libTest.js" as ScriptFilePath, libCode); + const code = ` + import { testRelative } from "./libTest"; + + export async function main(ns) { + await testRelative(ns) + } + `; + const calculated = calculateRamUsage( + code, + folderFilename, + new Map([["test/libTest.js" as ScriptFilePath, lib]]), + server, + ).cost; + expectCost(calculated, HackCost); + }); + it("Importing with a relative path - Two Layer Deep", async function () { + const libNameOne = "test/libTestOne.js" as ScriptFilePath; + const libNameTwo = "test/libTestTwo.js" as ScriptFilePath; + + const libCodeOne = ` + import { testRelativeAgain } from "./libTestTwo"; + export function testRelative(ns) { + return testRelativeAgain(ns) + } + `; + const libScriptOne = new Script(libNameOne, libCodeOne); + + const libCodeTwo = ` + export function testRelativeAgain(ns) { + return ns.hack("n00dles") + } + `; + const libScriptTwo = new Script(libNameTwo, libCodeTwo); + + const code = ` + import { testRelative } from "./libTestOne"; + + export async function main(ns) { + await testRelative(ns) + } + `; + const calculated = calculateRamUsage( + code, + folderFilename, + new Map([ + [libNameOne, libScriptOne], + [libNameTwo, libScriptTwo], + ]), + server, + ).cost; + expectCost(calculated, HackCost); + }); + it("Importing with a relative path - possible path conflict", async function () { + const libNameOne = "foo/libTestOne.js" as ScriptFilePath; + const libNameTwo = "foo/libTestTwo.js" as ScriptFilePath; + const incorrect_libNameTwo = "test/libTestTwo.js" as ScriptFilePath; + + const libCodeOne = ` + import { testRelativeAgain } from "./libTestTwo"; + export function testRelative(ns) { + return testRelativeAgain(ns) + } + `; + const libScriptOne = new Script(libNameOne, libCodeOne); + + const libCodeTwo = ` + export function testRelativeAgain(ns) { + return ns.hack("n00dles") + } + `; + const libScriptTwo = new Script(libNameTwo, libCodeTwo); + + const incorrect_libCodeTwo = ` + export function testRelativeAgain(ns) { + return ns.grow("n00dles") + } + `; + const incorrect_libScriptTwo = new Script(incorrect_libNameTwo, incorrect_libCodeTwo); + + const code = ` + import { testRelative } from "foo/libTestOne"; + + export async function main(ns) { + await testRelative(ns) + } + `; + const calculated = calculateRamUsage( + code, + folderFilename, + new Map([ + [libNameOne, libScriptOne], + [libNameTwo, libScriptTwo], + [incorrect_libNameTwo, incorrect_libScriptTwo], + ]), + server, + ).cost; + expectCost(calculated, HackCost); + }); }); });