Skip to content

Commit

Permalink
Merge branch 'main' into Zoom-Update
Browse files Browse the repository at this point in the history
  • Loading branch information
Aperaine authored Dec 20, 2024
2 parents 5a03962 + 324b64e commit ae65b5a
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 17 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ yarn-error.log
.env
dist/
games/metadata.json
public/*.json
public/*.json
firmware/spade/firmware.elf
firmware/spade/firmware.uf2
17 changes: 16 additions & 1 deletion src/components/big-interactive-pages/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { useEffect, useRef, useState} from "preact/hooks";
import { codeMirror, errorLog, isNewSaveStrat, muted, PersistenceState, RoomState, screenRef, cleanupRef } from "../../lib/state";
import EditorModal from "../popups-etc/editor-modal";
import { runGame } from "../../lib/engine";
import { runGame, _performSyntaxCheck } from "../../lib/engine";
import DraftWarningModal from "../popups-etc/draft-warning";
import { debounce } from "throttle-debounce";
import Help from "../popups-etc/help";
Expand All @@ -36,6 +36,15 @@ import { PersistenceStateKind } from "../../lib/state";

let screenShakeSignal: Signal<number> | null = null;

const performSyntaxCheck = () => {
const code = codeMirror.value?.state.doc.toString() ?? "";
const res = _performSyntaxCheck(code);
if (res.error) {
errorLog.value = [];
errorLog.value = [res.error];
}
}

export const onRun = async () => {
foldAllTemplateLiterals();
if (!screenRef.value) return;
Expand Down Expand Up @@ -262,6 +271,12 @@ export default function Editor({ persistenceState, cookies, roomState }: EditorP

const [sessionId] = useState(nanoid());

useEffect(() => {
setInterval(() => {
performSyntaxCheck();
}, 2000);
}, []);

useEffect(() => {
const channel = new BroadcastChannel('session_channel');
channel.onmessage = (event) => {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/custom-babel-transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,19 @@ export function BuildDuplicateFunctionDetector(engineApiKeys: string[]) {
}
}

}

export function dissallowBackticksInDoubleQuotes() {
return {
visitor: {
StringLiteral(path: any) {
const { value, extra } = path.node;
if (value.includes('`')) {
const loc = path.node.loc.start;
const quoteType = extra.raw[0];
throw path.buildCodeFrameError(`Backtick found within ${quoteType === '"' ? 'double' : 'single'}-quoted string at (${loc.line}:${loc.column})`);
}
}
}
}
}
44 changes: 44 additions & 0 deletions src/lib/engine/error.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
import { expect, test } from "vitest"
import TransformDetectInfiniteLoop, { BuildDuplicateFunctionDetector, dissallowBackticksInDoubleQuotes} from "../custom-babel-transforms";
import * as Babel from "@babel/standalone";
import { baseEngine } from "../../../engine/src/base";
import {normalizeGameError} from "./error"

// SyntaxErrors (not in evals) seem to go through babel (i.e. have an error.code), so they are not tested here

export function transformAndThrowErrors(code: string, engineAPIKeys: string[], runCb: (code: any) => any) {
try {
const transformedCode = Babel.transform(code, {
plugins: [TransformDetectInfiniteLoop, BuildDuplicateFunctionDetector(engineAPIKeys), dissallowBackticksInDoubleQuotes],
retainLines: true
});
runCb(transformedCode);
return null;
} catch (error: any) {
return normalizeGameError({ kind: "runtime", error });
}
}

test('detect infinite while loops', () => {
const code = 'while (true) {}'
const engine = baseEngine();
const res = transformAndThrowErrors(code, Object.keys(engine.api), (transformedCode) => {
const fn = new Function(transformedCode.code);
fn();
});

console.log(res?.description);
const expectedError = `RangeError: Potential infinite loop
at eval (/Volumes/Code/code/hackclub/sprig/src/lib/engine/error.test.ts:1:152)`;
expect(res?.description).toBe(expectedError);
});

test('detect infinite for loop', () => {
const code = 'for (;;) {}'
const engine = baseEngine();
const res = transformAndThrowErrors(code, Object.keys(engine.api), (transformedCode) => {
const fn = new Function(transformedCode.code);
fn();
});

console.log(res?.description);
const expectedError = `RangeError: Potential infinite loop
at eval (/Volumes/Code/code/hackclub/sprig/src/lib/engine/error.test.ts:1:148)`;
expect(res?.description).toBe(expectedError);
});

test('calls a mistyped console.log function (line 2)', () => {
const payload = {kind: "runtime", error: new TypeError("console.llog is not a function")}
payload.error.stack = `TypeError: console.llog is not a function
Expand Down
42 changes: 27 additions & 15 deletions src/lib/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { playTune } from './tune'
import { normalizeGameError } from './error'
import { bitmaps, NormalizedError } from '../state'
import type { PlayTuneRes } from '../../../engine/src/api'
import { textToTune } from '../../../engine/src/base'
import { baseEngine, textToTune } from '../../../engine/src/base'
import { webEngine } from '../../../engine/src/web'
import * as Babel from "@babel/standalone"
import TransformDetectInfiniteLoop, { BuildDuplicateFunctionDetector } from '../custom-babel-transforms'
import TransformDetectInfiniteLoop, { BuildDuplicateFunctionDetector, dissallowBackticksInDoubleQuotes } from '../custom-babel-transforms'
import {logInfo} from "../../components/popups-etc/help";

interface RunResult {
Expand Down Expand Up @@ -37,6 +37,26 @@ function parseErrorStack(err?: Error): [number | null, number | null] {
return [null, null];
}

export function transformAndThrowErrors(code: string, engineAPIKeys: string[], runCb: (code: any) => any) {
try {
const transformedCode = Babel.transform(code, {
plugins: [TransformDetectInfiniteLoop, BuildDuplicateFunctionDetector(engineAPIKeys), dissallowBackticksInDoubleQuotes],
retainLines: true
});
runCb(transformedCode);
return null;
} catch (error: any) {
return normalizeGameError({ kind: "runtime", error });
}
}

export function _performSyntaxCheck(code: string): { error: NormalizedError | null, cleanup: () => void } {
const game = baseEngine();

const engineAPIKeys = Object.keys(game.api);
return { error: transformAndThrowErrors(code, engineAPIKeys, () => {}), cleanup: () => void 0 };
}

export function runGame(code: string, canvas: HTMLCanvasElement, onPageError: (error: NormalizedError) => void): RunResult | undefined {
const game = webEngine(canvas)
const tunes: PlayTuneRes[] = []
Expand Down Expand Up @@ -112,19 +132,11 @@ export function runGame(code: string, canvas: HTMLCanvasElement, onPageError: (e
}

const engineAPIKeys = Object.keys(api);
try {
const transformResult = Babel.transform(code, {
plugins: [TransformDetectInfiniteLoop, BuildDuplicateFunctionDetector(engineAPIKeys)],
retainLines: true
});
logInfo.value = [];
const fn = new Function(...engineAPIKeys, transformResult.code!);
fn(...Object.values(api));
return { error: null, cleanup };
} catch (error: any) {
onPageError(normalizeGameError({ kind: "runtime", error }));
return { error: normalizeGameError({ kind: "runtime", error }), cleanup };
}
return { error: transformAndThrowErrors(code, engineAPIKeys, (transformedCode) => {
logInfo.value = [];
const fn = new Function(...engineAPIKeys, transformedCode.code!)
fn(...Object.values(api))
}), cleanup };
}

export function runGameHeadless(code: string): void {
Expand Down

0 comments on commit ae65b5a

Please sign in to comment.