tstyche 4.0.0-rc.0 → 4.0.0

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![install-size][install-size-badge]][install-size-url]
6
6
  [![coverage][coverage-badge]][coverage-url]
7
7
 
8
- The Essential Type Testing Tool.
8
+ Everything You Need for Type Testing.
9
9
 
10
10
  ---
11
11
 
@@ -37,21 +37,29 @@ To organize, debug and plan tests TSTyche has:
37
37
 
38
38
  ## Assertions
39
39
 
40
- The assertions can be used to write type tests (like in the above example) or mixed in your functional tests:
40
+ The assertions can be used to write type tests (like in the above example) or mixed in your unit tests:
41
41
 
42
42
  ```ts
43
43
  import assert from "node:assert";
44
44
  import test from "node:test";
45
45
  import * as tstyche from "tstyche";
46
46
 
47
- function secondItem<T>(target: Array<T>): T | undefined {
48
- return target[1];
47
+ function toMilliseconds(value: number) {
48
+ if (typeof value === "number" && !Number.isNaN(value)) {
49
+ return value * 1000;
50
+ }
51
+
52
+ throw new Error("Not a number");
49
53
  }
50
54
 
51
- test("handles numbers", () => {
52
- assert.strictEqual(secondItem([1, 2, 3]), 2);
55
+ test("toMilliseconds", () => {
56
+ const sample = toMilliseconds(10);
57
+
58
+ assert.equal(sample, 10_000);
59
+ tstyche.expect(sample).type.toBe<number>();
53
60
 
54
- tstyche.expect(secondItem([1, 2, 3])).type.toBe<number | undefined>();
61
+ // Will pass as a type test and not throw at runtime
62
+ tstyche.expect(toMilliseconds).type.not.toBeCallableWith("20");
55
63
  });
56
64
  ```
57
65
 
@@ -20,60 +20,6 @@ declare class Cli {
20
20
  run(commandLine: Array<string>, cancellationToken?: CancellationToken): Promise<void>;
21
21
  }
22
22
 
23
- declare enum TestTreeNodeBrand {
24
- Describe = "describe",
25
- Test = "test",
26
- Expect = "expect",
27
- When = "when"
28
- }
29
-
30
- declare enum TestTreeNodeFlags {
31
- None = 0,
32
- Fail = 1,
33
- Only = 2,
34
- Skip = 4,
35
- Todo = 8
36
- }
37
-
38
- declare class WhenNode extends TestTreeNode {
39
- actionNode: ts.CallExpression;
40
- actionNameNode: ts.PropertyAccessExpression;
41
- abilityDiagnostics: Set<ts.Diagnostic> | undefined;
42
- target: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
43
- constructor(compiler: typeof ts, brand: TestTreeNodeBrand, node: ts.CallExpression, parent: TestTree | TestTreeNode, flags: TestTreeNodeFlags, actionNode: ts.CallExpression, actionNameNode: ts.PropertyAccessExpression);
44
- }
45
-
46
- declare class TestTreeNode {
47
- brand: TestTreeNodeBrand;
48
- children: Array<TestTreeNode | AssertionNode | WhenNode>;
49
- diagnostics: Set<ts.Diagnostic>;
50
- flags: TestTreeNodeFlags;
51
- name: string;
52
- node: ts.CallExpression;
53
- parent: TestTree | TestTreeNode;
54
- constructor(compiler: typeof ts, brand: TestTreeNodeBrand, node: ts.CallExpression, parent: TestTree | TestTreeNode, flags: TestTreeNodeFlags);
55
- }
56
-
57
- declare class TestTree {
58
- children: Array<TestTreeNode | AssertionNode | WhenNode>;
59
- diagnostics: Set<ts.Diagnostic>;
60
- hasOnly: boolean;
61
- sourceFile: ts.SourceFile;
62
- constructor(diagnostics: Set<ts.Diagnostic>, sourceFile: ts.SourceFile);
63
- }
64
-
65
- declare class AssertionNode extends TestTreeNode {
66
- abilityDiagnostics: Set<ts.Diagnostic> | undefined;
67
- isNot: boolean;
68
- matcherNode: ts.CallExpression | ts.Decorator;
69
- matcherNameNode: ts.PropertyAccessExpression;
70
- modifierNode: ts.PropertyAccessExpression;
71
- notNode: ts.PropertyAccessExpression | undefined;
72
- source: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
73
- target: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode> | undefined;
74
- constructor(compiler: typeof ts, brand: TestTreeNodeBrand, node: ts.CallExpression, parent: TestTree | TestTreeNode, flags: TestTreeNodeFlags, matcherNode: ts.CallExpression | ts.Decorator, matcherNameNode: ts.PropertyAccessExpression, modifierNode: ts.PropertyAccessExpression, notNode?: ts.PropertyAccessExpression);
75
- }
76
-
77
23
  declare enum OptionBrand {
78
24
  String = "string",
79
25
  Number = "number",
@@ -94,58 +40,13 @@ declare class ConfigDiagnosticText {
94
40
  static requiresValueType(optionName: string, optionBrand: OptionBrand): string;
95
41
  static seen(element: string): string;
96
42
  static testFileMatchCannotStartWith(segment: string): Array<string>;
43
+ static unexpected(element: string): string;
97
44
  static unknownOption(optionName: string): string;
98
45
  static usage(optionName: string, optionBrand: OptionBrand): Array<string>;
99
46
  static versionIsNotSupported(value: string): string;
100
47
  static watchCannotBeEnabled(): string;
101
48
  }
102
49
 
103
- /**
104
- * Options loaded from the configuration file.
105
- */
106
- interface ConfigFileOptions {
107
- /**
108
- * Enable type error reporting for source files.
109
- */
110
- checkSourceFiles?: boolean;
111
- /**
112
- * Stop running tests after the first failed assertion.
113
- */
114
- failFast?: boolean;
115
- /**
116
- * The list of plugins to use.
117
- */
118
- plugins?: Array<string>;
119
- /**
120
- * Reject the 'any' type passed as an argument to the 'expect()' function or a matcher.
121
- */
122
- rejectAnyType?: boolean;
123
- /**
124
- * Reject the 'never' type passed as an argument to the 'expect()' function or a matcher.
125
- */
126
- rejectNeverType?: boolean;
127
- /**
128
- * The list of reporters to use.
129
- */
130
- reporters?: Array<string>;
131
- /**
132
- * The path to a directory containing files of a test project.
133
- */
134
- rootPath?: string;
135
- /**
136
- * The list of TypeScript versions to be tested on.
137
- */
138
- target?: Array<string>;
139
- /**
140
- * The list of glob patterns matching the test files.
141
- */
142
- testFileMatch?: Array<string>;
143
- /**
144
- * The look up strategy to be used to find the TSConfig file.
145
- */
146
- tsconfig?: string;
147
- }
148
-
149
50
  /**
150
51
  * Options passed through the command line.
151
52
  */
@@ -220,6 +121,58 @@ interface CommandLineOptions {
220
121
  watch?: boolean;
221
122
  }
222
123
 
124
+ /**
125
+ * Options loaded from the configuration file.
126
+ */
127
+ interface ConfigFileOptions {
128
+ /**
129
+ * Enable type error reporting for source files.
130
+ */
131
+ checkSourceFiles?: boolean;
132
+ /**
133
+ * Stop running tests after the first failed assertion.
134
+ */
135
+ failFast?: boolean;
136
+ /**
137
+ * The list of plugins to use.
138
+ */
139
+ plugins?: Array<string>;
140
+ /**
141
+ * Reject the 'any' type passed as an argument to the 'expect()' function or a matcher.
142
+ */
143
+ rejectAnyType?: boolean;
144
+ /**
145
+ * Reject the 'never' type passed as an argument to the 'expect()' function or a matcher.
146
+ */
147
+ rejectNeverType?: boolean;
148
+ /**
149
+ * The list of reporters to use.
150
+ */
151
+ reporters?: Array<string>;
152
+ /**
153
+ * The path to a directory containing files of a test project.
154
+ */
155
+ rootPath?: string;
156
+ /**
157
+ * The list of TypeScript versions to be tested on.
158
+ */
159
+ target?: Array<string>;
160
+ /**
161
+ * The list of glob patterns matching the test files.
162
+ */
163
+ testFileMatch?: Array<string>;
164
+ /**
165
+ * The look up strategy to be used to find the TSConfig file.
166
+ */
167
+ tsconfig?: string;
168
+ }
169
+
170
+ interface InlineConfig {
171
+ if?: {
172
+ target?: Array<string>;
173
+ };
174
+ template?: boolean;
175
+ }
223
176
  interface ResolvedConfig extends Omit<CommandLineOptions, "config" | keyof ConfigFileOptions>, Required<ConfigFileOptions> {
224
177
  /**
225
178
  * The path to a TSTyche configuration file.
@@ -230,6 +183,7 @@ interface ResolvedConfig extends Omit<CommandLineOptions, "config" | keyof Confi
230
183
  */
231
184
  pathMatch: Array<string>;
232
185
  }
186
+
233
187
  declare class Config {
234
188
  #private;
235
189
  static parseCommandLine(commandLine: Array<string>): Promise<{
@@ -249,6 +203,25 @@ declare class Config {
249
203
  static resolveConfigFilePath(filePath?: string): string;
250
204
  }
251
205
 
206
+ interface TextRange {
207
+ start: number;
208
+ end: number;
209
+ text: string;
210
+ }
211
+ interface DirectiveRange {
212
+ namespace: TextRange;
213
+ directive?: TextRange;
214
+ argument?: TextRange;
215
+ }
216
+ type DirectiveRanges = Array<DirectiveRange> & {
217
+ sourceFile: ts.SourceFile;
218
+ };
219
+ declare class Directive {
220
+ #private;
221
+ static getDirectiveRanges(compiler: typeof ts, sourceFile: ts.SourceFile, position?: number): DirectiveRanges | undefined;
222
+ static getInlineConfig(ranges: DirectiveRanges | undefined): Promise<InlineConfig | undefined>;
223
+ }
224
+
252
225
  declare enum DiagnosticCategory {
253
226
  Error = "error",
254
227
  Warning = "warning"
@@ -304,6 +277,7 @@ type DiagnosticsHandler<T extends Diagnostic | Array<Diagnostic> = Diagnostic> =
304
277
  declare enum OptionGroup {
305
278
  CommandLine = 2,
306
279
  ConfigFile = 4,
280
+ InlineConditions = 8,
307
281
  ResolvedConfig = 6
308
282
  }
309
283
 
@@ -334,6 +308,62 @@ declare class Options {
334
308
 
335
309
  declare const defaultOptions: Required<ConfigFileOptions>;
336
310
 
311
+ declare enum TestTreeNodeBrand {
312
+ Describe = "describe",
313
+ Test = "test",
314
+ Expect = "expect",
315
+ When = "when"
316
+ }
317
+
318
+ declare enum TestTreeNodeFlags {
319
+ None = 0,
320
+ Fail = 1,
321
+ Only = 2,
322
+ Skip = 4,
323
+ Todo = 8
324
+ }
325
+
326
+ declare class WhenNode extends TestTreeNode {
327
+ actionNode: ts.CallExpression;
328
+ actionNameNode: ts.PropertyAccessExpression;
329
+ abilityDiagnostics: Set<ts.Diagnostic> | undefined;
330
+ target: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
331
+ constructor(compiler: typeof ts, brand: TestTreeNodeBrand, node: ts.CallExpression, parent: TestTree | TestTreeNode, flags: TestTreeNodeFlags, actionNode: ts.CallExpression, actionNameNode: ts.PropertyAccessExpression);
332
+ }
333
+
334
+ declare class TestTreeNode {
335
+ brand: TestTreeNodeBrand;
336
+ children: Array<TestTreeNode | AssertionNode | WhenNode>;
337
+ diagnostics: Set<ts.Diagnostic>;
338
+ flags: TestTreeNodeFlags;
339
+ name: string;
340
+ node: ts.CallExpression;
341
+ parent: TestTree | TestTreeNode;
342
+ constructor(compiler: typeof ts, brand: TestTreeNodeBrand, node: ts.CallExpression, parent: TestTree | TestTreeNode, flags: TestTreeNodeFlags);
343
+ getDirectiveRanges(compiler: typeof ts): DirectiveRanges | undefined;
344
+ }
345
+
346
+ declare class TestTree {
347
+ children: Array<TestTreeNode | AssertionNode | WhenNode>;
348
+ diagnostics: Set<ts.Diagnostic>;
349
+ hasOnly: boolean;
350
+ sourceFile: ts.SourceFile;
351
+ constructor(diagnostics: Set<ts.Diagnostic>, sourceFile: ts.SourceFile);
352
+ getDirectiveRanges(compiler: typeof ts): DirectiveRanges | undefined;
353
+ }
354
+
355
+ declare class AssertionNode extends TestTreeNode {
356
+ abilityDiagnostics: Set<ts.Diagnostic> | undefined;
357
+ isNot: boolean;
358
+ matcherNode: ts.CallExpression | ts.Decorator;
359
+ matcherNameNode: ts.PropertyAccessExpression;
360
+ modifierNode: ts.PropertyAccessExpression;
361
+ notNode: ts.PropertyAccessExpression | undefined;
362
+ source: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode>;
363
+ target: ts.NodeArray<ts.Expression> | ts.NodeArray<ts.TypeNode> | undefined;
364
+ constructor(compiler: typeof ts, brand: TestTreeNodeBrand, node: ts.CallExpression, parent: TestTree | TestTreeNode, flags: TestTreeNodeFlags, matcherNode: ts.CallExpression | ts.Decorator, matcherNameNode: ts.PropertyAccessExpression, modifierNode: ts.PropertyAccessExpression, notNode: ts.PropertyAccessExpression | undefined);
365
+ }
366
+
337
367
  declare class ProjectService {
338
368
  #private;
339
369
  constructor(compiler: typeof ts, resolvedConfig: ResolvedConfig);
@@ -546,6 +576,8 @@ type Event = ["config:error", {
546
576
  result: TaskResult;
547
577
  }] | ["task:end", {
548
578
  result: TaskResult;
579
+ }] | ["directive:error", {
580
+ diagnostics: Array<Diagnostic>;
549
581
  }] | ["collect:start", {
550
582
  tree: TestTree;
551
583
  }] | ["collect:error", {
@@ -855,6 +887,7 @@ declare class Store {
855
887
  declare class Version {
856
888
  #private;
857
889
  static isGreaterThan(source: string, target: string): boolean;
890
+ static isIncluded(source: string, range: Array<string>): boolean;
858
891
  static isSatisfiedWith(source: string, target: string): boolean;
859
892
  }
860
893
 
@@ -886,5 +919,5 @@ declare class WhenService {
886
919
  action(when: WhenNode): void;
887
920
  }
888
921
 
889
- export { AssertionNode, BaseReporter, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, Config, ConfigDiagnosticText, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, EventEmitter, ExitCodeHandler, ExpectResult, ExpectService, FileWatcher, InputService, Line, ListReporter, OptionBrand, OptionGroup, Options, OutputService, Path, PluginService, ProjectResult, ProjectService, Reject, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, Runner, Scribbler, ScribblerJsx, Select, SelectDiagnosticText, SetupReporter, SourceFile, Store, SummaryReporter, TargetResult, Task, TaskResult, TestResult, TestTree, TestTreeNode, TestTreeNodeBrand, TestTreeNodeFlags, Text, Version, WatchReporter, WatchService, Watcher, WhenNode, WhenService, addsPackageText, argumentIsProvided, argumentOrTypeArgumentIsProvided, defaultOptions, describeNameText, diagnosticBelongsToNode, diagnosticText, environmentOptions, fileViewText, formattedText, getDiagnosticMessageText, getTextSpanEnd, helpText, isDiagnosticWithLocation, nodeBelongsToArgumentList, summaryText, taskStatusText, testNameText, usesCompilerText, waitingForFileChangesText, watchUsageText };
890
- export type { CodeFrameOptions, CommandLineOptions, ConfigFileOptions, DiagnosticsHandler, EnvironmentOptions, Event, EventHandler, FileWatchHandler, InputHandler, ItemDefinition, MatchResult, OptionDefinition, Plugin, Reporter, ReporterEvent, ResolvedConfig, ScribblerOptions, SelectHookContext, TargetResultStatus, TaskResultStatus, TypeChecker, WatchHandler, WatcherOptions };
922
+ export { AssertionNode, BaseReporter, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, Config, ConfigDiagnosticText, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, Directive, EventEmitter, ExitCodeHandler, ExpectResult, ExpectService, FileWatcher, InputService, Line, ListReporter, OptionBrand, OptionGroup, Options, OutputService, Path, PluginService, ProjectResult, ProjectService, Reject, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, Runner, Scribbler, ScribblerJsx, Select, SelectDiagnosticText, SetupReporter, SourceFile, Store, SummaryReporter, TargetResult, Task, TaskResult, TestResult, TestTree, TestTreeNode, TestTreeNodeBrand, TestTreeNodeFlags, Text, Version, WatchReporter, WatchService, Watcher, WhenNode, WhenService, addsPackageText, argumentIsProvided, argumentOrTypeArgumentIsProvided, defaultOptions, describeNameText, diagnosticBelongsToNode, diagnosticText, environmentOptions, fileViewText, formattedText, getDiagnosticMessageText, getTextSpanEnd, helpText, isDiagnosticWithLocation, nodeBelongsToArgumentList, summaryText, taskStatusText, testNameText, usesCompilerText, waitingForFileChangesText, watchUsageText };
923
+ export type { CodeFrameOptions, CommandLineOptions, ConfigFileOptions, DiagnosticsHandler, DirectiveRange, DirectiveRanges, EnvironmentOptions, Event, EventHandler, FileWatchHandler, InlineConfig, InputHandler, ItemDefinition, MatchResult, OptionDefinition, Plugin, Reporter, ReporterEvent, ResolvedConfig, ScribblerOptions, SelectHookContext, TargetResultStatus, TaskResultStatus, TypeChecker, WatchHandler, WatcherOptions };
package/build/tstyche.js CHANGED
@@ -49,6 +49,9 @@ class ConfigDiagnosticText {
49
49
  "The test files are only collected within the 'rootPath' directory.",
50
50
  ];
51
51
  }
52
+ static unexpected(element) {
53
+ return `Unexpected ${element}.`;
54
+ }
52
55
  static unknownOption(optionName) {
53
56
  return `Unknown option '${optionName}'.`;
54
57
  }
@@ -292,6 +295,7 @@ var OptionGroup;
292
295
  (function (OptionGroup) {
293
296
  OptionGroup[OptionGroup["CommandLine"] = 2] = "CommandLine";
294
297
  OptionGroup[OptionGroup["ConfigFile"] = 4] = "ConfigFile";
298
+ OptionGroup[OptionGroup["InlineConditions"] = 8] = "InlineConditions";
295
299
  OptionGroup[OptionGroup["ResolvedConfig"] = 6] = "ResolvedConfig";
296
300
  })(OptionGroup || (OptionGroup = {}));
297
301
 
@@ -376,6 +380,9 @@ class Version {
376
380
  static isGreaterThan(source, target) {
377
381
  return !(source === target) && Version.#satisfies(source, target);
378
382
  }
383
+ static isIncluded(source, range) {
384
+ return range.some((target) => source.startsWith(target));
385
+ }
379
386
  static isSatisfiedWith(source, target) {
380
387
  return source === target || Version.#satisfies(source, target);
381
388
  }
@@ -1004,7 +1011,7 @@ class Options {
1004
1011
  {
1005
1012
  brand: OptionBrand.List,
1006
1013
  description: "The list of TypeScript versions to be tested on.",
1007
- group: OptionGroup.CommandLine | OptionGroup.ConfigFile,
1014
+ group: OptionGroup.CommandLine | OptionGroup.ConfigFile | OptionGroup.InlineConditions,
1008
1015
  items: {
1009
1016
  brand: OptionBrand.String,
1010
1017
  name: "target",
@@ -1239,146 +1246,18 @@ class CommandLineParser {
1239
1246
  }
1240
1247
  }
1241
1248
 
1242
- class JsonNode {
1243
- origin;
1244
- text;
1245
- constructor(text, origin) {
1246
- this.origin = origin;
1247
- this.text = text;
1248
- }
1249
- getValue(options) {
1250
- if (this.text == null) {
1251
- return undefined;
1252
- }
1253
- if (/^['"]/.test(this.text)) {
1254
- return this.text.slice(1, -1);
1255
- }
1256
- if (options?.expectsIdentifier) {
1257
- return this.text;
1258
- }
1259
- if (this.text === "true") {
1260
- return true;
1261
- }
1262
- if (this.text === "false") {
1263
- return false;
1264
- }
1265
- if (/^\d/.test(this.text)) {
1266
- return Number.parseFloat(this.text);
1267
- }
1268
- return undefined;
1269
- }
1270
- }
1271
-
1272
- class JsonScanner {
1273
- #currentPosition = 0;
1274
- #previousPosition = 0;
1275
- #sourceFile;
1276
- constructor(sourceFile) {
1277
- this.#sourceFile = sourceFile;
1278
- }
1279
- #getOrigin() {
1280
- return new DiagnosticOrigin(this.#previousPosition, this.#currentPosition, this.#sourceFile);
1281
- }
1282
- isRead() {
1283
- return !(this.#currentPosition < this.#sourceFile.text.length);
1284
- }
1285
- #peekCharacter() {
1286
- return this.#sourceFile.text.charAt(this.#currentPosition);
1287
- }
1288
- #peekNextCharacter() {
1289
- return this.#sourceFile.text.charAt(this.#currentPosition + 1);
1290
- }
1291
- peekToken(token) {
1292
- this.#skipTrivia();
1293
- return this.#peekCharacter() === token;
1294
- }
1295
- read() {
1296
- this.#skipTrivia();
1297
- this.#previousPosition = this.#currentPosition;
1298
- if (/[\s,:\]}]/.test(this.#peekCharacter())) {
1299
- return new JsonNode(undefined, this.#getOrigin());
1300
- }
1301
- let text = "";
1302
- let closingTokenText = "";
1303
- if (/[[{'"]/.test(this.#peekCharacter())) {
1304
- text += this.#readCharacter();
1305
- switch (text) {
1306
- case "[":
1307
- closingTokenText = "]";
1308
- break;
1309
- case "{":
1310
- closingTokenText = "}";
1311
- break;
1312
- default:
1313
- closingTokenText = text;
1314
- }
1315
- }
1316
- while (!this.isRead()) {
1317
- text += this.#readCharacter();
1318
- if (text.slice(-1) === closingTokenText || (!closingTokenText && /[\s,:\]}]/.test(this.#peekCharacter()))) {
1319
- break;
1320
- }
1321
- }
1322
- return new JsonNode(text, this.#getOrigin());
1323
- }
1324
- #readCharacter() {
1325
- return this.#sourceFile.text.charAt(this.#currentPosition++);
1326
- }
1327
- readToken(token) {
1328
- this.#skipTrivia();
1329
- this.#previousPosition = this.#currentPosition;
1330
- if (this.#peekCharacter() === token) {
1331
- this.#currentPosition++;
1332
- return new JsonNode(token, this.#getOrigin());
1333
- }
1334
- return new JsonNode(undefined, this.#getOrigin());
1335
- }
1336
- #skipTrivia() {
1337
- while (!this.isRead()) {
1338
- if (/\s/.test(this.#peekCharacter())) {
1339
- this.#currentPosition++;
1340
- continue;
1341
- }
1342
- if (this.#peekCharacter() === "/") {
1343
- if (this.#peekNextCharacter() === "/") {
1344
- this.#currentPosition += 2;
1345
- while (!this.isRead()) {
1346
- if (this.#readCharacter() === "\n") {
1347
- break;
1348
- }
1349
- }
1350
- continue;
1351
- }
1352
- if (this.#peekNextCharacter() === "*") {
1353
- this.#currentPosition += 2;
1354
- while (!this.isRead()) {
1355
- if (this.#peekCharacter() === "*" && this.#peekNextCharacter() === "/") {
1356
- this.#currentPosition += 2;
1357
- break;
1358
- }
1359
- this.#currentPosition++;
1360
- }
1361
- continue;
1362
- }
1363
- }
1364
- break;
1365
- }
1366
- this.#previousPosition = this.#currentPosition;
1367
- }
1368
- }
1369
-
1370
- class ConfigFileParser {
1249
+ class ConfigParser {
1371
1250
  #configFileOptions;
1372
1251
  #jsonScanner;
1373
1252
  #onDiagnostics;
1374
1253
  #options;
1375
1254
  #sourceFile;
1376
- constructor(configFileOptions, sourceFile, onDiagnostics) {
1377
- this.#configFileOptions = configFileOptions;
1378
- this.#sourceFile = sourceFile;
1255
+ constructor(configOptions, optionGroup, sourceFile, jsonScanner, onDiagnostics) {
1256
+ this.#configFileOptions = configOptions;
1257
+ this.#jsonScanner = jsonScanner;
1379
1258
  this.#onDiagnostics = onDiagnostics;
1380
- this.#options = Options.for(OptionGroup.ConfigFile);
1381
- this.#jsonScanner = new JsonScanner(this.#sourceFile);
1259
+ this.#sourceFile = sourceFile;
1260
+ this.#options = Options.for(optionGroup);
1382
1261
  }
1383
1262
  #onRequiresValue(optionDefinition, jsonNode, isListItem) {
1384
1263
  const text = isListItem
@@ -1471,9 +1350,8 @@ class ConfigFileParser {
1471
1350
  if (!optionDefinition) {
1472
1351
  const text = ConfigDiagnosticText.unknownOption(optionName);
1473
1352
  this.#onDiagnostics(Diagnostic.error(text, optionNameNode.origin));
1474
- if (this.#jsonScanner.readToken(":")) {
1475
- this.#jsonScanner.read();
1476
- }
1353
+ this.#jsonScanner.readToken(":");
1354
+ this.#jsonScanner.read();
1477
1355
  const commaToken = this.#jsonScanner.readToken(",");
1478
1356
  if (!commaToken.text) {
1479
1357
  break;
@@ -1500,6 +1378,13 @@ class ConfigFileParser {
1500
1378
  related: [Diagnostic.error(relatedText, leftBraceToken.origin)],
1501
1379
  });
1502
1380
  this.#onDiagnostics(diagnostic);
1381
+ return;
1382
+ }
1383
+ const unexpectedToken = this.#jsonScanner.readToken(/\S/);
1384
+ if (unexpectedToken.text != null) {
1385
+ const text = ConfigDiagnosticText.unexpected("token");
1386
+ const diagnostic = Diagnostic.error(text, unexpectedToken.origin);
1387
+ this.#onDiagnostics(diagnostic);
1503
1388
  }
1504
1389
  }
1505
1390
  async parse() {
@@ -1507,6 +1392,139 @@ class ConfigFileParser {
1507
1392
  }
1508
1393
  }
1509
1394
 
1395
+ class JsonNode {
1396
+ origin;
1397
+ text;
1398
+ constructor(text, origin) {
1399
+ this.origin = origin;
1400
+ this.text = text;
1401
+ }
1402
+ getValue(options) {
1403
+ if (this.text == null) {
1404
+ return undefined;
1405
+ }
1406
+ if (/^['"]/.test(this.text)) {
1407
+ return this.text.slice(1, -1);
1408
+ }
1409
+ if (options?.expectsIdentifier) {
1410
+ return this.text;
1411
+ }
1412
+ if (this.text === "true") {
1413
+ return true;
1414
+ }
1415
+ if (this.text === "false") {
1416
+ return false;
1417
+ }
1418
+ if (/^\d/.test(this.text)) {
1419
+ return Number.parseFloat(this.text);
1420
+ }
1421
+ return undefined;
1422
+ }
1423
+ }
1424
+
1425
+ class JsonScanner {
1426
+ #end;
1427
+ #position;
1428
+ #previousPosition;
1429
+ #sourceFile;
1430
+ constructor(sourceFile, options) {
1431
+ this.#end = options?.end ?? sourceFile.text.length;
1432
+ this.#position = options?.start ?? 0;
1433
+ this.#previousPosition = options?.start ?? 0;
1434
+ this.#sourceFile = sourceFile;
1435
+ }
1436
+ #getOrigin() {
1437
+ return new DiagnosticOrigin(this.#previousPosition, this.#position, this.#sourceFile);
1438
+ }
1439
+ isRead() {
1440
+ return !(this.#position < this.#end);
1441
+ }
1442
+ #peekCharacter() {
1443
+ return this.#sourceFile.text.charAt(this.#position);
1444
+ }
1445
+ #peekNextCharacter() {
1446
+ return this.#sourceFile.text.charAt(this.#position + 1);
1447
+ }
1448
+ peekToken(token) {
1449
+ this.#skipTrivia();
1450
+ return this.#peekCharacter() === token;
1451
+ }
1452
+ read() {
1453
+ this.#skipTrivia();
1454
+ this.#previousPosition = this.#position;
1455
+ if (/[\s,:\]}]/.test(this.#peekCharacter())) {
1456
+ return new JsonNode(undefined, this.#getOrigin());
1457
+ }
1458
+ let text = "";
1459
+ let closingTokenText = "";
1460
+ if (/[[{'"]/.test(this.#peekCharacter())) {
1461
+ text += this.#readCharacter();
1462
+ switch (text) {
1463
+ case "[":
1464
+ closingTokenText = "]";
1465
+ break;
1466
+ case "{":
1467
+ closingTokenText = "}";
1468
+ break;
1469
+ default:
1470
+ closingTokenText = text;
1471
+ }
1472
+ }
1473
+ while (!this.isRead()) {
1474
+ text += this.#readCharacter();
1475
+ if (text.slice(-1) === closingTokenText || (!closingTokenText && /[\s,:\]}]/.test(this.#peekCharacter()))) {
1476
+ break;
1477
+ }
1478
+ }
1479
+ return new JsonNode(text, this.#getOrigin());
1480
+ }
1481
+ #readCharacter() {
1482
+ return this.#sourceFile.text.charAt(this.#position++);
1483
+ }
1484
+ readToken(token) {
1485
+ this.#skipTrivia();
1486
+ this.#previousPosition = this.#position;
1487
+ const character = this.#peekCharacter();
1488
+ if (typeof token === "string" ? token === character : token.test(character)) {
1489
+ this.#position++;
1490
+ return new JsonNode(character, this.#getOrigin());
1491
+ }
1492
+ return new JsonNode(undefined, this.#getOrigin());
1493
+ }
1494
+ #skipTrivia() {
1495
+ while (!this.isRead()) {
1496
+ if (/\s/.test(this.#peekCharacter())) {
1497
+ this.#position++;
1498
+ continue;
1499
+ }
1500
+ if (this.#peekCharacter() === "/") {
1501
+ if (this.#peekNextCharacter() === "/") {
1502
+ this.#position += 2;
1503
+ while (!this.isRead()) {
1504
+ if (this.#readCharacter() === "\n") {
1505
+ break;
1506
+ }
1507
+ }
1508
+ continue;
1509
+ }
1510
+ if (this.#peekNextCharacter() === "*") {
1511
+ this.#position += 2;
1512
+ while (!this.isRead()) {
1513
+ if (this.#peekCharacter() === "*" && this.#peekNextCharacter() === "/") {
1514
+ this.#position += 2;
1515
+ break;
1516
+ }
1517
+ this.#position++;
1518
+ }
1519
+ continue;
1520
+ }
1521
+ }
1522
+ break;
1523
+ }
1524
+ this.#previousPosition = this.#position;
1525
+ }
1526
+ }
1527
+
1510
1528
  const defaultOptions = {
1511
1529
  checkSourceFiles: true,
1512
1530
  failFast: false,
@@ -1544,7 +1562,7 @@ class Config {
1544
1562
  encoding: "utf8",
1545
1563
  });
1546
1564
  const sourceFile = new SourceFile(configFilePath, configFileText);
1547
- const configFileParser = new ConfigFileParser(configFileOptions, sourceFile, Config.#onDiagnostics);
1565
+ const configFileParser = new ConfigParser(configFileOptions, OptionGroup.ConfigFile, sourceFile, new JsonScanner(sourceFile), Config.#onDiagnostics);
1548
1566
  await configFileParser.parse();
1549
1567
  if (configFileOptions.target != null) {
1550
1568
  configFileOptions.target = await Target.expand(configFileOptions.target);
@@ -1570,6 +1588,114 @@ class Config {
1570
1588
  }
1571
1589
  }
1572
1590
 
1591
+ class DirectiveDiagnosticText {
1592
+ static doesNotTakeArgument(directiveName) {
1593
+ return `Directive '${directiveName}' does not take an argument.`;
1594
+ }
1595
+ static isNotSupported(directive) {
1596
+ return `The '${directive}' directive is not supported.`;
1597
+ }
1598
+ static requiresArgument(directiveName) {
1599
+ return `Directive '${directiveName}' requires an argument.`;
1600
+ }
1601
+ }
1602
+
1603
+ class Directive {
1604
+ static #commentSeparatorRegex = /--+/;
1605
+ static #directiveRegex = /^(\/\/\s*@tstyche)(\s*|-)?(\S*)?(\s*)?(.*)?/i;
1606
+ static getDirectiveRanges(compiler, sourceFile, position = 0) {
1607
+ const comments = compiler.getLeadingCommentRanges(sourceFile.text, position);
1608
+ if (!comments || comments.length === 0) {
1609
+ return;
1610
+ }
1611
+ const ranges = Object.assign([], { sourceFile });
1612
+ for (const comment of comments) {
1613
+ if (comment.kind !== compiler.SyntaxKind.SingleLineCommentTrivia) {
1614
+ continue;
1615
+ }
1616
+ const range = Directive.#getRange(sourceFile, comment);
1617
+ if (range != null) {
1618
+ ranges.push(range);
1619
+ }
1620
+ }
1621
+ return ranges;
1622
+ }
1623
+ static async getInlineConfig(ranges) {
1624
+ if (!ranges) {
1625
+ return;
1626
+ }
1627
+ const inlineConfig = {};
1628
+ for (const range of ranges) {
1629
+ await Directive.#parse(inlineConfig, ranges.sourceFile, range);
1630
+ }
1631
+ return inlineConfig;
1632
+ }
1633
+ static #getRange(sourceFile, comment) {
1634
+ const [text] = sourceFile.text.substring(comment.pos, comment.end).split(Directive.#commentSeparatorRegex);
1635
+ const found = text?.match(Directive.#directiveRegex);
1636
+ const namespaceText = found?.[1];
1637
+ if (!namespaceText) {
1638
+ return;
1639
+ }
1640
+ const ranges = {
1641
+ namespace: { start: comment.pos, end: comment.pos + namespaceText.length, text: namespaceText },
1642
+ };
1643
+ const directiveSeparatorText = found?.[2];
1644
+ const directiveText = found?.[3];
1645
+ if (directiveText != null && directiveText.length > 0) {
1646
+ const start = ranges.namespace.end + (directiveSeparatorText?.length ?? 0);
1647
+ ranges.directive = { start, end: start + directiveText.length, text: directiveText };
1648
+ }
1649
+ const argumentSeparatorText = found?.[4];
1650
+ const argumentText = found?.[5]?.trimEnd();
1651
+ if (ranges.directive != null && argumentText != null && argumentText.length > 0) {
1652
+ const start = ranges.directive.end + (argumentSeparatorText?.length ?? 0);
1653
+ ranges.argument = { start, end: start + argumentText.length, text: argumentText };
1654
+ }
1655
+ return ranges;
1656
+ }
1657
+ static #onDiagnostics(diagnostic) {
1658
+ EventEmitter.dispatch(["directive:error", { diagnostics: [diagnostic] }]);
1659
+ }
1660
+ static async #parse(inlineConfig, sourceFile, ranges) {
1661
+ switch (ranges.directive?.text) {
1662
+ case "if":
1663
+ {
1664
+ if (!ranges.argument?.text) {
1665
+ const text = DirectiveDiagnosticText.requiresArgument(ranges.directive.text);
1666
+ const origin = new DiagnosticOrigin(ranges.directive.start, ranges.directive.end, sourceFile);
1667
+ Directive.#onDiagnostics(Diagnostic.error(text, origin));
1668
+ return;
1669
+ }
1670
+ const value = await Directive.#parseJson(sourceFile, ranges.argument.start, ranges.argument.end);
1671
+ inlineConfig.if = value;
1672
+ }
1673
+ return;
1674
+ case "template":
1675
+ if (ranges.argument?.text != null) {
1676
+ const text = DirectiveDiagnosticText.doesNotTakeArgument(ranges.directive.text);
1677
+ const origin = new DiagnosticOrigin(ranges.directive.start, ranges.directive.end, sourceFile);
1678
+ Directive.#onDiagnostics(Diagnostic.error(text, origin));
1679
+ }
1680
+ inlineConfig.template = true;
1681
+ return;
1682
+ }
1683
+ const target = ranges?.directive ?? ranges.namespace;
1684
+ const text = DirectiveDiagnosticText.isNotSupported(target.text);
1685
+ const origin = new DiagnosticOrigin(target.start, target.end, sourceFile);
1686
+ Directive.#onDiagnostics(Diagnostic.error(text, origin));
1687
+ }
1688
+ static async #parseJson(sourceFile, start, end) {
1689
+ const inlineOptions = {};
1690
+ const configParser = new ConfigParser(inlineOptions, OptionGroup.InlineConditions, sourceFile, new JsonScanner(sourceFile, { start, end }), Directive.#onDiagnostics);
1691
+ await configParser.parse();
1692
+ if ("target" in inlineOptions) {
1693
+ inlineOptions["target"] = await Target.expand(inlineOptions["target"]);
1694
+ }
1695
+ return inlineOptions;
1696
+ }
1697
+ }
1698
+
1573
1699
  class CancellationHandler {
1574
1700
  #cancellationToken;
1575
1701
  #cancellationReason;
@@ -1777,6 +1903,7 @@ class ResultHandler {
1777
1903
  this.#taskResult.timing.start = Date.now();
1778
1904
  break;
1779
1905
  case "task:error":
1906
+ case "directive:error":
1780
1907
  case "collect:error":
1781
1908
  this.#targetResult.status = ResultStatus.Failed;
1782
1909
  this.#taskResult.status = ResultStatus.Failed;
@@ -2380,6 +2507,7 @@ class ListReporter extends BaseReporter {
2380
2507
  this.#hasReportedError = false;
2381
2508
  break;
2382
2509
  case "task:error":
2510
+ case "directive:error":
2383
2511
  case "collect:error":
2384
2512
  for (const diagnostic of payload.diagnostics) {
2385
2513
  this.#fileView.addMessage(diagnosticText(diagnostic));
@@ -2895,6 +3023,9 @@ class TestTreeNode {
2895
3023
  }
2896
3024
  }
2897
3025
  }
3026
+ getDirectiveRanges(compiler) {
3027
+ return Directive.getDirectiveRanges(compiler, this.node.getSourceFile(), this.node.getFullStart());
3028
+ }
2898
3029
  }
2899
3030
 
2900
3031
  class AssertionNode extends TestTreeNode {
@@ -2917,7 +3048,8 @@ class AssertionNode extends TestTreeNode {
2917
3048
  this.target = this.matcherNode.typeArguments ?? this.matcherNode.arguments;
2918
3049
  }
2919
3050
  for (const diagnostic of parent.diagnostics) {
2920
- if (diagnosticBelongsToNode(diagnostic, this.source)) {
3051
+ if (diagnosticBelongsToNode(diagnostic, this.source) ||
3052
+ (this.target != null && diagnosticBelongsToNode(diagnostic, this.target))) {
2921
3053
  this.diagnostics.add(diagnostic);
2922
3054
  parent.diagnostics.delete(diagnostic);
2923
3055
  }
@@ -3185,6 +3317,9 @@ class TestTree {
3185
3317
  this.diagnostics = diagnostics;
3186
3318
  this.sourceFile = sourceFile;
3187
3319
  }
3320
+ getDirectiveRanges(compiler) {
3321
+ return Directive.getDirectiveRanges(compiler, this.sourceFile);
3322
+ }
3188
3323
  }
3189
3324
 
3190
3325
  class WhenNode extends TestTreeNode {
@@ -4529,6 +4664,7 @@ class WhenService {
4529
4664
 
4530
4665
  class TestTreeWalker {
4531
4666
  #cancellationToken;
4667
+ #compiler;
4532
4668
  #expectService;
4533
4669
  #hasOnly;
4534
4670
  #onTaskDiagnostics;
@@ -4536,6 +4672,7 @@ class TestTreeWalker {
4536
4672
  #resolvedConfig;
4537
4673
  #whenService;
4538
4674
  constructor(compiler, typeChecker, resolvedConfig, onTaskDiagnostics, options) {
4675
+ this.#compiler = compiler;
4539
4676
  this.#resolvedConfig = resolvedConfig;
4540
4677
  this.#onTaskDiagnostics = onTaskDiagnostics;
4541
4678
  this.#cancellationToken = options.cancellationToken;
@@ -4545,43 +4682,46 @@ class TestTreeWalker {
4545
4682
  this.#expectService = new ExpectService(compiler, typeChecker, reject);
4546
4683
  this.#whenService = new WhenService(reject, onTaskDiagnostics);
4547
4684
  }
4548
- #resolveRunMode(mode, testNode) {
4549
- if (testNode.flags & TestTreeNodeFlags.Fail) {
4685
+ async #resolveRunMode(mode, node) {
4686
+ const directiveRanges = node.getDirectiveRanges(this.#compiler);
4687
+ const inlineConfig = await Directive.getInlineConfig(directiveRanges);
4688
+ if (inlineConfig?.if?.target != null && !Version.isIncluded(this.#compiler.version, inlineConfig.if.target)) {
4689
+ mode |= RunMode.Skip;
4690
+ }
4691
+ if (node.flags & TestTreeNodeFlags.Fail) {
4550
4692
  mode |= RunMode.Fail;
4551
4693
  }
4552
- if (testNode.flags & TestTreeNodeFlags.Only ||
4553
- (this.#resolvedConfig.only != null &&
4554
- testNode.name.toLowerCase().includes(this.#resolvedConfig.only.toLowerCase()))) {
4694
+ if (node.flags & TestTreeNodeFlags.Only ||
4695
+ (this.#resolvedConfig.only != null && node.name.toLowerCase().includes(this.#resolvedConfig.only.toLowerCase()))) {
4555
4696
  mode |= RunMode.Only;
4556
4697
  }
4557
- if (testNode.flags & TestTreeNodeFlags.Skip ||
4558
- (this.#resolvedConfig.skip != null &&
4559
- testNode.name.toLowerCase().includes(this.#resolvedConfig.skip.toLowerCase()))) {
4698
+ if (node.flags & TestTreeNodeFlags.Skip ||
4699
+ (this.#resolvedConfig.skip != null && node.name.toLowerCase().includes(this.#resolvedConfig.skip.toLowerCase()))) {
4560
4700
  mode |= RunMode.Skip;
4561
4701
  }
4562
- if (testNode.flags & TestTreeNodeFlags.Todo) {
4702
+ if (node.flags & TestTreeNodeFlags.Todo) {
4563
4703
  mode |= RunMode.Todo;
4564
4704
  }
4565
- if (this.#position != null && testNode.node.getStart() === this.#position) {
4705
+ if (this.#position != null && node.node.getStart() === this.#position) {
4566
4706
  mode |= RunMode.Only;
4567
4707
  mode &= ~RunMode.Skip;
4568
4708
  }
4569
4709
  return mode;
4570
4710
  }
4571
- visit(nodes, runMode, parentResult) {
4711
+ async visit(nodes, runMode, parentResult) {
4572
4712
  for (const node of nodes) {
4573
4713
  if (this.#cancellationToken?.isCancellationRequested) {
4574
4714
  break;
4575
4715
  }
4576
4716
  switch (node.brand) {
4577
4717
  case TestTreeNodeBrand.Describe:
4578
- this.#visitDescribe(node, runMode, parentResult);
4718
+ await this.#visitDescribe(node, runMode, parentResult);
4579
4719
  break;
4580
4720
  case TestTreeNodeBrand.Test:
4581
- this.#visitTest(node, runMode, parentResult);
4721
+ await this.#visitTest(node, runMode, parentResult);
4582
4722
  break;
4583
4723
  case TestTreeNodeBrand.Expect:
4584
- this.#visitAssertion(node, runMode, parentResult);
4724
+ await this.#visitAssertion(node, runMode, parentResult);
4585
4725
  break;
4586
4726
  case TestTreeNodeBrand.When:
4587
4727
  this.#visitWhen(node);
@@ -4589,11 +4729,11 @@ class TestTreeWalker {
4589
4729
  }
4590
4730
  }
4591
4731
  }
4592
- #visitAssertion(assertion, runMode, parentResult) {
4593
- this.visit(assertion.children, runMode, parentResult);
4732
+ async #visitAssertion(assertion, runMode, parentResult) {
4733
+ await this.visit(assertion.children, runMode, parentResult);
4594
4734
  const expectResult = new ExpectResult(assertion, parentResult);
4595
4735
  EventEmitter.dispatch(["expect:start", { result: expectResult }]);
4596
- runMode = this.#resolveRunMode(runMode, assertion);
4736
+ runMode = await this.#resolveRunMode(runMode, assertion);
4597
4737
  if (runMode & RunMode.Skip || (this.#hasOnly && !(runMode & RunMode.Only))) {
4598
4738
  EventEmitter.dispatch(["expect:skip", { result: expectResult }]);
4599
4739
  return;
@@ -4629,23 +4769,23 @@ class TestTreeWalker {
4629
4769
  EventEmitter.dispatch(["expect:fail", { diagnostics: matchResult.explain(), result: expectResult }]);
4630
4770
  }
4631
4771
  }
4632
- #visitDescribe(describe, runMode, parentResult) {
4772
+ async #visitDescribe(describe, runMode, parentResult) {
4633
4773
  const describeResult = new DescribeResult(describe, parentResult);
4634
4774
  EventEmitter.dispatch(["describe:start", { result: describeResult }]);
4635
- runMode = this.#resolveRunMode(runMode, describe);
4775
+ runMode = await this.#resolveRunMode(runMode, describe);
4636
4776
  if (!(runMode & RunMode.Skip || (this.#hasOnly && !(runMode & RunMode.Only)) || runMode & RunMode.Todo) &&
4637
4777
  describe.diagnostics.size > 0) {
4638
4778
  this.#onTaskDiagnostics(Diagnostic.fromDiagnostics([...describe.diagnostics]));
4639
4779
  }
4640
4780
  else {
4641
- this.visit(describe.children, runMode, describeResult);
4781
+ await this.visit(describe.children, runMode, describeResult);
4642
4782
  }
4643
4783
  EventEmitter.dispatch(["describe:end", { result: describeResult }]);
4644
4784
  }
4645
- #visitTest(test, runMode, parentResult) {
4785
+ async #visitTest(test, runMode, parentResult) {
4646
4786
  const testResult = new TestResult(test, parentResult);
4647
4787
  EventEmitter.dispatch(["test:start", { result: testResult }]);
4648
- runMode = this.#resolveRunMode(runMode, test);
4788
+ runMode = await this.#resolveRunMode(runMode, test);
4649
4789
  if (runMode & RunMode.Todo) {
4650
4790
  EventEmitter.dispatch(["test:todo", { result: testResult }]);
4651
4791
  return;
@@ -4660,7 +4800,7 @@ class TestTreeWalker {
4660
4800
  ]);
4661
4801
  return;
4662
4802
  }
4663
- this.visit(test.children, runMode, testResult);
4803
+ await this.visit(test.children, runMode, testResult);
4664
4804
  if (runMode & RunMode.Skip || (this.#hasOnly && !(runMode & RunMode.Only))) {
4665
4805
  EventEmitter.dispatch(["test:skip", { result: testResult }]);
4666
4806
  return;
@@ -4702,21 +4842,27 @@ class TaskRunner {
4702
4842
  EventEmitter.dispatch(["task:end", { result: taskResult }]);
4703
4843
  this.#projectService.closeFile(task.filePath);
4704
4844
  }
4705
- async #run(task, taskResult, cancellationToken) {
4706
- if (!existsSync(task.filePath)) {
4707
- this.#onDiagnostics([Diagnostic.error(`Test file '${task.filePath}' does not exist.`)], taskResult);
4708
- return;
4709
- }
4710
- let languageService = this.#projectService.getLanguageService(task.filePath);
4845
+ async #resolveTaskFacts(task, taskResult, runMode = RunMode.Normal) {
4846
+ const languageService = this.#projectService.getLanguageService(task.filePath);
4711
4847
  const syntacticDiagnostics = languageService?.getSyntacticDiagnostics(task.filePath);
4712
4848
  if (syntacticDiagnostics != null && syntacticDiagnostics.length > 0) {
4713
4849
  this.#onDiagnostics(Diagnostic.fromDiagnostics(syntacticDiagnostics), taskResult);
4714
4850
  return;
4715
4851
  }
4716
- let semanticDiagnostics = languageService?.getSemanticDiagnostics(task.filePath);
4717
- let program = languageService?.getProgram();
4718
- let sourceFile = program?.getSourceFile(task.filePath);
4719
- if (sourceFile?.text.startsWith("// @tstyche-template")) {
4852
+ const semanticDiagnostics = languageService?.getSemanticDiagnostics(task.filePath);
4853
+ const program = languageService?.getProgram();
4854
+ const typeChecker = program?.getTypeChecker();
4855
+ const sourceFile = program?.getSourceFile(task.filePath);
4856
+ if (!sourceFile) {
4857
+ return;
4858
+ }
4859
+ const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
4860
+ const directiveRanges = testTree.getDirectiveRanges(this.#compiler);
4861
+ const inlineConfig = await Directive.getInlineConfig(directiveRanges);
4862
+ if (inlineConfig?.if?.target != null && !Version.isIncluded(this.#compiler.version, inlineConfig.if.target)) {
4863
+ runMode |= RunMode.Skip;
4864
+ }
4865
+ if (inlineConfig?.template) {
4720
4866
  if (semanticDiagnostics != null && semanticDiagnostics.length > 0) {
4721
4867
  this.#onDiagnostics(Diagnostic.fromDiagnostics(semanticDiagnostics), taskResult);
4722
4868
  return;
@@ -4728,41 +4874,39 @@ class TaskRunner {
4728
4874
  return;
4729
4875
  }
4730
4876
  this.#projectService.openFile(task.filePath, testText, this.#resolvedConfig.rootPath);
4731
- languageService = this.#projectService.getLanguageService(task.filePath);
4732
- const syntacticDiagnostics = languageService?.getSyntacticDiagnostics(task.filePath);
4733
- if (syntacticDiagnostics != null && syntacticDiagnostics.length > 0) {
4734
- this.#onDiagnostics(Diagnostic.fromDiagnostics(syntacticDiagnostics), taskResult);
4735
- return;
4736
- }
4737
- semanticDiagnostics = languageService?.getSemanticDiagnostics(task.filePath);
4738
- program = languageService?.getProgram();
4739
- sourceFile = program?.getSourceFile(task.filePath);
4877
+ return this.#resolveTaskFacts(task, taskResult, runMode);
4740
4878
  }
4741
- if (!sourceFile) {
4879
+ return { runMode, testTree, typeChecker };
4880
+ }
4881
+ async #run(task, taskResult, cancellationToken) {
4882
+ if (!existsSync(task.filePath)) {
4883
+ this.#onDiagnostics([Diagnostic.error(`Test file '${task.filePath}' does not exist.`)], taskResult);
4742
4884
  return;
4743
4885
  }
4744
- const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
4745
- if (testTree.diagnostics.size > 0) {
4746
- this.#onDiagnostics(Diagnostic.fromDiagnostics([...testTree.diagnostics]), taskResult);
4886
+ const facts = await this.#resolveTaskFacts(task, taskResult);
4887
+ if (!facts) {
4888
+ return;
4889
+ }
4890
+ if (facts.testTree.diagnostics.size > 0) {
4891
+ this.#onDiagnostics(Diagnostic.fromDiagnostics([...facts.testTree.diagnostics]), taskResult);
4747
4892
  return;
4748
4893
  }
4749
- const typeChecker = program?.getTypeChecker();
4750
4894
  const onTaskDiagnostics = (diagnostics) => {
4751
4895
  this.#onDiagnostics(diagnostics, taskResult);
4752
4896
  };
4753
- const testTreeWalker = new TestTreeWalker(this.#compiler, typeChecker, this.#resolvedConfig, onTaskDiagnostics, {
4897
+ const testTreeWalker = new TestTreeWalker(this.#compiler, facts.typeChecker, this.#resolvedConfig, onTaskDiagnostics, {
4754
4898
  cancellationToken,
4755
- hasOnly: testTree.hasOnly,
4899
+ hasOnly: facts.testTree.hasOnly,
4756
4900
  position: task.position,
4757
4901
  });
4758
- testTreeWalker.visit(testTree.children, RunMode.Normal, undefined);
4902
+ await testTreeWalker.visit(facts.testTree.children, facts.runMode, undefined);
4759
4903
  }
4760
4904
  }
4761
4905
 
4762
4906
  class Runner {
4763
4907
  #eventEmitter = new EventEmitter();
4764
4908
  #resolvedConfig;
4765
- static version = "4.0.0-rc.0";
4909
+ static version = "4.0.0";
4766
4910
  constructor(resolvedConfig) {
4767
4911
  this.#resolvedConfig = resolvedConfig;
4768
4912
  }
@@ -4962,4 +5106,4 @@ class Cli {
4962
5106
  }
4963
5107
  }
4964
5108
 
4965
- export { AssertionNode, BaseReporter, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, Config, ConfigDiagnosticText, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, EventEmitter, ExitCodeHandler, ExpectResult, ExpectService, FileWatcher, InputService, Line, ListReporter, OptionBrand, OptionGroup, Options, OutputService, Path, PluginService, ProjectResult, ProjectService, Reject, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, Runner, Scribbler, Select, SelectDiagnosticText, SetupReporter, SourceFile, Store, SummaryReporter, TargetResult, Task, TaskResult, TestResult, TestTree, TestTreeNode, TestTreeNodeBrand, TestTreeNodeFlags, Text, Version, WatchReporter, WatchService, Watcher, WhenNode, WhenService, addsPackageText, argumentIsProvided, argumentOrTypeArgumentIsProvided, defaultOptions, describeNameText, diagnosticBelongsToNode, diagnosticText, environmentOptions, fileViewText, formattedText, getDiagnosticMessageText, getTextSpanEnd, helpText, isDiagnosticWithLocation, nodeBelongsToArgumentList, summaryText, taskStatusText, testNameText, usesCompilerText, waitingForFileChangesText, watchUsageText };
5109
+ export { AssertionNode, BaseReporter, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, Config, ConfigDiagnosticText, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, Directive, EventEmitter, ExitCodeHandler, ExpectResult, ExpectService, FileWatcher, InputService, Line, ListReporter, OptionBrand, OptionGroup, Options, OutputService, Path, PluginService, ProjectResult, ProjectService, Reject, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, Runner, Scribbler, Select, SelectDiagnosticText, SetupReporter, SourceFile, Store, SummaryReporter, TargetResult, Task, TaskResult, TestResult, TestTree, TestTreeNode, TestTreeNodeBrand, TestTreeNodeFlags, Text, Version, WatchReporter, WatchService, Watcher, WhenNode, WhenService, addsPackageText, argumentIsProvided, argumentOrTypeArgumentIsProvided, defaultOptions, describeNameText, diagnosticBelongsToNode, diagnosticText, environmentOptions, fileViewText, formattedText, getDiagnosticMessageText, getTextSpanEnd, helpText, isDiagnosticWithLocation, nodeBelongsToArgumentList, summaryText, taskStatusText, testNameText, usesCompilerText, waitingForFileChangesText, watchUsageText };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tstyche",
3
- "version": "4.0.0-rc.0",
4
- "description": "The Essential Type Testing Tool.",
3
+ "version": "4.0.0",
4
+ "description": "Everything You Need for Type Testing.",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "types",