1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
/**
* @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
* @author Teddy Katz
*/
'use strict';
const utils = require('../utils');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken',
category: 'Rules',
recommended: true,
},
type: 'suggestion',
fixable: 'code',
schema: [],
},
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) {
Array.from(utils.getSourceCodeIdentifiers(context, 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,
message: "Use '{{replacementText}}' instead.",
data: { replacementText },
fix (fixer) {
return fixer.replaceText(identifier.parent.parent, sourceCode.getText(identifier.parent.parent.arguments[0]));
},
});
});
},
};
},
};
|