Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

reflective-bind

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

reflective-bind - npm Package Compare versions

Comparing version 0.0.3-stalerender.1 to 0.0.3

568

babel/index.js

@@ -39,3 +39,2 @@ /**

const SKIP_RE = /^\/\/ @no-reflective-bind-babel$/m;
const ON_CALLBACK = /^on[A-Z].*$/;

@@ -45,3 +44,2 @@ module.exports = function(opts) {

let _fileName;
let _hoistedSlug;

@@ -74,4 +72,2 @@ let _hoistPath;

}
_fileName = file.opts.filename;
_hoistPath = path;

@@ -87,6 +83,3 @@ _hoistedSlug = hoistedSlug;

const state = {
shouldValidateStaleRender: true,
};
path.traverse(visitor, state);
path.traverse(visitor);

@@ -117,14 +110,3 @@ if (_needImport) {

JSXAttribute(path, state) {
// Don't transform ref callbacks
if (t.isJSXIdentifier(path.node.name)) {
if (path.node.name.name === "ref") {
path.skip();
} else if (ON_CALLBACK.test(path.node.name.name)) {
state.shouldValidateStaleRender = false;
}
}
},
JSXExpressionContainer(path, state) {
JSXExpressionContainer(path) {
const exprPath = path.get("expression");

@@ -136,3 +118,3 @@ if (t.isIdentifier(exprPath)) {

}
processPath(binding.path, state.shouldValidateStaleRender);
processPath(binding.path);

@@ -143,9 +125,6 @@ // constantViolations are just assignments to the variable after the

for (let i = 0, n = binding.constantViolations.length; i < n; i++) {
processPath(
binding.constantViolations[i],
state.shouldValidateStaleRender
);
processPath(binding.constantViolations[i]);
}
} else {
processPath(exprPath, state.shouldValidateStaleRender);
processPath(exprPath);
}

@@ -155,27 +134,22 @@ },

function processPath(path, shouldValidateStaleRender) {
function processPath(path) {
if (t.isVariableDeclarator(path)) {
processPath(path.get("init"), shouldValidateStaleRender);
processPath(path.get("init"));
} else if (t.isAssignmentExpression(path)) {
processPath(path.get("right"), shouldValidateStaleRender);
processPath(path.get("right"));
} else if (t.isConditionalExpression(path)) {
processPath(path.get("consequent"), shouldValidateStaleRender);
processPath(path.get("alternate"), shouldValidateStaleRender);
processPath(path.get("consequent"));
processPath(path.get("alternate"));
} else if (t.isCallExpression(path)) {
processCallExpression(path, shouldValidateStaleRender);
processCallExpression(path);
} else if (t.isArrowFunctionExpression(path)) {
processArrowFunctionExpression(path, shouldValidateStaleRender);
processArrowFunctionExpression(path);
}
}
function processCallExpression(path, shouldValidateStaleRender) {
const callee = path.node.callee;
function processCallExpression(path) {
if (
t.isMemberExpression(callee) &&
!callee.computed &&
t.isIdentifier(callee.property, {name: "bind"})
t.isMemberExpression(path.node.callee) &&
t.isIdentifier(path.node.callee.property, {name: "bind"})
) {
if (shouldValidateStaleRender) {
validateSafeBind(path.get("callee").get("object"));
}
_needImport = true;

@@ -191,417 +165,6 @@ path.replaceWith(

/**
* A function reference is deemed "safe" if the implementation does not
* return anything (not a render callback), or if there are no references to
* `this`.
*/
function validateSafeBind(boundObjectPath) {
const node = boundObjectPath.node;
// Handle function() {...}.bind(...)
if (t.isFunction(node)) {
validateSafeFunction(boundObjectPath, false);
return;
}
// Handle this.___.bind(...)
if (
t.isMemberExpression(node) &&
!node.computed &&
t.isThisExpression(node.object) &&
t.isIdentifier(node.property)
) {
const componentMethodPath = getComponentMethodPath(
boundObjectPath,
node.property.name
);
if (componentMethodPath) {
validateSafeFunction(componentMethodPath, false);
return;
}
}
// We don't know how to validate the bound object.
// Just emit warning to be safe.
handleStaleRenderViolation(
boundObjectPath,
"Don't know how to find method definition to validate for stale render."
);
}
function getComponentMethodPath(startSearchPath, fnName) {
let curPath = startSearchPath.getFunctionParent();
while (curPath) {
if (
curPath.parentPath &&
t.isProperty(curPath.parentPath.node) &&
curPath.parentPath.parentPath &&
t.isObjectExpression(curPath.parentPath.parentPath.node)
) {
return findFnPathByKey(
curPath.parentPath.parentPath.get("properties"),
fnName
);
}
if (
t.isClassMethod(curPath.node) &&
curPath.parentPath &&
t.isClassBody(curPath.parentPath.node)
) {
return findFnPathByKey(curPath.parentPath.get("body"), fnName);
}
if (
curPath.parentPath &&
t.isClassProperty(curPath.parentPath.node) &&
curPath.parentPath.parentPath &&
t.isClassBody(curPath.parentPath.parentPath.node)
) {
return findFnPathByKey(
curPath.parentPath.parentPath.get("body"),
fnName
);
}
curPath = curPath.parentPath;
}
return null;
}
function findFnPathByKey(pathList, keyName) {
for (let i = 0, n = pathList.length; i < n; i++) {
const path = pathList[i];
const node = path.node;
if (t.isIdentifier(node.key) && node.key.name === keyName) {
if (t.isFunction(path.node)) {
return path;
} else if (t.isFunction(path.node.value)) {
return path.get("value");
} else {
return null;
}
}
}
return null;
}
function validateSafeFunction(path, allowThisPropsState) {
function processArrowFunctionExpression(path) {
const state = {
allowThisPropsState,
// Does this arrow function return a value
hasReturn:
t.isArrowFunctionExpression(path.node) &&
!t.isBlockStatement(path.node.body),
// First unsafe call expression
unsafeCallExpression: null,
// First unsafe reference to `this`
unsafeThisPath: null,
};
path.traverse(componentMethodVisitor, state);
if (state.hasReturn && state.unsafeThisPath) {
handleStaleRenderViolation(
state.unsafeThisPath.parentPath,
"Dangerous reference to `this`."
);
}
if (state.hasReturn && state.unsafeCallExpression) {
handleStaleRenderViolation(
state.unsafeCallExpression,
"Dangerous call to function."
);
}
}
const componentMethodVisitor = {
CallExpression(path, state) {
state.unsafeCallExpression =
state.unsafeCallExpression ||
// If the node doesn't have a loc, then it is a new node that we
// created. Ignore these because the user can't take action on them.
(path.node.loc &&
!isCallExpressionWhitelisted(path) &&
isValueUsed(path)
? path
: null);
},
ReturnStatement(path, state) {
if (path.node.argument) {
state.hasReturn = true;
}
},
ThisExpression(path, state) {
if (inCallExpressionCallee(path)) {
// Don't reprocess call expressinos of the form this.*()
return;
}
state.unsafeThisPath =
state.unsafeThisPath ||
// If the node doesn't have a loc, then it is a new node that we
// created. Ignore these because the user can't take action on them.
(path.node.loc &&
isValueUsed(path) &&
!isSafeThisAccess(path, state.allowThisPropsState)
? path
: undefined);
},
};
function inCallExpressionCallee(path) {
let curPath = path;
while (curPath) {
const parentPath = curPath.parentPath;
if (!t.isMemberExpression(parentPath.node)) {
return (
t.isCallExpression(parentPath.node) &&
parentPath.node.callee === curPath.node
);
}
curPath = parentPath;
}
}
const WHITELISTED_FUNCTION_NAMES = {
isFinite: true,
isNaN: true,
parseFloat: true,
parseInt: true,
decodeURI: true,
decodeURIComponent: true,
encodeURI: true,
encodeURIComponent: true,
escape: true,
unescape: true,
};
// Just assume these methods are safe.
// Could be dangerous since we don't know the type of the object this method
// is being called on. Ideally this would be hooked into some sort of a type
// system.
const WHITELISTED_METHOD_NAMES = {
// Common methods from Array.prototype
concat: true,
entries: true,
every: true,
filter: true,
find: true,
findIndex: true,
forEach: true,
includes: true,
indexOf: true,
join: true,
keys: true,
lastIndexOf: true,
map: true,
reduce: true,
reduceRight: true,
slice: true,
some: true,
toSource: true,
values: true,
// Common methods from Date.prototype
getDate: true,
getDay: true,
getFullYear: true,
getHours: true,
getMilliseconds: true,
getMinutes: true,
getMonth: true,
getSeconds: true,
getTime: true,
getTimezoneOffset: true,
getUTCDate: true,
getUTCDay: true,
getUTCFullYear: true,
getUTCHours: true,
getUTCMilliseconds: true,
getUTCMinutes: true,
getUTCMonth: true,
getUTCSeconds: true,
getYear: true,
// Common methods from Function.prototype
apply: true,
bind: true,
call: true,
// Common methods from Number.prototype
toExponential: true,
toFixed: true,
toPrecision: true,
// Common methods from Object.prototype
hasOwnProperty: true,
isPrototypeOf: true,
propertyIsEnumerable: true,
toLocaleString: true,
toString: true,
valueOf: true,
// Common methods from RegExp.prototype
// exec: true, // Name too generic and implies side effects
test: true,
// Common methods from String.prototype
charAt: true,
charCodeAt: true,
codePointAt: true,
endsWith: true,
localeCompare: true,
match: true,
normalize: true,
padEnd: true,
padStart: true,
repeat: true,
split: true,
startsWith: true,
substr: true,
substring: true,
toLocaleLowerCase: true,
toLocaleUpperCase: true,
toLowerCase: true,
toUpperCase: true,
trim: true,
};
// If the value is `true`, then all static methods are allowed.
// Otherwise, provide a map of specific methods to whitelist.
const WHITELISTED_STATIC_METHODS = {
Array: true,
Math: true,
Intl: true,
JSON: true,
Number: true,
Object: {
create: true,
entries: true,
getOwnPropertyDescriptor: true,
getOwnPropertyDescriptors: true,
getOwnPropertyNames: true,
getOwnPropertySymbols: true,
getPrototypeOf: true,
is: true,
isExtensible: true,
isFrozen: true,
isSealed: true,
keys: true,
values: true,
},
String: true,
Symbol: true,
};
function isCallExpressionWhitelisted(callExpressionPath) {
if (isSetState(callExpressionPath.node)) {
return true;
}
const callee = callExpressionPath.node.callee;
if (t.isIdentifier(callee)) {
return isWhitelisted(WHITELISTED_FUNCTION_NAMES, callee.name);
}
if (
t.isMemberExpression(callee) &&
!callee.computed &&
t.isIdentifier(callee.property)
) {
const methodName = callee.property.name;
// Check if the method name is whitelisted
if (
(!t.isIdentifier(callee.object) || isLowerCase(callee.object.name)) &&
isWhitelisted(WHITELISTED_METHOD_NAMES, methodName)
) {
return true;
}
// Check if it is a call to a whitelisted static method
if (
t.isIdentifier(callee.object) &&
isWhitelisted(
WHITELISTED_STATIC_METHODS[callee.object.name],
methodName
)
) {
return true;
}
}
return false;
}
function isWhitelisted(obj, key) {
if (obj === true) {
return true;
}
return (
obj != null &&
typeof obj === "object" &&
Object.prototype.hasOwnProperty.call(obj, key) &&
obj[key]
);
}
function isSetState(node) {
return (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isThisExpression(node.callee.object) &&
t.isIdentifier(node.callee.property) &&
node.callee.property.name === "setState"
);
}
function isValueUsed(callExpressionPath) {
let curPath = callExpressionPath;
while (curPath) {
const curNode = curPath.node;
if (
t.isVariableDeclarator(curNode) ||
t.isAssignmentExpression(curNode) ||
t.isReturnStatement(curNode)
) {
return true;
}
if (
t.isArrowFunctionExpression(curNode) &&
!t.isBlockStatement(curNode.body)
) {
return true;
}
if (t.isFunction(curNode)) {
return false;
}
curPath = curPath.parentPath;
}
return false;
}
function isSafeThisAccess(thisPath, allowThisPropsState) {
const parentNode = thisPath.parentPath.node;
return (
allowThisPropsState &&
t.isMemberExpression(parentNode) &&
!parentNode.computed &&
t.isIdentifier(parentNode.property) &&
(parentNode.property.name === "props" ||
parentNode.property.name === "state")
);
}
/**
* Things that prevent arrow function from being hoisted:
* - Reference to identifier that is declared/reassigned after the fn.
* - If arrow function returns a value (render callback):
* - Any CallExpression that is not `this.props.___()`
* - Any ThisExpression that is not `this.props*` or `this.state*`
*/
function processArrowFunctionExpression(path, shouldValidateStaleRender) {
// Don't hoist if the arrow function is top level (defined in the same
// scope as the hoist path) since that is where it's going to be hoisted
// to anyways.
if (path.parentPath.scope === _hoistPath.scope) {
return;
}
const state = {
fnPath: path,
invalidIdentifier: null,
canHoist: true,
// Set of identifiers that are bound outside of the function.

@@ -614,21 +177,5 @@ outerIdentifierNames: new Set(),

path.traverse(arrowFnVisitor, state);
if (state.invalidIdentifier) {
// // eslint-disable-next-line no-console
// console.warn(
// "*** reflective-bind warning ***\n" +
// ` Not transforming arrow function because it closes over the variable "${state
// .invalidIdentifier.node
// .name}" that is assigned after the function definition.\n ` +
// _fileName +
// " " +
// JSON.stringify(state.invalidIdentifier.node.loc.start)
// );
if (!state.canHoist) {
return;
}
if (shouldValidateStaleRender) {
validateSafeFunction(path, true);
}
_needImport = true;

@@ -661,6 +208,2 @@

const arrowFnVisitor = {
Flow(path) {
path.skip();
},
Identifier(path, state) {

@@ -675,13 +218,5 @@ arrowFnIdentifier(path, state);

ThisExpression(path, state) {
// Ideally, we wouldn't need this check, but in babel 6.24.1 path.stop()
// does not actually stop the entire traversal, only the traversal of the
// remaining visitor keys of the parent node.
if (state.invalidIdentifier) {
path.stop();
return;
}
// Only allow hoisting if `this` refers to the same `this` as the arrow
// function we want to hoist.
if (!isDefinitelySameThisContext(state.fnPath, path)) {
if (!sameThisContext(state.fnPath, path)) {
return;

@@ -722,2 +257,6 @@ }

},
Flow(path) {
path.skip();
},
};

@@ -729,3 +268,3 @@

// remaining visitor keys of the parent node.
if (state.invalidIdentifier) {
if (!state.canHoist) {
path.stop();

@@ -738,3 +277,3 @@ return;

}
const canHoist =
state.canHoist =
// Since we have determined that this identifier is bound outside the

@@ -761,7 +300,6 @@ // scope of the function, we must pass this identifier into the hoisted

if (!canHoist) {
if (!state.canHoist) {
// This line unfortunately only stops traversing the visitor keys of the
// parent node, but ideally here it would stop the entire arrowFnVisitor
// traversal.
state.invalidIdentifier = path;
path.stop();

@@ -875,3 +413,3 @@ } else {

/* istanbul ignore if */
/* instanbul ignore if */
if (checkPathAncestorIdx < 0 || otherPathAncestorIdx <= 0) {

@@ -960,4 +498,3 @@ // This means that there is no common ancestor, which should never happen.

/**
* Returns true only if path is guaranteed to have the same `this` context as
* parentPath.
* Returns true only if path has the same `this` context as parentPath.
*

@@ -967,3 +504,3 @@ * This means that the function-ancestor chain must only consist of arrow

*/
function isDefinitelySameThisContext(parentFnPath, path) {
function sameThisContext(parentFnPath, path) {
let cur = path.getFunctionParent();

@@ -1064,50 +601,3 @@ while (cur) {

function isLowerCase(char) {
// Need the toUpperCase to cover non-letters
return char.toLowerCase() === char && char.toUpperCase() !== char;
}
function nodeToString(node) {
if (!node) {
return;
}
if (t.isNullLiteral(node)) {
// Must come before isLiteral
return "null";
} else if (t.isLiteral(node)) {
return node.extra.raw;
} else if (t.isIdentifier(node)) {
return node.name;
} else if (t.isThisExpression(node)) {
return "this";
} else if (t.isMemberExpression(node)) {
const objectStr = nodeToString(node.object);
const propertyStr = nodeToString(node.property);
return node.computed
? `${objectStr}[${propertyStr}]`
: `${objectStr}.${propertyStr}`;
} else if (t.isCallExpression(node)) {
const argsStr = node.arguments.map(nodeToString).join(", ");
return `${nodeToString(node.callee)}(${argsStr})`;
}
return `__${node.type}__`;
}
let _numStaleRenderViolations = 0;
function handleStaleRenderViolation(path, msg) {
_numStaleRenderViolations++;
const start = path.node.loc.start;
// eslint-disable-next-line no-console
console.warn(
"===== reflective-bind stale render warning =====\n" +
`${msg}\n\n` +
`${_fileName} ${start.line}:${start.column}\n` +
` ${nodeToString(path.node)}\n\n` +
`Total warnings: ${_numStaleRenderViolations}\n` +
"================================================\n"
);
}
return {visitor: rootVisitor};
};

2

package.json
{
"name": "reflective-bind",
"version": "0.0.3-stalerender.1",
"version": "0.0.3",
"description": "Eliminate wasteful re-rendering in React components caused by inline functions",

@@ -5,0 +5,0 @@ "author": "Dounan Shi",

@@ -6,8 +6,6 @@ [![Build Status](https://travis-ci.org/flexport/reflective-bind.svg?branch=master)](https://travis-ci.org/flexport/reflective-bind)

In React, using inline functions (arrow functions and `Function.prototype.bind`) in render will [cause pure components to wastefully re-render]((https://flexport.engineering/optimizing-react-rendering-part-1-9634469dca02)). As a result, many React developers encourage you to [never use inline functions](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md) in render. However, others think that [avoiding them is premature optimization](https://cdb.reacttraining.com/react-inline-functions-and-performance-bdff784f5578).
With reflective-bind, you can freely use inline functions in render without worrying about wasteful re-rendering of React pure components. The best part is, it requires almost no code change 🙌
With reflective-bind, you can freely use inline functions in render without worrying about wasteful re-rendering of pure components.
[Check out our blog post](https://flexport.engineering/ending-the-debate-on-inline-functions-in-react-8c03fabd144) for more info on the motivation and the inner workings of reflective-bind.
The best part is, it requires almost no code change 🙌
## Installation

@@ -14,0 +12,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc