Skip to content
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

Merged
merged 16 commits into from
Nov 13, 2016
Merged

Shorten assignments to itself #230

merged 16 commits into from
Nov 13, 2016

Conversation

kangax
Copy link
Member

@kangax kangax commented Oct 27, 2016

No description provided.

@kangax kangax added this to the 0.0.7 milestone Oct 27, 2016
@@ -1,6 +1,6 @@
{
"name": "babel-plugin-minify-simplify",
"version": "0.0.3",
"version": "0.0.4",
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, removed

Copy link
Member

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([
Copy link
Member Author

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.

@kangax kangax modified the milestone: 0.0.7 Oct 27, 2016
const canShorten = (
right.type === 'BinaryExpression' &&
operators.has(right.node.operator) &&
left.node.name === right.node.left.name
Copy link
Member

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' &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right.isBinaryExpression()

Copy link
Member Author

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) {
Copy link
Member

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.

Copy link
Member Author

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));
Copy link
Member

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 ?

Copy link
Member

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),
Copy link
Member

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]);
Copy link
Member

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.

Copy link
Member Author

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' &&
Copy link
Member

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)

@boopathi boopathi added the Tag: New Feature Pull Request adding a new feature label Oct 31, 2016
@kangax kangax changed the title Shorten assignments to same identifier Shorten assignments to itself Nov 1, 2016

function isEqual(arr1, arr2) {
return arr1.every((value, index) => {
return value.toString() === arr2[index].toString();
Copy link
Member Author

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' &&
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boopathi @shinew lmk if you see a more elegant/robust solution here, with both getPropNames and getName

Copy link
Contributor

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) ||
Copy link
Member Author

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()]

Copy link
Member

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++,
Copy link
Contributor

@shinew shinew Nov 1, 2016

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() &&
Copy link
Contributor

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?

Copy link
Member

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') {
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member

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) {
Copy link
Contributor

@shinew shinew Nov 1, 2016

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?

Copy link
Member Author

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');
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member

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 + '_'
Copy link
Contributor

@shinew shinew Nov 1, 2016

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;
}

Copy link
Member Author

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));
Copy link
Contributor

@shinew shinew Nov 1, 2016

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.

@kangax
Copy link
Member Author

kangax commented Nov 3, 2016

@boopathi @shinew fixed some things up, PTAL

@kangax
Copy link
Member Author

kangax commented Nov 10, 2016

@boopathi I'd love to merge this ASAP and cut a new release. Could you please take one last look?

@boopathi
Copy link
Member

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.

@shinew
Copy link
Contributor

shinew commented Nov 10, 2016

LGTM 👍

@kangax
Copy link
Member Author

kangax commented Nov 10, 2016

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) ||
Copy link
Member

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) {
Copy link
Member

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).

@kangax kangax merged commit 319d69d into master Nov 13, 2016
@kangax kangax deleted the assignment branch November 13, 2016 20:26
@kangax kangax added this to the 0.0.9 milestone Nov 13, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Tag: New Feature Pull Request adding a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants