Skip to content

Commit

Permalink
Fix some JSFCK cases
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdz committed Sep 7, 2024
1 parent edd9cd5 commit 50afda2
Show file tree
Hide file tree
Showing 8 changed files with 464 additions and 32 deletions.
12 changes: 11 additions & 1 deletion src/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const BUILTIN_REGEXP_METHODS_SUPPORTED = Object.keys(BUILTIN_REGEXP_METHO
export const BUILTIN_REGEXP_METHODS_SYMBOLS = Object.values(BUILTIN_REGEXP_METHOD_LOOKUP);
export const BUILTIN_STRING_PROTOTYPE = '$StringPrototype';
export const BUILTIN_STRING_METHOD_LOOKUP = {
charAt: '$String_chart',
charAt: '$String_charAt',
charCodeAt: '$String_charCodeAt',
concat: '$String_concat',
indexOf: '$String_indexOf',
Expand Down Expand Up @@ -113,6 +113,16 @@ export const BUILTIN_PROTO_TO_LOOKUP = {
//[BUILTIN_MAP_PROTOTYPE]: BUILTIN_MAP_METHOD_LOOKUP,
//[BUILTIN_SET_PROTOTYPE]: BUILTIN_SET_METHOD_LOOKUP,
}
// Used for serialization; `''+[].flat()` -> `'function flat() { [native code] }'` -> obj[$String_charAt] -> 'charAt'
export const BUILTIN_NAMESPACED_TO_FUNC_NAME = {
...BUILTIN_STRING_METHOD_LOOKUP_REV,
...BUILTIN_NUMBER_METHOD_LOOKUP_REV,
...BUILTIN_REGEXP_METHOD_LOOKUP_REV,
...BUILTIN_FUNCTION_METHOD_LOOKUP_REV,
...BUILTIN_BOOLEAN_METHOD_LOOKUP_REV,
...BUILTIN_ARRAY_METHOD_LOOKUP_REV,
//...BUILTIN_OBJECT_METHOD_LOOKUP_REV,
};

export const FRESH = true;
export const OLD = false;
Expand Down
42 changes: 40 additions & 2 deletions src/normalize/normalize.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
BLUE,
RESET,
BUILTIN_ARRAY_PROTOTYPE,
BUILTIN_NUMBER_PROTOTYPE,
BUILTIN_NUMBER_PROTOTYPE, BUILTIN_NAMESPACED_TO_FUNC_NAME,
} from '../constants.mjs';
import {
ASSERT,
Expand Down Expand Up @@ -1860,7 +1860,7 @@ export function phaseNormalize(fdata, fname, prng, options) {
// TODO: we could try to find the original point of this call and replace the arg but I'm sure there are plenty of cases where this is not feasible (like conditionals etc)
vlog('This call to `Function` has primitive args so we should be able to resolve it...');
riskyRule('Call to `Function` with primitive args can be resolved (although there is no guarantee it will work)');
example('const x = Function("return 10;"', 'const x = function(){ return 10; };');
example('const x = Function("return 10;");', 'const x = function(){ return 10; };');
before(node, body[i]);

const argString =
Expand Down Expand Up @@ -3055,6 +3055,26 @@ export function phaseNormalize(fdata, fname, prng, options) {

if (node.callee.type === 'Identifier' && node.callee.name === BUILTIN_FUNC_CALL_NAME) {
// This is $dotCall()

if (
node.arguments[0].type === 'Identifier' &&
node.arguments[0].name === 'Function' &&
!(node.arguments[1] === 'Identifier' && node.arguments[1].name === 'undefined')
) {
rule('dotCalling the global Function does not need a context');
example('$dotCall(Function, x, y);', 'Function(y);');
before(body[i]);

node.callee.name = 'Function';
node.arguments.shift();
const ctxArg = node.arguments.shift(); // TODO: we want to retain this tho
body.splice(i, 0, AST.expressionStatement(ctxArg));

after(body[i]);
after(body[i+1]);
return true;
}

// Lowest hanging fruit: `const x = a.b; $dotCall(x, a, 1, 2); -> a.b(1, 2)
// Check previous statement. If it is a const binding to the first arg of the $dotCall
// and it is a member expression where the object equals the second $dotCall arg... simplify!
Expand Down Expand Up @@ -5229,6 +5249,24 @@ export function phaseNormalize(fdata, fname, prng, options) {
before(node, body[i]);
return true;
}

if (BUILTIN_NAMESPACED_TO_FUNC_NAME[node.left.name] || BUILTIN_NAMESPACED_TO_FUNC_NAME[node.right.name]) {
rule('Binary expr with known built-in function can serialize function');
example('true + $Array_flat', 'true + "function flat() { [native code] }"');
before(node, body[i]);

if (BUILTIN_NAMESPACED_TO_FUNC_NAME[node.left.name]) {
node.left = AST.primitive('function ' + BUILTIN_NAMESPACED_TO_FUNC_NAME[node.left.name] + '() { [native code] }');
}
if (BUILTIN_NAMESPACED_TO_FUNC_NAME[node.right.name]) {
node.right = AST.primitive('function ' + BUILTIN_NAMESPACED_TO_FUNC_NAME[node.right.name] + '() { [native code] }');
}

after(body[i]);
return true;
}


}

if (
Expand Down
73 changes: 44 additions & 29 deletions src/reduce_static/coerced.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
findBodyOffset,
} from '../utils.mjs';
import * as AST from '../ast.mjs';
import { BUILTIN_NAMESPACED_TO_FUNC_NAME } from '../constants.mjs';

export function coercials(fdata) {
group('\n\n\nFind cases of $coerce to eliminate');
Expand All @@ -44,7 +45,7 @@ function _coercials(fdata) {
example('$coerce(500.123, "string");', '"500.123";');
example('$coerce(500.123, "number");', '500.123;');
example('$coerce("500.123", "plustr");', '"500.123";');
before(read.parentNode, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const pv = AST.getPrimitiveValue(argNode);
const pvc =
Expand All @@ -61,7 +62,7 @@ function _coercials(fdata) {
if (read.grandIndex < 0) read.grandNode[read.grandProp] = AST.primitive(pvc);
else read.grandNode[read.grandProp][read.grandIndex] = AST.primitive(pvc);

after(read.parentNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
}

Expand All @@ -87,18 +88,32 @@ function _coercials(fdata) {
example('f(0 + String);', 'f("0function Number() { [native code] }");', () => argName === 'Number');
example('f(0 + String);', 'f("0function String() { [native code] }");', () => argName === 'String');
example('f(0 + String);', 'f("0function RegExp() { [native code] }");', () => argName === 'RegExp');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

ASSERT(['number', 'string', 'plustr'].includes(kind), 'result is the same no matter the kind', kind);
const finalNode = AST.primitive('function ' + argName + '() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
}

if (BUILTIN_NAMESPACED_TO_FUNC_NAME[argName]) {
rule('$coerce on a namespaced preval built-in symbol should become a string'); // kind of regardless. even if NaN later.
example('$coerce($Array_flat, "plustr")', '"function flat() { [native code] }"');
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function ' + BUILTIN_NAMESPACED_TO_FUNC_NAME[argName] + '() { [native code] }');
if (read.grandIndex < 0) read.grandNode[read.grandProp] = finalNode;
else read.grandNode[read.grandProp][read.grandIndex] = finalNode;

after(read.blockBody[read.blockIndex]);
++changes;
return;
}

const argMeta = fdata.globallyUniqueNamingRegistry.get(argName);

if (argMeta.isImplicitGlobal) return;
Expand All @@ -114,38 +129,37 @@ function _coercials(fdata) {
rule('Coercing a string to a string is a noop');
example('const x = `a${b}c`; const b = $coerce(x, "string");', 'const x = `a${b}c`; const b = x;');
before(argWrite.blockBody[argWrite.blockIndex]);
before(read.parentNode, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

if (read.grandIndex < 0) read.grandNode[read.grandProp] = argNode;
else read.grandNode[read.grandProp][read.grandIndex] = argNode;

after(argWrite.blockBody[argWrite.blockIndex]);
after(AST.emptyStatement());
after(read.blockBody[read.blockIndex]);
++changes;
return;
}

if (at === 'number' && kind === 'plustr') {
rule('Calling $coerce on a value that is a number when asking for plustr can be changed to want a string');
example('const x = +y; $coerce(y, "plustr");', 'const x = +y; $coerce(y, "string");');
before(read.parentNode, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

read.parentNode['arguments'][1] = AST.primitive('string');

after(argNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
// Allow next checks to scan this change
}

if ((at === 'number' && kind === 'number') || (at === 'string' && kind === 'string')) {
rule('Calling $coerce on a value that is already of target type is a noop');
example('const x = +y; $coerce(y, "number");', 'const x = +y; y;');
before(read.parentNode, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

if (read.grandIndex < 0) read.grandNode[read.grandProp] = argNode;
else read.grandNode[read.grandProp][read.grandIndex] = argNode;

after(argNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
}

Expand All @@ -155,12 +169,12 @@ function _coercials(fdata) {
example('$coerce(/foo/, "number")', 'NaN');
example('$coerce(/foo/, "string")', '"/foo/"');
example('$coerce(/foo/, "plustr")', '"/foo/"');
before(read.node, read.blockBody);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive(coerce(argMeta.typing.mustBeValue || argMeta.constValueRef.node.raw, kind));
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
Expand All @@ -170,27 +184,28 @@ function _coercials(fdata) {
if (kind === 'number') {
rule('A regex as arg to $coerce with number is always NaN');
example('$coerce(function(){}, "number")', 'NaN');
before(read.node, read.blockBody);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive(NaN);
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody);
after(read.blockBody[read.blockIndex]);
++changes;
return;
} else {
// Note: if this is a builtin the name would appear in the function... and the body etc too.
vlog('Serializing a function. This will probably only work in fringe cases.');
fdata.reports.push('Serialized a function to a string, this string was unlikely to be the accurate and may lead to bad results');

rule('A function as arg to $coerce with string or plustr can be yolod');
example('$coerce(function(){}, "string")', 'function () {}');
example('$coerce(function(a,b){}, "plustr")', '"function (a, b) {}"');
before(read.node, read.blockBody);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function(){}');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
Expand All @@ -202,73 +217,73 @@ function _coercials(fdata) {
// This is a one-of example which serves to unravel the jsf*ck code (which uses Array#filter)
rule('Having `Array#filter` in $coerce can be resolved');
example('f($coerce(Array.prototype.filter. "plustr"))', 'f("function filter() { [native code] }")');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function filter() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
case 'Array#flat': {
// This is a one-of example which serves to unravel the jsf*ck code (which uses Array#flat)
rule('Having `Array#flat` in $coerce can be resolved');
example('f($coerce(Array.prototype.flat. "plustr"))', 'f("function flat() { [native code] }")');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function flat() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
case 'Array#pop': {
rule('Having `Array#pop` in $coerce can be resolved');
example('f($coerce(Array.prototype.pop. "plustr"))', 'f("function pop() { [native code] }")');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function pop() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
case 'Array#push': {
rule('Having `Array#push` in $coerce can be resolved');
example('f($coerce(Array.prototype.push. "plustr"))', 'f("function push() { [native code] }")');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function push() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
case 'Array#shift': {
rule('Having `Array#shift` in $coerce can be resolved');
example('f($coerce(Array.prototype.shift. "plustr"))', 'f("function shift() { [native code] }")');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function shift() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
case 'Array#unshift': {
rule('Having `Array#unshift` in $coerce can be resolved');
example('f($coerce(Array.prototype.unshift. "plustr"))', 'f("function unshift() { [native code] }")');
before(read.node, read.blockBody[read.blockIndex]);
before(read.blockBody[read.blockIndex]);

const finalNode = AST.primitive('function unshift() { [native code] }');
read.parentNode['arguments'][0] = finalNode;

after(finalNode, read.blockBody[read.blockIndex]);
after(read.blockBody[read.blockIndex]);
++changes;
return;
}
Expand Down
Loading

0 comments on commit 50afda2

Please sign in to comment.