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,
- },
- };
|