-
-
Notifications
You must be signed in to change notification settings - Fork 225
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
Shorten assignments to itself #230
Conversation
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "babel-plugin-minify-simplify", | |||
"version": "0.0.3", | |||
"version": "0.0.4", |
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.
this should be done by lerna and not manually.
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.
ok, removed
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.
don't need to change package.json either
@@ -28,6 +28,17 @@ module.exports = ({ types: t }) => { | |||
const or = (a, b) => t.logicalExpression("||", a, b); | |||
const and = (a, b) => t.logicalExpression("&&", a, b); | |||
|
|||
const operators = new Set([ |
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.
We need to consider splitting plugins into multiple modules. This kind of mixing of helpers and supplemental stuff is not great ^. I think we could even strive for 1 type-of-transform per file.
const canShorten = ( | ||
right.type === 'BinaryExpression' && | ||
operators.has(right.node.operator) && | ||
left.node.name === right.node.left.name |
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.
requires isIdentifier
check. if left.node
and right.node.left
are not identifiers, it can end up in undefined === undefined
check and canShorten will be true - eg: [a,b] = foo() + 1
- canShorten will be true for this.
const left = path.get('left'); | ||
|
||
const canShorten = ( | ||
right.type === 'BinaryExpression' && |
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.
right.isBinaryExpression()
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.
Good to know, thanks!
right.node.right.value === 1 && | ||
updateOperators.has(right.node.operator)); | ||
|
||
if (left.node.name === undefined) { |
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.
left.isMemberExpression()
is better IMO, rather than depending on left not being an identifier.
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.
I wasn't sure if it's always going to be member expression. This felt like a broader/safer check. But sure, probably ok to change it.
|
||
const newExpression = t.updateExpression( | ||
right.node.operator + right.node.operator, | ||
t.identifier(left.node.name)); |
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.
probably needs to handle member expression here ?
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.
Wonder if we can make a generic utility for handling both Identifier/MemberExpression?
|
||
const newExpression = t.assignmentExpression( | ||
right.node.operator + '=', | ||
t.identifier(left.node.name), |
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.
handle memberExpressions
]; | ||
|
||
actual.forEach((test, index) => { | ||
expect(transform(test)).toBe(expected[index]); |
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.
expect(actual.map((s) => transform(s))).toEqual(expected);
here you compare two arrays and even if one fails in the list, the others still run. But when you do forEach
when one of them fails, all else that follows won't be run at all.
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.
So you're thinking to wrap them in try/catch?
if (!canShorten) return; | ||
|
||
const canBeUpdateExpression = ( | ||
right.node.right.type === 'NumericLiteral' && |
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.
right.get("right").isNumericLiteral()
or t.isNumericLiteral(right.node.right)
|
||
function isEqual(arr1, arr2) { | ||
return arr1.every((value, index) => { | ||
return value.toString() === arr2[index].toString(); |
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.
Not using lodash.isequal here coz of slightly custom logic
const prop = path.get('property'); | ||
const propNames = [getName(prop.node)]; | ||
|
||
while (obj.type !== 'Identifier' && |
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.
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.
This is just to traverse the chain of MemberExpressions like a.b.c.d
, right? If so, can't we just let the condition be t.isMemberExpression(node)
?
const rightPropNames = getPropNames(rightExpr.get('left')); | ||
|
||
if (!leftPropNames || | ||
leftPropNames.includes(undefined) || |
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.
We bail out whenever we can't resolve "name" of one of the properties in the chain; e.g. foo[bar()]
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.
.indexOf
instead of .includes
for node4
foo *= function () {}, | ||
foo += 123, | ||
foo = 1 + foo, | ||
x = x++, |
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.
Not equivalent: if x = 0
at the start, then x = x = x + 1
makes x = 1
, while x = x++
makes x = 0
(tested in Chrome). Maybe x = ++x
is what we want? (see comment about prefix = true
below)
const leftExpr = path.get('left'); | ||
|
||
const canBeUpdateExpression = ( | ||
rightExpr.get('right').isNumericLiteral() && |
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.
What's the behavior if .get('right')
fails?
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.
Should be ok - gives a path anyway? http://astexplorer.net/#/qyjXEtbUxr
} | ||
|
||
function getName(node) { | ||
if (node.type === 'ThisExpression') { |
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.
nit: why not t.isThisExpression
or t.isSuper
?
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.
I don't really care; I would just want us to be consistent in the entire codebase. Do we want abstractions over performance or vice versa? Ditto with .get(...)
vs node access. Let's chose one and stick to that?
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.
It's also about being able to use type aliases. So t.isSomething
is a better check that you don't have to worry about whether it's an alias or not. If we forget that it's an alias and add some code, it'll be buggy and hard to debug 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.
Unless we start compiling t.isThisExpression
-> node.type === 'ThisExpression'
😛 - I was saying we could write a few linting rules for these
'+', '-' | ||
]); | ||
|
||
function isEqual(arr1, arr2) { |
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.
This function should be isEqualToPrefix
rather than isEqual
. isEqual([1], [1, 2]) === true
given this implementation -- is this expected?
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.
Ugh, good catch. So the reason it works is because property names are resolved RTL so foo.bar
won't match foo.bar.baz
due to arrays being ['bar', 'foo']
and ['baz', 'bar', 'foo']
. We can maybe do an additional length check which could be a nice compromise between speed and correctness?
if (node) { | ||
propNames.push(getName(node)); | ||
} | ||
obj = obj.get('object'); |
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.
nit: .get()
calls are more expensive than just directly referencing the nodes' children.
It's probably faster/clearer to use nodes instead of .get()
, which returns paths. This doesn't need path-specific methods, like scoping.
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.
get
was recommended by @boopathi so I tried to stick to that, but see response above. Let's just figure out which to use everywhere.
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.
it's useful to have path in many places especially when there is a bug... we can optimize to nodes later when we have it correct. path has more information and we might not know when we have a bug in the code. I faced issues where some information from path(at least parentPath) would be required to solve the issue.
// string/number literals | ||
// but still match against each other | ||
return node.name | ||
? node.name + '_' |
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.
This seems a bit hacky -- would it make sense to return an object with "type" and "value" properties instead?
Also, since the type of .property
is technically either Expression or Literal, it might be better to be more explicit:
if (t.isIdentifier(node)) {
return {'type': 'id', 'value': node.name};
} else if (t.isLiteral(node)) {
return {'type': 'lit', 'value': node.value};
} else {
return undefined;
}
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.
I thought about object-based abstraction but wasn't sure about perf. impact of all the "unnecessary" intermediate objects. Definitely would be less hackish.
The reason I went with duck-typing instead of explicit isIdentifier
/isLiteral
is in case I was forgetting some other abstraction. I'm also not sure about assumption of if something is a literal it will have a value and so on.
I just checked and I see that NullLiteral
doesn't have value. Neither does TemplateLiteral :/
null
also throws since I call toString
on it instead of String(...)
.
Let me clean this up a little.
if (canBeUpdateExpression) { | ||
newExpression = t.updateExpression( | ||
rightExpr.node.operator + rightExpr.node.operator, | ||
t.clone(leftExpr.node)); |
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.
Adding prefix = true
would probably resolve the prefix/postfix issues, since x += 1
and ++x
returns the new value of x
, while x++
returns the old value of x
.
@boopathi I'd love to merge this ASAP and cut a new release. Could you please take one last look? |
I'd try using it with other plugins - especially block-scoping, dce and simplify. Mangler is in broken state. Let's fix that before a release. |
LGTM 👍 |
I'm checking this addition on our codebase with all the other plugins. Agreed about mangler. |
if (!leftPropNames || | ||
leftPropNames.includes(undefined) || | ||
!rightPropNames || | ||
rightPropNames.includes(undefined) || |
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.
indexOf
"+", "-" | ||
]); | ||
|
||
function isEqual(arr1, arr2) { |
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.
this is specific to this feature, we should have a comment or change name to indicate that, since this is at the top level (nearly).
No description provided.