-
-
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
Open
sproutleaf
wants to merge
5
commits into
processing:dev-2.0
Choose a base branch
from
sproutleaf:dev-2.0
base: dev-2.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f8bcb56
update
sproutleaf 3ea00b5
Merge remote-tracking branch 'upstream/dev-2.0' into dev-2.0
sproutleaf d3e8f33
more updates
sproutleaf 8c13dde
Merge remote-tracking branch 'upstream/dev-2.0' into dev-2.0
sproutleaf b42960e
update
sproutleaf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/** | ||
* Loads and returns a mapping of p5 constructor names to their corresponding | ||
* functions. This function iterates through all properties of the p5 object | ||
* identifying constructor functions (those starting with a capital letter) and | ||
* storing them in an object. | ||
* @returns {Object} An object mapping p5 constructor names to their functions. | ||
*/ | ||
function loadP5Constructors(p5) { | ||
// Mapping names of p5 types to their constructor functions. | ||
// p5Constructors: | ||
// - Color: f() | ||
// - Graphics: f() | ||
// - Vector: f() | ||
// and so on. | ||
const p5Constructors = {}; | ||
|
||
// 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]; | ||
} | ||
} | ||
|
||
return p5Constructors; | ||
} | ||
|
||
export { loadP5Constructors }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,44 @@ | ||
import * as acorn from 'acorn'; | ||
import * as walk from 'acorn-walk'; | ||
import { parse } from 'acorn'; | ||
import { simple as walk } from 'acorn-walk'; | ||
import * as constants from '../constants'; | ||
import { loadP5Constructors } from './friendly_errors_utils'; | ||
|
||
/** | ||
* @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' | ||
]; | ||
|
||
/** | ||
* Fetches the contents of a script element in the user's sketch. | ||
* | ||
|
@@ -66,13 +99,13 @@ function sketchVerifier(p5, fn) { | |
const lineOffset = -1; | ||
|
||
try { | ||
const ast = acorn.parse(code, { | ||
const ast = parse(code, { | ||
ecmaVersion: 2021, | ||
sourceType: 'module', | ||
locations: true // This helps us get the line number. | ||
}); | ||
|
||
walk.simple(ast, { | ||
walk(ast, { | ||
VariableDeclarator(node) { | ||
if (node.id.type === 'Identifier') { | ||
const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type) | ||
|
@@ -111,11 +144,101 @@ 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, p5Constructors) { | ||
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 support declaring a ${errorType.toLowerCase()} more than once. p5.js reference: ${url}.`; | ||
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"); | ||
if (libValue !== userValue) { | ||
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 |
||
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; | ||
} | ||
} | ||
} | ||
|
||
// The new rules for attaching anything to global are (if true for both of | ||
// the following): | ||
// - It is a member of p5.prototype | ||
// - Its name does not start with `_` | ||
const globalFunctions = new Set( | ||
Object.keys(p5.prototype).filter(key => !key.startsWith('_')) | ||
); | ||
|
||
for (let { name, line } of allDefinitions) { | ||
if (!ignoreFunction.includes(name) && globalFunctions.has(name)) { | ||
const prototypeFunc = p5.prototype[name]; | ||
if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
fn.run = async function () { | ||
const p5Constructors = await new Promise(resolve => { | ||
if (document.readyState === 'complete') { | ||
resolve(loadP5Constructors(p5)); | ||
} else { | ||
window.addEventListener('load', () => resolve(loadP5Constructors(p5))); | ||
} | ||
}); | ||
|
||
const userCode = await fn.getUserCode(); | ||
const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); | ||
|
||
return userDefinedVariablesAndFuncs; | ||
if (fn.checkForConstsAndFuncs(userDefinedVariablesAndFuncs, p5Constructors)) { | ||
return; | ||
} | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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
?