BUGFIX: fix relative imports (#1305)

Relative paths work in imports, at last.
This commit is contained in:
Caldwell 2024-06-03 02:38:01 +02:00 committed by GitHub
parent 76ce2f9955
commit f40d4f8e92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 219 additions and 51 deletions

@ -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<ScriptFilePath, Scrip
let newCode = script.code;
// Loop through each node and replace the script name with a blob url.
for (const node of importNodes) {
const filename = resolveScriptFilePath(node.filename, root, ".js");
const filename = resolveScriptFilePath(node.filename, script.filename, ".js");
if (!filename) throw new Error(`Failed to parse import: ${node.filename}`);
// Find the corresponding script.
@ -149,6 +148,7 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
// script dedupe properly.
const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${script.filename}`;
// At this point we have the full code and can construct a new blob / assign the URL.
const url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL;
const module = config.doImport(url).catch((e) => {
script.invalidateModule();

@ -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<ScriptFilePath, Script>, 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<ScriptFilePath, Script>,
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 <name of the main script>.__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<ScriptFilePath, Script>, 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<ScriptFilePath, Script>, 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<ScriptFilePath, Script>, 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<string, Set<string> | 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<string, string | undefined> = {};
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<ScriptFilePath, Script>,
server: ServerName,
ns1?: boolean,
): RamCalculation {
try {
return parseOnlyRamCalculate(otherScripts, code, ns1);
return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1);
} catch (e) {
return {
errorCode: RamCalculationErrorCode.SyntaxError,

@ -73,7 +73,13 @@ export class Script implements ContentFile {
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/
updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): 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[];

@ -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<string[][]>([["???", ""]]);
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 = [];

@ -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();

@ -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

@ -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);
});
});
});