Skip to content

Commit ae8fe75

Browse files
authored
Merge pull request #5759 from davepagurek/fix/fes-reserved-regex
Make reserved word assignment regex work better with function calls
2 parents 59aff2a + d15cf55 commit ae8fe75

File tree

2 files changed

+136
-23
lines changed

2 files changed

+136
-23
lines changed

src/core/friendly_errors/sketch_reader.js

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,43 @@ if (typeof IS_MINIFIED !== 'undefined') {
116116

117117
//these regex are used to perform variable extraction
118118
//visit https://regexr.com/ for the detailed view
119-
const varName = /(?:(?:let|const|var)\s+)?([\w$]+)/;
120-
const varNameWithComma = /(?:(?:let|const|var)\s+)?([\w$,]+)/;
119+
const optionalVarKeyword = /(?:(?:let|const|var)\s+)?/;
120+
121+
// Bracketed expressions start with an opening bracket, some amount of non
122+
// bracket characters, then a closing bracket. Note that this won't properly
123+
// parse nested brackets: `constrain(millis(), 0, 1000)` will match
124+
// `constrain(millis()` only, but will still fail gracefully and not try to
125+
// mistakenly read any subsequent code as assignment expressions.
126+
const roundBracketedExpr = /(?:\([^)]*\))/;
127+
const squareBracketedExpr = /(?:\[[^\]]*\])/;
128+
const curlyBracketedExpr = /(?:\{[^}]*\})/;
129+
const bracketedExpr = new RegExp(
130+
[roundBracketedExpr, squareBracketedExpr, curlyBracketedExpr]
131+
.map(regex => regex.source)
132+
.join('|')
133+
);
134+
135+
// In an a = b expression, `b` can be any character up to a newline or comma,
136+
// unless the comma is inside of a bracketed expression of some kind (to make
137+
// sure we parse function calls with multiple arguments properly.)
138+
const rightHandSide = new RegExp('(?:' + bracketedExpr.source + '|[^\\n,])+');
139+
140+
const leftHandSide = /([\w$]+)/;
141+
const assignmentOperator = /\s*=\s*/;
142+
const singleAssignment = new RegExp(
143+
leftHandSide.source + assignmentOperator.source + rightHandSide.source
144+
);
145+
const listSeparator = /,\s*/;
146+
const oneOrMoreAssignments = new RegExp(
147+
'(?:' +
148+
singleAssignment.source +
149+
listSeparator.source +
150+
')*' +
151+
singleAssignment.source
152+
);
153+
const assignmentStatement = new RegExp(
154+
'^' + optionalVarKeyword.source + oneOrMoreAssignments.source
155+
);
121156
const letConstName = /(?:(?:let|const)\s+)([\w$]+)/;
122157

123158
/**
@@ -133,27 +168,12 @@ if (typeof IS_MINIFIED !== 'undefined') {
133168
//extract variable names from the user's code
134169
let matches = [];
135170
linesArray.forEach(ele => {
136-
if (ele.includes(',')) {
137-
matches.push(
138-
...ele.split(',').flatMap(s => {
139-
//below RegExps extract a, b, c from let/const a=10, b=20, c;
140-
//visit https://regexr.com/ for the detailed view.
141-
let match;
142-
if (s.includes('=')) {
143-
match = s.match(/(\w+)\s*(?==)/i);
144-
if (match !== null) return match[1];
145-
} else if (!s.match(new RegExp('[[]{}]'))) {
146-
let m = s.match(varName);
147-
if (m !== null) return s.match(varNameWithComma)[1];
148-
} else return [];
149-
})
150-
);
151-
} else {
152-
//extract a from let/const a=10;
153-
//visit https://regexr.com/ for the detailed view.
154-
const match = ele.match(letConstName);
155-
if (match !== null) matches.push(match[1]);
156-
}
171+
// Match 0 is the part of the line of code that the regex looked at.
172+
// Matches 1 and onward will be only the variable names on the left hand
173+
// side of assignment expressions.
174+
const match = ele.match(assignmentStatement);
175+
if (!match) return;
176+
matches.push(...match.slice(1).filter(group => group !== undefined));
157177
});
158178
//check if the obtained variables are a part of p5.js or not
159179
checkForConstsAndFuncs(matches);

test/unit/core/error_helpers.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,4 +974,97 @@ suite('Tests for p5.js sketch_reader', function() {
974974
});
975975
}
976976
);
977+
978+
testUnMinified(
979+
'detects reassignment of p5.js functions in declaration lists',
980+
function() {
981+
return new Promise(function(resolve) {
982+
prepSketchReaderTest(
983+
['function setup() {', 'let x = 2, text = 2;', '}'],
984+
resolve
985+
);
986+
}).then(function() {
987+
assert.strictEqual(log.length, 1);
988+
assert.match(log[0], /you have used a p5.js reserved function/);
989+
});
990+
}
991+
);
992+
993+
testUnMinified(
994+
'detects reassignment of p5.js functions in declaration lists after function calls',
995+
function() {
996+
return new Promise(function(resolve) {
997+
prepSketchReaderTest(
998+
[
999+
'function setup() {',
1000+
'let x = constrain(frameCount, 0, 1000), text = 2;',
1001+
'}'
1002+
],
1003+
resolve
1004+
);
1005+
}).then(function() {
1006+
assert.strictEqual(log.length, 1);
1007+
assert.match(log[0], /you have used a p5.js reserved function/);
1008+
});
1009+
}
1010+
);
1011+
1012+
testUnMinified(
1013+
'ignores p5.js functions used in the right hand side of assignment expressions',
1014+
function() {
1015+
return new Promise(function(resolve) {
1016+
prepSketchReaderTest(
1017+
// This will still log an error, as `text` isn't being used correctly
1018+
// here, but the important part is that it doesn't say that we're
1019+
// trying to reassign a reserved function.
1020+
['function draw() {', 'let x = constrain(100, 0, text);', '}'],
1021+
resolve
1022+
);
1023+
}).then(function() {
1024+
assert.ok(
1025+
!log.some(line =>
1026+
line.match(/you have used a p5.js reserved function/)
1027+
)
1028+
);
1029+
});
1030+
}
1031+
);
1032+
1033+
testUnMinified(
1034+
'ignores p5.js function names used as function arguments',
1035+
function() {
1036+
return new Promise(function(resolve) {
1037+
prepSketchReaderTest(
1038+
['function draw() {', 'let myLog = (text) => print(text);', '}'],
1039+
resolve
1040+
);
1041+
}).then(function() {
1042+
assert.strictEqual(log.length, 0);
1043+
});
1044+
}
1045+
);
1046+
1047+
testUnMinified(
1048+
'fails gracefully on inputs too complicated to parse',
1049+
function() {
1050+
return new Promise(function(resolve) {
1051+
prepSketchReaderTest(
1052+
// This technically is redefining text, but it should stop parsing
1053+
// after the double nested brackets rather than try and possibly
1054+
// give a false positive error. This particular assignment will get
1055+
// caught at runtime regardless by
1056+
// `_createFriendlyGlobalFunctionBinder`.
1057+
[
1058+
'function draw() {',
1059+
'let x = constrain(millis(), 0, text = 100)',
1060+
'}'
1061+
],
1062+
resolve
1063+
);
1064+
}).then(function() {
1065+
console.log(log);
1066+
assert.strictEqual(log.length, 0);
1067+
});
1068+
}
1069+
);
9771070
});

0 commit comments

Comments
 (0)