tstyche 1.0.0-beta.5 → 1.0.0-beta.7

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.
package/build/tstyche.js CHANGED
@@ -79,18 +79,6 @@ class Environment {
79
79
  }
80
80
  }
81
81
 
82
- var Color;
83
- (function (Color) {
84
- Color["Reset"] = "0";
85
- Color["Red"] = "31";
86
- Color["Green"] = "32";
87
- Color["Yellow"] = "33";
88
- Color["Blue"] = "34";
89
- Color["Magenta"] = "35";
90
- Color["Cyan"] = "36";
91
- Color["Gray"] = "90";
92
- })(Color || (Color = {}));
93
-
94
82
  class Scribbler {
95
83
  #noColor;
96
84
  constructor(options) {
@@ -123,7 +111,7 @@ class Scribbler {
123
111
  }
124
112
  if (element.type === "ansi" && !this.#noColor) {
125
113
  const flags = typeof element.props?.["escapes"] === "string" || Array.isArray(element.props?.["escapes"])
126
- ? element.props?.["escapes"]
114
+ ? element.props["escapes"]
127
115
  : undefined;
128
116
  if (flags != null) {
129
117
  return this.#escapeSequence(flags);
@@ -378,6 +366,154 @@ function fileViewText(lines, addEmptyFinalLine) {
378
366
  addEmptyFinalLine ? Scribbler.createElement(Line, null) : undefined));
379
367
  }
380
368
 
369
+ class JsonText {
370
+ props;
371
+ constructor(props) {
372
+ this.props = props;
373
+ }
374
+ render() {
375
+ return Scribbler.createElement(Line, null, JSON.stringify(this.#sortObject(this.props.input), null, 2));
376
+ }
377
+ #sortObject(target) {
378
+ if (Array.isArray(target)) {
379
+ return target;
380
+ }
381
+ return Object.keys(target)
382
+ .sort()
383
+ .reduce((result, key) => {
384
+ result[key] = target[key];
385
+ return result;
386
+ }, {});
387
+ }
388
+ }
389
+ function formattedText(input) {
390
+ if (typeof input === "string") {
391
+ return Scribbler.createElement(Line, null, input);
392
+ }
393
+ return Scribbler.createElement(JsonText, { input: input });
394
+ }
395
+
396
+ const usageExamples = [
397
+ ["tstyche", "Run all tests."],
398
+ ["tstyche path/to/first.test.ts second", "Only run the test files with matching path."],
399
+ ["tstyche --target 4.7,4.8,latest", "Test on all specified versions of TypeScript."],
400
+ ];
401
+ class HintText {
402
+ props;
403
+ constructor(props) {
404
+ this.props = props;
405
+ }
406
+ render() {
407
+ return (Scribbler.createElement(Text, { indent: 1, color: "90" }, this.props.children));
408
+ }
409
+ }
410
+ class HelpHeaderText {
411
+ props;
412
+ constructor(props) {
413
+ this.props = props;
414
+ }
415
+ render() {
416
+ const hint = (Scribbler.createElement(HintText, null,
417
+ Scribbler.createElement(Text, null, this.props.tstycheVersion)));
418
+ return (Scribbler.createElement(Line, null,
419
+ Scribbler.createElement(Text, null, "The TSTyche Type Test Runner"),
420
+ hint));
421
+ }
422
+ }
423
+ class CommandText {
424
+ props;
425
+ constructor(props) {
426
+ this.props = props;
427
+ }
428
+ render() {
429
+ let hint = undefined;
430
+ if (this.props.hint != null) {
431
+ hint = Scribbler.createElement(HintText, null, this.props.hint);
432
+ }
433
+ return (Scribbler.createElement(Line, { indent: 1 },
434
+ Scribbler.createElement(Text, { color: "34" }, this.props.text),
435
+ hint));
436
+ }
437
+ }
438
+ class OptionDescriptionText {
439
+ props;
440
+ constructor(props) {
441
+ this.props = props;
442
+ }
443
+ render() {
444
+ return Scribbler.createElement(Line, { indent: 1 }, this.props.text);
445
+ }
446
+ }
447
+ class CliUsageText {
448
+ render() {
449
+ const usageText = usageExamples.map(([commandText, descriptionText]) => (Scribbler.createElement(Text, null,
450
+ Scribbler.createElement(CommandText, { text: commandText }),
451
+ Scribbler.createElement(OptionDescriptionText, { text: descriptionText }),
452
+ Scribbler.createElement(Line, null))));
453
+ return Scribbler.createElement(Text, null, usageText);
454
+ }
455
+ }
456
+ class OptionNameText {
457
+ props;
458
+ constructor(props) {
459
+ this.props = props;
460
+ }
461
+ render() {
462
+ return Scribbler.createElement(Text, null,
463
+ "--",
464
+ this.props.text);
465
+ }
466
+ }
467
+ class OptionHintText {
468
+ props;
469
+ constructor(props) {
470
+ this.props = props;
471
+ }
472
+ render() {
473
+ if (this.props.definition.brand === "list") {
474
+ return (Scribbler.createElement(Text, null,
475
+ this.props.definition.brand,
476
+ " of ",
477
+ this.props.definition.items.brand,
478
+ "s"));
479
+ }
480
+ return Scribbler.createElement(Text, null, this.props.definition.brand);
481
+ }
482
+ }
483
+ class CliOptionsText {
484
+ props;
485
+ constructor(props) {
486
+ this.props = props;
487
+ }
488
+ render() {
489
+ const definitions = Array.from(this.props.optionDefinitions.values());
490
+ const optionsText = definitions.map((definition) => (Scribbler.createElement(Text, null,
491
+ Scribbler.createElement(CommandText, { text: Scribbler.createElement(OptionNameText, { text: definition.name }), hint: Scribbler.createElement(OptionHintText, { definition: definition }) }),
492
+ Scribbler.createElement(OptionDescriptionText, { text: definition.description }),
493
+ Scribbler.createElement(Line, null))));
494
+ return (Scribbler.createElement(Text, null,
495
+ Scribbler.createElement(Line, null, "CLI Options"),
496
+ Scribbler.createElement(Line, null),
497
+ optionsText));
498
+ }
499
+ }
500
+ class HelpFooterText {
501
+ render() {
502
+ return Scribbler.createElement(Line, null, "To learn more, visit https://tstyche.org");
503
+ }
504
+ }
505
+ function helpText(optionDefinitions, tstycheVersion) {
506
+ return (Scribbler.createElement(Text, null,
507
+ Scribbler.createElement(HelpHeaderText, { tstycheVersion: tstycheVersion }),
508
+ Scribbler.createElement(Line, null),
509
+ Scribbler.createElement(CliUsageText, null),
510
+ Scribbler.createElement(Line, null),
511
+ Scribbler.createElement(CliOptionsText, { optionDefinitions: optionDefinitions }),
512
+ Scribbler.createElement(Line, null),
513
+ Scribbler.createElement(HelpFooterText, null),
514
+ Scribbler.createElement(Line, null)));
515
+ }
516
+
381
517
  class RowText {
382
518
  props;
383
519
  constructor(props) {
@@ -651,6 +787,9 @@ class Diagnostic {
651
787
  if (options.code != null) {
652
788
  this.code = options.code;
653
789
  }
790
+ if (options.origin != null) {
791
+ this.origin = options.origin;
792
+ }
654
793
  if (options.related != null) {
655
794
  this.related = options.related;
656
795
  }
@@ -727,10 +866,6 @@ class FileViewService {
727
866
  return this.#messages.length > 0;
728
867
  }
729
868
  addMessage(message) {
730
- if (Array.isArray(message)) {
731
- this.#messages.push(...message);
732
- return;
733
- }
734
869
  this.#messages.push(message);
735
870
  }
736
871
  addTest(status, name) {
@@ -1199,14 +1334,12 @@ class TestMember {
1199
1334
  diagnostics;
1200
1335
  members = [];
1201
1336
  name;
1202
- typeChecker;
1203
1337
  constructor(brand, node, parent, flags) {
1204
1338
  this.brand = brand;
1205
1339
  this.node = node;
1206
1340
  this.parent = parent;
1207
1341
  this.flags = flags;
1208
1342
  this.compiler = parent.compiler;
1209
- this.typeChecker = parent.typeChecker;
1210
1343
  this.diagnostics = this.#mapDiagnostics(node, this.parent);
1211
1344
  this.name = this.#resolveName(node);
1212
1345
  }
@@ -1304,48 +1437,18 @@ class Assertion extends TestMember {
1304
1437
  get matcherName() {
1305
1438
  return this.matcherNode.expression.name;
1306
1439
  }
1307
- get sourceArguments() {
1440
+ get source() {
1441
+ if (this.node.typeArguments != null) {
1442
+ return this.node.typeArguments;
1443
+ }
1308
1444
  return this.node.arguments;
1309
1445
  }
1310
- get sourceType() {
1311
- if (!this.typeChecker) {
1312
- return;
1313
- }
1314
- if (this.node.typeArguments?.[0]) {
1315
- return {
1316
- source: 1,
1317
- type: this.typeChecker.getTypeFromTypeNode(this.node.typeArguments[0]),
1318
- };
1319
- }
1320
- if (this.node.arguments[0]) {
1321
- return {
1322
- source: 0,
1323
- type: this.typeChecker.getTypeAtLocation(this.node.arguments[0]),
1324
- };
1446
+ get target() {
1447
+ if (this.matcherNode.typeArguments != null) {
1448
+ return this.matcherNode.typeArguments;
1325
1449
  }
1326
- return;
1327
- }
1328
- get targetArguments() {
1329
1450
  return this.matcherNode.arguments;
1330
1451
  }
1331
- get targetType() {
1332
- if (!this.typeChecker) {
1333
- return;
1334
- }
1335
- if (this.matcherNode.typeArguments?.[0]) {
1336
- return {
1337
- source: 1,
1338
- type: this.typeChecker.getTypeFromTypeNode(this.matcherNode.typeArguments[0]),
1339
- };
1340
- }
1341
- if (this.matcherNode.arguments[0]) {
1342
- return {
1343
- source: 0,
1344
- type: this.typeChecker.getTypeAtLocation(this.matcherNode.arguments[0]),
1345
- };
1346
- }
1347
- return;
1348
- }
1349
1452
  #mapDiagnostics() {
1350
1453
  const mapped = [];
1351
1454
  const unmapped = [];
@@ -1372,12 +1475,6 @@ class Assertion extends TestMember {
1372
1475
  }
1373
1476
  }
1374
1477
 
1375
- var AssertionSource;
1376
- (function (AssertionSource) {
1377
- AssertionSource[AssertionSource["Argument"] = 0] = "Argument";
1378
- AssertionSource[AssertionSource["TypeArgument"] = 1] = "TypeArgument";
1379
- })(AssertionSource || (AssertionSource = {}));
1380
-
1381
1478
  class IdentifierLookup {
1382
1479
  compiler;
1383
1480
  #identifiers;
@@ -1478,13 +1575,11 @@ class TestTree {
1478
1575
  compiler;
1479
1576
  diagnostics;
1480
1577
  sourceFile;
1481
- typeChecker;
1482
1578
  members = [];
1483
- constructor(compiler, diagnostics, sourceFile, typeChecker) {
1579
+ constructor(compiler, diagnostics, sourceFile) {
1484
1580
  this.compiler = compiler;
1485
1581
  this.diagnostics = diagnostics;
1486
1582
  this.sourceFile = sourceFile;
1487
- this.typeChecker = typeChecker;
1488
1583
  }
1489
1584
  get hasOnly() {
1490
1585
  function hasOnly(root) {
@@ -1511,6 +1606,7 @@ class CollectService {
1511
1606
  "toBeUnknown",
1512
1607
  "toBeVoid",
1513
1608
  "toEqual",
1609
+ "toHaveProperty",
1514
1610
  "toMatch",
1515
1611
  "toRaiseError",
1516
1612
  ];
@@ -1560,8 +1656,8 @@ class CollectService {
1560
1656
  this.#collectTestMembers(node, identifiers, parent);
1561
1657
  });
1562
1658
  }
1563
- createTestTree(sourceFile, semanticDiagnostics = [], typeChecker) {
1564
- const testTree = new TestTree(this.compiler, semanticDiagnostics, sourceFile, typeChecker);
1659
+ createTestTree(sourceFile, semanticDiagnostics = []) {
1660
+ const testTree = new TestTree(this.compiler, semanticDiagnostics, sourceFile);
1565
1661
  this.#collectTestMembers(sourceFile, new IdentifierLookup(this.compiler), testTree);
1566
1662
  return testTree;
1567
1663
  }
@@ -1592,365 +1688,551 @@ var TestMemberFlags;
1592
1688
  TestMemberFlags[TestMemberFlags["Todo"] = 8] = "Todo";
1593
1689
  })(TestMemberFlags || (TestMemberFlags = {}));
1594
1690
 
1595
- class ProjectService {
1596
- compiler;
1597
- #service;
1598
- constructor(compiler) {
1599
- this.compiler = compiler;
1600
- function doNothing() {
1601
- }
1602
- function returnFalse() {
1603
- return false;
1604
- }
1605
- function returnUndefined() {
1606
- return undefined;
1607
- }
1608
- const noopWatcher = { close: doNothing };
1609
- const noopLogger = {
1610
- close: doNothing,
1611
- endGroup: doNothing,
1612
- getLogFileName: returnUndefined,
1613
- hasLevel: returnFalse,
1614
- info: doNothing,
1615
- loggingEnabled: returnFalse,
1616
- msg: doNothing,
1617
- perftrc: doNothing,
1618
- startGroup: doNothing,
1619
- };
1620
- const host = {
1621
- ...this.compiler.sys,
1622
- clearImmediate,
1623
- clearTimeout,
1624
- setImmediate,
1625
- setTimeout,
1626
- watchDirectory: () => noopWatcher,
1627
- watchFile: () => noopWatcher,
1628
- };
1629
- this.#service = new this.compiler.server.ProjectService({
1630
- cancellationToken: this.compiler.server.nullCancellationToken,
1631
- host,
1632
- logger: noopLogger,
1633
- session: undefined,
1634
- useInferredProjectPerProjectRoot: true,
1635
- useSingleInferredProject: false,
1636
- });
1637
- }
1638
- closeFile(filePath) {
1639
- this.#service.closeClientFile(filePath);
1640
- }
1641
- getDefaultProject(filePath) {
1642
- return this.#service.getDefaultProjectForFile(this.compiler.server.toNormalizedPath(filePath), true);
1691
+ class PrimitiveTypeMatcher {
1692
+ typeChecker;
1693
+ #targetTypeFlag;
1694
+ #targetTypeText;
1695
+ constructor(typeChecker, targetTypeFlag, targetTypeText) {
1696
+ this.typeChecker = typeChecker;
1697
+ this.#targetTypeFlag = targetTypeFlag;
1698
+ this.#targetTypeText = targetTypeText;
1643
1699
  }
1644
- getLanguageService(filePath) {
1645
- const project = this.getDefaultProject(filePath);
1646
- if (!project) {
1647
- return;
1648
- }
1649
- return project.getLanguageService(true);
1700
+ #explain(sourceType, isNot) {
1701
+ const sourceTypeText = this.typeChecker.typeToString(sourceType);
1702
+ return isNot
1703
+ ? [Diagnostic.error(`Type '${this.#targetTypeText}' is identical to type '${sourceTypeText}'.`)]
1704
+ : [Diagnostic.error(`Type '${this.#targetTypeText}' is not identical to type '${sourceTypeText}'.`)];
1650
1705
  }
1651
- openFile(filePath, sourceText, projectRootPath) {
1652
- const { configFileErrors, configFileName } = this.#service.openClientFile(filePath, sourceText, undefined, projectRootPath);
1653
- EventEmitter.dispatch([
1654
- "project:info",
1655
- { compilerVersion: this.compiler.version, projectConfigFilePath: configFileName },
1656
- ]);
1657
- if (configFileErrors && configFileErrors.length > 0) {
1658
- EventEmitter.dispatch([
1659
- "project:error",
1660
- { diagnostics: Diagnostic.fromDiagnostics(configFileErrors, this.compiler) },
1661
- ]);
1662
- }
1706
+ match(sourceType, isNot) {
1707
+ const isMatch = Boolean(sourceType.flags & this.#targetTypeFlag);
1708
+ return {
1709
+ explain: () => this.#explain(sourceType, isNot),
1710
+ isMatch,
1711
+ };
1663
1712
  }
1664
1713
  }
1665
1714
 
1666
- class Checker {
1667
- compiler;
1668
- constructor(compiler) {
1669
- this.compiler = compiler;
1670
- }
1671
- #assertNonNullish(value, message) {
1672
- if (value == null) {
1673
- throw Error(message);
1674
- }
1715
+ class ToBeAssignable {
1716
+ typeChecker;
1717
+ constructor(typeChecker) {
1718
+ this.typeChecker = typeChecker;
1675
1719
  }
1676
- #assertNonNullishSourceType(assertion) {
1677
- this.#assertNonNullish(assertion.sourceType, "An argument for 'source' was not provided.");
1720
+ #explain(sourceType, targetType, isNot) {
1721
+ const sourceTypeText = this.typeChecker.typeToString(sourceType);
1722
+ const targetTypeText = this.typeChecker.typeToString(targetType);
1723
+ return isNot
1724
+ ? [Diagnostic.error(`Type '${targetTypeText}' is assignable to type '${sourceTypeText}'.`)]
1725
+ : [Diagnostic.error(`Type '${targetTypeText}' is not assignable to type '${sourceTypeText}'.`)];
1678
1726
  }
1679
- #assertNonNullishTargetType(assertion) {
1680
- this.#assertNonNullish(assertion.targetType, "An argument for 'target' was not provided.");
1727
+ match(sourceType, targetType, isNot) {
1728
+ const isMatch = this.typeChecker.isTypeAssignableTo(targetType, sourceType);
1729
+ return {
1730
+ explain: () => this.#explain(sourceType, targetType, isNot),
1731
+ isMatch,
1732
+ };
1681
1733
  }
1682
- #assertNonNullishTypeChecker(assertion) {
1683
- this.#assertNonNullish(assertion.typeChecker, "The 'typeChecker' was not provided.");
1734
+ }
1735
+
1736
+ class ToEqual {
1737
+ typeChecker;
1738
+ constructor(typeChecker) {
1739
+ this.typeChecker = typeChecker;
1684
1740
  }
1685
- #assertStringsOrNumbers(nodes) {
1686
- return nodes.every((expression) => this.compiler.isStringLiteral(expression) ||
1687
- this.compiler.isNumericLiteral(expression) ||
1688
- this.compiler.isNoSubstitutionTemplateLiteral(expression));
1741
+ #explain(sourceType, targetType, isNot) {
1742
+ const sourceTypeText = this.typeChecker.typeToString(sourceType);
1743
+ const targetTypeText = this.typeChecker.typeToString(targetType);
1744
+ return isNot
1745
+ ? [Diagnostic.error(`Type '${targetTypeText}' is identical to type '${sourceTypeText}'.`)]
1746
+ : [Diagnostic.error(`Type '${targetTypeText}' is not identical to type '${sourceTypeText}'.`)];
1689
1747
  }
1690
- explain(assertion) {
1691
- this.#assertNonNullishTypeChecker(assertion);
1692
- const matcher = assertion.matcherName.getText();
1693
- const origin = {
1694
- breadcrumbs: assertion.ancestorNames,
1695
- end: assertion.matcherName.getEnd(),
1696
- file: assertion.matcherName.getSourceFile(),
1697
- start: assertion.matcherName.getStart(),
1748
+ match(sourceType, targetType, isNot) {
1749
+ const isMatch = this.typeChecker.isTypeIdenticalTo(sourceType, targetType);
1750
+ return {
1751
+ explain: () => this.#explain(sourceType, targetType, isNot),
1752
+ isMatch,
1698
1753
  };
1699
- switch (matcher) {
1700
- case "toBeAssignable": {
1701
- this.#assertNonNullishSourceType(assertion);
1702
- this.#assertNonNullishTargetType(assertion);
1703
- const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1704
- const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
1705
- return [
1706
- Diagnostic.error(assertion.isNot
1707
- ? `Type '${targetTypeText}' is assignable to type '${sourceTypeText}'.`
1708
- : `Type '${targetTypeText}' is not assignable to type '${sourceTypeText}'.`, origin),
1754
+ }
1755
+ }
1756
+
1757
+ class ToHaveProperty {
1758
+ compiler;
1759
+ typeChecker;
1760
+ constructor(compiler, typeChecker) {
1761
+ this.compiler = compiler;
1762
+ this.typeChecker = typeChecker;
1763
+ }
1764
+ #explain(sourceType, targetType, isNot) {
1765
+ const sourceTypeText = this.typeChecker.typeToString(sourceType);
1766
+ let targetArgumentText;
1767
+ if (this.#isStringOrNumberLiteralType(targetType)) {
1768
+ targetArgumentText = String(targetType.value);
1769
+ }
1770
+ else {
1771
+ targetArgumentText = `[${this.compiler.unescapeLeadingUnderscores(targetType.symbol.escapedName)}]`;
1772
+ }
1773
+ return isNot
1774
+ ? [Diagnostic.error(`Property '${targetArgumentText}' exists on type '${sourceTypeText}'.`)]
1775
+ : [Diagnostic.error(`Property '${targetArgumentText}' does not exist on type '${sourceTypeText}'.`)];
1776
+ }
1777
+ #isStringOrNumberLiteralType(type) {
1778
+ return Boolean(type.flags & this.compiler.TypeFlags.StringOrNumberLiteral);
1779
+ }
1780
+ match(sourceType, targetType, isNot) {
1781
+ let targetArgumentText;
1782
+ if (this.#isStringOrNumberLiteralType(targetType)) {
1783
+ targetArgumentText = String(targetType.value);
1784
+ }
1785
+ else {
1786
+ targetArgumentText = this.compiler.unescapeLeadingUnderscores(targetType.escapedName);
1787
+ }
1788
+ const isMatch = sourceType.getProperties().some((property) => {
1789
+ return this.compiler.unescapeLeadingUnderscores(property.escapedName) === targetArgumentText;
1790
+ });
1791
+ return {
1792
+ explain: () => this.#explain(sourceType, targetType, isNot),
1793
+ isMatch,
1794
+ };
1795
+ }
1796
+ }
1797
+
1798
+ class ToMatch {
1799
+ typeChecker;
1800
+ constructor(typeChecker) {
1801
+ this.typeChecker = typeChecker;
1802
+ }
1803
+ #explain(sourceType, targetType, isNot) {
1804
+ const sourceTypeText = this.typeChecker.typeToString(sourceType);
1805
+ const targetTypeText = this.typeChecker.typeToString(targetType);
1806
+ return isNot
1807
+ ? [Diagnostic.error(`Type '${targetTypeText}' is a subtype of type '${sourceTypeText}'.`)]
1808
+ : [Diagnostic.error(`Type '${targetTypeText}' is not a subtype of type '${sourceTypeText}'.`)];
1809
+ }
1810
+ match(sourceType, targetType, isNot) {
1811
+ const isMatch = this.typeChecker.isTypeSubtypeOf(sourceType, targetType);
1812
+ return {
1813
+ explain: () => this.#explain(sourceType, targetType, isNot),
1814
+ isMatch,
1815
+ };
1816
+ }
1817
+ }
1818
+
1819
+ class ToRaiseError {
1820
+ compiler;
1821
+ typeChecker;
1822
+ constructor(compiler, typeChecker) {
1823
+ this.compiler = compiler;
1824
+ this.typeChecker = typeChecker;
1825
+ }
1826
+ #explain(source, targetTypes, isNot) {
1827
+ const sourceText = this.compiler.isTypeNode(source.node) ? "Type expression" : "Expression";
1828
+ if (source.diagnostics.length === 0) {
1829
+ return [Diagnostic.error(`${sourceText} did not raise a type error.`)];
1830
+ }
1831
+ if (isNot && targetTypes.length === 0) {
1832
+ const related = [
1833
+ Diagnostic.error(`The raised type error${source.diagnostics.length === 1 ? "" : "s"}:`),
1834
+ ...Diagnostic.fromDiagnostics(source.diagnostics, this.compiler),
1835
+ ];
1836
+ const text = `${sourceText} raised ${source.diagnostics.length === 1 ? "a" : source.diagnostics.length} type error${source.diagnostics.length === 1 ? "" : "s"}.`;
1837
+ return [Diagnostic.error(text).add({ related })];
1838
+ }
1839
+ if (source.diagnostics.length !== targetTypes.length) {
1840
+ const expectedText = source.diagnostics.length > targetTypes.length
1841
+ ? `only ${targetTypes.length} type error${targetTypes.length === 1 ? "" : "s"}`
1842
+ : `${targetTypes.length} type error${targetTypes.length === 1 ? "" : "s"}`;
1843
+ const foundText = source.diagnostics.length > targetTypes.length
1844
+ ? `${source.diagnostics.length}`
1845
+ : `only ${source.diagnostics.length}`;
1846
+ const related = [
1847
+ Diagnostic.error(`The raised type error${source.diagnostics.length === 1 ? "" : "s"}:`),
1848
+ ...Diagnostic.fromDiagnostics(source.diagnostics, this.compiler),
1849
+ ];
1850
+ const text = `Expected ${expectedText}, but ${foundText} ${source.diagnostics.length === 1 ? "was" : "were"} raised.`;
1851
+ return [Diagnostic.error(text).add({ related })];
1852
+ }
1853
+ const diagnostics = [];
1854
+ targetTypes.forEach((argument, index) => {
1855
+ const diagnostic = source.diagnostics[index];
1856
+ if (!diagnostic) {
1857
+ return;
1858
+ }
1859
+ const isMatch = this.#matchExpectedError(diagnostic, argument);
1860
+ if (!isNot && !isMatch) {
1861
+ const expectedText = this.#isStringLiteralType(argument)
1862
+ ? `matching substring '${argument.value}'`
1863
+ : `with code ${argument.value}`;
1864
+ const related = [
1865
+ Diagnostic.error("The raised type error:"),
1866
+ ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
1867
+ ];
1868
+ const text = `${sourceText} did not raise a type error ${expectedText}.`;
1869
+ diagnostics.push(Diagnostic.error(text).add({ related }));
1870
+ }
1871
+ if (isNot && isMatch) {
1872
+ const expectedText = this.#isStringLiteralType(argument)
1873
+ ? `matching substring '${argument.value}'`
1874
+ : `with code ${argument.value}`;
1875
+ const related = [
1876
+ Diagnostic.error("The raised type error:"),
1877
+ ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
1709
1878
  ];
1879
+ const text = `${sourceText} raised a type error ${expectedText}.`;
1880
+ diagnostics.push(Diagnostic.error(text).add({ related }));
1710
1881
  }
1882
+ });
1883
+ return diagnostics;
1884
+ }
1885
+ #isStringLiteralType(type) {
1886
+ return Boolean(type.flags & this.compiler.TypeFlags.StringLiteral);
1887
+ }
1888
+ match(source, targetTypes, isNot) {
1889
+ const explain = () => this.#explain(source, targetTypes, isNot);
1890
+ if (targetTypes.length === 0) {
1891
+ return {
1892
+ explain,
1893
+ isMatch: source.diagnostics.length > 0,
1894
+ };
1895
+ }
1896
+ if (source.diagnostics.length !== targetTypes.length) {
1897
+ return {
1898
+ explain,
1899
+ isMatch: false,
1900
+ };
1901
+ }
1902
+ return {
1903
+ explain,
1904
+ isMatch: targetTypes.every((type, index) => this.#matchExpectedError(source.diagnostics[index], type)),
1905
+ };
1906
+ }
1907
+ #matchExpectedError(diagnostic, type) {
1908
+ if (this.#isStringLiteralType(type)) {
1909
+ return this.compiler.flattenDiagnosticMessageText(diagnostic?.messageText, " ", 0).includes(type.value);
1910
+ }
1911
+ return type.value === diagnostic?.code;
1912
+ }
1913
+ }
1914
+
1915
+ class Expect {
1916
+ compiler;
1917
+ typeChecker;
1918
+ toBeAny;
1919
+ toBeAssignable;
1920
+ toBeBigInt;
1921
+ toBeBoolean;
1922
+ toBeNever;
1923
+ toBeNull;
1924
+ toBeNumber;
1925
+ toBeString;
1926
+ toBeSymbol;
1927
+ toBeUndefined;
1928
+ toBeUniqueSymbol;
1929
+ toBeUnknown;
1930
+ toBeVoid;
1931
+ toEqual;
1932
+ toHaveProperty;
1933
+ toMatch;
1934
+ toRaiseError;
1935
+ constructor(compiler, typeChecker) {
1936
+ this.compiler = compiler;
1937
+ this.typeChecker = typeChecker;
1938
+ this.toBeAny = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Any, "any");
1939
+ this.toBeAssignable = new ToBeAssignable(this.typeChecker);
1940
+ this.toBeBigInt = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.BigInt, "bigint");
1941
+ this.toBeBoolean = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Boolean, "boolean");
1942
+ this.toBeNever = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Never, "never");
1943
+ this.toBeNull = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Null, "null");
1944
+ this.toBeNumber = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Number, "number");
1945
+ this.toBeString = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.String, "string");
1946
+ this.toBeSymbol = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.ESSymbol, "symbol");
1947
+ this.toBeUndefined = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Undefined, "undefined");
1948
+ this.toBeUniqueSymbol = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.UniqueESSymbol, "unique symbol");
1949
+ this.toBeUnknown = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Unknown, "unknown");
1950
+ this.toBeVoid = new PrimitiveTypeMatcher(this.typeChecker, this.compiler.TypeFlags.Void, "void");
1951
+ this.toEqual = new ToEqual(this.typeChecker);
1952
+ this.toHaveProperty = new ToHaveProperty(this.compiler, this.typeChecker);
1953
+ this.toMatch = new ToMatch(this.typeChecker);
1954
+ this.toRaiseError = new ToRaiseError(this.compiler, this.typeChecker);
1955
+ }
1956
+ static assertTypeChecker(typeChecker) {
1957
+ return ("isTypeAssignableTo" in typeChecker && "isTypeIdenticalTo" in typeChecker && "isTypeSubtypeOf" in typeChecker);
1958
+ }
1959
+ #getType(node) {
1960
+ return this.compiler.isExpression(node)
1961
+ ? this.typeChecker.getTypeAtLocation(node)
1962
+ : this.typeChecker.getTypeFromTypeNode(node);
1963
+ }
1964
+ #getTypes(nodes) {
1965
+ return nodes.map((node) => this.#getType(node));
1966
+ }
1967
+ #isArrayOfStringOrNumberLiteralTypes(types) {
1968
+ return types.every((type) => this.#isStringOrNumberLiteralType(type));
1969
+ }
1970
+ #isStringOrNumberLiteralType(type) {
1971
+ return Boolean(type.flags & this.compiler.TypeFlags.StringOrNumberLiteral);
1972
+ }
1973
+ #isUniqueSymbolType(type) {
1974
+ return Boolean(type.flags & this.compiler.TypeFlags.UniqueESSymbol);
1975
+ }
1976
+ match(assertion, expectResult) {
1977
+ const matcherNameText = assertion.matcherName.getText();
1978
+ switch (matcherNameText) {
1979
+ case "toBeAssignable":
1980
+ case "toEqual":
1981
+ case "toMatch":
1982
+ if (assertion.source[0] == null) {
1983
+ this.#onSourceArgumentMustBeProvided(assertion, expectResult);
1984
+ return;
1985
+ }
1986
+ if (assertion.target[0] == null) {
1987
+ this.#onTargetArgumentMustBeProvided(assertion, expectResult);
1988
+ return;
1989
+ }
1990
+ return this[matcherNameText].match(this.#getType(assertion.source[0]), this.#getType(assertion.target[0]), assertion.isNot);
1711
1991
  case "toBeAny":
1712
- return this.#isType(assertion, "any");
1713
1992
  case "toBeBigInt":
1714
- return this.#isType(assertion, "bigint");
1715
1993
  case "toBeBoolean":
1716
- return this.#isType(assertion, "boolean");
1717
1994
  case "toBeNever":
1718
- return this.#isType(assertion, "never");
1719
1995
  case "toBeNull":
1720
- return this.#isType(assertion, "null");
1721
1996
  case "toBeNumber":
1722
- return this.#isType(assertion, "number");
1723
1997
  case "toBeString":
1724
- return this.#isType(assertion, "string");
1725
1998
  case "toBeSymbol":
1726
- return this.#isType(assertion, "symbol");
1727
1999
  case "toBeUndefined":
1728
- return this.#isType(assertion, "undefined");
1729
2000
  case "toBeUniqueSymbol":
1730
- return this.#isType(assertion, "unique symbol");
1731
2001
  case "toBeUnknown":
1732
- return this.#isType(assertion, "unknown");
1733
2002
  case "toBeVoid":
1734
- return this.#isType(assertion, "void");
1735
- case "toEqual": {
1736
- this.#assertNonNullishSourceType(assertion);
1737
- this.#assertNonNullishTargetType(assertion);
1738
- const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1739
- const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
1740
- return [
1741
- Diagnostic.error(assertion.isNot
1742
- ? `Type '${targetTypeText}' is identical to type '${sourceTypeText}'.`
1743
- : `Type '${targetTypeText}' is not identical to type '${sourceTypeText}'.`, origin),
1744
- ];
1745
- }
1746
- case "toMatch": {
1747
- this.#assertNonNullishSourceType(assertion);
1748
- this.#assertNonNullishTargetType(assertion);
1749
- const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1750
- const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
1751
- return [
1752
- Diagnostic.error(assertion.isNot
1753
- ? `Type '${targetTypeText}' is a subtype of type '${sourceTypeText}'.`
1754
- : `Type '${targetTypeText}' is not a subtype of type '${sourceTypeText}'.`, origin),
1755
- ];
2003
+ if (assertion.source[0] == null) {
2004
+ this.#onSourceArgumentMustBeProvided(assertion, expectResult);
2005
+ return;
2006
+ }
2007
+ return this[matcherNameText].match(this.#getType(assertion.source[0]), assertion.isNot);
2008
+ case "toHaveProperty": {
2009
+ if (assertion.source[0] == null) {
2010
+ this.#onSourceArgumentMustBeProvided(assertion, expectResult);
2011
+ return;
2012
+ }
2013
+ const sourceType = this.#getType(assertion.source[0]);
2014
+ const nonPrimitiveType = { flags: this.compiler.TypeFlags.NonPrimitive };
2015
+ if (sourceType.flags & (this.compiler.TypeFlags.Any | this.compiler.TypeFlags.Never) ||
2016
+ !this.typeChecker.isTypeAssignableTo(sourceType, nonPrimitiveType)) {
2017
+ this.#onSourceArgumentMustBeObjectType(assertion.source[0], expectResult);
2018
+ return;
2019
+ }
2020
+ if (assertion.target[0] == null) {
2021
+ this.#onKeyArgumentMustBeProvided(assertion, expectResult);
2022
+ return;
2023
+ }
2024
+ const targetType = this.#getType(assertion.target[0]);
2025
+ if (!(this.#isStringOrNumberLiteralType(targetType) || this.#isUniqueSymbolType(targetType))) {
2026
+ this.#onKeyArgumentMustBeOfType(assertion.target[0], expectResult);
2027
+ return;
2028
+ }
2029
+ return this.toHaveProperty.match(sourceType, targetType, assertion.isNot);
1756
2030
  }
1757
2031
  case "toRaiseError": {
1758
- this.#assertNonNullishSourceType(assertion);
1759
- if (!this.#assertStringsOrNumbers(assertion.targetArguments)) {
1760
- throw new Error("An argument for 'target' must be of type 'string | number'.");
1761
- }
1762
- const sourceText = assertion.sourceType.source === 0 ? "Expression" : "Type definition";
1763
- if (assertion.diagnostics.length === 0) {
1764
- return [Diagnostic.error(`${sourceText} did not raise a type error.`, origin)];
1765
- }
1766
- if (assertion.isNot && assertion.targetArguments.length === 0) {
1767
- const related = [
1768
- Diagnostic.error(`The raised type error${assertion.diagnostics.length === 1 ? "" : "s"}:`),
1769
- ...Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
1770
- ];
1771
- return [
1772
- Diagnostic.error(`${sourceText} raised ${assertion.diagnostics.length === 1 ? "a" : assertion.diagnostics.length} type error${assertion.diagnostics.length === 1 ? "" : "s"}.`, origin).add({ related }),
1773
- ];
1774
- }
1775
- if (assertion.diagnostics.length !== assertion.targetArguments.length) {
1776
- const expectedText = assertion.diagnostics.length > assertion.targetArguments.length
1777
- ? `only ${assertion.targetArguments.length} type error${assertion.targetArguments.length === 1 ? "" : "s"}`
1778
- : `${assertion.targetArguments.length} type error${assertion.targetArguments.length === 1 ? "" : "s"}`;
1779
- const foundText = assertion.diagnostics.length > assertion.targetArguments.length
1780
- ? `${assertion.diagnostics.length}`
1781
- : `only ${assertion.diagnostics.length}`;
1782
- const related = [
1783
- Diagnostic.error(`The raised type error${assertion.diagnostics.length === 1 ? "" : "s"}:`),
1784
- ...Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
1785
- ];
1786
- const diagnostic = Diagnostic.error(`Expected ${expectedText}, but ${foundText} ${assertion.diagnostics.length === 1 ? "was" : "were"} raised.`, origin).add({
1787
- related,
1788
- });
1789
- return [diagnostic];
2032
+ if (assertion.source[0] == null) {
2033
+ this.#onSourceArgumentMustBeProvided(assertion, expectResult);
2034
+ return;
1790
2035
  }
1791
- const diagnostics = [];
1792
- assertion.targetArguments.forEach((argument, index) => {
1793
- const diagnostic = assertion.diagnostics[index];
1794
- if (!diagnostic) {
1795
- return;
1796
- }
1797
- const isMatch = this.#matchExpectedError(diagnostic, argument);
1798
- if (!assertion.isNot && !isMatch) {
1799
- const expectedText = this.compiler.isStringLiteral(argument)
1800
- ? `matching substring '${argument.text}'`
1801
- : `with code ${argument.text}`;
1802
- const related = [
1803
- Diagnostic.error("The raised type error:"),
1804
- ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
1805
- ];
1806
- diagnostics.push(Diagnostic.error(`${sourceText} did not raise a type error ${expectedText}.`, origin).add({ related }));
1807
- }
1808
- if (assertion.isNot && isMatch) {
1809
- const expectedText = this.compiler.isStringLiteral(argument)
1810
- ? `matching substring '${argument.text}'`
1811
- : `with code ${argument.text}`;
1812
- const related = [
1813
- Diagnostic.error("The raised type error:"),
1814
- ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
1815
- ];
1816
- diagnostics.push(Diagnostic.error(`${sourceText} raised a type error ${expectedText}.`, origin).add({ related }));
1817
- }
1818
- });
1819
- return diagnostics;
2036
+ const targetTypes = this.#getTypes(assertion.target);
2037
+ if (!this.#isArrayOfStringOrNumberLiteralTypes(targetTypes)) {
2038
+ this.#onTargetArgumentsMustBeStringOrNumberLiteralTypes(assertion.target, expectResult);
2039
+ return;
2040
+ }
2041
+ return this.toRaiseError.match({ diagnostics: assertion.diagnostics, node: assertion.source[0] }, targetTypes, assertion.isNot);
1820
2042
  }
1821
2043
  default:
1822
- throw new Error(`The '${matcher}' matcher is not supported.`);
2044
+ this.#onNotSupportedMatcherName(assertion, expectResult);
2045
+ return;
1823
2046
  }
1824
2047
  }
1825
- #hasTypeFlag(assertion, targetTypeFlag) {
1826
- this.#assertNonNullishSourceType(assertion);
1827
- return Boolean(assertion.sourceType.type.flags & targetTypeFlag);
2048
+ #onKeyArgumentMustBeOfType(node, expectResult) {
2049
+ const receivedTypeText = this.typeChecker.typeToString(this.#getType(node));
2050
+ const text = `An argument for 'key' must be of type 'string | number | symbol', received: '${receivedTypeText}'.`;
2051
+ const origin = {
2052
+ end: node.getEnd(),
2053
+ file: node.getSourceFile(),
2054
+ start: node.getStart(),
2055
+ };
2056
+ EventEmitter.dispatch(["expect:error", { diagnostics: [Diagnostic.error(text, origin)], result: expectResult }]);
1828
2057
  }
1829
- #isType(assertion, targetText) {
1830
- this.#assertNonNullishSourceType(assertion);
1831
- this.#assertNonNullishTypeChecker(assertion);
2058
+ #onKeyArgumentMustBeProvided(assertion, expectResult) {
1832
2059
  const origin = {
1833
- breadcrumbs: assertion.ancestorNames,
1834
2060
  end: assertion.matcherName.getEnd(),
1835
2061
  file: assertion.matcherName.getSourceFile(),
1836
2062
  start: assertion.matcherName.getStart(),
1837
2063
  };
1838
- const sourceText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1839
- return [
1840
- Diagnostic.error(assertion.isNot
1841
- ? `Type '${targetText}' is identical to type '${sourceText}'.`
1842
- : `Type '${targetText}' is not identical to type '${sourceText}'.`, origin),
1843
- ];
2064
+ EventEmitter.dispatch([
2065
+ "expect:error",
2066
+ {
2067
+ diagnostics: [Diagnostic.error("An argument for 'key' must be provided.", origin)],
2068
+ result: expectResult,
2069
+ },
2070
+ ]);
1844
2071
  }
1845
- match(assertion) {
1846
- const matcher = assertion.matcherName.getText();
1847
- switch (matcher) {
1848
- case "toBeAssignable":
1849
- this.#assertNonNullishSourceType(assertion);
1850
- this.#assertNonNullishTargetType(assertion);
1851
- this.#assertNonNullish(assertion.typeChecker?.isTypeAssignableTo, "The 'isTypeAssignableTo' method is missing in the provided type checker.");
1852
- return assertion.typeChecker.isTypeAssignableTo(assertion.targetType.type, assertion.sourceType.type);
1853
- case "toBeAny": {
1854
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Any);
1855
- }
1856
- case "toBeBigInt": {
1857
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.BigInt);
1858
- }
1859
- case "toBeBoolean": {
1860
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Boolean);
1861
- }
1862
- case "toBeNever": {
1863
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Never);
1864
- }
1865
- case "toBeNull": {
1866
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Null);
1867
- }
1868
- case "toBeNumber": {
1869
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Number);
1870
- }
1871
- case "toBeString": {
1872
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.String);
1873
- }
1874
- case "toBeSymbol": {
1875
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.ESSymbol);
1876
- }
1877
- case "toBeUndefined": {
1878
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Undefined);
1879
- }
1880
- case "toBeUniqueSymbol": {
1881
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.UniqueESSymbol);
1882
- }
1883
- case "toBeUnknown": {
1884
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Unknown);
1885
- }
1886
- case "toBeVoid": {
1887
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Void);
1888
- }
1889
- case "toEqual": {
1890
- this.#assertNonNullishSourceType(assertion);
1891
- this.#assertNonNullishTargetType(assertion);
1892
- this.#assertNonNullish(assertion.typeChecker?.isTypeIdenticalTo, "The 'isTypeIdenticalTo' method is missing in the provided type checker.");
1893
- return assertion.typeChecker.isTypeIdenticalTo(assertion.sourceType.type, assertion.targetType.type);
1894
- }
1895
- case "toMatch": {
1896
- this.#assertNonNullishSourceType(assertion);
1897
- this.#assertNonNullishTargetType(assertion);
1898
- this.#assertNonNullish(assertion.typeChecker?.isTypeSubtypeOf, "The 'isTypeSubtypeOf' method is missing in the provided type checker.");
1899
- return assertion.typeChecker.isTypeSubtypeOf(assertion.sourceType.type, assertion.targetType.type);
1900
- }
1901
- case "toRaiseError": {
1902
- if (!this.#assertStringsOrNumbers(assertion.targetArguments)) {
1903
- throw new Error("An argument for 'target' must be of type 'string | number'.");
1904
- }
1905
- if (assertion.targetArguments.length === 0) {
1906
- return assertion.diagnostics.length > 0;
1907
- }
1908
- if (assertion.diagnostics.length !== assertion.targetArguments.length) {
1909
- return false;
1910
- }
1911
- return assertion.targetArguments.every((expectedArgument, index) => {
1912
- if (this.compiler.isStringLiteral(expectedArgument)) {
1913
- return this.compiler
1914
- .flattenDiagnosticMessageText(assertion.diagnostics[index]?.messageText, " ", 0)
1915
- .includes(expectedArgument.text);
1916
- }
1917
- if (this.compiler.isNumericLiteral(expectedArgument)) {
1918
- return Number(expectedArgument.text) === assertion.diagnostics[index]?.code;
1919
- }
1920
- return false;
1921
- });
2072
+ #onNotSupportedMatcherName(assertion, expectResult) {
2073
+ const matcherNameText = assertion.matcherName.getText();
2074
+ const origin = {
2075
+ end: assertion.matcherName.getEnd(),
2076
+ file: assertion.matcherName.getSourceFile(),
2077
+ start: assertion.matcherName.getStart(),
2078
+ };
2079
+ EventEmitter.dispatch([
2080
+ "expect:error",
2081
+ {
2082
+ diagnostics: [Diagnostic.error(`The '${matcherNameText}()' matcher is not supported.`, origin)],
2083
+ result: expectResult,
2084
+ },
2085
+ ]);
2086
+ }
2087
+ #onSourceArgumentMustBeObjectType(node, expectResult) {
2088
+ const sourceText = this.compiler.isTypeNode(node) ? "A type argument for 'Source'" : "An argument for 'source'";
2089
+ const receivedTypeText = this.typeChecker.typeToString(this.#getType(node));
2090
+ const text = `${sourceText} must be of an object type, received: '${receivedTypeText}'.`;
2091
+ const origin = {
2092
+ end: node.getEnd(),
2093
+ file: node.getSourceFile(),
2094
+ start: node.getStart(),
2095
+ };
2096
+ EventEmitter.dispatch(["expect:error", { diagnostics: [Diagnostic.error(text, origin)], result: expectResult }]);
2097
+ }
2098
+ #onSourceArgumentMustBeProvided(assertion, expectResult) {
2099
+ const origin = {
2100
+ end: assertion.node.getEnd(),
2101
+ file: assertion.node.getSourceFile(),
2102
+ start: assertion.node.getStart(),
2103
+ };
2104
+ EventEmitter.dispatch([
2105
+ "expect:error",
2106
+ {
2107
+ diagnostics: [
2108
+ Diagnostic.error("An argument for 'source' or type argument for 'Source' must be provided.", origin),
2109
+ ],
2110
+ result: expectResult,
2111
+ },
2112
+ ]);
2113
+ }
2114
+ #onTargetArgumentMustBeProvided(assertion, expectResult) {
2115
+ const origin = {
2116
+ end: assertion.matcherName.getEnd(),
2117
+ file: assertion.matcherName.getSourceFile(),
2118
+ start: assertion.matcherName.getStart(),
2119
+ };
2120
+ EventEmitter.dispatch([
2121
+ "expect:error",
2122
+ {
2123
+ diagnostics: [
2124
+ Diagnostic.error("An argument for 'target' or type argument for 'Target' must be provided.", origin),
2125
+ ],
2126
+ result: expectResult,
2127
+ },
2128
+ ]);
2129
+ }
2130
+ #onTargetArgumentsMustBeStringOrNumberLiteralTypes(nodes, expectResult) {
2131
+ const diagnostics = [];
2132
+ for (const node of nodes) {
2133
+ const receivedType = this.#getType(node);
2134
+ if (!this.#isStringOrNumberLiteralType(receivedType)) {
2135
+ const receivedTypeText = this.typeChecker.typeToString(this.#getType(node));
2136
+ const origin = {
2137
+ end: node.getEnd(),
2138
+ file: node.getSourceFile(),
2139
+ start: node.getStart(),
2140
+ };
2141
+ diagnostics.push(Diagnostic.error(`An argument for 'target' must be of type 'string | number', received: '${receivedTypeText}'.`, origin));
1922
2142
  }
1923
- default:
1924
- throw new Error(`The '${matcher}' matcher is not supported.`);
1925
2143
  }
2144
+ EventEmitter.dispatch(["expect:error", { diagnostics, result: expectResult }]);
1926
2145
  }
1927
- #matchExpectedError(diagnostic, argument) {
1928
- if (this.compiler.isStringLiteral(argument)) {
1929
- return this.compiler.flattenDiagnosticMessageText(diagnostic.messageText, " ", 0).includes(argument.text);
2146
+ }
2147
+
2148
+ class ProjectService {
2149
+ compiler;
2150
+ #service;
2151
+ constructor(compiler) {
2152
+ this.compiler = compiler;
2153
+ function doNothing() {
1930
2154
  }
1931
- if (this.compiler.isNumericLiteral(argument)) {
1932
- return Number(argument.text) === diagnostic.code;
2155
+ function returnFalse() {
2156
+ return false;
2157
+ }
2158
+ function returnUndefined() {
2159
+ return undefined;
2160
+ }
2161
+ const noopWatcher = { close: doNothing };
2162
+ const noopLogger = {
2163
+ close: doNothing,
2164
+ endGroup: doNothing,
2165
+ getLogFileName: returnUndefined,
2166
+ hasLevel: returnFalse,
2167
+ info: doNothing,
2168
+ loggingEnabled: returnFalse,
2169
+ msg: doNothing,
2170
+ perftrc: doNothing,
2171
+ startGroup: doNothing,
2172
+ };
2173
+ const host = {
2174
+ ...this.compiler.sys,
2175
+ clearImmediate,
2176
+ clearTimeout,
2177
+ setImmediate,
2178
+ setTimeout,
2179
+ watchDirectory: () => noopWatcher,
2180
+ watchFile: () => noopWatcher,
2181
+ };
2182
+ this.#service = new this.compiler.server.ProjectService({
2183
+ allowLocalPluginLoads: true,
2184
+ cancellationToken: this.compiler.server.nullCancellationToken,
2185
+ host,
2186
+ logger: noopLogger,
2187
+ session: undefined,
2188
+ useInferredProjectPerProjectRoot: true,
2189
+ useSingleInferredProject: false,
2190
+ });
2191
+ }
2192
+ closeFile(filePath) {
2193
+ this.#service.closeClientFile(filePath);
2194
+ }
2195
+ getDefaultProject(filePath) {
2196
+ return this.#service.getDefaultProjectForFile(this.compiler.server.toNormalizedPath(filePath), true);
2197
+ }
2198
+ getLanguageService(filePath) {
2199
+ const project = this.getDefaultProject(filePath);
2200
+ if (!project) {
2201
+ return;
2202
+ }
2203
+ return project.getLanguageService(true);
2204
+ }
2205
+ openFile(filePath, sourceText, projectRootPath) {
2206
+ const { configFileErrors, configFileName } = this.#service.openClientFile(filePath, sourceText, undefined, projectRootPath);
2207
+ EventEmitter.dispatch([
2208
+ "project:info",
2209
+ { compilerVersion: this.compiler.version, projectConfigFilePath: configFileName },
2210
+ ]);
2211
+ if (configFileErrors && configFileErrors.length > 0) {
2212
+ EventEmitter.dispatch([
2213
+ "project:error",
2214
+ { diagnostics: Diagnostic.fromDiagnostics(configFileErrors, this.compiler) },
2215
+ ]);
1933
2216
  }
1934
- throw new Error("An argument for 'target' must be of type 'string | number'.");
1935
2217
  }
1936
2218
  }
1937
2219
 
1938
2220
  class TestTreeWorker {
1939
2221
  resolvedConfig;
1940
2222
  compiler;
1941
- #checker;
2223
+ #expect;
1942
2224
  #fileResult;
1943
2225
  #hasOnly;
1944
2226
  #position;
1945
2227
  #signal;
1946
- constructor(resolvedConfig, compiler, options) {
2228
+ constructor(resolvedConfig, compiler, expect, options) {
1947
2229
  this.resolvedConfig = resolvedConfig;
1948
2230
  this.compiler = compiler;
1949
- this.#checker = new Checker(compiler);
2231
+ this.#expect = expect;
2232
+ this.#fileResult = options.fileResult;
1950
2233
  this.#hasOnly = options.hasOnly || resolvedConfig.only != null || options.position != null;
1951
2234
  this.#position = options.position;
1952
2235
  this.#signal = options.signal;
1953
- this.#fileResult = options.fileResult;
1954
2236
  }
1955
2237
  #resolveRunMode(mode, member) {
1956
2238
  if (member.flags & 1) {
@@ -2018,8 +2300,11 @@ class TestTreeWorker {
2018
2300
  ]);
2019
2301
  return;
2020
2302
  }
2021
- const isPass = this.#checker.match(assertion);
2022
- if (assertion.isNot ? !isPass : isPass) {
2303
+ const matchResult = this.#expect.match(assertion, expectResult);
2304
+ if (matchResult == null) {
2305
+ return;
2306
+ }
2307
+ if (assertion.isNot ? !matchResult.isMatch : matchResult.isMatch) {
2023
2308
  if (runMode & 1) {
2024
2309
  const text = ["The assertion was supposed to fail, but it passed.", "Consider removing the '.fail' flag."];
2025
2310
  const origin = {
@@ -2029,10 +2314,7 @@ class TestTreeWorker {
2029
2314
  };
2030
2315
  EventEmitter.dispatch([
2031
2316
  "expect:error",
2032
- {
2033
- diagnostics: [Diagnostic.error(text, origin)],
2034
- result: expectResult,
2035
- },
2317
+ { diagnostics: [Diagnostic.error(text, origin)], result: expectResult },
2036
2318
  ]);
2037
2319
  }
2038
2320
  else {
@@ -2044,13 +2326,20 @@ class TestTreeWorker {
2044
2326
  EventEmitter.dispatch(["expect:pass", { result: expectResult }]);
2045
2327
  }
2046
2328
  else {
2047
- EventEmitter.dispatch([
2048
- "expect:fail",
2049
- {
2050
- diagnostics: this.#checker.explain(assertion),
2051
- result: expectResult,
2052
- },
2053
- ]);
2329
+ const origin = {
2330
+ breadcrumbs: assertion.ancestorNames,
2331
+ end: assertion.matcherName.getEnd(),
2332
+ file: assertion.matcherName.getSourceFile(),
2333
+ start: assertion.matcherName.getStart(),
2334
+ };
2335
+ const diagnostics = [];
2336
+ for (const diagnostic of matchResult.explain()) {
2337
+ if (diagnostic.origin == null) {
2338
+ diagnostic.add({ origin });
2339
+ }
2340
+ diagnostics.push(diagnostic);
2341
+ }
2342
+ EventEmitter.dispatch(["expect:fail", { diagnostics, result: expectResult }]);
2054
2343
  }
2055
2344
  }
2056
2345
  }
@@ -2154,8 +2443,7 @@ class TestFileRunner {
2154
2443
  if (!sourceFile) {
2155
2444
  return;
2156
2445
  }
2157
- const typeChecker = program.getTypeChecker();
2158
- const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics, typeChecker);
2446
+ const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
2159
2447
  if (testTree.diagnostics.length > 0) {
2160
2448
  EventEmitter.dispatch([
2161
2449
  "file:error",
@@ -2166,7 +2454,14 @@ class TestFileRunner {
2166
2454
  ]);
2167
2455
  return;
2168
2456
  }
2169
- const testTreeWorker = new TestTreeWorker(this.resolvedConfig, this.compiler, {
2457
+ const typeChecker = program.getTypeChecker();
2458
+ if (!Expect.assertTypeChecker(typeChecker)) {
2459
+ const text = "The required 'isTypeAssignableTo()', 'isTypeIdenticalTo()' and 'isTypeSubtypeOf()' methods are missing in the provided type checker.";
2460
+ EventEmitter.dispatch(["file:error", { diagnostics: [Diagnostic.error(text)], result: fileResult }]);
2461
+ return;
2462
+ }
2463
+ const expect = new Expect(this.compiler, typeChecker);
2464
+ const testTreeWorker = new TestTreeWorker(this.resolvedConfig, this.compiler, expect, {
2170
2465
  fileResult,
2171
2466
  hasOnly: testTree.hasOnly,
2172
2467
  position,
@@ -3066,11 +3361,13 @@ class ManifestWorker {
3066
3361
  #cachePath;
3067
3362
  #manifestFileName = "store-manifest.json";
3068
3363
  #manifestFilePath;
3364
+ #onDiagnostic;
3069
3365
  #prune;
3070
3366
  #registryUrl = new URL("https://registry.npmjs.org");
3071
3367
  #timeout = Environment.timeout * 1000;
3072
- constructor(cachePath, prune) {
3368
+ constructor(cachePath, onDiagnostic, prune) {
3073
3369
  this.#cachePath = cachePath;
3370
+ this.#onDiagnostic = onDiagnostic;
3074
3371
  this.#manifestFilePath = path.join(cachePath, this.#manifestFileName);
3075
3372
  this.#prune = prune;
3076
3373
  }
@@ -3158,9 +3455,6 @@ class ManifestWorker {
3158
3455
  }
3159
3456
  return manifest;
3160
3457
  }
3161
- #onDiagnostic(diagnostic) {
3162
- EventEmitter.dispatch(["store:error", { diagnostics: [diagnostic] }]);
3163
- }
3164
3458
  async open(signal) {
3165
3459
  let manifest;
3166
3460
  if (!existsSync(this.#manifestFilePath)) {
@@ -3224,7 +3518,7 @@ class StoreService {
3224
3518
  constructor() {
3225
3519
  this.#cachePath = Environment.storePath;
3226
3520
  this.#compilerModuleWorker = new CompilerModuleWorker(this.#cachePath, this.#onDiagnostic);
3227
- this.#manifestWorker = new ManifestWorker(this.#cachePath, async () => this.prune());
3521
+ this.#manifestWorker = new ManifestWorker(this.#cachePath, this.#onDiagnostic, this.prune);
3228
3522
  }
3229
3523
  get supportedTags() {
3230
3524
  if (!this.#manifest) {
@@ -3272,9 +3566,9 @@ class StoreService {
3272
3566
  }
3273
3567
  this.#manifest = await this.#manifestWorker.open(signal);
3274
3568
  }
3275
- async prune() {
3569
+ prune = async () => {
3276
3570
  await fs.rm(this.#cachePath, { force: true, recursive: true });
3277
- }
3571
+ };
3278
3572
  resolveTag(tag) {
3279
3573
  if (!this.#manifest) {
3280
3574
  this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
@@ -3327,154 +3621,6 @@ class StoreService {
3327
3621
  }
3328
3622
  }
3329
3623
 
3330
- class JsonText {
3331
- props;
3332
- constructor(props) {
3333
- this.props = props;
3334
- }
3335
- render() {
3336
- return Scribbler.createElement(Line, null, JSON.stringify(this.#sortObject(this.props.input), null, 2));
3337
- }
3338
- #sortObject(target) {
3339
- if (Array.isArray(target)) {
3340
- return target;
3341
- }
3342
- return Object.keys(target)
3343
- .sort()
3344
- .reduce((result, key) => {
3345
- result[key] = target[key];
3346
- return result;
3347
- }, {});
3348
- }
3349
- }
3350
- function formattedText(input) {
3351
- if (typeof input === "string") {
3352
- return Scribbler.createElement(Line, null, input);
3353
- }
3354
- return Scribbler.createElement(JsonText, { input: input });
3355
- }
3356
-
3357
- const usageExamples = [
3358
- ["tstyche", "Run all tests."],
3359
- ["tstyche path/to/first.test.ts second", "Only run the test files with matching path."],
3360
- ["tstyche --target 4.7,4.8,latest", "Test on all specified versions of TypeScript."],
3361
- ];
3362
- class HintText {
3363
- props;
3364
- constructor(props) {
3365
- this.props = props;
3366
- }
3367
- render() {
3368
- return (Scribbler.createElement(Text, { indent: 1, color: "90" }, this.props.children));
3369
- }
3370
- }
3371
- class HelpHeaderText {
3372
- props;
3373
- constructor(props) {
3374
- this.props = props;
3375
- }
3376
- render() {
3377
- const hint = (Scribbler.createElement(HintText, null,
3378
- Scribbler.createElement(Text, null, this.props.tstycheVersion)));
3379
- return (Scribbler.createElement(Line, null,
3380
- Scribbler.createElement(Text, null, "The TSTyche Type Test Runner"),
3381
- hint));
3382
- }
3383
- }
3384
- class CommandText {
3385
- props;
3386
- constructor(props) {
3387
- this.props = props;
3388
- }
3389
- render() {
3390
- let hint = undefined;
3391
- if (this.props.hint != null) {
3392
- hint = Scribbler.createElement(HintText, null, this.props.hint);
3393
- }
3394
- return (Scribbler.createElement(Line, { indent: 1 },
3395
- Scribbler.createElement(Text, { color: "34" }, this.props.text),
3396
- hint));
3397
- }
3398
- }
3399
- class OptionDescriptionText {
3400
- props;
3401
- constructor(props) {
3402
- this.props = props;
3403
- }
3404
- render() {
3405
- return Scribbler.createElement(Line, { indent: 1 }, this.props.text);
3406
- }
3407
- }
3408
- class CliUsageText {
3409
- render() {
3410
- const usageText = usageExamples.map(([commandText, descriptionText]) => (Scribbler.createElement(Text, null,
3411
- Scribbler.createElement(CommandText, { text: commandText }),
3412
- Scribbler.createElement(OptionDescriptionText, { text: descriptionText }),
3413
- Scribbler.createElement(Line, null))));
3414
- return Scribbler.createElement(Text, null, usageText);
3415
- }
3416
- }
3417
- class OptionNameText {
3418
- props;
3419
- constructor(props) {
3420
- this.props = props;
3421
- }
3422
- render() {
3423
- return Scribbler.createElement(Text, null,
3424
- "--",
3425
- this.props.text);
3426
- }
3427
- }
3428
- class OptionHintText {
3429
- props;
3430
- constructor(props) {
3431
- this.props = props;
3432
- }
3433
- render() {
3434
- if (this.props.definition.brand === "list") {
3435
- return (Scribbler.createElement(Text, null,
3436
- this.props.definition.brand,
3437
- " of ",
3438
- this.props.definition.items.brand,
3439
- "s"));
3440
- }
3441
- return Scribbler.createElement(Text, null, this.props.definition.brand);
3442
- }
3443
- }
3444
- class CliOptionsText {
3445
- props;
3446
- constructor(props) {
3447
- this.props = props;
3448
- }
3449
- render() {
3450
- const definitions = Array.from(this.props.optionDefinitions.values());
3451
- const optionsText = definitions.map((definition) => (Scribbler.createElement(Text, null,
3452
- Scribbler.createElement(CommandText, { text: Scribbler.createElement(OptionNameText, { text: definition.name }), hint: Scribbler.createElement(OptionHintText, { definition: definition }) }),
3453
- Scribbler.createElement(OptionDescriptionText, { text: definition.description }),
3454
- Scribbler.createElement(Line, null))));
3455
- return (Scribbler.createElement(Text, null,
3456
- Scribbler.createElement(Line, null, "CLI Options"),
3457
- Scribbler.createElement(Line, null),
3458
- optionsText));
3459
- }
3460
- }
3461
- class HelpFooterText {
3462
- render() {
3463
- return Scribbler.createElement(Line, null, "To learn more, visit https://tstyche.org");
3464
- }
3465
- }
3466
- function helpText(optionDefinitions, tstycheVersion) {
3467
- return (Scribbler.createElement(Text, null,
3468
- Scribbler.createElement(HelpHeaderText, { tstycheVersion: tstycheVersion }),
3469
- Scribbler.createElement(Line, null),
3470
- Scribbler.createElement(CliUsageText, null),
3471
- Scribbler.createElement(Line, null),
3472
- Scribbler.createElement(CliOptionsText, { optionDefinitions: optionDefinitions }),
3473
- Scribbler.createElement(Line, null),
3474
- Scribbler.createElement(HelpFooterText, null),
3475
- Scribbler.createElement(Line, null)));
3476
- }
3477
-
3478
3624
  class Cli {
3479
3625
  #abortController = new AbortController();
3480
3626
  #logger;
@@ -3573,4 +3719,4 @@ class Cli {
3573
3719
  }
3574
3720
  }
3575
3721
 
3576
- export { Assertion, AssertionSource, Checker, Cli, CollectService, Color, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, Environment, EventEmitter, ExpectResult, FileResult, Line, Logger, OptionBrand, OptionDefinitionsMap, OptionGroup, ProjectResult, ProjectService, Reporter, Result, ResultCount, ResultManager, ResultStatus, ResultTiming, Scribbler, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, Text, ThoroughReporter, addsPackageStepText, describeNameText, diagnosticText, fileStatusText, fileViewText, summaryText, testNameText, usesCompilerStepText };
3722
+ export { Assertion, Cli, CollectService, ConfigService, DescribeResult, Diagnostic, DiagnosticCategory, Environment, EventEmitter, Expect, ExpectResult, FileResult, Logger, OptionBrand, OptionDefinitionsMap, OptionGroup, ProjectResult, ProjectService, Reporter, Result, ResultCount, ResultManager, ResultStatus, ResultTiming, Scribbler, StoreService, SummaryReporter, TSTyche, TargetResult, TaskRunner, TestMember, TestMemberBrand, TestMemberFlags, TestResult, TestTree, ThoroughReporter, addsPackageStepText, describeNameText, diagnosticText, fileStatusText, fileViewText, formattedText, helpText, summaryText, testNameText, usesCompilerStepText };