UNPKG

eslint-plugin-canonical

Version:
268 lines (267 loc) 11.7 kB
"use strict"; /* eslint-disable jsdoc/no-undefined-types */ /* eslint-disable complexity */ /* eslint-disable func-style */ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("@typescript-eslint/utils"); const utilities_1 = require("../utilities"); /** * @file Rule to flag non-matching identifiers * @author Matthieu Larcher * * Adapted from https://github.com/eslint/eslint/blob/c4fffbcb089182d425ef1d5e45134fecc0e2da46/lib/rules/id-match.js * Related discussion about not adding this option to ESLint https://github.com/eslint/eslint/issues/14005 */ /** * Checks if a parent of a node is an ObjectPattern. * * @returns {boolean} if the node is inside an ObjectPattern * @private */ const isInsideObjectPattern = (node) => { let { parent } = node; while (parent) { if (parent.type === 'ObjectPattern') { return true; } parent = parent.parent; } return false; }; const defaultOptions = { classFields: false, ignoreDestructuring: false, ignoreNamedImports: false, onlyDeclarations: false, properties: false, }; exports.default = (0, utilities_1.createRule)({ create: (context, [inputPattern, options]) => { const pattern = inputPattern !== null && inputPattern !== void 0 ? inputPattern : '^.+$'; const regexp = new RegExp(pattern, 'u'); const checkProperties = Boolean(options === null || options === void 0 ? void 0 : options.properties); const checkClassFields = Boolean(options === null || options === void 0 ? void 0 : options.classFields); const onlyDeclarations = Boolean(options === null || options === void 0 ? void 0 : options.onlyDeclarations); const ignoreDestructuring = Boolean(options === null || options === void 0 ? void 0 : options.ignoreDestructuring); const ignoreNamedImports = Boolean(options === null || options === void 0 ? void 0 : options.ignoreNamedImports); // Contains reported nodes to avoid reporting twice on destructuring with shorthand notation const reportedNodes = new Set(); const ALLOWED_PARENT_TYPES = new Set(['CallExpression', 'NewExpression']); const DECLARATION_TYPES = new Set([ 'FunctionDeclaration', 'VariableDeclarator', ]); const IMPORT_TYPES = new Set([ 'ImportSpecifier', 'ImportNamespaceSpecifier', 'ImportDefaultSpecifier', ]); /** * Checks if a string matches the provided pattern * * @param {string} name The string to check. * @returns {boolean} if the string is a match * @private */ const isInvalid = (name) => { return !regexp.test(name); }; /** * Verifies if we should report an error or not based on the effective * parent node and the identifier name. * * @param {ASTNode} effectiveParent The effective parent node of the node to be reported * @param {string} name The identifier name of the identifier node * @returns {boolean} whether an error should be reported or not */ const shouldReport = (effectiveParent, name) => { return ((!onlyDeclarations || DECLARATION_TYPES.has(effectiveParent.type)) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type) && isInvalid(name)); }; /** * Reports an AST node as a rule violation. * * @param {ASTNode} node The node to report. * @returns {void} * @private */ const report = (node) => { /* * We used the range instead of the node because it's possible * for the same identifier to be represented by two different * nodes, with the most clear example being shorthand properties: * { foo } * In this case, "foo" is represented by one node for the name * and one for the value. The only way to know they are the same * is to look at the range. */ if (!reportedNodes.has(node.range.toString())) { const messageId = node.type === utils_1.AST_NODE_TYPES.PrivateIdentifier ? 'notMatchPrivate' : 'notMatch'; context.report({ data: { name: node.name, pattern, }, messageId, node, }); reportedNodes.add(node.range.toString()); } }; return { Identifier(node) { var _a; const { name } = node; const { parent } = node; if (!parent) { return; } const effectiveParent = parent.type === utils_1.AST_NODE_TYPES.MemberExpression ? parent.parent : parent; if (!effectiveParent) { return; } if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression) { if (!checkProperties) { return; } if (parent.object.type === utils_1.AST_NODE_TYPES.Identifier && parent.object.name === name) { if (isInvalid(name)) { report(node); } // Report AssignmentExpressions left side's assigned variable id } else if (effectiveParent.type === utils_1.AST_NODE_TYPES.AssignmentExpression && effectiveParent.left.type === utils_1.AST_NODE_TYPES.MemberExpression && effectiveParent.left.property.type === utils_1.AST_NODE_TYPES.Identifier && effectiveParent.left.property.name === node.name) { if (isInvalid(name)) { report(node); } // Report AssignmentExpressions only if they are the left side of the assignment } else if (effectiveParent.type === utils_1.AST_NODE_TYPES.AssignmentExpression && effectiveParent.right.type !== utils_1.AST_NODE_TYPES.MemberExpression && isInvalid(name)) { report(node); } /* * Properties have their own rules, and * AssignmentPattern nodes can be treated like Properties: * e.g.: const { no_camelcased = false } = bar; */ } else if (parent.type === utils_1.AST_NODE_TYPES.Property || parent.type === utils_1.AST_NODE_TYPES.AssignmentPattern) { if (((_a = parent.parent) === null || _a === void 0 ? void 0 : _a.type) === utils_1.AST_NODE_TYPES.ObjectPattern) { if (!ignoreDestructuring && 'shorthand' in parent && parent.shorthand && 'left' in parent.value && parent.value.left && isInvalid(name)) { report(node); } if (!('key' in parent)) { throw new Error('OK'); } const assignmentKeyEqualsValue = 'name' in parent.value && 'name' in parent.key && parent.key.name === parent.value.name; // Prevent checking right-hand side of destructured object if (!assignmentKeyEqualsValue && parent.key === node) { return; } const valueIsInvalid = 'name' in parent.value && parent.value.name && isInvalid(name); // ignore destructuring if the option is set, unless a new identifier is created if (valueIsInvalid && !(assignmentKeyEqualsValue && ignoreDestructuring)) { report(node); } } // Never check properties or always ignore destructuring if (!checkProperties || (ignoreDestructuring && isInsideObjectPattern(node))) { return; } // Don't check right hand side of AssignmentExpression to prevent duplicate warnings if ((!('right' in parent) || parent.right !== node) && shouldReport(effectiveParent, name)) { report(node); } // Check if it's an import specifier } else if (IMPORT_TYPES.has(parent.type)) { if (ignoreNamedImports && parent.type === utils_1.AST_NODE_TYPES.ImportSpecifier) { // Ignore named import } else if ('local' in parent && parent.local.type === utils_1.AST_NODE_TYPES.Identifier && parent.local.name === node.name && isInvalid(name)) { // Report only if the local imported identifier is invalid report(node); } } else if (parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition) { if (checkClassFields && isInvalid(name)) { report(node); } // Report anything that is invalid that isn't a CallExpression } else if (shouldReport(effectiveParent, name)) { report(node); } }, }; }, defaultOptions: ['^.+$', defaultOptions], meta: { docs: { description: 'require identifiers to match a specified regular expression', }, messages: { notMatch: "Identifier '{{name}}' does not match the pattern '{{pattern}}'.", notMatchPrivate: "Identifier '#{{name}}' does not match the pattern '{{pattern}}'.", }, schema: [ { type: 'string', }, { additionalProperties: false, properties: { classFields: { default: false, type: 'boolean', }, ignoreDestructuring: { default: false, type: 'boolean', }, ignoreNamedImports: { default: false, type: 'boolean', }, onlyDeclarations: { default: false, type: 'boolean', }, properties: { default: false, type: 'boolean', }, }, type: 'object', }, ], type: 'suggestion', }, name: 'id-match', });