EDITOR: Improved infinite loop checking (#1276)

This commit is contained in:
G4mingJon4s 2024-05-23 09:44:15 +02:00 committed by GitHub
parent 7ed64cbc9c
commit 08eb60d21b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 34 additions and 13 deletions

@ -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.
<br />
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:

@ -199,16 +199,16 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, 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<any>) => {
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 {

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