| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 | 'use strict';const {isSemicolonToken} = require('@eslint-community/eslint-utils');const getClassHeadLocation = require('./utils/get-class-head-location.js');const assertToken = require('./utils/assert-token.js');const {removeSpacesAfter} = require('./fix/index.js');const MESSAGE_ID = 'no-static-only-class';const messages = {	[MESSAGE_ID]: 'Use an object instead of a class with only static members.',};const selector = [	':matches(ClassDeclaration, ClassExpression)',	':not([superClass], [decorators.length>0])',	'[body.type="ClassBody"]',	'[body.body.length>0]',].join('');const isEqualToken = ({type, value}) => type === 'Punctuator' && value === '=';const isDeclarationOfExportDefaultDeclaration = node =>	node.type === 'ClassDeclaration'	&& node.parent.type === 'ExportDefaultDeclaration'	&& node.parent.declaration === node;const isPropertyDefinition = node => node.type === 'PropertyDefinition';const isMethodDefinition = node => node.type === 'MethodDefinition';function isStaticMember(node) {	const {		private: isPrivate,		static: isStatic,		declare: isDeclare,		readonly: isReadonly,		accessibility,		decorators,		key,	} = node;	// Avoid matching unexpected node. For example: https://github.com/tc39/proposal-class-static-block	if (!isPropertyDefinition(node) && !isMethodDefinition(node)) {		return false;	}	if (!isStatic || isPrivate || key.type === 'PrivateIdentifier') {		return false;	}	// TypeScript class	if (		isDeclare		|| isReadonly		|| accessibility !== undefined		|| (Array.isArray(decorators) && decorators.length > 0)		// TODO: Remove this when we drop support for `@typescript-eslint/parser` v4		|| key.type === 'TSPrivateIdentifier'	) {		return false;	}	return true;}function * switchClassMemberToObjectProperty(node, sourceCode, fixer) {	const staticToken = sourceCode.getFirstToken(node);	assertToken(staticToken, {		expected: {type: 'Keyword', value: 'static'},		ruleId: 'no-static-only-class',	});	yield fixer.remove(staticToken);	yield removeSpacesAfter(staticToken, sourceCode, fixer);	const maybeSemicolonToken = isPropertyDefinition(node)		? sourceCode.getLastToken(node)		: sourceCode.getTokenAfter(node);	const hasSemicolonToken = isSemicolonToken(maybeSemicolonToken);	if (isPropertyDefinition(node)) {		const {key, value} = node;		if (value) {			// Computed key may have `]` after `key`			const equalToken = sourceCode.getTokenAfter(key, isEqualToken);			yield fixer.replaceText(equalToken, ':');		} else if (hasSemicolonToken) {			yield fixer.insertTextBefore(maybeSemicolonToken, ': undefined');		} else {			yield fixer.insertTextAfter(node, ': undefined');		}	}	yield (		hasSemicolonToken			? fixer.replaceText(maybeSemicolonToken, ',')			: fixer.insertTextAfter(node, ',')	);}function switchClassToObject(node, sourceCode) {	const {		type,		id,		body,		declare: isDeclare,		abstract: isAbstract,		implements: classImplements,		parent,	} = node;	if (		isDeclare		|| isAbstract		|| (Array.isArray(classImplements) && classImplements.length > 0)	) {		return;	}	if (type === 'ClassExpression' && id) {		return;	}	const isExportDefault = isDeclarationOfExportDefaultDeclaration(node);	if (isExportDefault && id) {		return;	}	for (const node of body.body) {		if (			isPropertyDefinition(node)			&& (				node.typeAnnotation				// This is a stupid way to check if `value` of `PropertyDefinition` uses `this`				|| (node.value && sourceCode.getText(node.value).includes('this'))			)		) {			return;		}	}	return function * (fixer) {		const classToken = sourceCode.getFirstToken(node);		/* c8 ignore next */		assertToken(classToken, {			expected: {type: 'Keyword', value: 'class'},			ruleId: 'no-static-only-class',		});		if (isExportDefault || type === 'ClassExpression') {			/*				There are comments after return, and `{` is not on same line				```js				function a() {					return class // comment					{						static a() {}					}				}				```			*/			if (				type === 'ClassExpression'				&& parent.type === 'ReturnStatement'				&& body.loc.start.line !== parent.loc.start.line				&& sourceCode.text.slice(classToken.range[1], body.range[0]).trim()			) {				yield fixer.replaceText(classToken, '{');				const openingBraceToken = sourceCode.getFirstToken(body);				yield fixer.remove(openingBraceToken);			} else {				yield fixer.replaceText(classToken, '');				/*						Avoid breaking case like						```js						return class						{};						```				*/				yield removeSpacesAfter(classToken, sourceCode, fixer);			}			// There should not be ASI problem		} else {			yield fixer.replaceText(classToken, 'const');			yield fixer.insertTextBefore(body, '= ');			yield fixer.insertTextAfter(body, ';');		}		for (const node of body.body) {			yield * switchClassMemberToObjectProperty(node, sourceCode, fixer);		}	};}function create(context) {	const sourceCode = context.getSourceCode();	return {		[selector](node) {			if (node.body.body.some(node => !isStaticMember(node))) {				return;			}			return {				node,				loc: getClassHeadLocation(node, sourceCode),				messageId: MESSAGE_ID,				fix: switchClassToObject(node, sourceCode),			};		},	};}/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Disallow classes that only have static members.',		},		fixable: 'code',		messages,	},};
 |