UNPKG

ec0lint-plugin-ec0lint-plugin

Version:

An ec0lint plugin for linting ec0lint plugins

175 lines (162 loc) 6.9 kB
/** * @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` * @author Teddy Katz */ 'use strict'; const utils = require('../utils'); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', docs: { description: 'disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`', category: 'Rules', recommended: true, url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-useless-token-range.md', }, fixable: 'code', schema: [], messages: { useReplacement: "Use '{{replacementText}}' instead.", }, }, create(context) { const sourceCode = context.getSourceCode(); // ---------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------- /** * Determines whether a second argument to getFirstToken or getLastToken changes the output of the function. * This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`. * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken()` or `sourceCode.getLastToken()` * @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken */ function affectsGetTokenOutput(arg) { if (!arg) { return false; } if (arg.type !== 'ObjectExpression') { return true; } return ( arg.properties.length >= 2 || (arg.properties.length === 1 && (utils.getKeyName(arg.properties[0]) !== 'includeComments' || arg.properties[0].value.type !== 'Literal')) ); } /** * Determines whether a node is a MemberExpression that accesses the `range` property * @param {ASTNode} node The node * @returns {boolean} `true` if the node is a MemberExpression that accesses the `range` property */ function isRangeAccess(node) { return ( node.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'range' ); } /** * Determines whether a MemberExpression accesses the `start` property (either `.range[0]` or `.start`). * Note that this will also work correctly if the `.range` MemberExpression is passed. * @param {ASTNode} memberExpression The MemberExpression node to check * @returns {boolean} `true` if this node accesses either `.range[0]` or `.start` */ function isStartAccess(memberExpression) { if ( isRangeAccess(memberExpression) && memberExpression.parent.type === 'MemberExpression' ) { return isStartAccess(memberExpression.parent); } return ( (memberExpression.property.type === 'Identifier' && memberExpression.property.name === 'start') || (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 0 && isRangeAccess(memberExpression.object)) ); } /** * Determines whether a MemberExpression accesses the `start` property (either `.range[1]` or `.end`). * Note that this will also work correctly if the `.range` MemberExpression is passed. * @param {ASTNode} memberExpression The MemberExpression node to check * @returns {boolean} `true` if this node accesses either `.range[1]` or `.end` */ function isEndAccess(memberExpression) { if ( isRangeAccess(memberExpression) && memberExpression.parent.type === 'MemberExpression' ) { return isEndAccess(memberExpression.parent); } return ( (memberExpression.property.type === 'Identifier' && memberExpression.property.name === 'end') || (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 1 && isRangeAccess(memberExpression.object)) ); } // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- return { 'Program:exit'(ast) { [...utils.getSourceCodeIdentifiers(sourceCode.scopeManager, ast)] .filter( (identifier) => identifier.parent.type === 'MemberExpression' && identifier.parent.object === identifier && identifier.parent.property.type === 'Identifier' && identifier.parent.parent.type === 'CallExpression' && identifier.parent === identifier.parent.parent.callee && identifier.parent.parent.arguments.length <= 2 && !affectsGetTokenOutput(identifier.parent.parent.arguments[1]) && identifier.parent.parent.parent.type === 'MemberExpression' && identifier.parent.parent === identifier.parent.parent.parent.object && ((isStartAccess(identifier.parent.parent.parent) && identifier.parent.property.name === 'getFirstToken') || (isEndAccess(identifier.parent.parent.parent) && identifier.parent.property.name === 'getLastToken')) ) .forEach((identifier) => { const fullRangeAccess = isRangeAccess( identifier.parent.parent.parent ) ? identifier.parent.parent.parent.parent : identifier.parent.parent.parent; const replacementText = sourceCode.text.slice( fullRangeAccess.range[0], identifier.parent.parent.range[0] ) + sourceCode.getText(identifier.parent.parent.arguments[0]) + sourceCode.text.slice( identifier.parent.parent.range[1], fullRangeAccess.range[1] ); context.report({ node: identifier.parent.parent, messageId: 'useReplacement', data: { replacementText }, fix(fixer) { return fixer.replaceText( identifier.parent.parent, sourceCode.getText(identifier.parent.parent.arguments[0]) ); }, }); }); }, }; }, };