routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+226
@@ -0,0 +1,226 @@
|
||||
import {isSemicolonToken} from '@eslint-community/eslint-utils';
|
||||
import getClassHeadLocation from './utils/get-class-head-location.js';
|
||||
import assertToken from './utils/assert-token.js';
|
||||
import {removeSpacesAfter} from './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 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)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function * switchClassMemberToObjectProperty(node, context, fixer) {
|
||||
const {sourceCode} = context;
|
||||
const staticToken = sourceCode.getFirstToken(node);
|
||||
assertToken(staticToken, {
|
||||
expected: {type: 'Keyword', value: 'static'},
|
||||
ruleId: 'no-static-only-class',
|
||||
});
|
||||
|
||||
yield fixer.remove(staticToken);
|
||||
yield removeSpacesAfter(staticToken, context, 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, context) {
|
||||
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;
|
||||
}
|
||||
|
||||
const {sourceCode} = context;
|
||||
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'
|
||||
&& sourceCode.getLoc(body).start.line !== sourceCode.getLoc(parent).start.line
|
||||
&& sourceCode.text.slice(sourceCode.getRange(classToken)[1], sourceCode.getRange(body)[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, context, 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, context, fixer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function create(context) {
|
||||
context.on(['ClassDeclaration', 'ClassExpression'], node => {
|
||||
if (
|
||||
node.superClass
|
||||
|| (node.decorators && node.decorators.length > 0)
|
||||
|| node.body.type !== 'ClassBody'
|
||||
|| node.body.body.length === 0
|
||||
|| node.body.body.some(node => !isStaticMember(node))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
node,
|
||||
loc: getClassHeadLocation(node, context),
|
||||
messageId: MESSAGE_ID,
|
||||
fix: switchClassToObject(node, context),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow classes that only have static members.',
|
||||
recommended: 'unopinionated',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user