-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update sketch verifier to check for redefinitions and print friendly messages #7326
base: dev-2.0
Are you sure you want to change the base?
Changes from 3 commits
f8bcb56
3ea00b5
d3e8f33
8c13dde
b42960e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,11 +1,64 @@ | ||||||
import * as acorn from 'acorn'; | ||||||
import * as walk from 'acorn-walk'; | ||||||
import * as constants from '../constants'; | ||||||
|
||||||
/** | ||||||
* @for p5 | ||||||
* @requires core | ||||||
*/ | ||||||
function sketchVerifier(p5, fn) { | ||||||
// List of functions to ignore as they either are meant to be re-defined or | ||||||
// generate false positive outputs. | ||||||
const ignoreFunction = [ | ||||||
'setup', | ||||||
'draw', | ||||||
'preload', | ||||||
'deviceMoved', | ||||||
'deviceTurned', | ||||||
'deviceShaken', | ||||||
'doubleClicked', | ||||||
'mousePressed', | ||||||
'mouseReleased', | ||||||
'mouseMoved', | ||||||
'mouseDragged', | ||||||
'mouseClicked', | ||||||
'mouseWheel', | ||||||
'touchStarted', | ||||||
'touchMoved', | ||||||
'touchEnded', | ||||||
'keyPressed', | ||||||
'keyReleased', | ||||||
'keyTyped', | ||||||
'windowResized', | ||||||
'name', | ||||||
'parent', | ||||||
'toString', | ||||||
'print', | ||||||
'stop', | ||||||
'onended' | ||||||
]; | ||||||
|
||||||
// Mapping names of p5 types to their constructor functions. | ||||||
// p5Constructors: | ||||||
// - Color: f() | ||||||
// - Graphics: f() | ||||||
// - Vector: f() | ||||||
// and so on. | ||||||
const p5Constructors = {}; | ||||||
|
||||||
fn.loadP5Constructors = function () { | ||||||
// Make a list of all p5 classes to be used for argument validation | ||||||
// This must be done only when everything has loaded otherwise we get | ||||||
// an empty array | ||||||
for (let key of Object.keys(p5)) { | ||||||
// Get a list of all constructors in p5. They are functions whose names | ||||||
// start with a capital letter | ||||||
if (typeof p5[key] === 'function' && key[0] !== key[0].toLowerCase()) { | ||||||
p5Constructors[key] = p5[key]; | ||||||
} | ||||||
} | ||||||
} | ||||||
sproutleaf marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
/** | ||||||
* Fetches the contents of a script element in the user's sketch. | ||||||
* | ||||||
|
@@ -111,16 +164,116 @@ function sketchVerifier(p5, fn) { | |||||
return userDefinitions; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Checks user-defined variables and functions for conflicts with p5.js | ||||||
* constants and global functions. | ||||||
* | ||||||
* This function performs two main checks: | ||||||
* 1. Verifies if any user definition conflicts with p5.js constants. | ||||||
* 2. Checks if any user definition conflicts with global functions from | ||||||
* p5.js renderer classes. | ||||||
* | ||||||
* If a conflict is found, it reports a friendly error message and halts | ||||||
* further checking. | ||||||
* | ||||||
* @param {Object} userDefinitions - An object containing user-defined variables and functions. | ||||||
* @param {Array<{name: string, line: number}>} userDefinitions.variables - Array of user-defined variable names and their line numbers. | ||||||
* @param {Array<{name: string, line: number}>} userDefinitions.functions - Array of user-defined function names and their line numbers. | ||||||
* @returns {boolean} - Returns true if a conflict is found, false otherwise. | ||||||
*/ | ||||||
fn.checkForConstsAndFuncs = function (userDefinitions) { | ||||||
const allDefinitions = [ | ||||||
...userDefinitions.variables, | ||||||
...userDefinitions.functions | ||||||
]; | ||||||
|
||||||
// Helper function that generates a friendly error message that contains | ||||||
// the type of redefinition (constant or function), the name of the | ||||||
// redefinition, the line number in user's code, and a link to its | ||||||
// reference on the p5.js website. | ||||||
function generateFriendlyError(errorType, name, line) { | ||||||
const url = `https://p5js.org/reference/#/p5/${name}`; | ||||||
const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. JavaScript does not declaring a ${errorType.toLowerCase()} more than once. p5.js reference: ${url}.`; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we maybe lost a verb in here:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I guess JavaScript does technically allow it in different scopes? We could consider saying "your code might not behave correctly" or "your code might behave unexpectedly" or something like that. |
||||||
return message; | ||||||
} | ||||||
|
||||||
// Helper function that checks if a user definition has already been defined | ||||||
// in the p5.js library, either as a constant or as a function. | ||||||
function checkForRedefinition(name, libValue, line, type) { | ||||||
try { | ||||||
const userValue = eval("name"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what this line is meant to be doing but we want to avoid |
||||||
if (libValue !== userValue) { | ||||||
let message = generateFriendlyError(type, name, line); | ||||||
console.log(message); | ||||||
return true; | ||||||
} | ||||||
} catch (e) { | ||||||
// If eval fails, the function hasn't been redefined | ||||||
return false; | ||||||
} | ||||||
return false; | ||||||
} | ||||||
|
||||||
// Checks for constant redefinitions. | ||||||
for (let { name, line } of allDefinitions) { | ||||||
const libDefinition = constants[name]; | ||||||
if (libDefinition !== undefined) { | ||||||
if (checkForRedefinition(name, libDefinition, line, "Constant")) { | ||||||
return true; | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
// Get the names of all p5.js functions which are available globally | ||||||
const classesWithGlobalFns = ['Renderer', 'Renderer2D', 'RendererGL']; | ||||||
sproutleaf marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const globalFunctions = new Set( | ||||||
classesWithGlobalFns.flatMap(className => | ||||||
Object.keys(p5Constructors[className]?.prototype || {}) | ||||||
) | ||||||
); | ||||||
|
||||||
for (let { name, line } of allDefinitions) { | ||||||
if (!ignoreFunction.includes(name) && globalFunctions.has(name)) { | ||||||
for (let className of classesWithGlobalFns) { | ||||||
const prototypeFunc = p5Constructors[className]?.prototype[name]; | ||||||
if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { | ||||||
return true; | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
// Additional check for other p5 constructors | ||||||
const otherP5ConstructorKeys = Object.keys(p5Constructors).filter( | ||||||
key => !classesWithGlobalFns.includes(key) | ||||||
); | ||||||
for (let { name, line } of allDefinitions) { | ||||||
for (let key of otherP5ConstructorKeys) { | ||||||
if (p5Constructors[key].prototype[name] !== undefined) { | ||||||
const prototypeFunc = p5Constructors[key].prototype[name]; | ||||||
if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { | ||||||
return true; | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
return false; | ||||||
} | ||||||
|
||||||
fn.run = async function () { | ||||||
const userCode = await fn.getUserCode(); | ||||||
const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); | ||||||
|
||||||
return userDefinedVariablesAndFuncs; | ||||||
if (fn.checkForConstsAndFuncs(userDefinedVariablesAndFuncs)) { | ||||||
return; | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
export default sketchVerifier; | ||||||
|
||||||
if (typeof p5 !== 'undefined') { | ||||||
sketchVerifier(p5, p5.prototype); | ||||||
p5.prototype.loadP5Constructors(); | ||||||
sproutleaf marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functions meant to be redefined by the user make sense. Out of curiosity where do the other false positive entries come from? Is that from looking at
classesWithGlobalFunctions
?