Converted no-unnecessary-generics from TSLint to ESLint (#539)

* Converted no-unnecessary-generics from TSLint to ESLint

* ...and don't forget dtslint.json

* Add missing readFile

* Add valid test case from original

* Fix spelling of recur

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
This commit is contained in:
Josh Goldberg
2022-12-29 13:02:56 -05:00
committed by GitHub
parent 8f8ca80aa8
commit c941b4aa1c
12 changed files with 290 additions and 174 deletions

View File

@@ -28,6 +28,6 @@
"ts-jest": "^25.2.1",
"tslint": "^6.1.2",
"tslint-microsoft-contrib": "^6.2.0",
"typescript": "^4.7.4"
"typescript": "4.7.4"
}
}

View File

@@ -11,7 +11,6 @@
"strict-export-declare-modifiers": true,
"no-any-union": true,
"no-single-declare-module": true,
"no-unnecessary-generics": true,
"prefer-declare-function": true,
"unified-signatures": true,
"void-return": true,

View File

@@ -44,7 +44,7 @@
"@types/fs-extra": "^5.0.2",
"@types/json-stable-stringify": "^1.0.32",
"@types/strip-json-comments": "^0.0.28",
"typescript": "next"
"typescript": "4.7.4"
},
"engines": {
"node": ">=10.0.0"

View File

@@ -0,0 +1,126 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import * as ts from "typescript";
import { createRule } from "../util";
type ESTreeFunctionLikeWithTypeParameters = TSESTree.FunctionLike & {
typeParameters: {};
};
type TSSignatureDeclarationWithTypeParameters = ts.SignatureDeclaration & {
typeParameters: {};
};
const rule = createRule({
defaultOptions: [],
meta: {
docs: {
description: "Forbids signatures using a generic parameter only once.",
recommended: "error",
},
messages: {
never: "Type parameter {{name}} is never used.",
sole: "Type parameter {{name}} is used only once.",
},
schema: [],
type: "problem",
},
name: "no-relative-import-in-test",
create(context) {
return {
[[
"ArrowFunctionExpression[typeParameters]",
"FunctionDeclaration[typeParameters]",
"FunctionExpression[typeParameters]",
"TSCallSignatureDeclaration[typeParameters]",
"TSConstructorType[typeParameters]",
"TSDeclareFunction[typeParameters]",
"TSFunctionType[typeParameters]",
"TSMethodSignature[typeParameters]",
].join(", ")](esNode: ESTreeFunctionLikeWithTypeParameters) {
const parserServices = ESLintUtils.getParserServices(context);
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) as TSSignatureDeclarationWithTypeParameters;
if (!tsNode.typeParameters) {
return;
}
const checker = parserServices.program.getTypeChecker();
for (const typeParameter of tsNode.typeParameters) {
const name = typeParameter.name.text;
const res = getSoleUse(tsNode, assertDefined(checker.getSymbolAtLocation(typeParameter.name)), checker);
switch (res.type) {
case "sole":
context.report({
data: { name },
messageId: "sole",
node: parserServices.tsNodeToESTreeNodeMap.get(res.soleUse),
});
break;
case "never":
context.report({
data: { name },
messageId: "never",
node: parserServices.tsNodeToESTreeNodeMap.get(typeParameter),
});
break;
}
}
},
};
},
});
type Result = { type: "ok" | "never" } | { type: "sole"; soleUse: ts.Identifier };
function getSoleUse(sig: ts.SignatureDeclaration, typeParameterSymbol: ts.Symbol, checker: ts.TypeChecker): Result {
const exit = {};
let soleUse: ts.Identifier | undefined;
try {
if (sig.typeParameters) {
for (const tp of sig.typeParameters) {
if (tp.constraint) {
recur(tp.constraint);
}
}
}
for (const param of sig.parameters) {
if (param.type) {
recur(param.type);
}
}
if (sig.type) {
recur(sig.type);
}
} catch (err) {
if (err === exit) {
return { type: "ok" };
}
throw err;
}
return soleUse ? { type: "sole", soleUse } : { type: "never" };
function recur(node: ts.Node): void {
if (ts.isIdentifier(node)) {
if (checker.getSymbolAtLocation(node) === typeParameterSymbol) {
if (soleUse === undefined) {
soleUse = node;
} else {
throw exit;
}
}
} else {
node.forEachChild(recur);
}
}
}
export = rule;
function assertDefined<T>(value: T | undefined): T {
if (value === undefined) {
throw new Error("unreachable");
}
return value;
}

View File

@@ -1,115 +0,0 @@
import * as Lint from "tslint";
import * as ts from "typescript";
import { failure } from "../util";
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-unnecessary-generics",
description: "Forbids signatures using a generic parameter only once.",
optionsDescription: "Not configurable.",
options: null,
type: "style",
typescriptOnly: true,
};
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING(typeParameter: string) {
return failure(Rule.metadata.ruleName, `Type parameter ${typeParameter} is used only once.`);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static FAILURE_STRING_NEVER(typeParameter: string) {
return failure(Rule.metadata.ruleName, `Type parameter ${typeParameter} is never used.`);
}
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, (ctx) => walk(ctx, program.getTypeChecker()));
}
}
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
const { sourceFile } = ctx;
sourceFile.forEachChild(function cb(node) {
if (ts.isFunctionLike(node)) {
checkSignature(node);
}
node.forEachChild(cb);
});
function checkSignature(sig: ts.SignatureDeclaration) {
if (!sig.typeParameters) {
return;
}
for (const tp of sig.typeParameters) {
const typeParameter = tp.name.text;
const res = getSoleUse(sig, assertDefined(checker.getSymbolAtLocation(tp.name)), checker);
switch (res.type) {
case "ok":
break;
case "sole":
ctx.addFailureAtNode(res.soleUse, Rule.FAILURE_STRING(typeParameter));
break;
case "never":
ctx.addFailureAtNode(tp, Rule.FAILURE_STRING_NEVER(typeParameter));
break;
default:
assertNever(res);
}
}
}
}
type Result = { type: "ok" | "never" } | { type: "sole"; soleUse: ts.Identifier };
function getSoleUse(sig: ts.SignatureDeclaration, typeParameterSymbol: ts.Symbol, checker: ts.TypeChecker): Result {
const exit = {};
let soleUse: ts.Identifier | undefined;
try {
if (sig.typeParameters) {
for (const tp of sig.typeParameters) {
if (tp.constraint) {
recur(tp.constraint);
}
}
}
for (const param of sig.parameters) {
if (param.type) {
recur(param.type);
}
}
if (sig.type) {
recur(sig.type);
}
} catch (err) {
if (err === exit) {
return { type: "ok" };
}
throw err;
}
return soleUse ? { type: "sole", soleUse } : { type: "never" };
function recur(node: ts.Node): void {
if (ts.isIdentifier(node)) {
if (checker.getSymbolAtLocation(node) === typeParameterSymbol) {
if (soleUse === undefined) {
soleUse = node;
} else {
throw exit;
}
}
} else {
node.forEachChild(recur);
}
}
}
function assertDefined<T>(value: T | undefined): T {
if (value === undefined) {
throw new Error("unreachable");
}
return value;
}
function assertNever(_: never) {
throw new Error("unreachable");
}

View File

@@ -0,0 +1,152 @@
import { ESLintUtils } from "@typescript-eslint/utils";
import * as rule from "../src/rules/no-unnecessary-generics";
const ruleTester = new ESLintUtils.RuleTester({
parserOptions: {
ecmaVersion: 2018,
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
},
parser: "@typescript-eslint/parser",
});
ruleTester.run("no-unnecessary-generics", rule, {
invalid: [
{
code: `
const f2 = <T>(): T => {};
`,
errors: [
{
line: 2,
column: 19,
messageId: "sole",
},
],
},
{
code: `
class C {
constructor<T>(x: T) {}
}
`,
errors: [
{
line: 3,
column: 21,
messageId: "sole",
},
],
},
{
code: `
function f<T>(): T { }
`,
errors: [
{
line: 2,
column: 18,
messageId: "sole",
},
],
},
{
code: `
function f<T>(x: { T: number }): void;
`,
errors: [
{
line: 2,
column: 12,
messageId: "never",
},
],
},
{
code: `
function f<T, U extends T>(u: U): U;
`,
errors: [
{
line: 2,
column: 25,
messageId: "sole",
},
],
},
{
code: `
const f = function<T>(): T {};
`,
errors: [
{
line: 2,
column: 26,
messageId: "sole",
},
],
},
{
code: `
interface I {
<T>(value: T): void;
}
`,
errors: [
{
line: 3,
column: 14,
messageId: "sole",
},
],
},
{
code: `
interface I {
m<T>(x: T): void;
}
`,
errors: [
{
line: 3,
column: 11,
messageId: "sole",
},
],
},
{
code: `
type Fn = <T>() => T;
`,
errors: [
{
line: 2,
column: 20,
messageId: "sole",
},
],
},
{
code: `
type Ctr = new<T>() => T;
`,
errors: [
{
line: 2,
column: 24,
messageId: "sole",
},
],
},
],
valid: [
`function example(a: string): string;`,
`function example<T>(a: T): T;`,
`function example<T>(a: T[]): T;`,
`function example<T>(a: Set<T>): T;`,
`function example<T>(a: Set<T>, b: T[]): void;`,
`function example<T>(a: Map<T, T>): void;`,
`function example<T, U extends T>(t: T, u: U): U;`,
],
});

View File

@@ -1,38 +0,0 @@
interface I {
<T>(value: T): void;
~ [0]
m<T>(x: T): void;
~ [0]
}
class C {
constructor<T>(x: T) {}
~ [0]
}
type Fn = <T>() => T;
~ [0]
type Ctr = new<T>() => T;
~ [0]
function f<T>(): T { }
~ [0]
const f = function<T>(): T {};
~ [0]
const f2 = <T>(): T => {};
~ [0]
function f<T>(x: { T: number }): void;
~ [Type parameter T is never used. See: https://github.com/microsoft/DefinitelyTyped-tools/blob/master/packages/dtslint/docs/no-unnecessary-generics.md]
function f<T, U extends T>(u: U): U;
~ [0]
// OK:
// Uses type parameter twice
function foo<T>(m: Map<T, T>): void {}
// `T` appears in a constraint, so it appears twice.
function f<T, U extends T>(t: T, u: U): U;
[0]: Type parameter T is used only once. See: https://github.com/microsoft/DefinitelyTyped-tools/blob/master/packages/dtslint/docs/no-unnecessary-generics.md

View File

@@ -1,6 +0,0 @@
{
"rulesDirectory": ["../../dist/rules"],
"rules": {
"no-unnecessary-generics": true
}
}

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"skipLibCheck": true,
"strict": true,
"target": "esnext"
},
"files": ["file.ts"]
}

View File

@@ -15,6 +15,7 @@ export function createLanguageServiceHost(
getNewLine: () => ts.sys.newLine,
getScriptFileNames: () => testPaths,
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
getDirectories: ts.sys.getDirectories,
getScriptSnapshot: (fileName) => ts.ScriptSnapshot.fromString(ts.sys.readFile(ensureExists(fileName))!),
getScriptVersion: () => (version++).toString(),

View File

@@ -9165,21 +9165,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.1.0:
version "4.5.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
typescript@^4.7.4:
typescript@4.7.4, typescript@^4.1.0:
version "4.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@next:
version "4.6.0-dev.20211126"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.0-dev.20211126.tgz#d27ce3a360dc4da1dcdebd80efe42b51afdeebdb"
integrity sha512-m+LKstqVv6FYW363aIbO6bm8awsLbeSUCzU6FxPtzUF/WJkFieQfYmdVwEIzigeTpw4E2GETBXnk6P6AixcQJQ==
uglify-js@^3.1.4:
version "3.13.5"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113"