Skip to content

Commit 2fdb5eb

Browse files
authored
Merge pull request #7326 from sproutleaf/dev-2.0
Update sketch verifier to check for redefinitions and print friendly messages
2 parents 789cd5b + 9a595c3 commit 2fdb5eb

File tree

2 files changed

+248
-94
lines changed

2 files changed

+248
-94
lines changed
+150-39
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
1-
import * as acorn from 'acorn';
2-
import * as walk from 'acorn-walk';
1+
import { parse } from 'acorn';
2+
import { simple as walk } from 'acorn-walk';
3+
import * as constants from '../constants';
4+
5+
// List of functions to ignore as they either are meant to be re-defined or
6+
// generate false positive outputs.
7+
const ignoreFunction = [
8+
'setup',
9+
'draw',
10+
'preload',
11+
'deviceMoved',
12+
'deviceTurned',
13+
'deviceShaken',
14+
'doubleClicked',
15+
'mousePressed',
16+
'mouseReleased',
17+
'mouseMoved',
18+
'mouseDragged',
19+
'mouseClicked',
20+
'mouseWheel',
21+
'touchStarted',
22+
'touchMoved',
23+
'touchEnded',
24+
'keyPressed',
25+
'keyReleased',
26+
'keyTyped',
27+
'windowResized',
28+
// 'name',
29+
// 'parent',
30+
// 'toString',
31+
// 'print',
32+
// 'stop',
33+
// 'onended'
34+
];
35+
36+
export const verifierUtils = {
337

4-
/**
5-
* @for p5
6-
* @requires core
7-
*/
8-
function sketchVerifier(p5, fn) {
938
/**
1039
* Fetches the contents of a script element in the user's sketch.
11-
*
40+
*
41+
* @private
1242
* @method fetchScript
1343
* @param {HTMLScriptElement} script
1444
* @returns {Promise<string>}
15-
*/
16-
fn.fetchScript = async function (script) {
45+
*/
46+
fetchScript: async function (script) {
1747
if (script.src) {
1848
try {
1949
const contents = await fetch(script.src).then((res) => res.text());
@@ -26,37 +56,20 @@ function sketchVerifier(p5, fn) {
2656
} else {
2757
return script.textContent;
2858
}
29-
}
30-
31-
/**
32-
* Extracts the user's code from the script fetched. Note that this method
33-
* assumes that the user's code is always the last script element in the
34-
* sketch.
35-
*
36-
* @method getUserCode
37-
* @returns {Promise<string>} The user's code as a string.
38-
*/
39-
fn.getUserCode = async function () {
40-
// TODO: think of a more robust way to get the user's code. Refer to
41-
// https://github.com/processing/p5.js/pull/7293.
42-
const scripts = document.querySelectorAll('script');
43-
const userCodeScript = scripts[scripts.length - 1];
44-
const userCode = await fn.fetchScript(userCodeScript);
45-
46-
return userCode;
47-
}
59+
},
4860

4961
/**
5062
* Extracts the user-defined variables and functions from the user code with
5163
* the help of Espree parser.
52-
*
64+
*
65+
* @private
5366
* @method extractUserDefinedVariablesAndFuncs
5467
* @param {string} code - The code to extract variables and functions from.
5568
* @returns {Object} An object containing the user's defined variables and functions.
5669
* @returns {Array<{name: string, line: number}>} [userDefinitions.variables] Array of user-defined variable names and their line numbers.
5770
* @returns {Array<{name: string, line: number}>} [userDefinitions.functions] Array of user-defined function names and their line numbers.
5871
*/
59-
fn.extractUserDefinedVariablesAndFuncs = function (code) {
72+
extractUserDefinedVariablesAndFuncs: function (code) {
6073
const userDefinitions = {
6174
variables: [],
6275
functions: []
@@ -66,13 +79,13 @@ function sketchVerifier(p5, fn) {
6679
const lineOffset = -1;
6780

6881
try {
69-
const ast = acorn.parse(code, {
82+
const ast = parse(code, {
7083
ecmaVersion: 2021,
7184
sourceType: 'module',
7285
locations: true // This helps us get the line number.
7386
});
7487

75-
walk.simple(ast, {
88+
walk(ast, {
7689
VariableDeclarator(node) {
7790
if (node.id.type === 'Identifier') {
7891
const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type)
@@ -109,18 +122,116 @@ function sketchVerifier(p5, fn) {
109122
}
110123

111124
return userDefinitions;
112-
}
125+
},
113126

114-
fn.run = async function () {
115-
const userCode = await fn.getUserCode();
116-
const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode);
127+
/**
128+
* Checks user-defined variables and functions for conflicts with p5.js
129+
* constants and global functions.
130+
*
131+
* This function performs two main checks:
132+
* 1. Verifies if any user definition conflicts with p5.js constants.
133+
* 2. Checks if any user definition conflicts with global functions from
134+
* p5.js renderer classes.
135+
*
136+
* If a conflict is found, it reports a friendly error message and halts
137+
* further checking.
138+
*
139+
* @private
140+
* @param {Object} userDefinitions - An object containing user-defined variables and functions.
141+
* @param {Array<{name: string, line: number}>} userDefinitions.variables - Array of user-defined variable names and their line numbers.
142+
* @param {Array<{name: string, line: number}>} userDefinitions.functions - Array of user-defined function names and their line numbers.
143+
* @returns {boolean} - Returns true if a conflict is found, false otherwise.
144+
*/
145+
checkForConstsAndFuncs: function (userDefinitions, p5) {
146+
const allDefinitions = [
147+
...userDefinitions.variables,
148+
...userDefinitions.functions
149+
];
117150

118-
return userDefinedVariablesAndFuncs;
151+
// Helper function that generates a friendly error message that contains
152+
// the type of redefinition (constant or function), the name of the
153+
// redefinition, the line number in user's code, and a link to its
154+
// reference on the p5.js website.
155+
function generateFriendlyError(errorType, name, line) {
156+
const url = `https://p5js.org/reference/p5/${name}`;
157+
const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. p5.js reference: ${url}`;
158+
return message;
159+
}
160+
161+
// Checks for constant redefinitions.
162+
for (let { name, line } of allDefinitions) {
163+
const libDefinition = constants[name];
164+
if (libDefinition !== undefined) {
165+
const message = generateFriendlyError('Constant', name, line);
166+
console.log(message);
167+
return true;
168+
}
169+
}
170+
171+
// The new rules for attaching anything to global are (if true for both of
172+
// the following):
173+
// - It is a member of p5.prototype
174+
// - Its name does not start with `_`
175+
const globalFunctions = new Set(
176+
Object.getOwnPropertyNames(p5.prototype)
177+
.filter(key => !key.startsWith('_') && key !== 'constructor')
178+
);
179+
180+
for (let { name, line } of allDefinitions) {
181+
if (!ignoreFunction.includes(name) && globalFunctions.has(name)) {
182+
const message = generateFriendlyError('Function', name, line);
183+
console.log(message);
184+
return true;
185+
}
186+
}
187+
188+
return false;
189+
},
190+
191+
/**
192+
* Extracts the user's code from the script fetched. Note that this method
193+
* assumes that the user's code is always the last script element in the
194+
* sketch.
195+
*
196+
* @private
197+
* @method getUserCode
198+
* @returns {Promise<string>} The user's code as a string.
199+
*/
200+
getUserCode: async function () {
201+
// TODO: think of a more robust way to get the user's code. Refer to
202+
// https://github.com/processing/p5.js/pull/7293.
203+
const scripts = document.querySelectorAll('script');
204+
const userCodeScript = scripts[scripts.length - 1];
205+
const userCode = await verifierUtils.fetchScript(userCodeScript);
206+
207+
return userCode;
208+
},
209+
210+
/**
211+
* @private
212+
*/
213+
runFES: async function (p5) {
214+
const userCode = await verifierUtils.getUserCode();
215+
const userDefinedVariablesAndFuncs = verifierUtils.extractUserDefinedVariablesAndFuncs(userCode);
216+
217+
verifierUtils.checkForConstsAndFuncs(userDefinedVariablesAndFuncs, p5);
119218
}
219+
};
220+
221+
/**
222+
* @for p5
223+
* @requires core
224+
*/
225+
function sketchVerifier(p5, _fn, lifecycles) {
226+
lifecycles.presetup = async function() {
227+
if (!p5.disableFriendlyErrors) {
228+
verifierUtils.runFES(p5);
229+
}
230+
};
120231
}
121232

122233
export default sketchVerifier;
123234

124235
if (typeof p5 !== 'undefined') {
125236
sketchVerifier(p5, p5.prototype);
126-
}
237+
}

0 commit comments

Comments
 (0)