| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646 | 'use strict';const path = require('node:path');const {defaultsDeep, upperFirst, lowerFirst} = require('lodash');const avoidCapture = require('./utils/avoid-capture.js');const cartesianProductSamples = require('./utils/cartesian-product-samples.js');const isShorthandPropertyValue = require('./utils/is-shorthand-property-value.js');const isShorthandImportLocal = require('./utils/is-shorthand-import-local.js');const getVariableIdentifiers = require('./utils/get-variable-identifiers.js');const {defaultReplacements, defaultAllowList, defaultIgnore} = require('./shared/abbreviations.js');const {renameVariable} = require('./fix/index.js');const getScopes = require('./utils/get-scopes.js');const {isStaticRequire} = require('./ast/index.js');const MESSAGE_ID_REPLACE = 'replace';const MESSAGE_ID_SUGGESTION = 'suggestion';const anotherNameMessage = 'A more descriptive name will do too.';const messages = {	[MESSAGE_ID_REPLACE]: `The {{nameTypeText}} \`{{discouragedName}}\` should be named \`{{replacement}}\`. ${anotherNameMessage}`,	[MESSAGE_ID_SUGGESTION]: `Please rename the {{nameTypeText}} \`{{discouragedName}}\`. Suggested names are: {{replacementsText}}. ${anotherNameMessage}`,};const isUpperCase = string => string === string.toUpperCase();const isUpperFirst = string => isUpperCase(string[0]);const prepareOptions = ({	checkProperties = false,	checkVariables = true,	checkDefaultAndNamespaceImports = 'internal',	checkShorthandImports = 'internal',	checkShorthandProperties = false,	checkFilenames = true,	extendDefaultReplacements = true,	replacements = {},	extendDefaultAllowList = true,	allowList = {},	ignore = [],} = {}) => {	const mergedReplacements = extendDefaultReplacements		? defaultsDeep({}, replacements, defaultReplacements)		: replacements;	const mergedAllowList = extendDefaultAllowList		? defaultsDeep({}, allowList, defaultAllowList)		: allowList;	ignore = [...defaultIgnore, ...ignore];	ignore = ignore.map(		pattern => pattern instanceof RegExp ? pattern : new RegExp(pattern, 'u'),	);	return {		checkProperties,		checkVariables,		checkDefaultAndNamespaceImports,		checkShorthandImports,		checkShorthandProperties,		checkFilenames,		replacements: new Map(			Object.entries(mergedReplacements).map(				([discouragedName, replacements]) =>					[discouragedName, new Map(Object.entries(replacements))],			),		),		allowList: new Map(Object.entries(mergedAllowList)),		ignore,	};};const getWordReplacements = (word, {replacements, allowList}) => {	// Skip constants and allowList	if (isUpperCase(word) || allowList.get(word)) {		return [];	}	const replacement = replacements.get(lowerFirst(word))		|| replacements.get(word)		|| replacements.get(upperFirst(word));	let wordReplacement = [];	if (replacement) {		const transform = isUpperFirst(word) ? upperFirst : lowerFirst;		wordReplacement = [...replacement.keys()]			.filter(name => replacement.get(name))			.map(name => transform(name));	}	return wordReplacement.length > 0 ? wordReplacement.sort() : [];};const getNameReplacements = (name, options, limit = 3) => {	const {allowList, ignore} = options;	// Skip constants and allowList	if (isUpperCase(name) || allowList.get(name) || ignore.some(regexp => regexp.test(name))) {		return {total: 0};	}	// Find exact replacements	const exactReplacements = getWordReplacements(name, options);	if (exactReplacements.length > 0) {		return {			total: exactReplacements.length,			samples: exactReplacements.slice(0, limit),		};	}	// Split words	const words = name.split(/(?=[^a-z])|(?<=[^A-Za-z])/).filter(Boolean);	let hasReplacements = false;	const combinations = words.map(word => {		const wordReplacements = getWordReplacements(word, options);		if (wordReplacements.length > 0) {			hasReplacements = true;			return wordReplacements;		}		return [word];	});	// No replacements for any word	if (!hasReplacements) {		return {total: 0};	}	const {		total,		samples,	} = cartesianProductSamples(combinations, limit);	// `retVal` -> `['returnValue', 'Value']` -> `['returnValue']`	for (const parts of samples) {		for (let index = parts.length - 1; index > 0; index--) {			const word = parts[index];			if (/^[A-Za-z]+$/.test(word) && parts[index - 1].endsWith(parts[index])) {				parts.splice(index, 1);			}		}	}	return {		total,		samples: samples.map(words => words.join('')),	};};const getMessage = (discouragedName, replacements, nameTypeText) => {	const {total, samples = []} = replacements;	if (total === 1) {		return {			messageId: MESSAGE_ID_REPLACE,			data: {				nameTypeText,				discouragedName,				replacement: samples[0],			},		};	}	let replacementsText = samples		.map(replacement => `\`${replacement}\``)		.join(', ');	const omittedReplacementsCount = total - samples.length;	if (omittedReplacementsCount > 0) {		replacementsText += `, ... (${omittedReplacementsCount > 99 ? '99+' : omittedReplacementsCount} more omitted)`;	}	return {		messageId: MESSAGE_ID_SUGGESTION,		data: {			nameTypeText,			discouragedName,			replacementsText,		},	};};const isExportedIdentifier = identifier => {	if (		identifier.parent.type === 'VariableDeclarator'		&& identifier.parent.id === identifier	) {		return (			identifier.parent.parent.type === 'VariableDeclaration'			&& identifier.parent.parent.parent.type === 'ExportNamedDeclaration'		);	}	if (		identifier.parent.type === 'FunctionDeclaration'		&& identifier.parent.id === identifier	) {		return identifier.parent.parent.type === 'ExportNamedDeclaration';	}	if (		identifier.parent.type === 'ClassDeclaration'		&& identifier.parent.id === identifier	) {		return identifier.parent.parent.type === 'ExportNamedDeclaration';	}	if (		identifier.parent.type === 'TSTypeAliasDeclaration'		&& identifier.parent.id === identifier	) {		return identifier.parent.parent.type === 'ExportNamedDeclaration';	}	return false;};const shouldFix = variable => getVariableIdentifiers(variable)	.every(identifier =>		!isExportedIdentifier(identifier)		// In typescript parser, only `JSXOpeningElement` is added to variable		// `<foo></foo>` -> `<bar></foo>` will cause parse error		&& identifier.type !== 'JSXIdentifier',	);const isDefaultOrNamespaceImportName = identifier => {	if (		identifier.parent.type === 'ImportDefaultSpecifier'		&& identifier.parent.local === identifier	) {		return true;	}	if (		identifier.parent.type === 'ImportNamespaceSpecifier'		&& identifier.parent.local === identifier	) {		return true;	}	if (		identifier.parent.type === 'ImportSpecifier'		&& identifier.parent.local === identifier		&& identifier.parent.imported.type === 'Identifier'		&& identifier.parent.imported.name === 'default'	) {		return true;	}	if (		identifier.parent.type === 'VariableDeclarator'		&& identifier.parent.id === identifier		&& isStaticRequire(identifier.parent.init)	) {		return true;	}	return false;};const isClassVariable = variable => {	if (variable.defs.length !== 1) {		return false;	}	const [definition] = variable.defs;	return definition.type === 'ClassName';};const shouldReportIdentifierAsProperty = identifier => {	if (		identifier.parent.type === 'MemberExpression'		&& identifier.parent.property === identifier		&& !identifier.parent.computed		&& identifier.parent.parent.type === 'AssignmentExpression'		&& identifier.parent.parent.left === identifier.parent	) {		return true;	}	if (		identifier.parent.type === 'Property'		&& identifier.parent.key === identifier		&& !identifier.parent.computed		&& !identifier.parent.shorthand // Shorthand properties are reported and fixed as variables		&& identifier.parent.parent.type === 'ObjectExpression'	) {		return true;	}	if (		identifier.parent.type === 'ExportSpecifier'		&& identifier.parent.exported === identifier		&& identifier.parent.local !== identifier // Same as shorthand properties above	) {		return true;	}	if (		(			identifier.parent.type === 'MethodDefinition'			|| identifier.parent.type === 'PropertyDefinition'		)		&& identifier.parent.key === identifier		&& !identifier.parent.computed	) {		return true;	}	return false;};const isInternalImport = node => {	let source = '';	if (node.type === 'Variable') {		source = node.node.init.arguments[0].value;	} else if (node.type === 'ImportBinding') {		source = node.parent.source.value;	}	return (		!source.includes('node_modules')		&& (source.startsWith('.') || source.startsWith('/'))	);};/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const options = prepareOptions(context.options[0]);	const filenameWithExtension = context.getPhysicalFilename();	// A `class` declaration produces two variables in two scopes:	// the inner class scope, and the outer one (wherever the class is declared).	// This map holds the outer ones to be later processed when the inner one is encountered.	// For why this is not a eslint issue see https://github.com/eslint/eslint-scope/issues/48#issuecomment-464358754	const identifierToOuterClassVariable = new WeakMap();	const checkPossiblyWeirdClassVariable = variable => {		if (isClassVariable(variable)) {			if (variable.scope.type === 'class') { // The inner class variable				const [definition] = variable.defs;				const outerClassVariable = identifierToOuterClassVariable.get(definition.name);				if (!outerClassVariable) {					return checkVariable(variable);				}				// Create a normal-looking variable (like a `var` or a `function`)				// For which a single `variable` holds all references, unlike with a `class`				const combinedReferencesVariable = {					name: variable.name,					scope: variable.scope,					defs: variable.defs,					identifiers: variable.identifiers,					references: [...variable.references, ...outerClassVariable.references],				};				// Call the common checker with the newly forged normalized class variable				return checkVariable(combinedReferencesVariable);			}			// The outer class variable, we save it for later, when it's inner counterpart is encountered			const [definition] = variable.defs;			identifierToOuterClassVariable.set(definition.name, variable);			return;		}		return checkVariable(variable);	};	// Holds a map from a `Scope` to a `Set` of new variable names generated by our fixer.	// Used to avoid generating duplicate names, see for instance `let errCb, errorCb` test.	const scopeToNamesGeneratedByFixer = new WeakMap();	const isSafeName = (name, scopes) => scopes.every(scope => {		const generatedNames = scopeToNamesGeneratedByFixer.get(scope);		return !generatedNames || !generatedNames.has(name);	});	const checkVariable = variable => {		if (variable.defs.length === 0) {			return;		}		const [definition] = variable.defs;		if (isDefaultOrNamespaceImportName(definition.name)) {			if (!options.checkDefaultAndNamespaceImports) {				return;			}			if (				options.checkDefaultAndNamespaceImports === 'internal'				&& !isInternalImport(definition)			) {				return;			}		}		if (isShorthandImportLocal(definition.name)) {			if (!options.checkShorthandImports) {				return;			}			if (				options.checkShorthandImports === 'internal'				&& !isInternalImport(definition)			) {				return;			}		}		if (			!options.checkShorthandProperties			&& isShorthandPropertyValue(definition.name)		) {			return;		}		const variableReplacements = getNameReplacements(variable.name, options);		if (variableReplacements.total === 0) {			return;		}		const scopes = [			...variable.references.map(reference => reference.from),			variable.scope,		];		variableReplacements.samples = variableReplacements.samples.map(			name => avoidCapture(name, scopes, isSafeName),		);		const problem = {			...getMessage(definition.name.name, variableReplacements, 'variable'),			node: definition.name,		};		if (			variableReplacements.total === 1			&& shouldFix(variable)			&& variableReplacements.samples[0]			&& !variable.references.some(reference => reference.vueUsedInTemplate)		) {			const [replacement] = variableReplacements.samples;			for (const scope of scopes) {				if (!scopeToNamesGeneratedByFixer.has(scope)) {					scopeToNamesGeneratedByFixer.set(scope, new Set());				}				const generatedNames = scopeToNamesGeneratedByFixer.get(scope);				generatedNames.add(replacement);			}			problem.fix = fixer => renameVariable(variable, replacement, fixer);		}		context.report(problem);	};	const checkVariables = scope => {		for (const variable of scope.variables) {			checkPossiblyWeirdClassVariable(variable);		}	};	const checkScope = scope => {		const scopes = getScopes(scope);		for (const scope of scopes) {			checkVariables(scope);		}	};	return {		Identifier(node) {			if (!options.checkProperties) {				return;			}			if (node.name === '__proto__') {				return;			}			const identifierReplacements = getNameReplacements(node.name, options);			if (identifierReplacements.total === 0) {				return;			}			if (!shouldReportIdentifierAsProperty(node)) {				return;			}			const problem = {				...getMessage(node.name, identifierReplacements, 'property'),				node,			};			context.report(problem);		},		Program(node) {			if (!options.checkFilenames) {				return;			}			if (				filenameWithExtension === '<input>'				|| filenameWithExtension === '<text>'			) {				return;			}			const filename = path.basename(filenameWithExtension);			const extension = path.extname(filename);			const filenameReplacements = getNameReplacements(path.basename(filename, extension), options);			if (filenameReplacements.total === 0) {				return;			}			filenameReplacements.samples = filenameReplacements.samples.map(replacement => `${replacement}${extension}`);			context.report({				...getMessage(filename, filenameReplacements, 'filename'),				node,			});		},		'Program:exit'() {			if (!options.checkVariables) {				return;			}			checkScope(context.getScope());		},	};};const schema = {	type: 'array',	additionalItems: false,	items: [		{			type: 'object',			additionalProperties: false,			properties: {				checkProperties: {					type: 'boolean',				},				checkVariables: {					type: 'boolean',				},				checkDefaultAndNamespaceImports: {					type: [						'boolean',						'string',					],					pattern: 'internal',				},				checkShorthandImports: {					type: [						'boolean',						'string',					],					pattern: 'internal',				},				checkShorthandProperties: {					type: 'boolean',				},				checkFilenames: {					type: 'boolean',				},				extendDefaultReplacements: {					type: 'boolean',				},				replacements: {					$ref: '#/definitions/abbreviations',				},				extendDefaultAllowList: {					type: 'boolean',				},				allowList: {					$ref: '#/definitions/booleanObject',				},				ignore: {					type: 'array',					uniqueItems: true,				},			},		},	],	definitions: {		abbreviations: {			type: 'object',			additionalProperties: {				$ref: '#/definitions/replacements',			},		},		replacements: {			anyOf: [				{					enum: [						false,					],				},				{					$ref: '#/definitions/booleanObject',				},			],		},		booleanObject: {			type: 'object',			additionalProperties: {				type: 'boolean',			},		},	},};/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prevent abbreviations.',		},		fixable: 'code',		schema,		messages,	},};
 |