Skip to content

Instantly share code, notes, and snippets.

@captbaritone
Created October 12, 2020 21:06
Show Gist options
  • Save captbaritone/75fcbd34b046be1154dc460798be9929 to your computer and use it in GitHub Desktop.
Save captbaritone/75fcbd34b046be1154dc460798be9929 to your computer and use it in GitHub Desktop.
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