From 0a7cec82c333c79d88b00f0b3ab9f802f1a5b879 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Mon, 30 Sep 2024 15:42:31 -0400 Subject: [PATCH 1/4] add new sketch verifier to FES; it currently can read all user-defined variables and functions --- package-lock.json | 76 ++++++++++---- package.json | 3 +- preview/index.html | 52 ++++++---- src/core/friendly_errors/index.js | 2 + src/core/friendly_errors/sketch_verifier.js | 107 ++++++++++++++++++++ 5 files changed, 195 insertions(+), 45 deletions(-) create mode 100644 src/core/friendly_errors/sketch_verifier.js diff --git a/package-lock.json b/package-lock.json index 96a28156d2..e753dbafb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "LGPL-2.1", "dependencies": { "colorjs.io": "^0.5.2", + "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", "omggif": "^1.0.10", - "opentype.js": "^1.3.1", - "zod-validation-error": "^3.3.1" + "opentype.js": "^1.3.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", @@ -828,6 +828,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2547,7 +2564,6 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2559,7 +2575,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -4563,6 +4578,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4603,17 +4635,27 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -11606,21 +11648,11 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-validation-error": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.1.tgz", - "integrity": "sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.18.0" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index ef709e7e47..750620c1ea 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "version": "1.9.4", "dependencies": { "colorjs.io": "^0.5.2", + "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", @@ -82,4 +83,4 @@ "pre-commit": "lint-staged" } } -} \ No newline at end of file +} diff --git a/preview/index.html b/preview/index.html index 847423dc4d..c8357b5675 100644 --- a/preview/index.html +++ b/preview/index.html @@ -1,5 +1,6 @@ + P5 test @@ -7,35 +8,42 @@ + - + new p5(sketch); + + \ No newline at end of file diff --git a/src/core/friendly_errors/index.js b/src/core/friendly_errors/index.js index 4cf7db60ba..8f1b0e56e0 100644 --- a/src/core/friendly_errors/index.js +++ b/src/core/friendly_errors/index.js @@ -1,5 +1,7 @@ import validateParams from './param_validator.js'; +import sketchVerifier from './sketch_verifier.js'; export default function (p5) { p5.registerAddon(validateParams); + p5.registerAddon(sketchVerifier); } \ No newline at end of file diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js new file mode 100644 index 0000000000..fe88ab9fda --- /dev/null +++ b/src/core/friendly_errors/sketch_verifier.js @@ -0,0 +1,107 @@ +import * as espree from 'espree'; + +/** + * @for p5 + * @requires core + */ +function sketchVerifier(p5, fn) { + /** + * Fetches the contents of a script element in the user's sketch. + * + * @method fetchScript + * @param {HTMLScriptElement} script + * @returns {Promise} + */ + fn.fetchScript = async function (script) { + if (script.src) { + const contents = await fetch(script.src).then((res) => res.text()); + return contents; + } else { + return script.textContent; + } + } + + /** + * Extracts the user's code from the script fetched. Note that this method + * assumes that the user's code is always the last script element in the + * sketch. + * + * @method getUserCode + * @returns {Promise} The user's code as a string. + */ + fn.getUserCode = async function () { + const scripts = document.querySelectorAll('script'); + const userCodeScript = scripts[scripts.length - 1]; + const userCode = await fn.fetchScript(userCodeScript); + + return userCode; + } + + fn.extractUserDefinedVariablesAndFuncs = function (codeStr) { + const userDefinitions = { + variables: [], + functions: [] + }; + + try { + const ast = espree.parse(codeStr, { + ecmaVersion: 2021, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }); + + function traverse(node) { + switch (node.type) { + case 'VariableDeclaration': + node.declarations.forEach(declaration => { + if (declaration.id.type === 'Identifier') { + userDefinitions.variables.push(declaration.id.name); + } + }); + break; + case 'FunctionDeclaration': + if (node.id && node.id.type === 'Identifier') { + userDefinitions.functions.push(node.id.name); + } + break; + case 'ArrowFunctionExpression': + case 'FunctionExpression': + if (node.parent && node.parent.type === 'VariableDeclarator') { + userDefinitions.functions.push(node.parent.id.name); + } + break; + } + + for (const key in node) { + if (node[key] && typeof node[key] === 'object') { + if (Array.isArray(node[key])) { + node[key].forEach(child => traverse(child)); + } else { + traverse(node[key]); + } + } + } + } + + traverse(ast); + } catch (error) { + console.error('Error parsing code:', error); + } + + return userDefinitions; + } + + fn.run = async function () { + const userCode = await fn.getUserCode(); + const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); + console.log(userDefinedVariablesAndFuncs); + } +} + +export default sketchVerifier; + +if (typeof p5 !== 'undefined') { + sketchVerifier(p5, p5.prototype); +} \ No newline at end of file From 135187f1f429334a9d82cd67529f59a2c3805f98 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Tue, 1 Oct 2024 22:23:34 -0400 Subject: [PATCH 2/4] add test file for sketch verifier --- src/core/friendly_errors/sketch_verifier.js | 37 +++-- test/unit/core/sketch_overrides.js | 142 ++++++++++++++++++++ test/unit/spec.js | 1 + 3 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 test/unit/core/sketch_overrides.js diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index fe88ab9fda..c646360f81 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -37,6 +37,16 @@ function sketchVerifier(p5, fn) { return userCode; } + /** + * Extracts the user-defined variables and functions from the user code with + * the help of Espree parser. + * + * @method extractUserDefinedVariablesAndFuncs + * @param {string} codeStr - The code to extract variables and functions from. + * @returns {Object} An object containing the user's defined variables and functions. + * @returns {string[]} [userDefinitions.variables] Array of user-defined variable names. + * @returns {strings[]} [userDefinitions.functions] Array of user-defined function names. + */ fn.extractUserDefinedVariablesAndFuncs = function (codeStr) { const userDefinitions = { variables: [], @@ -53,23 +63,22 @@ function sketchVerifier(p5, fn) { }); function traverse(node) { - switch (node.type) { + const { type, declarations, id, init } = node; + + switch (type) { case 'VariableDeclaration': - node.declarations.forEach(declaration => { - if (declaration.id.type === 'Identifier') { - userDefinitions.variables.push(declaration.id.name); + declarations.forEach(({ id, init }) => { + if (id.type === 'Identifier') { + const category = init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(init.type) + ? 'functions' + : 'variables'; + userDefinitions[category].push(id.name); } }); break; case 'FunctionDeclaration': - if (node.id && node.id.type === 'Identifier') { - userDefinitions.functions.push(node.id.name); - } - break; - case 'ArrowFunctionExpression': - case 'FunctionExpression': - if (node.parent && node.parent.type === 'VariableDeclarator') { - userDefinitions.functions.push(node.parent.id.name); + if (id?.type === 'Identifier') { + userDefinitions.functions.push(id.name); } break; } @@ -87,6 +96,7 @@ function sketchVerifier(p5, fn) { traverse(ast); } catch (error) { + // TODO: Replace this with a friendly error message. console.error('Error parsing code:', error); } @@ -96,7 +106,8 @@ function sketchVerifier(p5, fn) { fn.run = async function () { const userCode = await fn.getUserCode(); const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); - console.log(userDefinedVariablesAndFuncs); + + return userDefinedVariablesAndFuncs; } } diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js new file mode 100644 index 0000000000..7c183ffd73 --- /dev/null +++ b/test/unit/core/sketch_overrides.js @@ -0,0 +1,142 @@ +import sketchVerifier from '../../../src/core/friendly_errors/sketch_verifier.js'; + +suite('Validate Params', function () { + const mockP5 = { + _validateParameters: vi.fn() + }; + const mockP5Prototype = {}; + + beforeAll(function () { + sketchVerifier(mockP5, mockP5Prototype); + }); + + afterAll(function () { + }); + + suite('fetchScript()', function () { + const url = 'https://www.p5test.com/sketch.js'; + const code = 'p.createCanvas(200, 200);'; + + test('Fetches script content from src', async function () { + const mockFetch = vi.fn(() => + Promise.resolve({ + text: () => Promise.resolve(code) + }) + ); + vi.stubGlobal('fetch', mockFetch); + + const mockScript = { src: url }; + const result = await mockP5Prototype.fetchScript(mockScript); + + expect(mockFetch).toHaveBeenCalledWith(url); + expect(result).toBe(code); + + vi.unstubAllGlobals(); + }); + + test('Fetches code when there is no src attribute', async function () { + const mockScript = { textContent: code }; + const result = await mockP5Prototype.fetchScript(mockScript); + + expect(result).toBe(code); + }); + }); + + suite('getUserCode()', function () { + const userCode = "let c = p5.Color(20, 20, 20);"; + + test('fetches the last script element', async function () { + document.body.innerHTML = ` + + + + `; + + mockP5Prototype.fetchScript = vi.fn(() => Promise.resolve(userCode)); + + const result = await mockP5Prototype.getUserCode(); + + expect(mockP5Prototype.fetchScript).toHaveBeenCalledTimes(1); + expect(result).toBe(userCode); + }); + }); + + suite('extractUserDefinedVariablesAndFuncs()', function () { + test('Extracts user-defined variables and functions', function () { + const code = ` + let x = 5; + const y = 10; + var z = 15; + let v1, v2, v3 + function foo() {} + const bar = () => {}; + const baz = (x) => x * 2; + `; + + const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); + + expect(result.variables).toEqual(['x', 'y', 'z', 'v1', 'v2', 'v3']); + expect(result.functions).toEqual(['foo', 'bar', 'baz']); + }); + + // Sketch verifier should ignore the following types of lines: + // - Comments (both single line and multi-line) + // - Function calls + // - Non-declaration code + test('Ignores other lines', function () { + const code = ` + // This is a comment + let x = 5; + /* This is a multi-line comment. + * This is a multi-line comment. + */ + const y = 10; + console.log("This is a statement"); + foo(5); + p5.Math.random(); + if (true) { + let z = 15; + } + for (let i = 0; i < 5; i++) {} + `; + + const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); + + expect(result.variables).toEqual(['x', 'y', 'z', 'i']); + expect(result.functions).toEqual([]); + }); + + test('Handles parsing errors', function () { + const invalidCode = 'let x = ;'; + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + + const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(invalidCode); + + expect(consoleSpy).toHaveBeenCalled(); + expect(result).toEqual({ variables: [], functions: [] }); + + consoleSpy.mockRestore(); + }); + }); + + suite('run()', function () { + test('Returns extracted variables and functions', async function () { + const mockScript = ` + let x = 5; + const y = 10; + function foo() {} + const bar = () => {}; + `; + mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript)); + + const result = await mockP5Prototype.run(); + + expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); + + expect(result).toEqual({ + variables: ['x', 'y'], + functions: ['foo', 'bar'] + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/spec.js b/test/unit/spec.js index 995b152693..8df31317f2 100644 --- a/test/unit/spec.js +++ b/test/unit/spec.js @@ -13,6 +13,7 @@ var spec = { 'param_errors', 'preload', 'rendering', + 'sketch_overrides', 'structure', 'transform', 'version', From 9067152ef239a9a5e74f578efacaaee571470f10 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Tue, 1 Oct 2024 22:26:25 -0400 Subject: [PATCH 3/4] remove all the silly definitions in index.html --- preview/index.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/preview/index.html b/preview/index.html index c8357b5675..299cf2ccb0 100644 --- a/preview/index.html +++ b/preview/index.html @@ -22,14 +22,9 @@ // p5.registerAddon(calculation); - let apple = 10; - function banana() { - console.log('banana'); - } const sketch = function (p) { p.setup = function () { p.createCanvas(200, 200); - p.run(); }; p.draw = function () { From 07f4b1b21d4a50d88795e416c3a3996c50f27f58 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Sat, 5 Oct 2024 15:08:53 -0400 Subject: [PATCH 4/4] update sketch verifier to use acorn/acorn walk and include line numbers in results --- package-lock.json | 16 +++- package.json | 5 +- src/core/friendly_errors/sketch_verifier.js | 90 ++++++++++-------- test/unit/core/sketch_overrides.js | 100 +++++++++++++++++--- 4 files changed, 155 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index e753dbafb6..8faf4d39d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,9 @@ "version": "1.9.4", "license": "LGPL-2.1", "dependencies": { + "acorn": "^8.12.1", + "acorn-walk": "^8.3.4", "colorjs.io": "^0.5.2", - "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", @@ -2579,6 +2580,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -11664,4 +11676,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 750620c1ea..37c0804100 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ }, "version": "1.9.4", "dependencies": { + "acorn": "^8.12.1", + "acorn-walk": "^8.3.4", "colorjs.io": "^0.5.2", - "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", @@ -83,4 +84,4 @@ "pre-commit": "lint-staged" } } -} +} \ No newline at end of file diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index c646360f81..f90eb4f204 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -1,4 +1,5 @@ -import * as espree from 'espree'; +import * as acorn from 'acorn'; +import * as walk from 'acorn-walk'; /** * @for p5 @@ -14,8 +15,14 @@ function sketchVerifier(p5, fn) { */ fn.fetchScript = async function (script) { if (script.src) { - const contents = await fetch(script.src).then((res) => res.text()); - return contents; + try { + const contents = await fetch(script.src).then((res) => res.text()); + return contents; + } catch (error) { + // TODO: Handle CORS error here. + console.error('Error fetching script:', error); + return ''; + } } else { return script.textContent; } @@ -30,6 +37,8 @@ function sketchVerifier(p5, fn) { * @returns {Promise} The user's code as a string. */ fn.getUserCode = async function () { + // TODO: think of a more robust way to get the user's code. Refer to + // https://github.com/processing/p5.js/pull/7293. const scripts = document.querySelectorAll('script'); const userCodeScript = scripts[scripts.length - 1]; const userCode = await fn.fetchScript(userCodeScript); @@ -42,59 +51,58 @@ function sketchVerifier(p5, fn) { * the help of Espree parser. * * @method extractUserDefinedVariablesAndFuncs - * @param {string} codeStr - The code to extract variables and functions from. + * @param {string} code - The code to extract variables and functions from. * @returns {Object} An object containing the user's defined variables and functions. - * @returns {string[]} [userDefinitions.variables] Array of user-defined variable names. - * @returns {strings[]} [userDefinitions.functions] Array of user-defined function names. + * @returns {Array<{name: string, line: number}>} [userDefinitions.variables] Array of user-defined variable names and their line numbers. + * @returns {Array<{name: string, line: number}>} [userDefinitions.functions] Array of user-defined function names and their line numbers. */ - fn.extractUserDefinedVariablesAndFuncs = function (codeStr) { + fn.extractUserDefinedVariablesAndFuncs = function (code) { const userDefinitions = { variables: [], functions: [] }; + // The line numbers from the parser are consistently off by one, add + // `lineOffset` here to correct them. + const lineOffset = -1; try { - const ast = espree.parse(codeStr, { + const ast = acorn.parse(code, { ecmaVersion: 2021, sourceType: 'module', - ecmaFeatures: { - jsx: true - } + locations: true // This helps us get the line number. }); - function traverse(node) { - const { type, declarations, id, init } = node; - - switch (type) { - case 'VariableDeclaration': - declarations.forEach(({ id, init }) => { - if (id.type === 'Identifier') { - const category = init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(init.type) - ? 'functions' - : 'variables'; - userDefinitions[category].push(id.name); - } + walk.simple(ast, { + VariableDeclarator(node) { + if (node.id.type === 'Identifier') { + const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type) + ? 'functions' + : 'variables'; + userDefinitions[category].push({ + name: node.id.name, + line: node.loc.start.line + lineOffset + }); + } + }, + FunctionDeclaration(node) { + if (node.id && node.id.type === 'Identifier') { + userDefinitions.functions.push({ + name: node.id.name, + line: node.loc.start.line + lineOffset + }); + } + }, + // We consider class declarations to be a special form of variable + // declaration. + ClassDeclaration(node) { + if (node.id && node.id.type === 'Identifier') { + userDefinitions.variables.push({ + name: node.id.name, + line: node.loc.start.line + lineOffset }); - break; - case 'FunctionDeclaration': - if (id?.type === 'Identifier') { - userDefinitions.functions.push(id.name); - } - break; - } - - for (const key in node) { - if (node[key] && typeof node[key] === 'object') { - if (Array.isArray(node[key])) { - node[key].forEach(child => traverse(child)); - } else { - traverse(node[key]); - } } } - } - - traverse(ast); + }); } catch (error) { // TODO: Replace this with a friendly error message. console.error('Error parsing code:', error); diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js index 7c183ffd73..cf16edbff7 100644 --- a/test/unit/core/sketch_overrides.js +++ b/test/unit/core/sketch_overrides.js @@ -74,9 +74,49 @@ suite('Validate Params', function () { `; const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); - - expect(result.variables).toEqual(['x', 'y', 'z', 'v1', 'v2', 'v3']); - expect(result.functions).toEqual(['foo', 'bar', 'baz']); + const expectedResult = { + "functions": [ + { + "line": 5, + "name": "foo", + }, + { + "line": 6, + "name": "bar", + }, + { + "line": 7, + "name": "baz", + }, + ], + "variables": [ + { + "line": 1, + "name": "x", + }, + { + "line": 2, + "name": "y", + }, + { + "line": 3, + "name": "z", + }, + { + "line": 4, + "name": "v1", + }, + { + "line": 4, + "name": "v2", + }, + { + "line": 4, + "name": "v3", + }, + ], + }; + expect(result).toEqual(expectedResult); }); // Sketch verifier should ignore the following types of lines: @@ -101,9 +141,29 @@ suite('Validate Params', function () { `; const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); - - expect(result.variables).toEqual(['x', 'y', 'z', 'i']); - expect(result.functions).toEqual([]); + const expectedResult = { + "functions": [], + "variables": [ + { + "line": 2, + "name": "x", + }, + { + "line": 6, + "name": "y", + }, + { + "line": 11, + "name": "z", + }, + { + "line": 13, + "name": "i", + }, + ], + }; + + expect(result).toEqual(expectedResult); }); test('Handles parsing errors', function () { @@ -130,13 +190,31 @@ suite('Validate Params', function () { mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript)); const result = await mockP5Prototype.run(); + const expectedResult = { + "functions": [ + { + "line": 3, + "name": "foo", + }, + { + "line": 4, + "name": "bar", + }, + ], + "variables": [ + { + "line": 1, + "name": "x", + }, + { + "line": 2, + "name": "y", + }, + ], + }; expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); - - expect(result).toEqual({ - variables: ['x', 'y'], - functions: ['foo', 'bar'] - }); + expect(result).toEqual(expectedResult); }); }); }); \ No newline at end of file