eslint-plugin-etc
Version:
More general-purpose ESLint rules
122 lines (121 loc) • 5.28 kB
JavaScript
const eslint_etc_1 = require("eslint-etc");
const tsutils = require("tsutils");
const utils_1 = require("../utils");
const rule = (0, utils_1.ruleCreator)({
defaultOptions: [],
meta: {
docs: {
description: "Forbids type parameters without inference sites and type parameters that don't add type safety to declarations.",
recommended: false,
},
fixable: undefined,
hasSuggestions: false,
messages: {
canReplace: "Type parameter '{{name}}' is not used to enforce a constraint between types and can be replaced with '{{replacement}}'.",
cannotInfer: "Type parameter '{{name}}' cannot be inferred from any parameter.",
},
schema: [],
type: "problem",
},
name: "no-misused-generics",
create: (context) => {
const { esTreeNodeToTSNodeMap } = (0, eslint_etc_1.getParserServices)(context);
let usage;
function checkSignature(node) {
const tsNode = esTreeNodeToTSNodeMap.get(node);
if (tsutils.isSignatureDeclaration(tsNode) &&
tsNode.typeParameters !== undefined) {
checkTypeParameters(tsNode.typeParameters, tsNode);
}
}
function checkTypeParameters(typeParameters, signature) {
if (usage === undefined) {
usage = tsutils.collectVariableUsage(signature.getSourceFile());
}
outer: for (const typeParameter of typeParameters) {
let usedInParameters = false;
let usedInReturnOrExtends = tsutils.isFunctionWithBody(signature);
for (const use of usage.get(typeParameter.name).uses) {
if (use.location.pos > signature.parameters.pos &&
use.location.pos < signature.parameters.end) {
if (usedInParameters) {
continue outer;
}
usedInParameters = true;
}
else if (!usedInReturnOrExtends) {
usedInReturnOrExtends =
use.location.pos > signature.parameters.end ||
isUsedInConstraint(use.location, typeParameters);
}
}
if (!usedInParameters) {
context.report({
data: {
name: typeParameter.name.text,
},
loc: (0, eslint_etc_1.getLoc)(typeParameter),
messageId: "cannotInfer",
});
}
else if (!usedInReturnOrExtends &&
!isConstrainedByOtherTypeParameter(typeParameter, typeParameters)) {
context.report({
data: {
name: typeParameter.name.text,
replacement: typeParameter.constraint
? typeParameter.constraint.getText(signature.getSourceFile())
: "unknown",
},
loc: (0, eslint_etc_1.getLoc)(typeParameter),
messageId: "canReplace",
});
}
}
}
function isConstrainedByOtherTypeParameter(current, all) {
if (current.constraint === undefined) {
return false;
}
for (const typeParameter of all) {
if (typeParameter === current) {
continue;
}
for (const use of usage.get(typeParameter.name).uses) {
if (use.location.pos >= current.constraint.pos &&
use.location.pos < current.constraint.end) {
return true;
}
}
}
return false;
}
function isUsedInConstraint(use, typeParameters) {
for (const typeParameter of typeParameters) {
if (typeParameter.constraint !== undefined &&
use.pos >= typeParameter.constraint.pos &&
use.pos < typeParameter.constraint.end) {
return true;
}
}
return false;
}
return {
ArrowFunctionExpression: checkSignature,
FunctionDeclaration: checkSignature,
FunctionExpression: checkSignature,
MethodDefinition: checkSignature,
"Program:exit": () => (usage = undefined),
TSCallSignatureDeclaration: checkSignature,
TSConstructorType: checkSignature,
TSConstructSignatureDeclaration: checkSignature,
TSDeclareFunction: checkSignature,
TSFunctionType: checkSignature,
TSIndexSignature: checkSignature,
TSMethodSignature: checkSignature,
TSPropertySignature: checkSignature,
};
},
});
module.exports = rule;
;