eslint-plugin-unicorn-x
Version:
More than 100 powerful ESLint rules
219 lines (189 loc) • 5.57 kB
JavaScript
import path from 'node:path';
import {
getFunctionHeadLocation,
getFunctionNameWithKind,
isOpeningParenToken,
} from '@eslint-community/eslint-utils';
import helperValidatorIdentifier from '@babel/helper-validator-identifier';
import getClassHeadLocation from './utils/get-class-head-location.js';
import {camelCase, upperFirst} from 'scule';
import {getParenthesizedRange} from './utils/parentheses.js';
import {getScopes, getAvailableVariableName} from './utils/index.js';
import {isMemberExpression} from './ast/index.js';
const {isIdentifierName} = helperValidatorIdentifier;
const MESSAGE_ID_ERROR = 'no-anonymous-default-export/error';
const MESSAGE_ID_SUGGESTION = 'no-anonymous-default-export/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'The {{description}} should be named.',
[MESSAGE_ID_SUGGESTION]: 'Name it as `{{name}}`.',
};
const isClassKeywordToken = (token) =>
token.type === 'Keyword' && token.value === 'class';
const isAnonymousClassOrFunction = (node) =>
((node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ClassDeclaration' ||
node.type === 'ClassExpression') &&
!node.id) ||
node.type === 'ArrowFunctionExpression';
function getSuggestionName(node, filename, sourceCode) {
if (filename === '<input>' || filename === '<text>') {
return;
}
let [name] = path.basename(filename).split('.');
name = camelCase(name, {normalize: true});
if (!isIdentifierName(name)) {
return;
}
name =
node.type === 'ClassDeclaration' || node.type === 'ClassExpression'
? upperFirst(name)
: name;
name = getAvailableVariableName(name, getScopes(sourceCode.getScope(node)));
return name;
}
function addName(fixer, node, name, sourceCode) {
switch (node.type) {
case 'ClassDeclaration':
case 'ClassExpression': {
const lastDecorator = node.decorators?.at(-1);
const classToken = lastDecorator
? sourceCode.getTokenAfter(lastDecorator, isClassKeywordToken)
: sourceCode.getFirstToken(node, isClassKeywordToken);
return fixer.insertTextAfter(classToken, ` ${name}`);
}
case 'FunctionDeclaration':
case 'FunctionExpression': {
const openingParenthesisToken = sourceCode.getFirstToken(
node,
isOpeningParenToken,
);
const characterBefore = sourceCode.text.charAt(
sourceCode.getRange(openingParenthesisToken)[0] - 1,
);
return fixer.insertTextBefore(
openingParenthesisToken,
`${characterBefore === ' ' ? '' : ' '}${name} `,
);
}
case 'ArrowFunctionExpression': {
const [exportDeclarationStart, exportDeclarationEnd] =
sourceCode.getRange(
node.parent.type === 'ExportDefaultDeclaration'
? node.parent
: node.parent.parent,
);
const [arrowFunctionStart, arrowFunctionEnd] = getParenthesizedRange(
node,
sourceCode,
);
let textBefore = sourceCode.text.slice(
exportDeclarationStart,
arrowFunctionStart,
);
let textAfter = sourceCode.text.slice(
arrowFunctionEnd,
exportDeclarationEnd,
);
textBefore = `\n${textBefore}`;
if (!/\s$/.test(textBefore)) {
textBefore = `${textBefore} `;
}
if (!textAfter.endsWith(';')) {
textAfter = `${textAfter};`;
}
return [
fixer.replaceTextRange(
[exportDeclarationStart, arrowFunctionStart],
`const ${name} = `,
),
fixer.replaceTextRange([arrowFunctionEnd, exportDeclarationEnd], ';'),
fixer.insertTextAfterRange(
[exportDeclarationEnd, exportDeclarationEnd],
`${textBefore}${name}${textAfter}`,
),
];
}
// No default
}
}
function getProblem(node, context) {
const {sourceCode, physicalFilename} = context;
const suggestionName = getSuggestionName(node, physicalFilename, sourceCode);
let loc;
let description;
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
loc = getClassHeadLocation(node, sourceCode);
description = 'class';
} else {
loc = getFunctionHeadLocation(node, sourceCode);
// [TODO: @fisker]: Ask `@eslint-community/eslint-utils` to expose `getFunctionKind`
const nameWithKind = getFunctionNameWithKind(node);
description = nameWithKind.replace(/ '.*?'$/, '');
}
const problem = {
node,
loc,
messageId: MESSAGE_ID_ERROR,
data: {
description,
},
};
if (!suggestionName) {
return problem;
}
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
data: {
name: suggestionName,
},
fix: (fixer) => addName(fixer, node, suggestionName, sourceCode),
},
];
return problem;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = (context) => {
context.on('ExportDefaultDeclaration', (node) => {
if (!isAnonymousClassOrFunction(node.declaration)) {
return;
}
return getProblem(node.declaration, context);
});
context.on('AssignmentExpression', (node) => {
if (
!isAnonymousClassOrFunction(node.right) ||
!(
node.parent.type === 'ExpressionStatement' &&
node.parent.expression === node
) ||
!(
isMemberExpression(node.left, {
objects: 'module',
properties: 'exports',
computed: false,
optional: false,
}) || (node.left.type === 'Identifier', node.left.name === 'exports')
)
) {
return;
}
return getProblem(node.right, context);
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description:
'Disallow anonymous functions and classes as the default export.',
recommended: true,
},
hasSuggestions: true,
messages,
},
};
export default config;