tstyche 1.0.0-beta.6 → 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) {
@@ -1561,8 +1656,8 @@ class CollectService {
1561
1656
  this.#collectTestMembers(node, identifiers, parent);
1562
1657
  });
1563
1658
  }
1564
- createTestTree(sourceFile, semanticDiagnostics = [], typeChecker) {
1565
- const testTree = new TestTree(this.compiler, semanticDiagnostics, sourceFile, typeChecker);
1659
+ createTestTree(sourceFile, semanticDiagnostics = []) {
1660
+ const testTree = new TestTree(this.compiler, semanticDiagnostics, sourceFile);
1566
1661
  this.#collectTestMembers(sourceFile, new IdentifierLookup(this.compiler), testTree);
1567
1662
  return testTree;
1568
1663
  }
@@ -1593,408 +1688,551 @@ var TestMemberFlags;
1593
1688
  TestMemberFlags[TestMemberFlags["Todo"] = 8] = "Todo";
1594
1689
  })(TestMemberFlags || (TestMemberFlags = {}));
1595
1690
 
1596
- class ProjectService {
1597
- compiler;
1598
- #service;
1599
- constructor(compiler) {
1600
- this.compiler = compiler;
1601
- function doNothing() {
1602
- }
1603
- function returnFalse() {
1604
- return false;
1605
- }
1606
- function returnUndefined() {
1607
- return undefined;
1608
- }
1609
- const noopWatcher = { close: doNothing };
1610
- const noopLogger = {
1611
- close: doNothing,
1612
- endGroup: doNothing,
1613
- getLogFileName: returnUndefined,
1614
- hasLevel: returnFalse,
1615
- info: doNothing,
1616
- loggingEnabled: returnFalse,
1617
- msg: doNothing,
1618
- perftrc: doNothing,
1619
- startGroup: doNothing,
1620
- };
1621
- const host = {
1622
- ...this.compiler.sys,
1623
- clearImmediate,
1624
- clearTimeout,
1625
- setImmediate,
1626
- setTimeout,
1627
- watchDirectory: () => noopWatcher,
1628
- watchFile: () => noopWatcher,
1629
- };
1630
- this.#service = new this.compiler.server.ProjectService({
1631
- cancellationToken: this.compiler.server.nullCancellationToken,
1632
- host,
1633
- logger: noopLogger,
1634
- session: undefined,
1635
- useInferredProjectPerProjectRoot: true,
1636
- useSingleInferredProject: false,
1637
- });
1638
- }
1639
- closeFile(filePath) {
1640
- this.#service.closeClientFile(filePath);
1641
- }
1642
- getDefaultProject(filePath) {
1643
- 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;
1644
1699
  }
1645
- getLanguageService(filePath) {
1646
- const project = this.getDefaultProject(filePath);
1647
- if (!project) {
1648
- return;
1649
- }
1650
- 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}'.`)];
1651
1705
  }
1652
- openFile(filePath, sourceText, projectRootPath) {
1653
- const { configFileErrors, configFileName } = this.#service.openClientFile(filePath, sourceText, undefined, projectRootPath);
1654
- EventEmitter.dispatch([
1655
- "project:info",
1656
- { compilerVersion: this.compiler.version, projectConfigFilePath: configFileName },
1657
- ]);
1658
- if (configFileErrors && configFileErrors.length > 0) {
1659
- EventEmitter.dispatch([
1660
- "project:error",
1661
- { diagnostics: Diagnostic.fromDiagnostics(configFileErrors, this.compiler) },
1662
- ]);
1663
- }
1706
+ match(sourceType, isNot) {
1707
+ const isMatch = Boolean(sourceType.flags & this.#targetTypeFlag);
1708
+ return {
1709
+ explain: () => this.#explain(sourceType, isNot),
1710
+ isMatch,
1711
+ };
1664
1712
  }
1665
1713
  }
1666
1714
 
1667
- class Checker {
1668
- compiler;
1669
- constructor(compiler) {
1670
- this.compiler = compiler;
1671
- }
1672
- #assertNonNullish(value, message) {
1673
- if (value == null) {
1674
- throw Error(message);
1675
- }
1715
+ class ToBeAssignable {
1716
+ typeChecker;
1717
+ constructor(typeChecker) {
1718
+ this.typeChecker = typeChecker;
1676
1719
  }
1677
- #assertNonNullishSourceType(assertion) {
1678
- 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}'.`)];
1679
1726
  }
1680
- #assertNonNullishTargetType(assertion) {
1681
- 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
+ };
1682
1733
  }
1683
- #assertNonNullishTypeChecker(assertion) {
1684
- this.#assertNonNullish(assertion.typeChecker, "The 'typeChecker' was not provided.");
1734
+ }
1735
+
1736
+ class ToEqual {
1737
+ typeChecker;
1738
+ constructor(typeChecker) {
1739
+ this.typeChecker = typeChecker;
1685
1740
  }
1686
- #assertStringsOrNumber(expression) {
1687
- return (expression != null &&
1688
- (this.compiler.isStringLiteralLike(expression) || this.compiler.isNumericLiteral(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
- #assertStringsOrNumbers(nodes) {
1691
- return nodes.every((expression) => this.#assertStringsOrNumber(expression));
1748
+ match(sourceType, targetType, isNot) {
1749
+ const isMatch = this.typeChecker.isTypeIdenticalTo(sourceType, targetType);
1750
+ return {
1751
+ explain: () => this.#explain(sourceType, targetType, isNot),
1752
+ isMatch,
1753
+ };
1692
1754
  }
1693
- explain(assertion) {
1694
- this.#assertNonNullishTypeChecker(assertion);
1695
- const matcher = assertion.matcherName.getText();
1696
- const origin = {
1697
- breadcrumbs: assertion.ancestorNames,
1698
- end: assertion.matcherName.getEnd(),
1699
- file: assertion.matcherName.getSourceFile(),
1700
- start: assertion.matcherName.getStart(),
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,
1701
1794
  };
1702
- switch (matcher) {
1703
- case "toBeAssignable": {
1704
- this.#assertNonNullishSourceType(assertion);
1705
- this.#assertNonNullishTargetType(assertion);
1706
- const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1707
- const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
1708
- return [
1709
- Diagnostic.error(assertion.isNot
1710
- ? `Type '${targetTypeText}' is assignable to type '${sourceTypeText}'.`
1711
- : `Type '${targetTypeText}' is not assignable to type '${sourceTypeText}'.`, origin),
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),
1712
1878
  ];
1879
+ const text = `${sourceText} raised a type error ${expectedText}.`;
1880
+ diagnostics.push(Diagnostic.error(text).add({ related }));
1713
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);
1714
1991
  case "toBeAny":
1715
- return this.#isType(assertion, "any");
1716
1992
  case "toBeBigInt":
1717
- return this.#isType(assertion, "bigint");
1718
1993
  case "toBeBoolean":
1719
- return this.#isType(assertion, "boolean");
1720
1994
  case "toBeNever":
1721
- return this.#isType(assertion, "never");
1722
1995
  case "toBeNull":
1723
- return this.#isType(assertion, "null");
1724
1996
  case "toBeNumber":
1725
- return this.#isType(assertion, "number");
1726
1997
  case "toBeString":
1727
- return this.#isType(assertion, "string");
1728
1998
  case "toBeSymbol":
1729
- return this.#isType(assertion, "symbol");
1730
1999
  case "toBeUndefined":
1731
- return this.#isType(assertion, "undefined");
1732
2000
  case "toBeUniqueSymbol":
1733
- return this.#isType(assertion, "unique symbol");
1734
2001
  case "toBeUnknown":
1735
- return this.#isType(assertion, "unknown");
1736
2002
  case "toBeVoid":
1737
- return this.#isType(assertion, "void");
1738
- case "toEqual": {
1739
- this.#assertNonNullishSourceType(assertion);
1740
- this.#assertNonNullishTargetType(assertion);
1741
- const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1742
- const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
1743
- return [
1744
- Diagnostic.error(assertion.isNot
1745
- ? `Type '${targetTypeText}' is identical to type '${sourceTypeText}'.`
1746
- : `Type '${targetTypeText}' is not identical to type '${sourceTypeText}'.`, origin),
1747
- ];
1748
- }
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);
1749
2008
  case "toHaveProperty": {
1750
- this.#assertNonNullishSourceType(assertion);
1751
- this.#assertNonNullishTargetType(assertion);
1752
- const sourceText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1753
- let targetArgumentText;
1754
- if (assertion.targetType.type.flags & this.compiler.TypeFlags.StringOrNumberLiteral) {
1755
- targetArgumentText = String(assertion.targetType.type.value);
2009
+ if (assertion.source[0] == null) {
2010
+ this.#onSourceArgumentMustBeProvided(assertion, expectResult);
2011
+ return;
1756
2012
  }
1757
- else if (assertion.targetType.type.flags & this.compiler.TypeFlags.UniqueESSymbol) {
1758
- targetArgumentText = `[${this.compiler.unescapeLeadingUnderscores(assertion.targetType.type.symbol.escapedName)}]`;
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;
1759
2019
  }
1760
- else {
1761
- throw new Error("An argument for 'key' must be of type 'string | number | symbol'.");
2020
+ if (assertion.target[0] == null) {
2021
+ this.#onKeyArgumentMustBeProvided(assertion, expectResult);
2022
+ return;
1762
2023
  }
1763
- return [
1764
- Diagnostic.error(assertion.isNot
1765
- ? `Property '${targetArgumentText}' exists on type '${sourceText}'.`
1766
- : `Property '${targetArgumentText}' does not exist on type '${sourceText}'.`, origin),
1767
- ];
1768
- }
1769
- case "toMatch": {
1770
- this.#assertNonNullishSourceType(assertion);
1771
- this.#assertNonNullishTargetType(assertion);
1772
- const sourceTypeText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1773
- const targetTypeText = assertion.typeChecker.typeToString(assertion.targetType.type);
1774
- return [
1775
- Diagnostic.error(assertion.isNot
1776
- ? `Type '${targetTypeText}' is a subtype of type '${sourceTypeText}'.`
1777
- : `Type '${targetTypeText}' is not a subtype of type '${sourceTypeText}'.`, origin),
1778
- ];
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);
1779
2030
  }
1780
2031
  case "toRaiseError": {
1781
- this.#assertNonNullishSourceType(assertion);
1782
- if (!this.#assertStringsOrNumbers(assertion.targetArguments)) {
1783
- throw new Error("An argument for 'target' must be of type 'string | number'.");
1784
- }
1785
- const sourceText = assertion.sourceType.source === 0 ? "Expression" : "Type definition";
1786
- if (assertion.diagnostics.length === 0) {
1787
- return [Diagnostic.error(`${sourceText} did not raise a type error.`, origin)];
1788
- }
1789
- if (assertion.isNot && assertion.targetArguments.length === 0) {
1790
- const related = [
1791
- Diagnostic.error(`The raised type error${assertion.diagnostics.length === 1 ? "" : "s"}:`),
1792
- ...Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
1793
- ];
1794
- return [
1795
- Diagnostic.error(`${sourceText} raised ${assertion.diagnostics.length === 1 ? "a" : assertion.diagnostics.length} type error${assertion.diagnostics.length === 1 ? "" : "s"}.`, origin).add({ related }),
1796
- ];
1797
- }
1798
- if (assertion.diagnostics.length !== assertion.targetArguments.length) {
1799
- const expectedText = assertion.diagnostics.length > assertion.targetArguments.length
1800
- ? `only ${assertion.targetArguments.length} type error${assertion.targetArguments.length === 1 ? "" : "s"}`
1801
- : `${assertion.targetArguments.length} type error${assertion.targetArguments.length === 1 ? "" : "s"}`;
1802
- const foundText = assertion.diagnostics.length > assertion.targetArguments.length
1803
- ? `${assertion.diagnostics.length}`
1804
- : `only ${assertion.diagnostics.length}`;
1805
- const related = [
1806
- Diagnostic.error(`The raised type error${assertion.diagnostics.length === 1 ? "" : "s"}:`),
1807
- ...Diagnostic.fromDiagnostics(assertion.diagnostics, this.compiler),
1808
- ];
1809
- const diagnostic = Diagnostic.error(`Expected ${expectedText}, but ${foundText} ${assertion.diagnostics.length === 1 ? "was" : "were"} raised.`, origin).add({
1810
- related,
1811
- });
1812
- return [diagnostic];
2032
+ if (assertion.source[0] == null) {
2033
+ this.#onSourceArgumentMustBeProvided(assertion, expectResult);
2034
+ return;
1813
2035
  }
1814
- const diagnostics = [];
1815
- assertion.targetArguments.forEach((argument, index) => {
1816
- const diagnostic = assertion.diagnostics[index];
1817
- if (!diagnostic) {
1818
- return;
1819
- }
1820
- const isMatch = this.#matchExpectedError(diagnostic, argument);
1821
- if (!assertion.isNot && !isMatch) {
1822
- const expectedText = this.compiler.isStringLiteralLike(argument)
1823
- ? `matching substring '${argument.text}'`
1824
- : `with code ${argument.text}`;
1825
- const related = [
1826
- Diagnostic.error("The raised type error:"),
1827
- ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
1828
- ];
1829
- diagnostics.push(Diagnostic.error(`${sourceText} did not raise a type error ${expectedText}.`, origin).add({ related }));
1830
- }
1831
- if (assertion.isNot && isMatch) {
1832
- const expectedText = this.compiler.isStringLiteralLike(argument)
1833
- ? `matching substring '${argument.text}'`
1834
- : `with code ${argument.text}`;
1835
- const related = [
1836
- Diagnostic.error("The raised type error:"),
1837
- ...Diagnostic.fromDiagnostics([diagnostic], this.compiler),
1838
- ];
1839
- diagnostics.push(Diagnostic.error(`${sourceText} raised a type error ${expectedText}.`, origin).add({ related }));
1840
- }
1841
- });
1842
- 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);
1843
2042
  }
1844
2043
  default:
1845
- throw new Error(`The '${matcher}' matcher is not supported.`);
2044
+ this.#onNotSupportedMatcherName(assertion, expectResult);
2045
+ return;
1846
2046
  }
1847
2047
  }
1848
- #hasTypeFlag(assertion, targetTypeFlag) {
1849
- this.#assertNonNullishSourceType(assertion);
1850
- 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 }]);
2057
+ }
2058
+ #onKeyArgumentMustBeProvided(assertion, expectResult) {
2059
+ const origin = {
2060
+ end: assertion.matcherName.getEnd(),
2061
+ file: assertion.matcherName.getSourceFile(),
2062
+ start: assertion.matcherName.getStart(),
2063
+ };
2064
+ EventEmitter.dispatch([
2065
+ "expect:error",
2066
+ {
2067
+ diagnostics: [Diagnostic.error("An argument for 'key' must be provided.", origin)],
2068
+ result: expectResult,
2069
+ },
2070
+ ]);
1851
2071
  }
1852
- #isType(assertion, targetText) {
1853
- this.#assertNonNullishSourceType(assertion);
1854
- this.#assertNonNullishTypeChecker(assertion);
2072
+ #onNotSupportedMatcherName(assertion, expectResult) {
2073
+ const matcherNameText = assertion.matcherName.getText();
1855
2074
  const origin = {
1856
- breadcrumbs: assertion.ancestorNames,
1857
2075
  end: assertion.matcherName.getEnd(),
1858
2076
  file: assertion.matcherName.getSourceFile(),
1859
2077
  start: assertion.matcherName.getStart(),
1860
2078
  };
1861
- const sourceText = assertion.typeChecker.typeToString(assertion.sourceType.type);
1862
- return [
1863
- Diagnostic.error(assertion.isNot
1864
- ? `Type '${targetText}' is identical to type '${sourceText}'.`
1865
- : `Type '${targetText}' is not identical to type '${sourceText}'.`, origin),
1866
- ];
2079
+ EventEmitter.dispatch([
2080
+ "expect:error",
2081
+ {
2082
+ diagnostics: [Diagnostic.error(`The '${matcherNameText}()' matcher is not supported.`, origin)],
2083
+ result: expectResult,
2084
+ },
2085
+ ]);
1867
2086
  }
1868
- match(assertion) {
1869
- const matcher = assertion.matcherName.getText();
1870
- switch (matcher) {
1871
- case "toBeAssignable":
1872
- this.#assertNonNullishSourceType(assertion);
1873
- this.#assertNonNullishTargetType(assertion);
1874
- this.#assertNonNullish(assertion.typeChecker?.isTypeAssignableTo, "The 'isTypeAssignableTo' method is missing in the provided type checker.");
1875
- return assertion.typeChecker.isTypeAssignableTo(assertion.targetType.type, assertion.sourceType.type);
1876
- case "toBeAny": {
1877
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Any);
1878
- }
1879
- case "toBeBigInt": {
1880
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.BigInt);
1881
- }
1882
- case "toBeBoolean": {
1883
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Boolean);
1884
- }
1885
- case "toBeNever": {
1886
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Never);
1887
- }
1888
- case "toBeNull": {
1889
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Null);
1890
- }
1891
- case "toBeNumber": {
1892
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Number);
1893
- }
1894
- case "toBeString": {
1895
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.String);
1896
- }
1897
- case "toBeSymbol": {
1898
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.ESSymbol);
1899
- }
1900
- case "toBeUndefined": {
1901
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Undefined);
1902
- }
1903
- case "toBeUniqueSymbol": {
1904
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.UniqueESSymbol);
1905
- }
1906
- case "toBeUnknown": {
1907
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Unknown);
1908
- }
1909
- case "toBeVoid": {
1910
- return this.#hasTypeFlag(assertion, this.compiler.TypeFlags.Void);
1911
- }
1912
- case "toEqual": {
1913
- this.#assertNonNullishSourceType(assertion);
1914
- this.#assertNonNullishTargetType(assertion);
1915
- this.#assertNonNullish(assertion.typeChecker?.isTypeIdenticalTo, "The 'isTypeIdenticalTo' method is missing in the provided type checker.");
1916
- return assertion.typeChecker.isTypeIdenticalTo(assertion.sourceType.type, assertion.targetType.type);
1917
- }
1918
- case "toHaveProperty": {
1919
- this.#assertNonNullishSourceType(assertion);
1920
- this.#assertNonNullishTargetType(assertion);
1921
- if (!(assertion.sourceType.type.flags & this.compiler.TypeFlags.StructuredType)) {
1922
- const receivedText = assertion.typeChecker?.typeToString(assertion.sourceType.type);
1923
- throw new Error(`An argument for 'source' must be of object type, received: '${receivedText}'.`);
1924
- }
1925
- let targetArgumentText;
1926
- if (assertion.targetType.type.flags & this.compiler.TypeFlags.StringOrNumberLiteral) {
1927
- targetArgumentText = String(assertion.targetType.type.value);
1928
- }
1929
- else if (assertion.targetType.type.flags & this.compiler.TypeFlags.UniqueESSymbol) {
1930
- targetArgumentText = this.compiler.unescapeLeadingUnderscores(assertion.targetType.type.escapedName);
1931
- }
1932
- else {
1933
- throw new Error("An argument for 'key' must be of type 'string | number | symbol'.");
1934
- }
1935
- return assertion.sourceType.type.getProperties().some((property) => {
1936
- return this.compiler.unescapeLeadingUnderscores(property.escapedName) === targetArgumentText;
1937
- });
1938
- }
1939
- case "toMatch": {
1940
- this.#assertNonNullishSourceType(assertion);
1941
- this.#assertNonNullishTargetType(assertion);
1942
- this.#assertNonNullish(assertion.typeChecker?.isTypeSubtypeOf, "The 'isTypeSubtypeOf' method is missing in the provided type checker.");
1943
- return assertion.typeChecker.isTypeSubtypeOf(assertion.sourceType.type, assertion.targetType.type);
1944
- }
1945
- case "toRaiseError": {
1946
- if (!this.#assertStringsOrNumbers(assertion.targetArguments)) {
1947
- throw new Error("An argument for 'target' must be of type 'string | number'.");
1948
- }
1949
- if (assertion.targetArguments.length === 0) {
1950
- return assertion.diagnostics.length > 0;
1951
- }
1952
- if (assertion.diagnostics.length !== assertion.targetArguments.length) {
1953
- return false;
1954
- }
1955
- return assertion.targetArguments.every((expectedArgument, index) => {
1956
- if (this.compiler.isStringLiteralLike(expectedArgument)) {
1957
- return this.compiler
1958
- .flattenDiagnosticMessageText(assertion.diagnostics[index]?.messageText, " ", 0)
1959
- .includes(expectedArgument.text);
1960
- }
1961
- if (this.compiler.isNumericLiteral(expectedArgument)) {
1962
- return Number(expectedArgument.text) === assertion.diagnostics[index]?.code;
1963
- }
1964
- return false;
1965
- });
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));
1966
2142
  }
1967
- default:
1968
- throw new Error(`The '${matcher}' matcher is not supported.`);
1969
2143
  }
2144
+ EventEmitter.dispatch(["expect:error", { diagnostics, result: expectResult }]);
2145
+ }
2146
+ }
2147
+
2148
+ class ProjectService {
2149
+ compiler;
2150
+ #service;
2151
+ constructor(compiler) {
2152
+ this.compiler = compiler;
2153
+ function doNothing() {
2154
+ }
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);
1970
2194
  }
1971
- #matchExpectedError(diagnostic, argument) {
1972
- if (this.compiler.isStringLiteralLike(argument)) {
1973
- return this.compiler.flattenDiagnosticMessageText(diagnostic.messageText, " ", 0).includes(argument.text);
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;
1974
2202
  }
1975
- if (this.compiler.isNumericLiteral(argument)) {
1976
- return Number(argument.text) === diagnostic.code;
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
+ ]);
1977
2216
  }
1978
- throw new Error("An argument for 'target' must be of type 'string | number'.");
1979
2217
  }
1980
2218
  }
1981
2219
 
1982
2220
  class TestTreeWorker {
1983
2221
  resolvedConfig;
1984
2222
  compiler;
1985
- #checker;
2223
+ #expect;
1986
2224
  #fileResult;
1987
2225
  #hasOnly;
1988
2226
  #position;
1989
2227
  #signal;
1990
- constructor(resolvedConfig, compiler, options) {
2228
+ constructor(resolvedConfig, compiler, expect, options) {
1991
2229
  this.resolvedConfig = resolvedConfig;
1992
2230
  this.compiler = compiler;
1993
- this.#checker = new Checker(compiler);
2231
+ this.#expect = expect;
2232
+ this.#fileResult = options.fileResult;
1994
2233
  this.#hasOnly = options.hasOnly || resolvedConfig.only != null || options.position != null;
1995
2234
  this.#position = options.position;
1996
2235
  this.#signal = options.signal;
1997
- this.#fileResult = options.fileResult;
1998
2236
  }
1999
2237
  #resolveRunMode(mode, member) {
2000
2238
  if (member.flags & 1) {
@@ -2062,8 +2300,11 @@ class TestTreeWorker {
2062
2300
  ]);
2063
2301
  return;
2064
2302
  }
2065
- const isPass = this.#checker.match(assertion);
2066
- 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) {
2067
2308
  if (runMode & 1) {
2068
2309
  const text = ["The assertion was supposed to fail, but it passed.", "Consider removing the '.fail' flag."];
2069
2310
  const origin = {
@@ -2073,10 +2314,7 @@ class TestTreeWorker {
2073
2314
  };
2074
2315
  EventEmitter.dispatch([
2075
2316
  "expect:error",
2076
- {
2077
- diagnostics: [Diagnostic.error(text, origin)],
2078
- result: expectResult,
2079
- },
2317
+ { diagnostics: [Diagnostic.error(text, origin)], result: expectResult },
2080
2318
  ]);
2081
2319
  }
2082
2320
  else {
@@ -2088,13 +2326,20 @@ class TestTreeWorker {
2088
2326
  EventEmitter.dispatch(["expect:pass", { result: expectResult }]);
2089
2327
  }
2090
2328
  else {
2091
- EventEmitter.dispatch([
2092
- "expect:fail",
2093
- {
2094
- diagnostics: this.#checker.explain(assertion),
2095
- result: expectResult,
2096
- },
2097
- ]);
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 }]);
2098
2343
  }
2099
2344
  }
2100
2345
  }
@@ -2198,8 +2443,7 @@ class TestFileRunner {
2198
2443
  if (!sourceFile) {
2199
2444
  return;
2200
2445
  }
2201
- const typeChecker = program.getTypeChecker();
2202
- const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics, typeChecker);
2446
+ const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
2203
2447
  if (testTree.diagnostics.length > 0) {
2204
2448
  EventEmitter.dispatch([
2205
2449
  "file:error",
@@ -2210,7 +2454,14 @@ class TestFileRunner {
2210
2454
  ]);
2211
2455
  return;
2212
2456
  }
2213
- 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, {
2214
2465
  fileResult,
2215
2466
  hasOnly: testTree.hasOnly,
2216
2467
  position,
@@ -3110,11 +3361,13 @@ class ManifestWorker {
3110
3361
  #cachePath;
3111
3362
  #manifestFileName = "store-manifest.json";
3112
3363
  #manifestFilePath;
3364
+ #onDiagnostic;
3113
3365
  #prune;
3114
3366
  #registryUrl = new URL("https://registry.npmjs.org");
3115
3367
  #timeout = Environment.timeout * 1000;
3116
- constructor(cachePath, prune) {
3368
+ constructor(cachePath, onDiagnostic, prune) {
3117
3369
  this.#cachePath = cachePath;
3370
+ this.#onDiagnostic = onDiagnostic;
3118
3371
  this.#manifestFilePath = path.join(cachePath, this.#manifestFileName);
3119
3372
  this.#prune = prune;
3120
3373
  }
@@ -3202,9 +3455,6 @@ class ManifestWorker {
3202
3455
  }
3203
3456
  return manifest;
3204
3457
  }
3205
- #onDiagnostic(diagnostic) {
3206
- EventEmitter.dispatch(["store:error", { diagnostics: [diagnostic] }]);
3207
- }
3208
3458
  async open(signal) {
3209
3459
  let manifest;
3210
3460
  if (!existsSync(this.#manifestFilePath)) {
@@ -3268,7 +3518,7 @@ class StoreService {
3268
3518
  constructor() {
3269
3519
  this.#cachePath = Environment.storePath;
3270
3520
  this.#compilerModuleWorker = new CompilerModuleWorker(this.#cachePath, this.#onDiagnostic);
3271
- this.#manifestWorker = new ManifestWorker(this.#cachePath, async () => this.prune());
3521
+ this.#manifestWorker = new ManifestWorker(this.#cachePath, this.#onDiagnostic, this.prune);
3272
3522
  }
3273
3523
  get supportedTags() {
3274
3524
  if (!this.#manifest) {
@@ -3316,9 +3566,9 @@ class StoreService {
3316
3566
  }
3317
3567
  this.#manifest = await this.#manifestWorker.open(signal);
3318
3568
  }
3319
- async prune() {
3569
+ prune = async () => {
3320
3570
  await fs.rm(this.#cachePath, { force: true, recursive: true });
3321
- }
3571
+ };
3322
3572
  resolveTag(tag) {
3323
3573
  if (!this.#manifest) {
3324
3574
  this.#onDiagnostic(Diagnostic.error("Store manifest is not open. Call 'StoreService.open()' first."));
@@ -3371,154 +3621,6 @@ class StoreService {
3371
3621
  }
3372
3622
  }
3373
3623
 
3374
- class JsonText {
3375
- props;
3376
- constructor(props) {
3377
- this.props = props;
3378
- }
3379
- render() {
3380
- return Scribbler.createElement(Line, null, JSON.stringify(this.#sortObject(this.props.input), null, 2));
3381
- }
3382
- #sortObject(target) {
3383
- if (Array.isArray(target)) {
3384
- return target;
3385
- }
3386
- return Object.keys(target)
3387
- .sort()
3388
- .reduce((result, key) => {
3389
- result[key] = target[key];
3390
- return result;
3391
- }, {});
3392
- }
3393
- }
3394
- function formattedText(input) {
3395
- if (typeof input === "string") {
3396
- return Scribbler.createElement(Line, null, input);
3397
- }
3398
- return Scribbler.createElement(JsonText, { input: input });
3399
- }
3400
-
3401
- const usageExamples = [
3402
- ["tstyche", "Run all tests."],
3403
- ["tstyche path/to/first.test.ts second", "Only run the test files with matching path."],
3404
- ["tstyche --target 4.7,4.8,latest", "Test on all specified versions of TypeScript."],
3405
- ];
3406
- class HintText {
3407
- props;
3408
- constructor(props) {
3409
- this.props = props;
3410
- }
3411
- render() {
3412
- return (Scribbler.createElement(Text, { indent: 1, color: "90" }, this.props.children));
3413
- }
3414
- }
3415
- class HelpHeaderText {
3416
- props;
3417
- constructor(props) {
3418
- this.props = props;
3419
- }
3420
- render() {
3421
- const hint = (Scribbler.createElement(HintText, null,
3422
- Scribbler.createElement(Text, null, this.props.tstycheVersion)));
3423
- return (Scribbler.createElement(Line, null,
3424
- Scribbler.createElement(Text, null, "The TSTyche Type Test Runner"),
3425
- hint));
3426
- }
3427
- }
3428
- class CommandText {
3429
- props;
3430
- constructor(props) {
3431
- this.props = props;
3432
- }
3433
- render() {
3434
- let hint = undefined;
3435
- if (this.props.hint != null) {
3436
- hint = Scribbler.createElement(HintText, null, this.props.hint);
3437
- }
3438
- return (Scribbler.createElement(Line, { indent: 1 },
3439
- Scribbler.createElement(Text, { color: "34" }, this.props.text),
3440
- hint));
3441
- }
3442
- }
3443
- class OptionDescriptionText {
3444
- props;
3445
- constructor(props) {
3446
- this.props = props;
3447
- }
3448
- render() {
3449
- return Scribbler.createElement(Line, { indent: 1 }, this.props.text);
3450
- }
3451
- }
3452
- class CliUsageText {
3453
- render() {
3454
- const usageText = usageExamples.map(([commandText, descriptionText]) => (Scribbler.createElement(Text, null,
3455
- Scribbler.createElement(CommandText, { text: commandText }),
3456
- Scribbler.createElement(OptionDescriptionText, { text: descriptionText }),
3457
- Scribbler.createElement(Line, null))));
3458
- return Scribbler.createElement(Text, null, usageText);
3459
- }
3460
- }
3461
- class OptionNameText {
3462
- props;
3463
- constructor(props) {
3464
- this.props = props;
3465
- }
3466
- render() {
3467
- return Scribbler.createElement(Text, null,
3468
- "--",
3469
- this.props.text);
3470
- }
3471
- }
3472
- class OptionHintText {
3473
- props;
3474
- constructor(props) {
3475
- this.props = props;
3476
- }
3477
- render() {
3478
- if (this.props.definition.brand === "list") {
3479
- return (Scribbler.createElement(Text, null,
3480
- this.props.definition.brand,
3481
- " of ",
3482
- this.props.definition.items.brand,
3483
- "s"));
3484
- }
3485
- return Scribbler.createElement(Text, null, this.props.definition.brand);
3486
- }
3487
- }
3488
- class CliOptionsText {
3489
- props;
3490
- constructor(props) {
3491
- this.props = props;
3492
- }
3493
- render() {
3494
- const definitions = Array.from(this.props.optionDefinitions.values());
3495
- const optionsText = definitions.map((definition) => (Scribbler.createElement(Text, null,
3496
- Scribbler.createElement(CommandText, { text: Scribbler.createElement(OptionNameText, { text: definition.name }), hint: Scribbler.createElement(OptionHintText, { definition: definition }) }),
3497
- Scribbler.createElement(OptionDescriptionText, { text: definition.description }),
3498
- Scribbler.createElement(Line, null))));
3499
- return (Scribbler.createElement(Text, null,
3500
- Scribbler.createElement(Line, null, "CLI Options"),
3501
- Scribbler.createElement(Line, null),
3502
- optionsText));
3503
- }
3504
- }
3505
- class HelpFooterText {
3506
- render() {
3507
- return Scribbler.createElement(Line, null, "To learn more, visit https://tstyche.org");
3508
- }
3509
- }
3510
- function helpText(optionDefinitions, tstycheVersion) {
3511
- return (Scribbler.createElement(Text, null,
3512
- Scribbler.createElement(HelpHeaderText, { tstycheVersion: tstycheVersion }),
3513
- Scribbler.createElement(Line, null),
3514
- Scribbler.createElement(CliUsageText, null),
3515
- Scribbler.createElement(Line, null),
3516
- Scribbler.createElement(CliOptionsText, { optionDefinitions: optionDefinitions }),
3517
- Scribbler.createElement(Line, null),
3518
- Scribbler.createElement(HelpFooterText, null),
3519
- Scribbler.createElement(Line, null)));
3520
- }
3521
-
3522
3624
  class Cli {
3523
3625
  #abortController = new AbortController();
3524
3626
  #logger;
@@ -3617,4 +3719,4 @@ class Cli {
3617
3719
  }
3618
3720
  }
3619
3721
 
3620
- 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 };