From 28195d61dab91c4b1de1eb2ab14f86145db5fdfa Mon Sep 17 00:00:00 2001
From: Dimitri B <BendingBender@users.noreply.github.com>
Date: Fri, 4 Jun 2021 20:53:15 +0200
Subject: [PATCH] Replace TSLint with ESLint (#119)

---
 .eslintignore                                 |  1 +
 .eslintrc                                     |  9 +++++
 package.json                                  | 12 ++++---
 source/cli.ts                                 |  9 +++--
 source/lib/assertions/assert.ts               | 35 ++++++++++---------
 .../assertions/handlers/expect-deprecated.ts  |  2 +-
 source/lib/compiler.ts                        | 12 ++++---
 source/lib/config.ts                          | 10 +++---
 source/lib/formatter.ts                       | 14 ++++++--
 source/lib/index.ts                           | 10 +++---
 source/lib/interfaces.ts                      | 11 +++---
 source/lib/parser.ts                          |  2 +-
 source/lib/rules/files-property.ts            |  2 +-
 source/lib/utils/typescript.ts                |  6 ++--
 source/test/test.ts                           |  5 +--
 source/types.d.ts                             |  1 +
 tsconfig.tsd.json                             |  3 +-
 tslint.json                                   |  3 --
 18 files changed, 89 insertions(+), 58 deletions(-)
 create mode 100644 .eslintignore
 create mode 100644 .eslintrc
 create mode 100644 source/types.d.ts
 delete mode 100644 tslint.json

diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..4143bf84
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+source/test/fixtures
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..992a2ac0
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,9 @@
+{
+	"extends": ["xo", "xo-typescript"],
+	"parserOptions": {
+		"project": "tsconfig.tsd.json"
+	},
+	"rules": {
+		"@typescript-eslint/comma-dangle": "off"
+	}
+}
diff --git a/package.json b/package.json
index ad2ab4f6..cfc818a6 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,8 @@
 		"test": "npm run lint && ava",
 		"build": "npm run clean && tsc --project tsconfig.tsd.json && chmod +x dist/cli.js",
 		"clean": "del-cli dist",
-		"lint": "tslint -p tsconfig.tsd.json --format stylish"
+		"lint": "eslint 'source/**/*'",
+		"lint:fix": "eslint --fix 'source/**/*'"
 	},
 	"files": [
 		"dist/**/*.js",
@@ -49,15 +50,18 @@
 		"@ava/typescript": "^1.1.1",
 		"@types/node": "^14.0.0",
 		"@types/react": "^16.9.2",
+		"@typescript-eslint/eslint-plugin": "^4.26.0",
+		"@typescript-eslint/parser": "^4.26.0",
 		"ava": "^3.8.2",
 		"cpy-cli": "^3.0.0",
 		"del-cli": "^3.0.0",
+		"eslint": "^7.27.0",
+		"eslint-config-xo": "^0.36.0",
+		"eslint-config-xo-typescript": "^0.41.1",
 		"execa": "^5.0.0",
 		"react": "^16.9.0",
 		"rxjs": "^6.5.3",
-		"tslint": "^5.11.0",
-		"tslint-xo": "^0.9.0",
-		"typescript": "^4.1.5"
+		"typescript": "^4.3.2"
 	},
 	"ava": {
 		"timeout": "2m",
diff --git a/source/cli.ts b/source/cli.ts
index 7d298c07..7e15aba1 100644
--- a/source/cli.ts
+++ b/source/cli.ts
@@ -16,7 +16,7 @@ const cli = meow(`
 	    ✖  10:20  Argument of type string is not assignable to parameter of type number.
 `);
 
-(async () => {	// tslint:disable-line:no-floating-promises
+(async () => {
 	try {
 		const options = cli.input.length > 0 ? {cwd: cli.input[0]} : undefined;
 
@@ -25,8 +25,11 @@ const cli = meow(`
 		if (diagnostics.length > 0) {
 			throw new Error(formatter(diagnostics));
 		}
-	} catch (error) {
-		console.error(error.message);
+	} catch (error: unknown) {
+		if (error && typeof (error as Error).message === 'string') {
+			console.error((error as Error).message);
+		}
+
 		process.exit(1);
 	}
 })();
diff --git a/source/lib/assertions/assert.ts b/source/lib/assertions/assert.ts
index 6101bda8..93fc8bd9 100644
--- a/source/lib/assertions/assert.ts
+++ b/source/lib/assertions/assert.ts
@@ -1,10 +1,12 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+
 /**
  * Check that the type of `value` is identical to type `T`.
  *
  * @param value - Value that should be identical to type `T`.
  */
-// @ts-ignore
-export const expectType = <T>(value: T) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectType = <T>(value: T) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
 
@@ -13,8 +15,9 @@ export const expectType = <T>(value: T) => {		// tslint:disable-line:no-unused
  *
  * @param value - Value that should be identical to type `T`.
  */
-// @ts-ignore
-export const expectNotType = <T>(value: any) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectNotType = <T>(value: any) => {
+	// eslint-disable-next-line no-warning-comments
 	// TODO Use a `not T` type when possible https://github.com/microsoft/TypeScript/pull/29317
 	// Do nothing, the TypeScript compiler handles this for us
 };
@@ -24,8 +27,8 @@ export const expectNotType = <T>(value: any) => {		// tslint:disable-line:no-unu
  *
  * @param value - Value that should be assignable to type `T`.
  */
-// @ts-ignore
-export const expectAssignable = <T>(value: T) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectAssignable = <T>(value: T) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
 
@@ -34,8 +37,8 @@ export const expectAssignable = <T>(value: T) => {		// tslint:disable-line:no-un
  *
  * @param value - Value that should not be assignable to type `T`.
  */
-// @ts-ignore
-export const expectNotAssignable = <T>(value: any) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectNotAssignable = <T>(value: any) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
 
@@ -44,8 +47,8 @@ export const expectNotAssignable = <T>(value: any) => {		// tslint:disable-line:
  *
  * @param value - Value that should be checked.
  */
-// @ts-ignore
-export const expectError = <T = any>(value: T) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectError = <T = any>(value: T) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
 
@@ -54,8 +57,8 @@ export const expectError = <T = any>(value: T) => {		// tslint:disable-line:no-u
  *
  * @param expression - Expression that should be marked as `@deprecated`.
  */
-// @ts-ignore
-export const expectDeprecated = (expression: any) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectDeprecated = (expression: any) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
 
@@ -64,8 +67,8 @@ export const expectDeprecated = (expression: any) => {		// tslint:disable-line:n
  *
  * @param expression - Expression that should not be marked as `@deprecated`.
  */
-// @ts-ignore
-export const expectNotDeprecated = (expression: any) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const expectNotDeprecated = (expression: any) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
 
@@ -74,7 +77,7 @@ export const expectNotDeprecated = (expression: any) => {		// tslint:disable-lin
  *
  * @param expression - Expression whose type should be printed as a warning.
  */
-// @ts-ignore
-export const printType = (expression: any) => {		// tslint:disable-line:no-unused
+// @ts-expect-error
+export const printType = (expression: any) => {
 	// Do nothing, the TypeScript compiler handles this for us
 };
diff --git a/source/lib/assertions/handlers/expect-deprecated.ts b/source/lib/assertions/handlers/expect-deprecated.ts
index 14245519..44972089 100644
--- a/source/lib/assertions/handlers/expect-deprecated.ts
+++ b/source/lib/assertions/handlers/expect-deprecated.ts
@@ -29,7 +29,7 @@ const expectDeprecatedHelper = (options: Options): Handler => {
 
 			const message = tsutils.expressionToString(checker, argument);
 
-			diagnostics.push(makeDiagnostic(node, options.message(message || '?')));
+			diagnostics.push(makeDiagnostic(node, options.message(message ?? '?')));
 		}
 
 		return diagnostics;
diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts
index f822af06..102a5a40 100644
--- a/source/lib/compiler.ts
+++ b/source/lib/compiler.ts
@@ -1,8 +1,7 @@
 import {
 	flattenDiagnosticMessageText,
 	createProgram,
-	Diagnostic as TSDiagnostic,
-	SourceFile
+	Diagnostic as TSDiagnostic
 } from '@tsd/typescript';
 import {ExpectedError, extractAssertions, parseErrorAssertionToLocation} from './parser';
 import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces';
@@ -63,10 +62,12 @@ const ignoreDiagnostic = (
 		return 'preserve';
 	}
 
-	const diagnosticFileName = (diagnostic.file as SourceFile).fileName;
+	// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+	const diagnosticFileName = diagnostic.file!.fileName;
 
 	for (const [location] of expectedErrors) {
-		const start = diagnostic.start as number;
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		const start = diagnostic.start!;
 
 		if (diagnosticFileName === location.fileName && start > location.start && start < location.end) {
 			return location;
@@ -116,7 +117,8 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
 			continue;
 		}
 
-		const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start as number);
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
 
 		diagnostics.push({
 			fileName: diagnostic.file.fileName,
diff --git a/source/lib/config.ts b/source/lib/config.ts
index 9f1261c8..a57af9b4 100644
--- a/source/lib/config.ts
+++ b/source/lib/config.ts
@@ -10,7 +10,7 @@ import {
 	parseJsonSourceFileConfigFileContent,
 	ModuleKind
 } from '@tsd/typescript';
-import {Config, RawConfig, RawCompilerOptions} from './interfaces';
+import {Config, PackageJsonWithTsdConfig, RawCompilerOptions} from './interfaces';
 
 /**
  * Load the configuration settings.
@@ -18,12 +18,12 @@ import {Config, RawConfig, RawCompilerOptions} from './interfaces';
  * @param pkg - The package.json object.
  * @returns The config object.
  */
-export default (pkg: {tsd?: RawConfig}, cwd: string): Config => {
-	const pkgConfig = pkg.tsd || {};
+export default (pkg: PackageJsonWithTsdConfig, cwd: string): Config => {
+	const pkgConfig = pkg.tsd ?? {};
 
 	const tsConfigCompilerOptions = getOptionsFromTsConfig(cwd);
 	const packageJsonCompilerOptions = parseCompilerConfigObject(
-		pkgConfig.compilerOptions || {},
+		pkgConfig.compilerOptions ?? {},
 		cwd
 	);
 
@@ -70,5 +70,5 @@ function parseCompilerConfigObject(compilerOptions: RawCompilerOptions, cwd: str
 }
 
 function parseRawLibs(libs: string[], cwd: string): string[] {
-	return parseCompilerConfigObject({lib: libs}, cwd).lib || [];
+	return parseCompilerConfigObject({lib: libs}, cwd).lib ?? [];
 }
diff --git a/source/lib/formatter.ts b/source/lib/formatter.ts
index 3fdb23eb..fb135b33 100644
--- a/source/lib/formatter.ts
+++ b/source/lib/formatter.ts
@@ -1,14 +1,21 @@
 import * as formatter from 'eslint-formatter-pretty';
 import {Diagnostic} from './interfaces';
 
+interface FileWithDiagnostics {
+	filePath: string;
+	errorCount: number;
+	warningCount: number;
+	messages: Diagnostic[];
+}
+
 /**
  * Format the TypeScript diagnostics to a human readable output.
  *
  * @param diagnostics - List of TypeScript diagnostics.
  * @returns Beautiful diagnostics output
  */
-export default (diagnostics: Diagnostic[]) => {
-	const fileMap = new Map<string, any>();
+export default (diagnostics: Diagnostic[]): string => {
+	const fileMap = new Map<string, FileWithDiagnostics>();
 
 	for (const diagnostic of diagnostics) {
 		let entry = fileMap.get(diagnostic.fileName);
@@ -28,5 +35,6 @@ export default (diagnostics: Diagnostic[]) => {
 		entry.messages.push(diagnostic);
 	}
 
-	return formatter(Array.from(fileMap.values()));
+	// eslint-disable-next-line @typescript-eslint/no-unsafe-call
+	return String(formatter([...fileMap.values()]));
 };
diff --git a/source/lib/index.ts b/source/lib/index.ts
index 3c1580dc..b36e5db5 100644
--- a/source/lib/index.ts
+++ b/source/lib/index.ts
@@ -5,7 +5,7 @@ import * as globby from 'globby';
 import {getDiagnostics as getTSDiagnostics} from './compiler';
 import loadConfig from './config';
 import getCustomDiagnostics from './rules';
-import {Context, Config} from './interfaces';
+import {Context, Config, Diagnostic, PackageJsonWithTsdConfig} from './interfaces';
 
 export interface Options {
 	cwd: string;
@@ -13,7 +13,7 @@ export interface Options {
 	testFiles?: readonly string[];
 }
 
-const findTypingsFile = async (pkg: any, options: Options) => {
+const findTypingsFile = async (pkg: PackageJsonWithTsdConfig, options: Options): Promise<string> => {
 	const typings =
 		options.typingsFile ||
 		pkg.types ||
@@ -78,15 +78,15 @@ const findTestFiles = async (typingsFilePath: string, options: Options & {config
  *
  * @returns A promise which resolves the diagnostics of the type definition.
  */
-export default async (options: Options = {cwd: process.cwd()}) => {
+export default async (options: Options = {cwd: process.cwd()}): Promise<Diagnostic[]> => {
 	const pkgResult = await readPkgUp({cwd: options.cwd});
 
 	if (!pkgResult) {
 		throw new Error('No `package.json` file found. Make sure you are running the command in a Node.js project.');
 	}
 
-	const pkg = pkgResult.packageJson;
-	const config = loadConfig(pkg as any, options.cwd);
+	const pkg = pkgResult.packageJson as PackageJsonWithTsdConfig;
+	const config = loadConfig(pkg, options.cwd);
 
 	// Look for a typings file, otherwise use `index.d.ts` in the root directory. If the file is not found, throw an error.
 	const typingsFile = await findTypingsFile(pkg, options);
diff --git a/source/lib/interfaces.ts b/source/lib/interfaces.ts
index 685c9614..a15c0f0d 100644
--- a/source/lib/interfaces.ts
+++ b/source/lib/interfaces.ts
@@ -1,8 +1,7 @@
 import {CompilerOptions} from '@tsd/typescript';
+import {NormalizedPackageJson} from 'read-pkg-up';
 
-export interface RawCompilerOptions {
-	[option: string]: any;
-}
+export type RawCompilerOptions = Record<string, any>;
 
 export interface Config<Options = CompilerOptions> {
 	directory: string;
@@ -11,9 +10,13 @@ export interface Config<Options = CompilerOptions> {
 
 export type RawConfig = Partial<Config<RawCompilerOptions>>;
 
+export type PackageJsonWithTsdConfig = NormalizedPackageJson & {
+	tsd?: RawConfig;
+};
+
 export interface Context {
 	cwd: string;
-	pkg: any;
+	pkg: PackageJsonWithTsdConfig;
 	typingsFile: string;
 	testFiles: string[];
 	config: Config;
diff --git a/source/lib/parser.ts b/source/lib/parser.ts
index 6bc8cf05..0a7da37e 100644
--- a/source/lib/parser.ts
+++ b/source/lib/parser.ts
@@ -23,7 +23,7 @@ export const extractAssertions = (program: Program): Map<Assertion, Set<CallExpr
 			if (assertionFnNames.has(identifier)) {
 				const assertion = identifier as Assertion;
 
-				const nodes = assertions.get(assertion) || new Set<CallExpression>();
+				const nodes = assertions.get(assertion) ?? new Set<CallExpression>();
 
 				nodes.add(node);
 
diff --git a/source/lib/rules/files-property.ts b/source/lib/rules/files-property.ts
index 61a2094e..5f5e9b9e 100644
--- a/source/lib/rules/files-property.ts
+++ b/source/lib/rules/files-property.ts
@@ -43,7 +43,7 @@ export default (context: Context): Diagnostic[] => {
 function processGitIgnoreStylePatterns(patterns: readonly string[]): string[] {
 	const processedPatterns = patterns
 		.map(pattern => {
-			const [negatePatternMatch] = pattern.match(/^!+/) || [];
+			const [negatePatternMatch] = /^!+/.exec(pattern) ?? [];
 			const negationMarkersCount = negatePatternMatch ? negatePatternMatch.length : 0;
 
 			return [
diff --git a/source/lib/utils/typescript.ts b/source/lib/utils/typescript.ts
index f039db35..9886edb7 100644
--- a/source/lib/utils/typescript.ts
+++ b/source/lib/utils/typescript.ts
@@ -8,9 +8,9 @@ import {TypeChecker, Expression, isCallLikeExpression, JSDocTagInfo} from '@tsd/
  * @return A unique Set of JSDoc tags or `undefined` if they couldn't be resolved.
  */
 export const resolveJSDocTags = (checker: TypeChecker, expression: Expression): Map<string, JSDocTagInfo> | undefined => {
-	const ref = isCallLikeExpression(expression)
-		? checker.getResolvedSignature(expression)
-		: checker.getSymbolAtLocation(expression);
+	const ref = isCallLikeExpression(expression) ?
+		checker.getResolvedSignature(expression) :
+		checker.getSymbolAtLocation(expression);
 
 	if (!ref) {
 		return;
diff --git a/source/test/test.ts b/source/test/test.ts
index 0abfd794..ea773147 100644
--- a/source/test/test.ts
+++ b/source/test/test.ts
@@ -370,7 +370,7 @@ test('includes extended config files along with found ones', async t => {
 test('errors in libs from node_modules are not reported', async t => {
 	const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/exclude-node-modules')});
 
-	const [nodeModuleDiagnostics, testFileDiagnostics, otherDiagnostics] = diagnostics.reduce(
+	const [nodeModuleDiagnostics, testFileDiagnostics, otherDiagnostics] = diagnostics.reduce<Diagnostic[][]>(
 		([nodeModuleDiags, testFileDiags, otherDiags], diagnostic) => {
 			if (/[/\\]node_modules[/\\]/.test(diagnostic.fileName)) {
 				nodeModuleDiags.push(diagnostic);
@@ -379,9 +379,10 @@ test('errors in libs from node_modules are not reported', async t => {
 			} else {
 				otherDiags.push(diagnostic);
 			}
+
 			return [nodeModuleDiags, testFileDiags, otherDiags];
 		},
-		[[], [], []] as Diagnostic[][]
+		[[], [], []]
 	);
 
 	t.deepEqual(
diff --git a/source/types.d.ts b/source/types.d.ts
new file mode 100644
index 00000000..11ecc1ec
--- /dev/null
+++ b/source/types.d.ts
@@ -0,0 +1 @@
+declare module 'eslint-formatter-pretty';
diff --git a/tsconfig.tsd.json b/tsconfig.tsd.json
index e7ad6173..fcf58f64 100644
--- a/tsconfig.tsd.json
+++ b/tsconfig.tsd.json
@@ -12,8 +12,8 @@
 		"newLine": "lf",
 		"stripInternal": true,
 		"strict": true,
-		"noImplicitAny": false,
 		"noImplicitReturns": true,
+		"noImplicitOverride": true,
 		"noUnusedLocals": true,
 		"noUnusedParameters": true,
 		"noFallthroughCasesInSwitch": true,
@@ -24,6 +24,5 @@
 		"node_modules",
 		"dist",
 		"source/test/fixtures",
-		"libraries"
 	]
 }
diff --git a/tslint.json b/tslint.json
deleted file mode 100644
index 55e9f361..00000000
--- a/tslint.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-	"extends": "tslint-xo"
-}