eslint-plugin-complete
Version:
An ESLint plugin that contains useful rules.
116 lines (115 loc) • 4.11 kB
JavaScript
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
import ts from "typescript";
import { createRule } from "../utils.js";
const JSDOC_EXCEPTION_TAG = "allowEmptyVariadic";
const LOGGER_METHOD_NAMES = new Set([
"trace",
"debug",
"info",
"warn",
"error",
"fatal",
]);
export const requireVariadicFunctionArgument = createRule({
name: "require-variadic-function-argument",
meta: {
type: "problem",
docs: {
description: "Requires that variadic functions must be supplied with at least one argument",
recommended: true,
requiresTypeChecking: true,
},
schema: [],
messages: {
noArgument: "You must pass at least one argument to a variadic function.",
},
},
defaultOptions: [],
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
return {
CallExpression(node) {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const signature = checker.getResolvedSignature(tsNode);
if (signature === undefined) {
return;
}
const declaration = signature.getDeclaration();
// The `getDeclaration` method actually returns `ts.SignatureDeclaration | undefined`, not
// `ts.SignatureDeclaration`.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (declaration === undefined) {
return;
}
if (isHardCodedException(node)
|| hasJSDocExceptionTag(checker, declaration)) {
return;
}
for (let i = 0; i < declaration.parameters.length; i++) {
const parameter = declaration.parameters[i];
if (parameter === undefined) {
continue;
}
if (ts.isRestParameter(parameter)
&& node.arguments[i] === undefined) {
context.report({
loc: node.loc,
messageId: "noArgument",
});
}
}
},
};
},
});
function isHardCodedException(node) {
const { callee } = node;
const methodName = getMethodName(callee) ?? "unknown";
return (isConsoleOrWindowOrLoggerFunction(callee)
|| isTimeoutFunction(callee)
|| LOGGER_METHOD_NAMES.has(methodName));
}
function getMethodName(node) {
if (node.type !== AST_NODE_TYPES.MemberExpression) {
return undefined;
}
const { property } = node;
if (property.type !== AST_NODE_TYPES.Identifier) {
return undefined;
}
return property.name;
}
/**
* This rule has a false positive with any logger function. (Both `console.log` and logger functions
* from logging libraries like Pino and Winston are affected.)
*
* e.g. `logger.info("hello world");`
*/
function isConsoleOrWindowOrLoggerFunction(callee) {
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
return false;
}
const { object } = callee;
if (object.type !== AST_NODE_TYPES.Identifier) {
return false;
}
return (object.name === "console"
|| object.name === "window"
|| object.name === "logger");
}
function isTimeoutFunction(callee) {
if (callee.type !== AST_NODE_TYPES.Identifier) {
return false;
}
return callee.name === "setTimeout" || callee.name === "setInterval";
}
function hasJSDocExceptionTag(checker, declaration) {
const type = checker.getTypeAtLocation(declaration);
const symbol = type.getSymbol();
if (symbol === undefined) {
return false;
}
const jsDocTagInfoArray = symbol.getJsDocTags(checker);
return jsDocTagInfoArray.some((tagInfo) => tagInfo.name === JSDOC_EXCEPTION_TAG);
}