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

Add prefer-ternary rule #514

Merged
merged 56 commits into from Sep 29, 2020
Merged

Add prefer-ternary rule #514

merged 56 commits into from Sep 29, 2020

Conversation

jmoore914
Copy link
Contributor

Fixes #486

Copy link
Collaborator

@fisker fisker left a comment

Choose a reason for hiding this comment

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

I think we can also add new , throw, await, yeild as well.

docs/rules/prefer-ternary.md Outdated Show resolved Hide resolved
index.js Outdated Show resolved Hide resolved
@sindresorhus

This comment has been minimized.

}
else{
throw Error(456)
}
Copy link
Owner

Choose a reason for hiding this comment

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

This should not be a ternary even with an option.

Copy link
Collaborator

@fisker fisker Feb 17, 2020

Choose a reason for hiding this comment

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

Better example should be

if (bar) {
	throw new Error('foo');
} else{
	throw new Error('bar');
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I'm not fully understanding. Are the two of you disagreeing that this should be a use case or are you agreeing that it is just a bad example?

Copy link
Owner

Choose a reason for hiding this comment

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

I'm saying that throw should never be made into a ternary. @fisker is commenting about the example itself as you're throwing a number, which is not valid, and the code style is off.

}
else{
baz()
}
Copy link
Owner

Choose a reason for hiding this comment

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

Same as https://github.com/sindresorhus/eslint-plugin-unicorn/pull/514/files#r380260375. It's a bad practice to put void function calls in a ternary. Only function calls that return a value should be in a ternary.

@sindresorhus
Copy link
Owner

sindresorhus commented Feb 17, 2020

Basically; If all the conditional branches assign to the same thing, it can be made a ternary, if not, we don't do anything. No options needed.

test/prefer-ternary.js Outdated Show resolved Hide resolved
@jmoore914
Copy link
Contributor Author

Basically; If all the conditional branches assign to the same thing, it can be made a ternary, if not, we don't do anything. No options needed.

Yep, that's what my thinking was as well. I would add return as well though or do you disagree? You could say that return would be assigning to the same value which is the function itself.

@sindresorhus
Copy link
Owner

If all the branches return, then yes, that would be possible too.

@fisker
Copy link
Collaborator

fisker commented Feb 17, 2020

As I understand, we can check the two branches has same node type.

Same AssignmentExpression: (only check left is the same)

if (foo) {
	bar = A;
} else {
	bar = B;
}

Same ReturnStatement:

if (foo) {
	return A;
} else {
	return B;
}

Same ThrowStatement:

if (foo) {
	throw A;
} else {
	throw B;
}

and same YieldExpression or same AwaitExpression


No need care what A B is, unless they are already a ternary. Even

if (foo) {
	return function() {};
} else {
	return a.b.c.d.e.f();
}

If you want, you can compare deeper and try merge the same part of A and B, for example:

if (foo) {
	return a(1, 2, 3, 4);
} else {
	return a(1, 2, 3, 5);
}

You can simply fix to return foo ? a(1, 2, 3, 4) : a(1, 2, 3, 5) or you can fix to return a(1, 2, 3, foo ? 4 : 5), if you want compare this two nodes. I think both acceptable.

@fisker
Copy link
Collaborator

fisker commented Feb 17, 2020

Another thing, I suggest alway add () to the condition, no-extra-parens rule can remove it if it's not necessary, this can avoid strange stuff

if (a = foo()) {return A} {return B} 
// return a = foo() ? A : B
// return (a = foo()) ? A : B

@jmoore914
Copy link
Contributor Author

@fisker Can you give me an example of an Await statement that would trigger the rule? I am not seeing how it is any different from a normal function call which we do not want included in the rule.

@fisker
Copy link
Collaborator

fisker commented Feb 28, 2020

if (foo) {
	await promise1;
} else {
	await promise2;
}

can fix to

await (foo ? promise1 : promise2)

@jmoore914
Copy link
Contributor Author

I'm not sure whether I would agree that a ternary is preferable in that case. @sindresorhus , do you have any thoughts regarding await?

@sindresorhus
Copy link
Owner

Ideally, it should be:

const something = foo ? promise1 : promise2;
await something;

But I think await (foo ? promise1 : promise2) is fine too.

@fisker

This comment has been minimized.

rules/prefer-ternary.js Outdated Show resolved Hide resolved
Comment on lines 27 to 39
function checkConsequentAndAlternateType(node) {
const consequentType = node.consequent.body[0].type;
return (consequentType === node.alternate.body[0].type &&
(consequentType === 'ReturnStatement' ||
(consequentType === 'ExpressionStatement' && checkConsequentAndAlternateExpressionStatement(node))));
}

function checkConsequentAndAlternateExpressionStatement(node) {
const consequentType = node.consequent.body[0].expression.type;
return consequentType === node.alternate.body[0].expression.type &&
(consequentType === 'AssignmentExpression' ? compareConsequentAndAlternateAssignments(node) :
consequentType === 'YieldExpression');
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This part is not good, you can compare type first, something like

if (isSameType(node)) {
  // Handle `AssignmentExpression`
  // ...
  // Handle `ReturnStatement`
  // ...
  // Handle `ThrowStatement`
  // ...
  // Handle `ExpressionStatement > AwaitExpression`
  // ...
  // ...
}

rules/prefer-ternary.js Outdated Show resolved Hide resolved
@fisker

This comment has been minimized.

Comment on lines +3 to +10
} catch (err) {
console.log(err);

if (test) {
throw a;
} else {
throw b;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have a trouble here, the catch-error-name and prevent-abbreviations fix err to error, and this rule is creating const error = test ? a : b; cause Parsing error: Identifier 'error' has already been declared.

I have no idea how to fix this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

@futpib

Glad to see you here, you are the one add avoidCapture(great util, BTW), do you have an idea how to fix this problem?

I was thinking use a global Map to manage the variables created by our rules, but I'm not sure if it will work. And this may still can't solve the real problem, if there is a new variable created by other rule from another plugin.

Copy link
Collaborator

@futpib futpib Jun 7, 2020

Choose a reason for hiding this comment

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

Even if we make all rules in eslint-plugin-unicorn generate non-conflicting names, we can still hit name conflicts across eslint plugins, so I think this is a eslint issue.

Having a plugin-global map seems like an ok workaround. (It should probably be a WeakMap<Scope, Set<string>> like in prevent-abbreviations)

// Holds a map from a `Scope` to a `Set` of new variable names generated by our fixer.
// Used to avoid generating duplicate names, see for instance `let errCb, errorCb` test.
const scopeToNamesGeneratedByFixer = new WeakMap();
const isSafeName = (name, scopes) => scopes.every(scope => {
const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
return !generatedNames || !generatedNames.has(name);
});

EDIT: A rule-global map is ok for this PR. Making it plugin-global is an issue for another PR to fix.

Copy link
Owner

Choose a reason for hiding this comment

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

we can still hit name conflicts across eslint plugins, so I think this is a eslint issue.

Can you open an issue on https://github.com/eslint/eslint ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@fisker
Copy link
Collaborator

fisker commented Jun 3, 2020

A note to myself, the if block can be some node's body, which will break by current fix, need investigate.

Example:

while (foo) if (test) {throw a} else {throw b}

Wrong:

while (foo) const error = test ? a : b;
throw error;

Correct:

while (foo) {
  const error = test ? a : b;
  throw error;
}

@sindresorhus
Copy link
Owner

Bump

@sindresorhus
Copy link
Owner

@jmoore914 Bump

@fisker
Copy link
Collaborator

fisker commented Sep 27, 2020

Fixed #514 (comment) with this suggestion.

#514 (comment) also fixed.

Ready for review.

@fisker
Copy link
Collaborator

fisker commented Sep 27, 2020

Result on prettier codebase fisker/prettier#807

@sindresorhus sindresorhus merged commit d59cf95 into sindresorhus:master Sep 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rule proposal: prefer-ternary
4 participants