Created
October 12, 2020 21:06
-
-
Save captbaritone/75fcbd34b046be1154dc460798be9929 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const EQUALITY_OPERATORS = new Set(['==', '===', '!=', '!==']); | |
module.exports = { | |
meta: { | |
messages: { | |
constant: 'Constant condition.', | |
coalescingNeverNullish: 'Constant condition. Value can never be nullish.', | |
coalescingAlwaysNullish: | |
'Constant condition. Value will always be nullish.', | |
}, | |
}, | |
create(context) { | |
return { | |
BinaryExpression(node) { | |
if (!EQUALITY_OPERATORS.has(node.operator)) { | |
return; | |
} | |
const scope = context.getScope(); | |
const resolvedLeft = resolve(node.left, scope); | |
const resolvedRight = resolve(node.right, scope); | |
if ( | |
(isNullish(resolvedLeft, scope) && | |
constantNullishness(resolvedRight, scope)) || | |
(isNullish(resolvedRight, scope) && | |
constantNullishness(resolvedLeft, scope)) | |
) { | |
context.report({node, messageId: 'constant'}); | |
} | |
}, | |
'LogicalExpression[operator="??"]'(node) { | |
const scope = context.getScope(); | |
const resolvedLeft = resolve(node.left, scope); | |
if (canNeverBeNullOrUndefined(resolvedLeft, scope)) { | |
context.report({ | |
node: node.left, | |
messageId: 'coalescingNeverNullish', | |
}); | |
} | |
if ( | |
willAlwaysBeNull(resolvedLeft, scope) || | |
willAlwaysBeUndefined(resolvedLeft, scope) | |
) { | |
context.report({ | |
node: node.left, | |
messageId: 'coalescingAlwaysNullish', | |
}); | |
} | |
}, | |
}; | |
}, | |
}; | |
// Note that we not only assert that the node's nullisheness constant, but | |
// also that its type of nullishness (null/undefined) is constant. | |
// In short, the node can't vary between null and undefined. | |
function constantNullishness(node, scope) { | |
return ( | |
canNeverBeNullOrUndefined(node, scope) || | |
willAlwaysBeUndefined(node, scope) || | |
willAlwaysBeNull(node, scope) | |
); | |
} | |
function isNullish(node, scope) { | |
return willAlwaysBeNull(node, scope) || willAlwaysBeUndefined(node, scope); | |
} | |
function canNeverBeNullOrUndefined(node, scope) { | |
// Splitting this up into separate checks is somewhat inefficient, but it | |
// makes the code easier to read and it opens the door to using these | |
// primitives for other types of checks. | |
if ( | |
isObject(node, scope) || | |
isBoolean(node, scope) || | |
isString(node, scope) || | |
isNumber(node, scope) | |
) { | |
return true; | |
} | |
switch (node.type) { | |
case 'Literal': | |
return node.value !== null; | |
case 'AssignmentExpression': | |
if (node.operator !== '=') { | |
return false; | |
} | |
return canNeverBeNullOrUndefined(resolve(node.right, scope), scope); | |
} | |
return false; | |
} | |
function willAlwaysBeUndefined(node, scope) { | |
switch (node.type) { | |
case 'Identifier': | |
return node.name === 'undefined'; | |
case 'UnaryExpression': | |
return node.operator === 'void'; | |
case 'AssignmentExpression': | |
if (node.operator !== '=') { | |
return false; | |
} | |
return willAlwaysBeUndefined(resolve(node.right, scope), scope); | |
} | |
return false; | |
} | |
function willAlwaysBeNull(node, scope) { | |
switch (node.type) { | |
case 'Literal': | |
return node.value === null; | |
case 'AssignmentExpression': | |
if (node.operator !== '=') { | |
return false; | |
} | |
return willAlwaysBeNull(resolve(node.right, scope), scope); | |
} | |
return false; | |
} | |
function isObject(node, scope) { | |
switch (node.type) { | |
case 'ObjectExpression': | |
case 'ArrayExpression': | |
case 'ArrowFunctionExpression': | |
case 'FunctionExpression': | |
case 'JSXElement': | |
case 'JSXFragment': | |
case 'ClassExpression': | |
case 'NewExpression': | |
return true; | |
case 'Literal': | |
return node.value instanceof RegExp; | |
case 'AssignmentExpression': | |
if (node.operator !== '=') { | |
return false; | |
} | |
return isObject(resolve(node.right, scope), scope); | |
} | |
return false; | |
} | |
function isBoolean(node, scope) { | |
switch (node.type) { | |
case 'BinaryExpression': | |
return true; | |
case 'UnaryExpression': | |
return node.operator === '!' || node.operator === 'delete'; | |
case 'Literal': | |
return typeof node.value === 'boolean'; | |
case 'AssignmentExpression': | |
if (node.operator !== '=') { | |
return false; | |
} | |
return isString(resolve(node.right, scope), scope); | |
case 'CallExpression': | |
return ( | |
node.callee.type === 'Identifier' && node.callee.name === 'Boolean' | |
); | |
} | |
return false; | |
} | |
function isString(node, scope) { | |
switch (node.type) { | |
case 'TemplateLiteral': | |
return true; | |
case 'Literal': | |
return typeof node.value === 'string'; | |
case 'AssignmentExpression': | |
// Possible improvement: += might result in a string | |
if (node.operator !== '=') { | |
return false; | |
} | |
return isString(resolve(node.right, scope), scope); | |
case 'CallExpression': | |
return node.callee.type === 'Identifier' && node.callee.name === 'String'; | |
case 'UnaryExpression': | |
return node.operator === 'typeof'; | |
} | |
return false; | |
} | |
function isNumber(node, scope) { | |
switch (node.type) { | |
case 'UpdateExpression': | |
return true; | |
case 'AssignmentExpression': | |
// Possible improvement: +=, -= and others might result in a number | |
if (node.operator !== '=') { | |
return false; | |
} | |
return isNumber(resolve(node.right, scope), scope); | |
case 'UnaryExpression': | |
return ( | |
node.operator === '+' || node.operator === '-' || node.operator === '~' | |
); | |
case 'Literal': | |
return typeof node.value === 'number'; | |
case 'CallExpression': | |
return node.callee.type === 'Identifier' && node.callee.name === 'Number'; | |
} | |
return false; | |
} | |
function getVariable(name, scope) { | |
let currentScope = scope; | |
while (currentScope) { | |
const variable = currentScope.set.get(name); | |
if (variable) { | |
return variable; | |
} | |
currentScope = currentScope.upper; | |
} | |
return null; | |
} | |
// Given a node, if it's an Identifier, try to recursively resolve it to its | |
// concrete definition. If that is not possible, then we return the Identifier | |
// node. | |
function resolve(_node, scope) { | |
let node = _node; | |
while (node.type === 'Identifier') { | |
const variable = getVariable(node.name, scope); | |
if (variable == null || variable.defs == null) { | |
// If it's not in scope, some other lint rule will be yelling. | |
return node; | |
} | |
// Gets the last variable identity | |
const variableDefs = variable.defs; | |
const def = variableDefs[variableDefs.length - 1]; | |
if (def == null || def.type !== 'Variable' || variableDefs.length !== 1) { | |
// Parameter or an unusual pattern. Bail out. | |
// In the future we could detect things like function or class declarations. | |
return node; | |
} | |
if ( | |
def.kind !== 'const' && | |
variable.references.filter(ref => ref.isWrite()).length !== 1 | |
) { | |
return node; | |
} | |
const init = def.node.init; | |
if ( | |
init == null || | |
// Ignore complicated cases like destructuring for now. | |
def.node.id.type !== 'Identifier' | |
) { | |
return node; | |
} | |
node = init; | |
} | |
return node; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment