UNPKG

@angular-eslint/eslint-plugin

Version:

ESLint plugin for Angular applications, following https://angular.dev/style-guide

211 lines (210 loc) • 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RULE_NAME = void 0; const utils_1 = require("@angular-eslint/utils"); const utils_2 = require("@typescript-eslint/utils"); const create_eslint_rule_1 = require("../utils/create-eslint-rule"); exports.RULE_NAME = 'no-input-rename'; const STYLE_GUIDE_LINK = 'https://angular.dev/style-guide#style-05-13'; exports.default = (0, create_eslint_rule_1.createESLintRule)({ name: exports.RULE_NAME, meta: { type: 'suggestion', docs: { description: 'Ensures that input bindings are not aliased', recommended: 'recommended', }, fixable: 'code', hasSuggestions: true, schema: [ { type: 'object', properties: { allowedNames: { type: 'array', items: { type: 'string', }, description: 'A list with allowed input names', uniqueItems: true, }, }, additionalProperties: false, }, ], messages: { noInputRename: `Input bindings should not be aliased (${STYLE_GUIDE_LINK})`, suggestRemoveAliasName: 'Remove alias name', suggestReplaceOriginalNameWithAliasName: 'Remove alias name and use it as the original name', }, }, defaultOptions: [{ allowedNames: [] }], create(context, [{ allowedNames = [] }]) { let selectors = new Set(); const ariaAttributeKeys = (0, utils_1.getAriaAttributeKeys)(); let selectorDirectiveName; return { [utils_1.Selectors.COMPONENT_OR_DIRECTIVE_SELECTOR_LITERAL](node) { const nodeRawText = utils_1.ASTUtils.getRawText(node); const bracketMatchResults = nodeRawText.match(/\[(.*?)\]/); if (bracketMatchResults) { selectorDirectiveName = bracketMatchResults[1]; } selectors = new Set((0, utils_1.withoutBracketsAndWhitespaces)(nodeRawText).split(',')); }, [utils_1.Selectors.INPUT_ALIAS](node) { const propertyOrMethodDefinition = utils_1.ASTUtils.getNearestNodeFrom(node, utils_1.ASTUtils.isPropertyOrMethodDefinition); if (!propertyOrMethodDefinition || !utils_2.ASTUtils.isIdentifier(propertyOrMethodDefinition.key)) { return; } const aliasName = utils_1.ASTUtils.getRawText(node); const propertyName = utils_1.ASTUtils.getRawText(propertyOrMethodDefinition.key); if (allowedNames.includes(aliasName) || (ariaAttributeKeys.has(aliasName) && propertyName === (0, utils_1.kebabToCamelCase)(aliasName))) { return; } // The alias is either a string in the `@Input()` decorator function, // or a string on an `alias` property that is in an object expression // that is in the `@Input()` decorator or the `input()` function or the // `input.required()` function. If it's on the `alias` property, then we // want to remove that whole property rather than just the string literal. const stringToRemove = utils_1.ASTUtils.isTemplateElement(node) ? node.parent : node; let rangeToRemove = stringToRemove.range; if (utils_1.ASTUtils.isProperty(stringToRemove.parent)) { const property = stringToRemove.parent; rangeToRemove = property.range; if (utils_1.ASTUtils.isObjectExpression(property.parent)) { const objectExpression = property.parent; if (objectExpression.properties.length === 1) { // The property is the only property in the // object, so we can remove the whole object. rangeToRemove = objectExpression.range; // If the object is in an `input()` function, then // the object will be the second argument. The first // argument will be the default value. We need to // remove the comma after the default value. const tokenBefore = context.sourceCode.getTokenBefore(objectExpression); if (tokenBefore && utils_2.ASTUtils.isCommaToken(tokenBefore)) { rangeToRemove = [tokenBefore.range[0], rangeToRemove[1]]; } } else { // There are other properties in the object, so we // can only remove the property. How we remove it // will depend on where the property is in the object. const propertyIndex = objectExpression.properties.indexOf(property); if (propertyIndex < objectExpression.properties.length - 1) { // The property is not the last one, so we can // remove everything up to the next property // which will remove the comma after it. rangeToRemove = [ property.range[0], objectExpression.properties[propertyIndex + 1].range[0], ]; } else { // The property is the last one. If the object has a // trailing comma, then we want to keep the trailing comma. // The simplest way to do that is to remove the property // and the comma that precedes it. const tokenBefore = context.sourceCode.getTokenBefore(property); if (tokenBefore && utils_2.ASTUtils.isCommaToken(tokenBefore)) { rangeToRemove = [tokenBefore.range[0], rangeToRemove[1]]; } } } } } if (aliasName === propertyName) { context.report({ node, messageId: 'noInputRename', fix: (fixer) => fixer.removeRange(rangeToRemove), }); } else if (!isAliasNameAllowed(selectors, propertyName, aliasName, selectorDirectiveName)) { context.report({ node, messageId: 'noInputRename', suggest: [ { messageId: 'suggestRemoveAliasName', fix: (fixer) => fixer.removeRange(rangeToRemove), }, { messageId: 'suggestReplaceOriginalNameWithAliasName', fix: (fixer) => [ fixer.removeRange(rangeToRemove), fixer.replaceText(propertyOrMethodDefinition.key, aliasName.includes('-') ? `'${aliasName}'` : aliasName), ], }, ], }); } }, [utils_1.Selectors.INPUTS_METADATA_PROPERTY_LITERAL](node) { const ancestorMaybeHostDirectiveAPI = node.parent?.parent?.parent?.parent?.parent; if (ancestorMaybeHostDirectiveAPI && utils_1.ASTUtils.isProperty(ancestorMaybeHostDirectiveAPI)) { /** * Angular v15 introduced the directive composition API: https://angular.dev/guide/directives/directive-composition-api * Renaming host directive inputs using this API is not a bad practice and should not be reported */ const hostDirectiveAPIPropertyName = 'hostDirectives'; if ((utils_1.ASTUtils.isLiteral(ancestorMaybeHostDirectiveAPI.key) && ancestorMaybeHostDirectiveAPI.key.value === hostDirectiveAPIPropertyName) || (utils_2.ASTUtils.isIdentifier(ancestorMaybeHostDirectiveAPI.key) && ancestorMaybeHostDirectiveAPI.key.name === hostDirectiveAPIPropertyName)) { return; } } const [propertyName, aliasName] = (0, utils_1.withoutBracketsAndWhitespaces)(utils_1.ASTUtils.getRawText(node)).split(':'); if (!aliasName || allowedNames.includes(aliasName) || (ariaAttributeKeys.has(aliasName) && propertyName === (0, utils_1.kebabToCamelCase)(aliasName))) { return; } if (aliasName === propertyName) { context.report({ node, messageId: 'noInputRename', fix: (fixer) => fixer.replaceText(node, utils_1.ASTUtils.getReplacementText(node, propertyName)), }); } else if (!isAliasNameAllowed(selectors, propertyName, aliasName, selectorDirectiveName)) { context.report({ node, messageId: 'noInputRename', suggest: [ ['suggestRemoveAliasName', propertyName], ['suggestReplaceOriginalNameWithAliasName', aliasName], ].map(([messageId, name]) => ({ messageId, fix: (fixer) => fixer.replaceText(node, utils_1.ASTUtils.getReplacementText(node, name)), })), }); } }, 'ClassDeclaration:exit'() { selectors = new Set(); }, }; }, }); function composedName(selector, propertyName) { return `${selector}${(0, utils_1.capitalize)(propertyName)}`; } function isAliasNameAllowed(selectors, propertyName, aliasName, selectorDirectiveName) { return [...selectors].some((selector) => { return (selector === aliasName || selectorDirectiveName === aliasName || composedName(selector, propertyName) === aliasName); }); }