2019-05-17 15:51:28 -07:00
|
|
|
/**
|
|
|
|
* Implements RAM Calculation functionality.
|
|
|
|
*
|
|
|
|
* Uses the acorn.js library to parse a script's code into an AST and
|
|
|
|
* recursively walk through that AST, calculating RAM usage along
|
|
|
|
* the way
|
|
|
|
*/
|
2019-05-06 18:01:06 -07:00
|
|
|
import * as walk from "acorn-walk";
|
2021-10-28 23:04:26 -04:00
|
|
|
import acorn, { parse } from "acorn";
|
2019-03-04 17:40:28 -08:00
|
|
|
|
2019-05-11 19:20:20 -07:00
|
|
|
import { RamCalculationErrorCode } from "./RamCalculationErrorCodes";
|
|
|
|
|
2019-05-04 21:03:40 -07:00
|
|
|
import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
|
2022-03-06 05:05:55 +01:00
|
|
|
import { Script } from "./Script";
|
2022-07-15 23:34:27 -04:00
|
|
|
import { Node } from "../NetscriptJSEvaluator";
|
FILES: Path rework & typesafety (#479)
* Added new types for various file paths, all in the Paths folder.
* TypeSafety and other helper functions related to these types
* Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands
* Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way
* Server.textFiles is now a map
* TextFile no longer uses a fn property, now it is filename
* Added a shared ContentFile interface for shared functionality between TextFile and Script.
* related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa.
* File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root.
* Singularized the MessageFilename and LiteratureName enums
* Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath).
* Fix several issues with tab completion, which included pretty much a complete rewrite
* Changed the autocomplete display options so there's less chance it clips outside the display area.
* Turned CompletedProgramName into an enum.
* Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine.
* For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 10:26:57 -04:00
|
|
|
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
2024-06-03 02:38:01 +02:00
|
|
|
import { ServerName } from "../Types/strings";
|
2019-03-04 17:40:28 -08:00
|
|
|
|
2022-01-05 16:41:48 -05:00
|
|
|
export interface RamUsageEntry {
|
2022-03-27 15:53:13 +08:00
|
|
|
type: "ns" | "dom" | "fn" | "misc";
|
2022-01-05 16:41:48 -05:00
|
|
|
name: string;
|
|
|
|
cost: number;
|
|
|
|
}
|
|
|
|
|
2023-06-19 09:49:32 +02:00
|
|
|
export type RamCalculationSuccess = {
|
2022-01-05 16:41:48 -05:00
|
|
|
cost: number;
|
2023-06-19 09:49:32 +02:00
|
|
|
entries: RamUsageEntry[];
|
|
|
|
errorCode?: never;
|
|
|
|
errorMessage?: never;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type RamCalculationFailure = {
|
|
|
|
cost?: never;
|
|
|
|
entries?: never;
|
|
|
|
errorCode: RamCalculationErrorCode;
|
|
|
|
errorMessage?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type RamCalculation = RamCalculationSuccess | RamCalculationFailure;
|
2022-01-05 16:41:48 -05:00
|
|
|
|
2019-03-04 17:40:28 -08:00
|
|
|
// These special strings are used to reference the presence of a given logical
|
|
|
|
// construct within a user script.
|
|
|
|
const specialReferenceIF = "__SPECIAL_referenceIf";
|
|
|
|
const specialReferenceFOR = "__SPECIAL_referenceFor";
|
|
|
|
const specialReferenceWHILE = "__SPECIAL_referenceWhile";
|
|
|
|
|
|
|
|
// The global scope of a script is registered under this key during parsing.
|
|
|
|
const memCheckGlobalKey = ".__GLOBAL__";
|
|
|
|
|
2023-05-05 03:55:59 -04:00
|
|
|
/** Function for getting a function's ram cost, either from the ramcost function (singularity) or the static cost */
|
|
|
|
function getNumericCost(cost: number | (() => number)): number {
|
|
|
|
return typeof cost === "function" ? cost() : cost;
|
|
|
|
}
|
|
|
|
|
2019-05-17 15:51:28 -07:00
|
|
|
/**
|
|
|
|
* Parses code into an AST and walks through it recursively to calculate
|
|
|
|
* RAM usage. Also accounts for imported modules.
|
FILES: Path rework & typesafety (#479)
* Added new types for various file paths, all in the Paths folder.
* TypeSafety and other helper functions related to these types
* Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands
* Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way
* Server.textFiles is now a map
* TextFile no longer uses a fn property, now it is filename
* Added a shared ContentFile interface for shared functionality between TextFile and Script.
* related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa.
* File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root.
* Singularized the MessageFilename and LiteratureName enums
* Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath).
* Fix several issues with tab completion, which included pretty much a complete rewrite
* Changed the autocomplete display options so there's less chance it clips outside the display area.
* Turned CompletedProgramName into an enum.
* Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine.
* For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 10:26:57 -04:00
|
|
|
* @param otherScripts - All other scripts on the server. Used to account for imported scripts
|
2024-06-03 02:38:01 +02:00
|
|
|
* @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 {
|
2023-05-05 03:55:59 -04:00
|
|
|
/**
|
|
|
|
* Maps dependent identifiers to their dependencies.
|
|
|
|
*
|
2024-06-03 02:38:01 +02:00
|
|
|
* The initial identifier is <name of the main script>.__GLOBAL__.
|
2023-05-05 03:55:59 -04:00
|
|
|
* 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.
|
|
|
|
* We walk the dependency graph to calculate RAM usage, given that some identifiers
|
|
|
|
* reference Netscript functions which have a RAM cost.
|
|
|
|
*/
|
|
|
|
let dependencyMap: Record<string, string[]> = {};
|
|
|
|
|
|
|
|
// Scripts we've parsed.
|
|
|
|
const completedParses = new Set();
|
|
|
|
|
|
|
|
// Scripts we've discovered that need to be parsed.
|
2024-06-03 02:38:01 +02:00
|
|
|
const parseQueue: ScriptFilePath[] = [];
|
2023-05-05 03:55:59 -04:00
|
|
|
// Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap.
|
2024-06-03 02:38:01 +02:00
|
|
|
function parseCode(code: string, moduleName: ScriptFilePath): void {
|
|
|
|
const result = parseOnlyCalculateDeps(code, moduleName, ns1);
|
2023-05-05 03:55:59 -04:00
|
|
|
completedParses.add(moduleName);
|
|
|
|
|
|
|
|
// Add any additional modules to the parse queue;
|
|
|
|
for (let i = 0; i < result.additionalModules.length; ++i) {
|
|
|
|
if (!completedParses.has(result.additionalModules[i])) {
|
|
|
|
parseQueue.push(result.additionalModules[i]);
|
2021-09-04 19:09:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-05 03:55:59 -04:00
|
|
|
// Splice all the references in
|
|
|
|
dependencyMap = Object.assign(dependencyMap, result.dependencyMap);
|
|
|
|
}
|
|
|
|
|
2023-06-19 09:49:32 +02:00
|
|
|
// Parse the initial module, which is the "main" script that is being run
|
2024-06-03 02:38:01 +02:00
|
|
|
const initialModule = scriptname;
|
2023-06-19 09:49:32 +02:00
|
|
|
parseCode(code, initialModule);
|
|
|
|
|
|
|
|
// Process additional modules, which occurs if the "main" script has any imports
|
|
|
|
while (parseQueue.length > 0) {
|
|
|
|
const nextModule = parseQueue.shift();
|
|
|
|
if (nextModule === undefined) throw new Error("nextModule should not be undefined");
|
|
|
|
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue;
|
|
|
|
|
2024-06-03 02:38:01 +02:00
|
|
|
const script = otherScripts.get(nextModule);
|
2023-06-19 09:49:32 +02:00
|
|
|
if (!script) {
|
2024-06-03 02:38:01 +02:00
|
|
|
return {
|
|
|
|
errorCode: RamCalculationErrorCode.ImportError,
|
|
|
|
errorMessage: `File: "${nextModule}" not found on server: ${server}`,
|
|
|
|
};
|
2021-09-04 19:09:30 -04:00
|
|
|
}
|
|
|
|
|
2023-06-19 09:49:32 +02:00
|
|
|
parseCode(script.code, nextModule);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
|
2024-06-03 02:38:01 +02:00
|
|
|
// are those that start with the name of the main script.
|
2023-06-19 09:49:32 +02:00
|
|
|
let ram = RamCostConstants.Base;
|
|
|
|
const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }];
|
|
|
|
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
|
|
|
|
const resolvedRefs = new Set();
|
|
|
|
const loadedFns: Record<string, boolean> = {};
|
|
|
|
while (unresolvedRefs.length > 0) {
|
|
|
|
const ref = unresolvedRefs.shift();
|
|
|
|
if (ref === undefined) throw new Error("ref should not be undefined");
|
|
|
|
|
|
|
|
// Check if this is one of the special keys, and add the appropriate ram cost if so.
|
|
|
|
if (ref === "hacknet" && !resolvedRefs.has("hacknet")) {
|
|
|
|
ram += RamCostConstants.HacknetNodes;
|
|
|
|
detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.HacknetNodes });
|
|
|
|
}
|
|
|
|
if (ref === "document" && !resolvedRefs.has("document")) {
|
|
|
|
ram += RamCostConstants.Dom;
|
|
|
|
detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.Dom });
|
|
|
|
}
|
|
|
|
if (ref === "window" && !resolvedRefs.has("window")) {
|
|
|
|
ram += RamCostConstants.Dom;
|
|
|
|
detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.Dom });
|
|
|
|
}
|
2021-09-04 19:09:30 -04:00
|
|
|
|
2023-06-19 09:49:32 +02:00
|
|
|
resolvedRefs.add(ref);
|
2021-09-04 19:09:30 -04:00
|
|
|
|
2023-06-19 09:49:32 +02:00
|
|
|
if (ref.endsWith(".*")) {
|
|
|
|
// A prefix reference. We need to find all matching identifiers.
|
|
|
|
const prefix = ref.slice(0, ref.length - 2);
|
|
|
|
for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) {
|
|
|
|
for (const dep of dependencyMap[ident] || []) {
|
2021-09-04 19:09:30 -04:00
|
|
|
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
|
|
|
|
}
|
|
|
|
}
|
2023-06-19 09:49:32 +02:00
|
|
|
} else {
|
|
|
|
// An exact reference. Add all dependencies of this ref.
|
|
|
|
for (const dep of dependencyMap[ref] || []) {
|
|
|
|
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
|
|
|
|
}
|
|
|
|
}
|
2019-03-04 17:40:28 -08:00
|
|
|
|
2023-06-19 09:49:32 +02:00
|
|
|
// Check if this identifier is a function in the workerScript environment.
|
|
|
|
// If it is, then we need to get its RAM cost.
|
|
|
|
try {
|
|
|
|
// Only count each function once
|
|
|
|
if (loadedFns[ref]) {
|
2021-09-04 19:09:30 -04:00
|
|
|
continue;
|
|
|
|
}
|
2023-06-19 09:49:32 +02:00
|
|
|
loadedFns[ref] = true;
|
|
|
|
|
|
|
|
// This accounts for namespaces (Bladeburner, CodingContract, etc.)
|
|
|
|
const findFunc = (
|
|
|
|
prefix: string,
|
|
|
|
obj: object,
|
|
|
|
ref: string,
|
|
|
|
): { func: () => number | number; refDetail: string } | undefined => {
|
|
|
|
if (!obj) return;
|
|
|
|
const elem = Object.entries(obj).find(([key]) => key === ref);
|
|
|
|
if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) {
|
|
|
|
return { func: elem[1], refDetail: `${prefix}${ref}` };
|
|
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
|
|
const found = findFunc(`${key}.`, value, ref);
|
|
|
|
if (found) return found;
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
const details = findFunc("", RamCosts, ref);
|
|
|
|
const fnRam = getNumericCost(details?.func ?? 0);
|
|
|
|
ram += fnRam;
|
|
|
|
detailedCosts.push({ type: "fn", name: details?.refDetail ?? "", cost: fnRam });
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
continue;
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|
2021-09-04 19:09:30 -04:00
|
|
|
}
|
2023-06-19 09:49:32 +02:00
|
|
|
if (ram > RamCostConstants.Max) {
|
|
|
|
ram = RamCostConstants.Max;
|
|
|
|
detailedCosts.push({ type: "misc", name: "Max Ram Cap", cost: RamCostConstants.Max });
|
|
|
|
}
|
|
|
|
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|
|
|
|
|
2024-05-23 09:44:15 +02:00
|
|
|
export function checkInfiniteLoop(code: string): number[] {
|
2023-05-16 11:27:11 -04:00
|
|
|
let ast: acorn.Node;
|
|
|
|
try {
|
|
|
|
ast = parse(code, { sourceType: "module", ecmaVersion: "latest" });
|
|
|
|
} catch (e) {
|
|
|
|
// If code cannot be parsed, do not provide infinite loop detection warning
|
2024-05-23 09:44:15 +02:00
|
|
|
return [];
|
2023-05-16 11:27:11 -04:00
|
|
|
}
|
2021-10-28 23:04:26 -04:00
|
|
|
function nodeHasTrueTest(node: acorn.Node): boolean {
|
2024-05-23 09:44:15 +02:00
|
|
|
return node.type === "Literal" && "raw" in node && (node.raw === "true" || node.raw === "1");
|
2021-10-28 23:04:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function hasAwait(ast: acorn.Node): boolean {
|
|
|
|
let hasAwait = false;
|
|
|
|
walk.recursive(
|
|
|
|
ast,
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
AwaitExpression: () => {
|
|
|
|
hasAwait = true;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return hasAwait;
|
|
|
|
}
|
|
|
|
|
2024-05-23 09:44:15 +02:00
|
|
|
const possibleLines: number[] = [];
|
2021-10-28 23:04:26 -04:00
|
|
|
walk.recursive(
|
|
|
|
ast,
|
|
|
|
{},
|
|
|
|
{
|
2022-07-15 23:34:27 -04:00
|
|
|
WhileStatement: (node: Node, st: unknown, walkDeeper: walk.WalkerCallback<any>) => {
|
2024-05-23 09:44:15 +02:00
|
|
|
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/)) {
|
|
|
|
return;
|
|
|
|
}
|
2022-07-15 23:34:27 -04:00
|
|
|
if (nodeHasTrueTest(node.test) && !hasAwait(node)) {
|
2024-05-23 09:44:15 +02:00
|
|
|
possibleLines.push(lineNumber);
|
2021-10-28 23:04:26 -04:00
|
|
|
} else {
|
2022-07-15 23:34:27 -04:00
|
|
|
node.body && walkDeeper(node.body, st);
|
2021-10-28 23:04:26 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-05-23 09:44:15 +02:00
|
|
|
return possibleLines;
|
2021-10-28 23:04:26 -04:00
|
|
|
}
|
|
|
|
|
2022-07-15 23:34:27 -04:00
|
|
|
interface ParseDepsResult {
|
2023-05-05 03:55:59 -04:00
|
|
|
dependencyMap: Record<string, Set<string> | undefined>;
|
2024-06-03 02:38:01 +02:00
|
|
|
additionalModules: ScriptFilePath[];
|
2022-07-15 23:34:27 -04:00
|
|
|
}
|
|
|
|
|
2019-05-17 15:51:28 -07:00
|
|
|
/**
|
|
|
|
* Helper function that parses a single script. It returns a map of all dependencies,
|
|
|
|
* which are items in the code's AST that potentially need to be evaluated
|
|
|
|
* for RAM usage calculations. It also returns an array of additional modules
|
|
|
|
* that need to be parsed (i.e. are 'import'ed scripts).
|
|
|
|
*/
|
2024-06-03 02:38:01 +02:00
|
|
|
function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1?: boolean): ParseDepsResult {
|
2021-09-04 19:09:30 -04:00
|
|
|
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.
|
|
|
|
const globalKey = currentModule + memCheckGlobalKey;
|
2023-05-05 03:55:59 -04:00
|
|
|
const dependencyMap: Record<string, Set<string> | undefined> = {};
|
2021-09-24 18:13:20 -04:00
|
|
|
dependencyMap[globalKey] = new Set<string>();
|
2021-09-04 19:09:30 -04:00
|
|
|
|
|
|
|
// If we reference this internal name, we're really referencing that external name.
|
|
|
|
// Filled when we import names from other modules.
|
2023-05-05 03:55:59 -04:00
|
|
|
const internalToExternal: Record<string, string | undefined> = {};
|
2021-09-04 19:09:30 -04:00
|
|
|
|
2024-06-03 02:38:01 +02:00
|
|
|
const additionalModules: ScriptFilePath[] = [];
|
2021-09-04 19:09:30 -04:00
|
|
|
|
|
|
|
// References get added pessimistically. They are added for thisModule.name, name, and for
|
|
|
|
// any aliases.
|
2023-07-11 20:09:23 +02:00
|
|
|
function addRef(key: string, name: string, module = currentModule): void {
|
2021-09-04 19:09:30 -04:00
|
|
|
const s = dependencyMap[key] || (dependencyMap[key] = new Set());
|
2021-09-24 18:13:20 -04:00
|
|
|
const external = internalToExternal[name];
|
|
|
|
if (external !== undefined) {
|
|
|
|
s.add(external);
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|
2023-07-11 20:09:23 +02:00
|
|
|
s.add(module + "." + name);
|
2021-09-04 19:09:30 -04:00
|
|
|
s.add(name); // For builtins like hack.
|
|
|
|
}
|
|
|
|
|
|
|
|
//A list of identifiers that resolve to "native Javascript code"
|
2021-09-08 23:47:34 -04:00
|
|
|
const objectPrototypeProperties = Object.getOwnPropertyNames(Object.prototype);
|
2021-09-04 19:09:30 -04:00
|
|
|
|
2022-07-15 23:34:27 -04:00
|
|
|
interface State {
|
|
|
|
key: string;
|
|
|
|
}
|
|
|
|
|
2021-09-04 19:09:30 -04:00
|
|
|
// If we discover a dependency identifier, state.key is the dependent identifier.
|
|
|
|
// walkDeeper is for doing recursive walks of expressions in composites that we handle.
|
2022-07-15 23:34:27 -04:00
|
|
|
function commonVisitors(): walk.RecursiveVisitors<State> {
|
2021-09-04 19:09:30 -04:00
|
|
|
return {
|
2022-07-15 23:34:27 -04:00
|
|
|
Identifier: (node: Node, st: State) => {
|
2021-09-04 19:09:30 -04:00
|
|
|
if (objectPrototypeProperties.includes(node.name)) {
|
|
|
|
return;
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|
2021-09-04 19:09:30 -04:00
|
|
|
addRef(st.key, node.name);
|
|
|
|
},
|
2022-07-15 23:34:27 -04:00
|
|
|
WhileStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
|
2021-09-04 19:09:30 -04:00
|
|
|
addRef(st.key, specialReferenceWHILE);
|
|
|
|
node.test && walkDeeper(node.test, st);
|
|
|
|
node.body && walkDeeper(node.body, st);
|
|
|
|
},
|
2022-07-15 23:34:27 -04:00
|
|
|
DoWhileStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
|
2021-09-04 19:09:30 -04:00
|
|
|
addRef(st.key, specialReferenceWHILE);
|
|
|
|
node.test && walkDeeper(node.test, st);
|
|
|
|
node.body && walkDeeper(node.body, st);
|
|
|
|
},
|
2022-07-15 23:34:27 -04:00
|
|
|
ForStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
|
2021-09-04 19:09:30 -04:00
|
|
|
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);
|
|
|
|
},
|
2022-07-15 23:34:27 -04:00
|
|
|
IfStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
|
2021-09-04 19:09:30 -04:00
|
|
|
addRef(st.key, specialReferenceIF);
|
|
|
|
node.test && walkDeeper(node.test, st);
|
|
|
|
node.consequent && walkDeeper(node.consequent, st);
|
|
|
|
node.alternate && walkDeeper(node.alternate, st);
|
|
|
|
},
|
2022-07-15 23:34:27 -04:00
|
|
|
MemberExpression: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
|
2021-09-04 19:09:30 -04:00
|
|
|
node.object && walkDeeper(node.object, st);
|
|
|
|
node.property && walkDeeper(node.property, st);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-07-15 23:34:27 -04:00
|
|
|
walk.recursive<State>(
|
2021-09-04 19:09:30 -04:00
|
|
|
ast,
|
|
|
|
{ key: globalKey },
|
|
|
|
Object.assign(
|
|
|
|
{
|
2022-07-15 23:34:27 -04:00
|
|
|
ImportDeclaration: (node: Node, st: State) => {
|
2024-06-03 02:38:01 +02:00
|
|
|
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}`,
|
|
|
|
);
|
|
|
|
|
2021-09-04 19:09:30 -04:00
|
|
|
additionalModules.push(importModuleName);
|
|
|
|
|
|
|
|
// This module's global scope refers to that module's global scope, no matter how we
|
|
|
|
// import it.
|
2021-09-24 18:13:20 -04:00
|
|
|
const set = dependencyMap[st.key];
|
|
|
|
if (set === undefined) throw new Error("set should not be undefined");
|
|
|
|
set.add(importModuleName + memCheckGlobalKey);
|
2021-09-04 19:09:30 -04:00
|
|
|
|
|
|
|
for (let i = 0; i < node.specifiers.length; ++i) {
|
|
|
|
const spec = node.specifiers[i];
|
|
|
|
if (spec.imported !== undefined && spec.local !== undefined) {
|
|
|
|
// We depend on specific things.
|
2021-09-08 23:47:34 -04:00
|
|
|
internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name;
|
2021-09-04 19:09:30 -04:00
|
|
|
} else {
|
|
|
|
// We depend on everything.
|
2021-09-24 18:13:20 -04:00
|
|
|
const set = dependencyMap[st.key];
|
|
|
|
if (set === undefined) throw new Error("set should not be undefined");
|
|
|
|
set.add(importModuleName + ".*");
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|
2021-09-04 19:09:30 -04:00
|
|
|
}
|
2019-03-04 17:40:28 -08:00
|
|
|
},
|
2022-07-15 23:34:27 -04:00
|
|
|
FunctionDeclaration: (node: Node) => {
|
2022-01-17 13:16:03 -05:00
|
|
|
// node.id will be null when using 'export default'. Add a module name indicating the default export.
|
|
|
|
const key = currentModule + "." + (node.id === null ? "__SPECIAL_DEFAULT_EXPORT__" : node.id.name);
|
2021-09-04 19:09:30 -04:00
|
|
|
walk.recursive(node, { key: key }, commonVisitors());
|
2019-03-04 17:40:28 -08:00
|
|
|
},
|
2023-07-09 14:08:43 +02:00
|
|
|
ExportNamedDeclaration: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-07-11 20:09:23 +02:00
|
|
|
for (const specifier of node.specifiers) {
|
|
|
|
const exportedDepName = currentModule + "." + specifier.exported.name;
|
|
|
|
|
|
|
|
if (node.source !== null) {
|
|
|
|
// 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
|
|
|
|
// if this is not true, we don't need to add anything
|
|
|
|
addRef(exportedDepName, specifier.local.name);
|
|
|
|
}
|
2023-07-09 14:08:43 +02:00
|
|
|
}
|
|
|
|
},
|
2021-09-04 19:09:30 -04:00
|
|
|
},
|
|
|
|
commonVisitors(),
|
|
|
|
),
|
|
|
|
);
|
2019-03-04 17:40:28 -08:00
|
|
|
|
2021-09-04 19:09:30 -04:00
|
|
|
return { dependencyMap: dependencyMap, additionalModules: additionalModules };
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|
|
|
|
|
2019-05-17 15:51:28 -07:00
|
|
|
/**
|
|
|
|
* Calculate's a scripts RAM Usage
|
2023-06-19 09:49:32 +02:00
|
|
|
* @param {string} code - The script's code
|
2024-06-03 02:38:01 +02:00
|
|
|
* @param {ScriptFilePath} scriptname - The script's name. Used to resolve relative paths
|
2019-05-17 15:51:28 -07:00
|
|
|
* @param {Script[]} otherScripts - All other scripts on the server.
|
|
|
|
* Used to account for imported scripts
|
2024-06-03 02:38:01 +02:00
|
|
|
* @param {ServerName} server - Servername of the scripts for Error Message
|
|
|
|
* @param {boolean} ns1 - Deprecated: is the fileExtension .script or .js
|
2019-05-17 15:51:28 -07:00
|
|
|
*/
|
2023-04-18 03:19:45 -04:00
|
|
|
export function calculateRamUsage(
|
2023-06-19 09:49:32 +02:00
|
|
|
code: string,
|
2024-06-03 02:38:01 +02:00
|
|
|
scriptname: ScriptFilePath,
|
FILES: Path rework & typesafety (#479)
* Added new types for various file paths, all in the Paths folder.
* TypeSafety and other helper functions related to these types
* Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands
* Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way
* Server.textFiles is now a map
* TextFile no longer uses a fn property, now it is filename
* Added a shared ContentFile interface for shared functionality between TextFile and Script.
* related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa.
* File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root.
* Singularized the MessageFilename and LiteratureName enums
* Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath).
* Fix several issues with tab completion, which included pretty much a complete rewrite
* Changed the autocomplete display options so there's less chance it clips outside the display area.
* Turned CompletedProgramName into an enum.
* Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine.
* For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 10:26:57 -04:00
|
|
|
otherScripts: Map<ScriptFilePath, Script>,
|
2024-06-03 02:38:01 +02:00
|
|
|
server: ServerName,
|
2023-04-18 03:19:45 -04:00
|
|
|
ns1?: boolean,
|
|
|
|
): RamCalculation {
|
2021-09-04 19:09:30 -04:00
|
|
|
try {
|
2024-06-03 02:38:01 +02:00
|
|
|
return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1);
|
2021-09-04 19:09:30 -04:00
|
|
|
} catch (e) {
|
2023-06-19 09:49:32 +02:00
|
|
|
return {
|
|
|
|
errorCode: RamCalculationErrorCode.SyntaxError,
|
|
|
|
errorMessage: e instanceof Error ? e.message : undefined,
|
|
|
|
};
|
2021-09-04 19:09:30 -04:00
|
|
|
}
|
2019-03-04 17:40:28 -08:00
|
|
|
}
|