| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 | 'use strict';const {	getNegativeIndexLengthNode,	removeLengthNode,} = require('./shared/negative-index.js');const typedArray = require('./shared/typed-array.js');const {isLiteral} = require('./ast/index.js');const MESSAGE_ID = 'prefer-negative-index';const messages = {	[MESSAGE_ID]: 'Prefer negative index over length minus index for `{{method}}`.',};const methods = new Map([	[		'slice',		{			argumentsIndexes: [0, 1],			supportObjects: new Set([				'Array',				'String',				'ArrayBuffer',				...typedArray,				// `{Blob,File}#slice()` are not generally used				// 'Blob'				// 'File'			]),		},	],	[		'splice',		{			argumentsIndexes: [0],			supportObjects: new Set([				'Array',			]),		},	],	[		'at',		{			argumentsIndexes: [0],			supportObjects: new Set([				'Array',				'String',				...typedArray,			]),		},	],]);const getMemberName = node => {	const {type, property} = node;	if (		type === 'MemberExpression'		&& property.type === 'Identifier'	) {		return property.name;	}};function parse(node) {	const {callee, arguments: originalArguments} = node;	let method = callee.property.name;	let target = callee.object;	let argumentsNodes = originalArguments;	if (methods.has(method)) {		return {			method,			target,			argumentsNodes,		};	}	if (method !== 'call' && method !== 'apply') {		return;	}	const isApply = method === 'apply';	method = getMemberName(callee.object);	if (!methods.has(method)) {		return;	}	const {supportObjects} = methods.get(method);	const parentCallee = callee.object.object;	if (		// [].{slice,splice}		(			parentCallee.type === 'ArrayExpression'			&& parentCallee.elements.length === 0		)		// ''.slice		|| (			method === 'slice'			&& isLiteral(parentCallee, '')		)		// {Array,String...}.prototype.slice		// Array.prototype.splice		|| (			getMemberName(parentCallee) === 'prototype'			&& parentCallee.object.type === 'Identifier'			&& supportObjects.has(parentCallee.object.name)		)	) {		[target] = originalArguments;		if (isApply) {			const [, secondArgument] = originalArguments;			if (!secondArgument || secondArgument.type !== 'ArrayExpression') {				return;			}			argumentsNodes = secondArgument.elements;		} else {			argumentsNodes = originalArguments.slice(1);		}		return {			method,			target,			argumentsNodes,		};	}}/** @param {import('eslint').Rule.RuleContext} context */const create = context => ({	'CallExpression[callee.type="MemberExpression"]'(node) {		const parsed = parse(node);		if (!parsed) {			return;		}		const {			method,			target,			argumentsNodes,		} = parsed;		const {argumentsIndexes} = methods.get(method);		const removableNodes = argumentsIndexes			.map(index => getNegativeIndexLengthNode(argumentsNodes[index], target))			.filter(Boolean);		if (removableNodes.length === 0) {			return;		}		return {			node,			messageId: MESSAGE_ID,			data: {method},			* fix(fixer) {				const sourceCode = context.getSourceCode();				for (const node of removableNodes) {					yield removeLengthNode(node, fixer, sourceCode);				}			},		};	},});/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer negative index over `.length - index` for `{String,Array,TypedArray}#{slice,at}()` and `Array#splice()`.',		},		fixable: 'code',		messages,	},};
 |