diff --git a/src/Documentation/doc/basic/scripts.md b/src/Documentation/doc/basic/scripts.md index 2b2a70575..577333afc 100644 --- a/src/Documentation/doc/basic/scripts.md +++ b/src/Documentation/doc/basic/scripts.md @@ -62,6 +62,21 @@ For example, if a script run with 1 thread is able to hack \$10,000, then runnin When "multithreading" a script, the total [RAM](ram.md) cost can be calculated by simply multiplying the [RAM](ram.md) cost of a single instance of your script by the number of threads you will use. [See [`ns.getScriptRam()`](https://github.com/bitburner-official/bitburner-src/blob/bec737a25307be29c7efef147fc31effca65eedc/markdown/bitburner.ns.getscriptram.md) or the `mem` terminal command detailed below] +## Never-ending scripts + +Sometimes it might be necessary for a script to never end and keep doing a particular task. +In that case you would want to write your script in a never-ending loop, like `while (true)`. + +However, if you are not careful, this can crash your game. +If the code inside the loop doesn't `await` for some time, it will never give other scripts and the game itself time to process. + +
+ +To help you find this potential bug, any `while (true)` loop without any `await` statement inside it will be marked. +A red decoration will appear on the left side of the script editor, telling you about the issue. + +If you are really sure that this is not an oversight, you can suppress the warning using the comment `// @ignore-infinite` directly above the loop. + ## Working with Scripts in Terminal Here are some [terminal](terminal.md) commands you will find useful when working with scripts: diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index 394858581..1a5222f49 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -199,16 +199,16 @@ function parseOnlyRamCalculate(otherScripts: Map, code: return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) }; } -export function checkInfiniteLoop(code: string): number { +export function checkInfiniteLoop(code: string): number[] { 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 - return -1; + return []; } function nodeHasTrueTest(node: acorn.Node): boolean { - return node.type === "Literal" && "raw" in node && node.raw === "true"; + return node.type === "Literal" && "raw" in node && (node.raw === "true" || node.raw === "1"); } function hasAwait(ast: acorn.Node): boolean { @@ -225,14 +225,19 @@ export function checkInfiniteLoop(code: string): number { return hasAwait; } - let missingAwaitLine = -1; + const possibleLines: number[] = []; walk.recursive( ast, {}, { WhileStatement: (node: Node, 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/)) { + return; + } if (nodeHasTrueTest(node.test) && !hasAwait(node)) { - missingAwaitLine = (code.slice(0, node.start).match(/\n/g) || []).length + 1; + possibleLines.push(lineNumber); } else { node.body && walkDeeper(node.body, st); } @@ -240,7 +245,7 @@ export function checkInfiniteLoop(code: string): number { }, ); - return missingAwaitLine; + return possibleLines; } interface ParseDepsResult { diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 052d37ad0..6562dfe78 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -116,10 +116,10 @@ function Root(props: IProps): React.ReactElement { if (editorRef.current === null || currentScript === null) return; if (!decorations) decorations = editorRef.current.createDecorationsCollection(); if (!currentScript.path.endsWith(".js")) return; - const awaitWarning = checkInfiniteLoop(newCode); - if (awaitWarning !== -1) { - decorations.set([ - { + const possibleLines = checkInfiniteLoop(newCode); + if (possibleLines.length !== 0) { + decorations.set( + possibleLines.map((awaitWarning) => ({ range: { startLineNumber: awaitWarning, startColumn: 1, @@ -130,11 +130,12 @@ function Root(props: IProps): React.ReactElement { isWholeLine: true, glyphMarginClassName: "myGlyphMarginClass", glyphMarginHoverMessage: { - value: "Possible infinite loop, await something.", + value: + "Possible infinite loop, await something. If this is a false-positive, use `// @ignore-infinite` to suppress.", }, }, - }, - ]); + })), + ); } else decorations.clear(); }