Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): [prefer-nullish-coalescing] add support for assignment expressions#10152
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
1e3281a
3d6a209
df78ea3
ff5db69
2a705a3
4c3fb24
731cb4a
52066e0
da2aeb8
de4fb70
bc0786e
22841da
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -56,10 +56,10 @@ export default createRule<Options, MessageIds>({ | ||
noStrictNullCheck: | ||
'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', | ||
preferNullishOverOr: | ||
'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a logical{{ description }}(`||{{ equals }}`), as it is a safer operator.', | ||
preferNullishOverTernary: | ||
'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a ternary expression, as it is simpler to read.', | ||
suggestNullish: 'Fix to nullish coalescing operator (`??{{ equals }}`).', | ||
}, | ||
schema: [ | ||
{ | ||
@@ -171,7 +171,107 @@ export default createRule<Options, MessageIds>({ | ||
}); | ||
} | ||
// todo: rename to something more specific? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more.
What's the resolution here? FWIW I don't mind the name as-is. | ||
function checkAssignmentOrLogicalExpression( | ||
node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, | ||
description: string, | ||
equals: string, | ||
): void { | ||
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); | ||
const type = checker.getTypeAtLocation(tsNode.left); | ||
if (!isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { | ||
return; | ||
} | ||
if (ignoreConditionalTests === true && isConditionalTest(node)) { | ||
return; | ||
} | ||
if ( | ||
ignoreMixedLogicalExpressions === true && | ||
isMixedLogicalExpression(node) | ||
) { | ||
return; | ||
} | ||
// https://github.com/typescript-eslint/typescript-eslint/issues/5439 | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
const ignorableFlags = [ | ||
(ignorePrimitives === true || ignorePrimitives!.bigint) && | ||
ts.TypeFlags.BigIntLike, | ||
(ignorePrimitives === true || ignorePrimitives!.boolean) && | ||
ts.TypeFlags.BooleanLike, | ||
(ignorePrimitives === true || ignorePrimitives!.number) && | ||
ts.TypeFlags.NumberLike, | ||
(ignorePrimitives === true || ignorePrimitives!.string) && | ||
ts.TypeFlags.StringLike, | ||
] | ||
.filter((flag): flag is number => typeof flag === 'number') | ||
.reduce((previous, flag) => previous | flag, 0); | ||
if ( | ||
type.flags !== ts.TypeFlags.Null && | ||
type.flags !== ts.TypeFlags.Undefined && | ||
(type as ts.UnionOrIntersectionType).types.some(t => | ||
tsutils | ||
.intersectionTypeParts(t) | ||
.some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), | ||
) | ||
) { | ||
return; | ||
} | ||
/* eslint-enable @typescript-eslint/no-non-null-assertion */ | ||
const barBarOperator = nullThrows( | ||
context.sourceCode.getTokenAfter( | ||
node.left, | ||
token => | ||
token.type === AST_TOKEN_TYPES.Punctuator && | ||
token.value === node.operator, | ||
), | ||
NullThrowsReasons.MissingToken('operator', node.type), | ||
); | ||
function* fix( | ||
fixer: TSESLint.RuleFixer, | ||
): IterableIterator<TSESLint.RuleFix> { | ||
if (isLogicalOrOperator(node.parent)) { | ||
// '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c) | ||
if ( | ||
node.left.type === AST_NODE_TYPES.LogicalExpression && | ||
!isLogicalOrOperator(node.left.left) | ||
) { | ||
yield fixer.insertTextBefore(node.left.right, '('); | ||
} else { | ||
yield fixer.insertTextBefore(node.left, '('); | ||
} | ||
yield fixer.insertTextAfter(node.right, ')'); | ||
} | ||
yield fixer.replaceText( | ||
barBarOperator, | ||
node.operator.replace('||', '??'), | ||
); | ||
} | ||
context.report({ | ||
node: barBarOperator, | ||
messageId: 'preferNullishOverOr', | ||
data: { description, equals }, | ||
suggest: [ | ||
{ | ||
messageId: 'suggestNullish', | ||
data: { equals }, | ||
fix, | ||
}, | ||
], | ||
}); | ||
} | ||
return { | ||
'AssignmentExpression[operator = "||="]'( | ||
node: TSESTree.AssignmentExpression, | ||
): void { | ||
checkAssignmentOrLogicalExpression(node, 'assignment', '='); | ||
}, | ||
ConditionalExpression(node: TSESTree.ConditionalExpression): void { | ||
if (ignoreTernaryTests) { | ||
return; | ||
@@ -200,7 +300,7 @@ export default createRule<Options, MessageIds>({ | ||
node.test.right.left, | ||
node.test.right.right, | ||
]; | ||
if (['||','||='].includes(node.test.operator)) { | ||
if ( | ||
node.test.left.operator === '===' && | ||
node.test.right.operator === '===' | ||
@@ -304,9 +404,12 @@ export default createRule<Options, MessageIds>({ | ||
context.report({ | ||
node, | ||
messageId: 'preferNullishOverTernary', | ||
// TODO: also account for = in the ternary clause | ||
data: { equals: '' }, | ||
suggest: [ | ||
{ | ||
messageId: 'suggestNullish', | ||
data: { equals: '' }, | ||
fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { | ||
const [left, right] = | ||
operator === '===' || operator === '==' | ||
@@ -325,90 +428,10 @@ export default createRule<Options, MessageIds>({ | ||
}); | ||
} | ||
}, | ||
'LogicalExpression[operator = "||"]'( | ||
node: TSESTree.LogicalExpression, | ||
): void { | ||
checkAssignmentOrLogicalExpression(node, 'or', ''); | ||
}, | ||
}; | ||
}, | ||
@@ -451,7 +474,9 @@ function isConditionalTest(node: TSESTree.Node): boolean { | ||
return false; | ||
} | ||
function isMixedLogicalExpression( | ||
node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, | ||
): boolean { | ||
const seen = new Set<TSESTree.Node | undefined>(); | ||
const queue = [node.parent, node.left, node.right]; | ||
for (const current of queue) { | ||
@@ -463,7 +488,7 @@ function isMixedLogicalExpression(node: TSESTree.LogicalExpression): boolean { | ||
if (current.type === AST_NODE_TYPES.LogicalExpression) { | ||
if (current.operator === '&&') { | ||
return true; | ||
} else if (['||','||='].includes(current.operator)) { | ||
// check the pieces of the node to catch cases like `a || b || c && d` | ||
queue.push(current.parent, current.left, current.right); | ||
} | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.