UNPKG

eslint-plugin-unicorn

Version:
208 lines (191 loc) 5.15 kB
import {getStaticValue, getPropertyName} from '@eslint-community/eslint-utils'; import {isMethodCall} from './ast/index.js'; const MESSAGE_ID_OBJECT = 'no-thenable-object'; const MESSAGE_ID_EXPORT = 'no-thenable-export'; const MESSAGE_ID_CLASS = 'no-thenable-class'; const messages = { [MESSAGE_ID_OBJECT]: 'Do not add `then` to an object.', [MESSAGE_ID_EXPORT]: 'Do not export `then`.', [MESSAGE_ID_CLASS]: 'Do not add `then` to a class.', }; const isStringThen = (node, context) => getStaticValue(node, context.sourceCode.getScope(node))?.value === 'then'; // Resolves the property/member key name, avoiding the cost of `sourceCode.getScope()` // for the common non-computed and string-literal cases. Only computed keys that // reference variables need scope resolution. const isThenKey = (node, context) => { const keyNode = node.type === 'MemberExpression' ? node.property : node.key; if (keyNode.type === 'Literal') { return keyNode.value === 'then'; } if (!node.computed && keyNode.type === 'Identifier') { return keyNode.name === 'then'; } return getPropertyName(node, context.sourceCode.getScope(node)) === 'then'; }; const cases = [ // `{then() {}}`, // `{get then() {}}`, // `{[computedKey]() {}}`, // `{get [computedKey]() {}}`, { selector: 'ObjectExpression', * getNodes(node, context) { for (const property of node.properties) { if (property.type === 'Property' && isThenKey(property, context)) { yield property.key; } } }, messageId: MESSAGE_ID_OBJECT, }, // `class Foo {then}`, // `class Foo {static then}`, // `class Foo {get then() {}}`, // `class Foo {static get then() {}}`, { selectors: ['PropertyDefinition', 'MethodDefinition'], * getNodes(node, context) { if (isThenKey(node, context)) { yield node.key; } }, messageId: MESSAGE_ID_CLASS, }, // `foo.then = …` // `foo[computedKey] = …` { selector: 'MemberExpression', * getNodes(node, context) { if (!(node.parent.type === 'AssignmentExpression' && node.parent.left === node)) { return; } if (isThenKey(node, context)) { yield node.property; } }, messageId: MESSAGE_ID_OBJECT, }, // `Object.defineProperty(foo, 'then', …)` // `Reflect.defineProperty(foo, 'then', …)` { selector: 'CallExpression', * getNodes(node, context) { if (!isMethodCall(node, { objects: ['Object', 'Reflect'], method: 'defineProperty', minimumArguments: 3, optionalCall: false, optionalMember: false, }) || node.arguments[0].type === 'SpreadElement') { return; } const [, secondArgument] = node.arguments; if (isStringThen(secondArgument, context)) { yield secondArgument; } }, messageId: MESSAGE_ID_OBJECT, }, // `Object.fromEntries([['then', …]])` { selector: 'CallExpression', * getNodes(node, context) { if (!( isMethodCall(node, { object: 'Object', method: 'fromEntries', argumentsLength: 1, optionalCall: false, optionalMember: false, }) && node.arguments[0].type === 'ArrayExpression' )) { return; } for (const pairs of node.arguments[0].elements) { if (!(pairs?.type === 'ArrayExpression' && pairs.elements[0]) || pairs.elements[0].type === 'SpreadElement') { continue; } const [key] = pairs.elements; if (isStringThen(key, context)) { yield key; } } }, messageId: MESSAGE_ID_OBJECT, }, // `export {then}` { selector: 'Identifier', * getNodes(node) { if ( node.name === 'then' && node.parent.type === 'ExportSpecifier' && node.parent.exported === node ) { yield node; } }, messageId: MESSAGE_ID_EXPORT, }, // `export function then() {}`, // `export class then {}`, { selector: 'Identifier', * getNodes(node) { if ( node.name === 'then' && (node.parent.type === 'FunctionDeclaration' || node.parent.type === 'ClassDeclaration') && node.parent.id === node && node.parent.parent.type === 'ExportNamedDeclaration' && node.parent.parent.declaration === node.parent ) { yield node; } }, messageId: MESSAGE_ID_EXPORT, }, // `export const … = …`; { selector: 'VariableDeclaration', * getNodes(node, context) { if (!(node.parent.type === 'ExportNamedDeclaration' && node.parent.declaration === node)) { return; } for (const variable of context.sourceCode.getDeclaredVariables(node)) { if (variable.name === 'then') { yield * variable.identifiers; } } }, messageId: MESSAGE_ID_EXPORT, }, ]; /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { for (const {selector, selectors, messageId, getNodes} of cases) { context.on(selector ?? selectors, function * (node) { for (const problematicNode of getNodes(node, context)) { yield {node: problematicNode, messageId}; } }); } }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'problem', docs: { description: 'Disallow `then` property.', recommended: 'unopinionated', }, messages, languages: [ 'js/js', ], }, }; export default config;