forked from daren.hsu/line_push
165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
'use strict';
|
|
const eslintTemplateVisitor = require('eslint-template-visitor');
|
|
const getDocumentationUrl = require('./utils/get-documentation-url');
|
|
|
|
const templates = eslintTemplateVisitor();
|
|
|
|
const objectVariable = templates.variable();
|
|
const argumentsVariable = templates.spreadVariable();
|
|
|
|
const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
|
|
const substringCallTemplate = templates.template`${objectVariable}.substring(${argumentsVariable})`;
|
|
|
|
const isLiteralNumber = node => node && node.type === 'Literal' && typeof node.value === 'number';
|
|
|
|
const getNumericValue = node => {
|
|
if (isLiteralNumber(node)) {
|
|
return node.value;
|
|
}
|
|
|
|
if (node.type === 'UnaryExpression' && node.operator === '-') {
|
|
return -getNumericValue(node.argument);
|
|
}
|
|
};
|
|
|
|
// This handles cases where the argument is very likely to be a number, such as `.substring('foo'.length)`.
|
|
const isLengthProperty = node => (
|
|
node &&
|
|
node.type === 'MemberExpression' &&
|
|
node.computed === false &&
|
|
node.property.type === 'Identifier' &&
|
|
node.property.name === 'length'
|
|
);
|
|
|
|
const isLikelyNumeric = node => isLiteralNumber(node) || isLengthProperty(node);
|
|
|
|
const create = context => {
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
const getNodeText = node => {
|
|
const text = sourceCode.getText(node);
|
|
const before = sourceCode.getTokenBefore(node);
|
|
const after = sourceCode.getTokenAfter(node);
|
|
if (
|
|
(before && before.type === 'Punctuator' && before.value === '(') &&
|
|
(after && after.type === 'Punctuator' && after.value === ')')
|
|
) {
|
|
return `(${text})`;
|
|
}
|
|
|
|
return text;
|
|
};
|
|
|
|
return templates.visitor({
|
|
[substrCallTemplate](node) {
|
|
const objectNode = substrCallTemplate.context.getMatch(objectVariable);
|
|
const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable);
|
|
|
|
const problem = {
|
|
node,
|
|
message: 'Prefer `String#slice()` over `String#substr()`.'
|
|
};
|
|
|
|
const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
|
|
const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
|
|
|
|
let slice;
|
|
|
|
if (argumentNodes.length === 0) {
|
|
slice = [];
|
|
} else if (argumentNodes.length === 1) {
|
|
slice = [firstArgument];
|
|
} else if (argumentNodes.length === 2) {
|
|
if (firstArgument === '0') {
|
|
slice = [firstArgument, secondArgument];
|
|
} else if (
|
|
isLiteralNumber(argumentNodes[0]) &&
|
|
isLiteralNumber(argumentNodes[1])
|
|
) {
|
|
slice = [
|
|
firstArgument,
|
|
argumentNodes[0].value + argumentNodes[1].value
|
|
];
|
|
} else if (
|
|
isLikelyNumeric(argumentNodes[0]) &&
|
|
isLikelyNumeric(argumentNodes[1])
|
|
) {
|
|
slice = [firstArgument, firstArgument + ' + ' + secondArgument];
|
|
}
|
|
}
|
|
|
|
if (slice) {
|
|
const objectText = getNodeText(objectNode);
|
|
|
|
problem.fix = fixer => fixer.replaceText(node, `${objectText}.slice(${slice.join(', ')})`);
|
|
}
|
|
|
|
context.report(problem);
|
|
},
|
|
|
|
[substringCallTemplate](node) {
|
|
const objectNode = substringCallTemplate.context.getMatch(objectVariable);
|
|
const argumentNodes = substringCallTemplate.context.getMatch(argumentsVariable);
|
|
|
|
const problem = {
|
|
node,
|
|
message: 'Prefer `String#slice()` over `String#substring()`.'
|
|
};
|
|
|
|
const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
|
|
const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
|
|
|
|
const firstNumber = argumentNodes[0] ? getNumericValue(argumentNodes[0]) : undefined;
|
|
|
|
let slice;
|
|
|
|
if (argumentNodes.length === 0) {
|
|
slice = [];
|
|
} else if (argumentNodes.length === 1) {
|
|
if (firstNumber !== undefined) {
|
|
slice = [Math.max(0, firstNumber)];
|
|
} else if (isLengthProperty(argumentNodes[0])) {
|
|
slice = [firstArgument];
|
|
} else {
|
|
slice = [`Math.max(0, ${firstArgument})`];
|
|
}
|
|
} else if (argumentNodes.length === 2) {
|
|
const secondNumber = argumentNodes[1] ? getNumericValue(argumentNodes[1]) : undefined;
|
|
|
|
if (firstNumber !== undefined && secondNumber !== undefined) {
|
|
slice = firstNumber > secondNumber ?
|
|
[Math.max(0, secondNumber), Math.max(0, firstNumber)] :
|
|
[Math.max(0, firstNumber), Math.max(0, secondNumber)];
|
|
} else if (firstNumber === 0 || secondNumber === 0) {
|
|
slice = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`];
|
|
} else {
|
|
// As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
|
|
// .substring(0, 2) and .substring(2, 0) returns the same result
|
|
// .slice(0, 2) and .slice(2, 0) doesn't return the same result
|
|
// There's also an issue with us now knowing whether the value will be negative or not, due to:
|
|
// .substring() treats a negative number the same as it treats a zero.
|
|
// The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
|
|
}
|
|
}
|
|
|
|
if (slice) {
|
|
const objectText = getNodeText(objectNode);
|
|
problem.fix = fixer => fixer.replaceText(node, `${objectText}.slice(${slice.join(', ')})`);
|
|
}
|
|
|
|
context.report(problem);
|
|
}
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
create,
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
url: getDocumentationUrl(__filename)
|
|
},
|
|
fixable: 'code'
|
|
}
|
|
};
|