@definitelytyped/eslint-plugin 0.0.195 → 0.0.197

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,556 @@
1
+ import { isDeclarationPath } from "@definitelytyped/utils";
2
+ import { createRule, findTypesPackage, findUp } from "../util";
3
+ import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
4
+ import type * as ts from "typescript";
5
+ import path from "path";
6
+ import fs from "fs";
7
+ import { ReportDescriptorMessageData } from "@typescript-eslint/utils/ts-eslint";
8
+
9
+ type TSModule = typeof ts;
10
+ const builtinTypeScript = require("typescript") as TSModule;
11
+
12
+ type Options = [
13
+ {
14
+ versionsToTest?: {
15
+ readonly versionName: string;
16
+ readonly path: string;
17
+ }[];
18
+ },
19
+ ];
20
+ type MessageIds =
21
+ | "noTsconfig"
22
+ | "twoAssertions"
23
+ | "failure"
24
+ | "diagnostic"
25
+ | "programContents"
26
+ | "noMatch"
27
+ | "needInstall";
28
+
29
+ const rule = createRule<Options, MessageIds>({
30
+ name: "expect",
31
+ meta: {
32
+ type: "problem",
33
+ docs: {
34
+ description: "Asserts types with $ExpectType.",
35
+ },
36
+ messages: {
37
+ noTsconfig: `Could not find a tsconfig.json file.`,
38
+ twoAssertions: "This line has 2 $ExpectType assertions.",
39
+ failure: `TypeScript{{versionNameString}} expected type to be:\n {{expectedType}}\ngot:\n {{actualType}}`,
40
+ diagnostic: `TypeScript{{versionNameString}} {{message}}`,
41
+ programContents:
42
+ `Program source files differ between TypeScript versions. This may be a dtslint bug.\n` +
43
+ `Expected to find a file '{{fileName}}' present in ${builtinTypeScript.versionMajorMinor}, but did not find it in ts@{{versionName}}.`,
44
+ noMatch:
45
+ "Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above.",
46
+ needInstall: `A module look-up failed, this often occurs when you need to run \`pnpm install\` on a dependent module before you can lint.
47
+
48
+ Before you debug, first try running:
49
+
50
+ pnpm install -w --filter '...{./types/{{dirPath}}}...'
51
+
52
+ Then re-run.`,
53
+ },
54
+ schema: [
55
+ {
56
+ type: "object",
57
+ properties: {
58
+ versionsToTest: {
59
+ type: "array",
60
+ items: {
61
+ type: "object",
62
+ properties: {
63
+ versionName: { type: "string" },
64
+ path: { type: "string" },
65
+ },
66
+ required: ["versionName", "path"],
67
+ additionalProperties: false,
68
+ },
69
+ },
70
+ },
71
+ additionalProperties: false,
72
+ },
73
+ ],
74
+ },
75
+ defaultOptions: [{}],
76
+ create(context) {
77
+ if (isDeclarationPath(context.filename) || !context.sourceCode.text.includes("$ExpectType")) {
78
+ return {};
79
+ }
80
+
81
+ const tsconfigPath = findUp(context.filename, (dir) => {
82
+ const tsconfig = path.join(dir, "tsconfig.json");
83
+ return fs.existsSync(tsconfig) ? tsconfig : undefined;
84
+ });
85
+
86
+ if (!tsconfigPath) {
87
+ context.report({
88
+ messageId: "noTsconfig",
89
+ loc: zeroSourceLocation,
90
+ });
91
+ return {};
92
+ }
93
+
94
+ // TODO: determine TS versions to run based on this package
95
+ const dirPath = path.dirname(tsconfigPath);
96
+
97
+ const parserServices = ESLintUtils.getParserServices(context);
98
+
99
+ return {
100
+ // eslint-disable-next-line @typescript-eslint/naming-convention
101
+ Program(node) {
102
+ // Grab the filename as known by TS, just to make sure we get the right normalization.
103
+ const fileName = parserServices.esTreeNodeToTSNodeMap.get(node).fileName;
104
+ const getLocFromIndex = (index: number) => context.sourceCode.getLocFromIndex(index);
105
+
106
+ const toReport = new Map<string, Omit<ReporterInfo, "versionName"> & { versions: Set<string> }>();
107
+ const reporter: Reporter = ({ versionName, messageId, data, loc }) => {
108
+ const key = JSON.stringify({ messageId, data, loc });
109
+ let existing = toReport.get(key);
110
+ if (existing === undefined) {
111
+ toReport.set(key, (existing = { messageId, data, loc, versions: new Set() }));
112
+ }
113
+ existing.versions.add(versionName);
114
+ };
115
+
116
+ let versionsToTest = context.options[0]?.versionsToTest;
117
+ if (!versionsToTest?.length) {
118
+ // In the editor, just use the built-in install of TypeScript.
119
+ versionsToTest = [{ versionName: "", path: require.resolve("typescript") }];
120
+ }
121
+
122
+ for (const version of versionsToTest) {
123
+ const ts = require(version.path) as TSModule;
124
+ const program = getProgram(tsconfigPath, ts, version.versionName, parserServices.program);
125
+ walk(
126
+ getLocFromIndex,
127
+ reporter,
128
+ fileName,
129
+ program,
130
+ ts,
131
+ version.versionName,
132
+ /*nextHigherVersion*/ undefined,
133
+ dirPath,
134
+ );
135
+ }
136
+
137
+ for (const { messageId, data, loc, versions } of toReport.values()) {
138
+ const versionNames = [...versions].sort().join(", ");
139
+ context.report({
140
+ messageId,
141
+ data: { ...data, versionNameString: versionNames ? `@${versionNames}` : "" },
142
+ loc,
143
+ });
144
+ }
145
+ },
146
+ };
147
+ },
148
+ });
149
+
150
+ const programCache = new WeakMap<ts.Program, Map<string, ts.Program>>();
151
+ /** Maps a tslint Program to one created with the version specified in `options`. */
152
+ function getProgram(configFile: string, ts: TSModule, versionName: string, lintProgram: ts.Program): ts.Program {
153
+ let versionToProgram = programCache.get(lintProgram);
154
+ if (versionToProgram === undefined) {
155
+ versionToProgram = new Map<string, ts.Program>();
156
+ programCache.set(lintProgram, versionToProgram);
157
+ }
158
+
159
+ let newProgram = versionToProgram.get(versionName);
160
+ if (newProgram === undefined) {
161
+ newProgram = createProgram(configFile, ts);
162
+ versionToProgram.set(versionName, newProgram);
163
+ }
164
+ return newProgram;
165
+ }
166
+
167
+ function createProgram(configFile: string, ts: TSModule): ts.Program {
168
+ const projectDirectory = path.dirname(configFile);
169
+ const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
170
+ const parseConfigHost: ts.ParseConfigHost = {
171
+ fileExists: fs.existsSync,
172
+ readDirectory: ts.sys.readDirectory,
173
+ readFile: (file) => fs.readFileSync(file, "utf8"),
174
+ useCaseSensitiveFileNames: true,
175
+ };
176
+ const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, path.resolve(projectDirectory), {
177
+ noEmit: true,
178
+ });
179
+
180
+ if (config.compilerOptions?.module === "node16" && parsed.options.module === undefined) {
181
+ // TypeScript version is too old to handle the "node16" module option,
182
+ // but we can run tests falling back to commonjs/node.
183
+ parsed.options.module = ts.ModuleKind.CommonJS;
184
+ parsed.options.moduleResolution = ts.ModuleResolutionKind.NodeJs;
185
+ }
186
+
187
+ const host = ts.createCompilerHost(parsed.options, true);
188
+ return ts.createProgram(parsed.fileNames, parsed.options, host);
189
+ }
190
+
191
+ interface ReporterInfo {
192
+ versionName: string;
193
+ messageId: MessageIds;
194
+ data?: ReportDescriptorMessageData;
195
+ loc: Readonly<TSESTree.SourceLocation>;
196
+ }
197
+
198
+ type Reporter = (info: ReporterInfo) => void;
199
+
200
+ const zeroSourceLocation: Readonly<TSESTree.SourceLocation> = {
201
+ start: { line: 0, column: 0 },
202
+ end: { line: 0, column: 0 },
203
+ };
204
+
205
+ function walk(
206
+ getLocFromIndex: (index: number) => Readonly<TSESTree.Position>,
207
+ report: Reporter,
208
+ fileName: string,
209
+ program: ts.Program,
210
+ ts: TSModule,
211
+ versionName: string,
212
+ nextHigherVersion: string | undefined,
213
+ dirPath: string,
214
+ ): void {
215
+ const sourceFile = program.getSourceFile(fileName)!;
216
+ if (!sourceFile) {
217
+ report({
218
+ versionName,
219
+ messageId: "programContents",
220
+ data: { fileName, versionName },
221
+ loc: zeroSourceLocation,
222
+ });
223
+ return;
224
+ }
225
+
226
+ const checker = program.getTypeChecker();
227
+
228
+ if (versionName) {
229
+ // If we're using the built-in version of TS, then we're in the editor and tsserver will report diagnostics.
230
+
231
+ // Don't care about emit errors.
232
+ const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
233
+ for (const diagnostic of diagnostics) {
234
+ addDiagnosticFailure(diagnostic);
235
+ }
236
+
237
+ const cannotFindDepsDiags = diagnostics
238
+ .filter((d) => !d.file || isExternalDependency(d.file, dirPath, program))
239
+ .find((d) => d.code === 2307 && d.messageText.toString().includes("Cannot find module"));
240
+ if (cannotFindDepsDiags && cannotFindDepsDiags.file) {
241
+ const packageInfo = findTypesPackage(fileName);
242
+ if (!packageInfo) {
243
+ throw new Error("Could not find package info for " + fileName);
244
+ }
245
+ const dtRoot = findUp(packageInfo.dir, (dir) => {
246
+ if (fs.existsSync(path.join(dir, "notNeededPackages.json"))) {
247
+ return dir;
248
+ }
249
+ return undefined;
250
+ });
251
+ if (dtRoot) {
252
+ const dirPath = path.relative(dtRoot, path.dirname(packageInfo.dir));
253
+ report({
254
+ versionName,
255
+ messageId: "needInstall",
256
+ data: { dirPath },
257
+ loc: zeroSourceLocation,
258
+ });
259
+ }
260
+ }
261
+ }
262
+
263
+ if (sourceFile.isDeclarationFile || !sourceFile.text.includes("$ExpectType")) {
264
+ // Normal file.
265
+ return;
266
+ }
267
+
268
+ const { typeAssertions, duplicates } = parseAssertions(sourceFile);
269
+
270
+ for (const line of duplicates) {
271
+ addFailureAtLine(report, { versionName, messageId: "twoAssertions" }, line);
272
+ }
273
+
274
+ const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts);
275
+ for (const { node, expected, actual } of unmetExpectations) {
276
+ report({
277
+ versionName,
278
+ messageId: "failure",
279
+ data: {
280
+ expectedType: expected,
281
+ actualType: actual,
282
+ },
283
+ loc: {
284
+ start: getLocFromIndex(node.getStart(sourceFile)),
285
+ end: getLocFromIndex(node.getEnd()),
286
+ },
287
+ });
288
+ }
289
+ for (const line of unusedAssertions) {
290
+ addFailureAtLine(
291
+ report,
292
+ {
293
+ versionName,
294
+ messageId: "noMatch",
295
+ },
296
+ line - 1,
297
+ );
298
+ }
299
+
300
+ function addDiagnosticFailure(diagnostic: ts.Diagnostic): void {
301
+ const intro = getIntro();
302
+ if (diagnostic.file === sourceFile) {
303
+ const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`;
304
+ report({
305
+ versionName,
306
+ messageId: "diagnostic",
307
+ data: { message: msg },
308
+ loc: {
309
+ start: getLocFromIndex(diagnostic.start!),
310
+ end: getLocFromIndex(diagnostic.start! + diagnostic.length!),
311
+ },
312
+ });
313
+ } else {
314
+ report({
315
+ versionName,
316
+ messageId: "diagnostic",
317
+ data: { message: `${intro}\n${fileName}${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}` },
318
+ loc: zeroSourceLocation,
319
+ });
320
+ }
321
+ }
322
+
323
+ function getIntro(): string {
324
+ if (nextHigherVersion === undefined) {
325
+ return `compile error: `;
326
+ } else {
327
+ const msg = `Compile error in typescript@${versionName} but not in typescript@${nextHigherVersion}.\n`;
328
+ const explain =
329
+ nextHigherVersion === "next"
330
+ ? "TypeScript@next features not yet supported."
331
+ : `Fix by adding '"minimumTypeScriptVersion": "${nextHigherVersion}"' to package.json.`;
332
+ return msg + explain;
333
+ }
334
+ }
335
+
336
+ function addFailureAtLine(report: Reporter, info: Omit<ReporterInfo, "loc">, line: number): void {
337
+ const start = sourceFile.getPositionOfLineAndCharacter(line, 0);
338
+ let end = start + sourceFile.text.split("\n")[line].length;
339
+ if (sourceFile.text[end - 1] === "\r") {
340
+ end--;
341
+ }
342
+ report({
343
+ ...info,
344
+ loc: {
345
+ start: getLocFromIndex(start),
346
+ end: getLocFromIndex(end),
347
+ },
348
+ });
349
+ }
350
+ }
351
+
352
+ // TODO(jakebailey): dedupe these copied frunctions from dtslint
353
+ function normalizePath(file: string) {
354
+ // replaces '\' with '/' and forces all DOS drive letters to be upper-case
355
+ return path
356
+ .normalize(file)
357
+ .replace(/\\/g, "/")
358
+ .replace(/^[a-z](?=:)/, (c) => c.toUpperCase());
359
+ }
360
+
361
+ function startsWithDirectory(filePath: string, dirPath: string): boolean {
362
+ const normalFilePath = normalizePath(filePath);
363
+ const normalDirPath = normalizePath(dirPath).replace(/\/$/, "");
364
+ return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\");
365
+ }
366
+
367
+ function isExternalDependency(file: ts.SourceFile, dirPath: string, program: ts.Program): boolean {
368
+ return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file);
369
+ }
370
+
371
+ interface Assertions {
372
+ /** Map from a line number to the expected type at that line. */
373
+ readonly typeAssertions: Map<number, string>;
374
+ /** Lines with more than one assertion (these are errors). */
375
+ readonly duplicates: readonly number[];
376
+ }
377
+
378
+ function parseAssertions(sourceFile: ts.SourceFile): Assertions {
379
+ const typeAssertions = new Map<number, string>();
380
+ const duplicates: number[] = [];
381
+
382
+ const { text } = sourceFile;
383
+ const commentRegexp = /\/\/(.*)/g;
384
+ const lineStarts = sourceFile.getLineStarts();
385
+ let curLine = 0;
386
+
387
+ while (true) {
388
+ const commentMatch = commentRegexp.exec(text);
389
+ if (commentMatch === null) {
390
+ break;
391
+ }
392
+ // Match on the contents of that comment so we do nothing in a commented-out assertion,
393
+ // i.e. `// foo; // $ExpectType number`
394
+ if (!commentMatch[1].startsWith(" $ExpectType ")) {
395
+ continue;
396
+ }
397
+ const line = getLine(commentMatch.index);
398
+ const expectedType = commentMatch[1].slice(" $ExpectType ".length);
399
+ // Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate.
400
+ if (typeAssertions.delete(line)) {
401
+ duplicates.push(line);
402
+ } else {
403
+ typeAssertions.set(line, expectedType);
404
+ }
405
+ }
406
+
407
+ return { typeAssertions, duplicates };
408
+
409
+ function getLine(pos: number): number {
410
+ // advance curLine to be the line preceding 'pos'
411
+ while (lineStarts[curLine + 1] <= pos) {
412
+ curLine++;
413
+ }
414
+ // If this is the first token on the line, it applies to the next line.
415
+ // Otherwise, it applies to the text to the left of it.
416
+ return isFirstOnLine(text, lineStarts[curLine], pos) ? curLine + 1 : curLine;
417
+ }
418
+ }
419
+
420
+ function isFirstOnLine(text: string, lineStart: number, pos: number): boolean {
421
+ for (let i = lineStart; i < pos; i++) {
422
+ if (text[i] !== " ") {
423
+ return false;
424
+ }
425
+ }
426
+ return true;
427
+ }
428
+
429
+ interface ExpectTypeFailures {
430
+ /** Lines with an $ExpectType, but a different type was there. */
431
+ readonly unmetExpectations: readonly { node: ts.Node; expected: string; actual: string }[];
432
+ /** Lines with an $ExpectType, but no node could be found. */
433
+ readonly unusedAssertions: Iterable<number>;
434
+ }
435
+
436
+ function getExpectTypeFailures(
437
+ sourceFile: ts.SourceFile,
438
+ typeAssertions: Map<number, string>,
439
+ checker: ts.TypeChecker,
440
+ ts: TSModule,
441
+ ): ExpectTypeFailures {
442
+ const unmetExpectations: { node: ts.Node; expected: string; actual: string }[] = [];
443
+ // Match assertions to the first node that appears on the line they apply to.
444
+ // `forEachChild` isn't available as a method in older TypeScript versions, so must use `ts.forEachChild` instead.
445
+ ts.forEachChild(sourceFile, function iterate(node) {
446
+ if (node.kind === ts.SyntaxKind.EndOfFileToken) {
447
+ return;
448
+ }
449
+
450
+ const line = lineOfPosition(node.getStart(sourceFile), sourceFile);
451
+ const expected = typeAssertions.get(line);
452
+ if (expected !== undefined) {
453
+ // https://github.com/Microsoft/TypeScript/issues/14077
454
+ if (node.kind === ts.SyntaxKind.ExpressionStatement) {
455
+ node = (node as ts.ExpressionStatement).expression;
456
+ }
457
+
458
+ const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts));
459
+
460
+ const actual = type
461
+ ? checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation)
462
+ : "";
463
+
464
+ let actualNormalized: string | undefined;
465
+
466
+ const candidates = expected.split(/\s*\|\|\s*/).map((s) => s.trim());
467
+
468
+ if (
469
+ !(
470
+ // Fast path
471
+ (
472
+ candidates.some((s) => s === actual) ||
473
+ candidates.some((s) => {
474
+ actualNormalized ??= normalizedTypeToString(ts, actual);
475
+ const normalized = normalizedTypeToString(ts, s);
476
+ return normalized === actualNormalized;
477
+ })
478
+ )
479
+ )
480
+ ) {
481
+ unmetExpectations.push({ node, expected, actual });
482
+ }
483
+
484
+ typeAssertions.delete(line);
485
+ }
486
+
487
+ ts.forEachChild(node, iterate);
488
+ });
489
+ return { unmetExpectations, unusedAssertions: typeAssertions.keys() };
490
+ }
491
+
492
+ function normalizedTypeToString(ts: TSModule, type: string) {
493
+ const sourceFile = ts.createSourceFile("foo.ts", `declare var x: ${type};`, ts.ScriptTarget.Latest);
494
+ const typeNode = (sourceFile.statements[0] as ts.VariableStatement).declarationList.declarations[0].type!;
495
+
496
+ const printer = ts.createPrinter({});
497
+ function print(node: ts.Node) {
498
+ return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
499
+ }
500
+ // TODO: pass undefined instead once https://github.com/microsoft/TypeScript/pull/52941 is released
501
+ const context = (ts as any).nullTransformationContext;
502
+
503
+ function visit(node: ts.Node) {
504
+ node = ts.visitEachChild(node, visit, context);
505
+
506
+ if (ts.isUnionTypeNode(node)) {
507
+ const types = node.types
508
+ .map((t) => [t, print(t)] as const)
509
+ .sort((a, b) => (a[1] < b[1] ? -1 : 1))
510
+ .map((t) => t[0]);
511
+ return ts.factory.updateUnionTypeNode(node, ts.factory.createNodeArray(types));
512
+ }
513
+
514
+ if (
515
+ ts.isTypeOperatorNode(node) &&
516
+ node.operator === ts.SyntaxKind.ReadonlyKeyword &&
517
+ ts.isArrayTypeNode(node.type)
518
+ ) {
519
+ // It's possible that this would conflict with a library which defines their own type with this name,
520
+ // but that's unlikely (and was not previously handled in a prior revision of type string normalization).
521
+ return ts.factory.createTypeReferenceNode("ReadonlyArray", [skipTypeParentheses(ts, node.type.elementType)]);
522
+ }
523
+
524
+ return node;
525
+ }
526
+
527
+ const visited = visit(typeNode);
528
+ return print(visited);
529
+ }
530
+
531
+ function skipTypeParentheses(ts: TSModule, node: ts.TypeNode): ts.TypeNode {
532
+ while (ts.isParenthesizedTypeNode(node)) node = node.type;
533
+ return node;
534
+ }
535
+
536
+ function getNodeForExpectType(node: ts.Node, ts: TSModule): ts.Node {
537
+ if (node.kind === ts.SyntaxKind.VariableStatement) {
538
+ // ts2.0 doesn't have `isVariableStatement`
539
+ const {
540
+ declarationList: { declarations },
541
+ } = node as ts.VariableStatement;
542
+ if (declarations.length === 1) {
543
+ const { initializer } = declarations[0];
544
+ if (initializer) {
545
+ return initializer;
546
+ }
547
+ }
548
+ }
549
+ return node;
550
+ }
551
+
552
+ function lineOfPosition(pos: number, sourceFile: ts.SourceFile): number {
553
+ return sourceFile.getLineAndCharacterOfPosition(pos).line;
554
+ }
555
+
556
+ export = rule;
@@ -16,6 +16,7 @@ import strictExportDeclareModifiers = require("./strict-export-declare-modifiers
16
16
  import noSingleDeclareModule = require("./no-single-declare-module");
17
17
  import noOldDTHeader = require("./no-old-dt-header");
18
18
  import noImportOfDevDependencies = require("./no-import-of-dev-dependencies");
19
+ import expect = require("./expect");
19
20
 
20
21
  export const rules = {
21
22
  "export-just-namespace": exportJustNamespace,
@@ -36,4 +37,5 @@ export const rules = {
36
37
  "no-single-declare-module": noSingleDeclareModule,
37
38
  "no-old-dt-header": noOldDTHeader,
38
39
  "no-import-of-dev-dependencies": noImportOfDevDependencies,
40
+ expect,
39
41
  };
package/src/util.ts CHANGED
@@ -35,7 +35,7 @@ export function commentsMatching(
35
35
  }
36
36
  }
37
37
 
38
- function findUp<T extends {}>(p: string, fn: (p: string) => T | undefined): T | undefined {
38
+ export function findUp<T extends {}>(p: string, fn: (p: string) => T | undefined): T | undefined {
39
39
  p = path.resolve(p);
40
40
  const root = path.parse(p).root;
41
41
 
@@ -0,0 +1,79 @@
1
+ types/expect/expect-tests.ts
2
+ 6:1 error TypeScript expected type to be:
3
+ number
4
+ got:
5
+ 1234 @definitelytyped/expect
6
+ 14:1 error TypeScript expected type to be:
7
+ NotRightAtAll
8
+ got:
9
+ 1234 @definitelytyped/expect
10
+ 45:1 error Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above @definitelytyped/expect
11
+ 49:1 error Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above @definitelytyped/expect
12
+
13
+ ✖ 4 problems (4 errors, 0 warnings)
14
+
15
+ ==== types/expect/expect-tests.ts ====
16
+
17
+ // eslint-disable-next-line @definitelytyped/no-relative-import-in-test
18
+ import * as expect from "./";
19
+
20
+
21
+ // $ExpectType number
22
+ expect.foo;
23
+ ~~~~~~~~~~
24
+ !!! @definitelytyped/expect: TypeScript expected type to be:
25
+ !!! : number
26
+ !!! : got:
27
+ !!! : 1234
28
+
29
+
30
+ // $ExpectType 1234
31
+ expect.foo;
32
+
33
+
34
+ // $ExpectType NotRightAtAll
35
+ expect.foo;
36
+ ~~~~~~~~~~
37
+ !!! @definitelytyped/expect: TypeScript expected type to be:
38
+ !!! : NotRightAtAll
39
+ !!! : got:
40
+ !!! : 1234
41
+
42
+
43
+ // $ExpectType string | number | undefined
44
+ expect.aUnion;
45
+
46
+ // $ExpectType string | undefined | number
47
+ expect.aUnion;
48
+
49
+ // $ExpectType number | string | undefined
50
+ expect.aUnion;
51
+
52
+ // $ExpectType number | undefined | string
53
+ expect.aUnion;
54
+
55
+ // $ExpectType undefined | string | number
56
+ expect.aUnion;
57
+
58
+ // $ExpectType undefined | number | string
59
+ expect.aUnion;
60
+
61
+ // $ExpectType any || undefined | number | string
62
+ expect.aUnion;
63
+
64
+ // $ExpectType { prop1: "a" | "b" | "c"; prop2: readonly (string | number)[]; prop3: readonly (string | number)[]; }
65
+ expect.complicatedUnion(1, 2);
66
+
67
+ // $ExpectType { prop1: "c" | "b" | "a"; prop2: ReadonlyArray<number | string>; prop3: ReadonlyArray<string | number>; }
68
+ expect.complicatedUnion(1, 2);
69
+
70
+
71
+ // $ExpectType NotMatched
72
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
73
+ !!! @definitelytyped/expect: Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above.
74
+ // Whoops
75
+
76
+
77
+ // $ExpectType NotMatched
78
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
79
+ !!! @definitelytyped/expect: Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above.
@@ -0,0 +1,19 @@
1
+ types/expect/index.d.ts
2
+ 7:12 error Array type using 'readonly T[]' is forbidden for non-simple types. Use 'ReadonlyArray<T>' instead @typescript-eslint/array-type
3
+
4
+ ✖ 1 problem (1 error, 0 warnings)
5
+ 1 error and 0 warnings potentially fixable with the `--fix` option.
6
+
7
+ ==== types/expect/index.d.ts ====
8
+
9
+ export const foo = 1234;
10
+
11
+ export const aUnion: string | number | undefined;
12
+
13
+ export function complicatedUnion<T extends string | number>(x: T, y: T): {
14
+ prop1: "a" | "b" | "c";
15
+ prop2: readonly (string | number)[];
16
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
+ !!! @typescript-eslint/array-type: Array type using 'readonly T[]' is forbidden for non-simple types. Use 'ReadonlyArray<T>' instead.
18
+ prop3: ReadonlyArray<string | number>;
19
+ };