216 lines
5.1 KiB
JavaScript
216 lines
5.1 KiB
JavaScript
import {
|
|
checkVueTemplate,
|
|
getParenthesizedRange,
|
|
getTokenStore,
|
|
} from './utils/index.js';
|
|
import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
|
|
import builtinErrors from './shared/builtin-errors.js';
|
|
import typedArray from './shared/typed-array.js';
|
|
|
|
const isInstanceofToken = token => token.value === 'instanceof' && token.type === 'Keyword';
|
|
|
|
const MESSAGE_ID = 'no-instanceof-builtins';
|
|
const MESSAGE_ID_SWITCH_TO_TYPE_OF = 'switch-to-type-of';
|
|
const messages = {
|
|
[MESSAGE_ID]: 'Avoid using `instanceof` for type checking as it can lead to unreliable results.',
|
|
[MESSAGE_ID_SWITCH_TO_TYPE_OF]: 'Switch to `typeof … === \'{{type}}\'`.',
|
|
};
|
|
|
|
const primitiveWrappers = new Set([
|
|
'String',
|
|
'Number',
|
|
'Boolean',
|
|
'BigInt',
|
|
'Symbol',
|
|
]);
|
|
|
|
const strictStrategyConstructors = [
|
|
// Error types
|
|
...builtinErrors,
|
|
|
|
// Collection types
|
|
'Map',
|
|
'Set',
|
|
'WeakMap',
|
|
'WeakRef',
|
|
'WeakSet',
|
|
|
|
// Arrays and Typed Arrays
|
|
'ArrayBuffer',
|
|
...typedArray,
|
|
|
|
// Data types
|
|
'Object',
|
|
|
|
// Regular Expressions
|
|
'RegExp',
|
|
|
|
// Async and functions
|
|
'Promise',
|
|
'Proxy',
|
|
|
|
// Other
|
|
'DataView',
|
|
'Date',
|
|
'SharedArrayBuffer',
|
|
'FinalizationRegistry',
|
|
];
|
|
|
|
const replaceWithFunctionCall = (node, context, functionName) => function * (fixer) {
|
|
const {left, right} = node;
|
|
const tokenStore = getTokenStore(context, node);
|
|
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
|
|
|
|
yield * fixSpaceAroundKeyword(fixer, node, context);
|
|
|
|
const range = getParenthesizedRange(left, {sourceCode: tokenStore});
|
|
yield fixer.insertTextBeforeRange(range, functionName + '(');
|
|
yield fixer.insertTextAfterRange(range, ')');
|
|
|
|
yield * replaceNodeOrTokenAndSpacesBefore(instanceofToken, '', fixer, context, tokenStore);
|
|
yield * replaceNodeOrTokenAndSpacesBefore(right, '', fixer, context, tokenStore);
|
|
};
|
|
|
|
const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
|
|
const {left, right} = node;
|
|
const tokenStore = getTokenStore(context, node);
|
|
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
|
|
const {sourceCode} = context;
|
|
|
|
// Check if the node is in a Vue template expression
|
|
const vueExpressionContainer = sourceCode.getAncestors(node).findLast(ancestor => ancestor.type === 'VExpressionContainer');
|
|
|
|
// Get safe quote
|
|
const safeQuote = vueExpressionContainer ? (sourceCode.getText(vueExpressionContainer)[0] === '"' ? '\'' : '"') : '\'';
|
|
|
|
yield * fixSpaceAroundKeyword(fixer, node, context);
|
|
|
|
const leftRange = getParenthesizedRange(left, {sourceCode: tokenStore});
|
|
yield fixer.insertTextBeforeRange(leftRange, 'typeof ');
|
|
|
|
yield fixer.replaceText(instanceofToken, '===');
|
|
|
|
const rightRange = getParenthesizedRange(right, {sourceCode: tokenStore});
|
|
|
|
yield fixer.replaceTextRange(rightRange, safeQuote + sourceCode.getText(right).toLowerCase() + safeQuote);
|
|
};
|
|
|
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
const create = context => {
|
|
const {
|
|
useErrorIsError = false,
|
|
strategy = 'loose',
|
|
include = [],
|
|
exclude = [],
|
|
} = context.options[0] ?? {};
|
|
|
|
const forbiddenConstructors = new Set(
|
|
strategy === 'strict'
|
|
? [...strictStrategyConstructors, ...include]
|
|
: include,
|
|
);
|
|
|
|
return {
|
|
/** @param {import('estree').BinaryExpression} node */
|
|
BinaryExpression(node) {
|
|
const {right, operator} = node;
|
|
|
|
if (right.type !== 'Identifier' || operator !== 'instanceof' || exclude.includes(right.name)) {
|
|
return;
|
|
}
|
|
|
|
const constructorName = right.name;
|
|
|
|
/** @type {import('eslint').Rule.ReportDescriptor} */
|
|
const problem = {
|
|
node,
|
|
messageId: MESSAGE_ID,
|
|
};
|
|
|
|
if (
|
|
constructorName === 'Array'
|
|
|| (constructorName === 'Error' && useErrorIsError)
|
|
) {
|
|
const functionName = constructorName === 'Array' ? 'Array.isArray' : 'Error.isError';
|
|
problem.fix = replaceWithFunctionCall(node, context, functionName);
|
|
return problem;
|
|
}
|
|
|
|
if (constructorName === 'Function') {
|
|
problem.fix = replaceWithTypeOfExpression(node, context);
|
|
return problem;
|
|
}
|
|
|
|
if (primitiveWrappers.has(constructorName)) {
|
|
problem.suggest = [
|
|
{
|
|
messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
|
|
data: {type: constructorName.toLowerCase()},
|
|
fix: replaceWithTypeOfExpression(node, context),
|
|
},
|
|
];
|
|
return problem;
|
|
}
|
|
|
|
if (!forbiddenConstructors.has(constructorName)) {
|
|
return;
|
|
}
|
|
|
|
return problem;
|
|
},
|
|
};
|
|
};
|
|
|
|
const schema = [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
useErrorIsError: {
|
|
type: 'boolean',
|
|
},
|
|
strategy: {
|
|
enum: [
|
|
'loose',
|
|
'strict',
|
|
],
|
|
},
|
|
include: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
exclude: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
];
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
const config = {
|
|
create: checkVueTemplate(create),
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Disallow `instanceof` with built-in objects',
|
|
recommended: 'unopinionated',
|
|
},
|
|
fixable: 'code',
|
|
schema,
|
|
defaultOptions: [{
|
|
useErrorIsError: false,
|
|
strategy: 'loose',
|
|
include: [],
|
|
exclude: [],
|
|
}],
|
|
hasSuggestions: true,
|
|
messages,
|
|
},
|
|
};
|
|
|
|
export default config;
|