test-inventory 0.1.1 → 0.2.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/dist/values.js CHANGED
@@ -56,6 +56,35 @@ export class Values {
56
56
  bindingsByScope = new Map();
57
57
  cache = new Map();
58
58
  pending = new Set();
59
+ /** The functions being read at one of their calls, innermost last. */
60
+ calls = [];
61
+ /**
62
+ * Runs `read` with a function's parameters bound to the arguments of one call, worked out where the call is, so that
63
+ * titles and values built from them resolve, as in `test(\`${side} rejects a zero quantity\`)` inside
64
+ * `validatesQuantity('buy')`. `args` stop at a spread argument, after which no position is known. A missing argument
65
+ * takes the parameter's default value, worked out in `scopes`, the scopes around the function.
66
+ */
67
+ withArguments(fn, args, scopes, read) {
68
+ const parameters = new Map();
69
+ for (const [index, parameter] of fn.params.entries()) {
70
+ const target = parameter.type === 'AssignmentPattern' ? parameter.left : parameter;
71
+ if (target.type !== 'Identifier')
72
+ continue;
73
+ const argument = args.at(index);
74
+ if (argument)
75
+ parameters.set(target.name, () => argument);
76
+ else if (parameter.type === 'AssignmentPattern') {
77
+ parameters.set(target.name, () => this.resolve(parameter.right, [...scopes, fn]));
78
+ }
79
+ }
80
+ this.calls.push({ fn, parameters, cache: new Map() });
81
+ try {
82
+ return read();
83
+ }
84
+ finally {
85
+ this.calls.pop();
86
+ }
87
+ }
59
88
  /** `scopes` lists the scope nodes around the expression, outermost first. */
60
89
  resolve(node, scopes) {
61
90
  const expression = unwrap(node);
@@ -179,6 +208,11 @@ export class Values {
179
208
  declarationOf(name, scopes) {
180
209
  return this.lookup(name, scopes)?.binding.declaration ?? null;
181
210
  }
211
+ /** The module a name is imported from where it is used; null when it isn't an import. */
212
+ importSource(name, scopes) {
213
+ const binding = this.lookup(name, scopes)?.binding;
214
+ return binding?.kind === 'import' ? binding.source : null;
215
+ }
182
216
  /** Whether a name refers to a function or class declared in the file. */
183
217
  isFunction(name, scopes) {
184
218
  return this.lookup(name, scopes)?.binding.kind === 'function';
@@ -203,12 +237,19 @@ export class Values {
203
237
  case 'import':
204
238
  return { ok: false, importedFrom: binding.source };
205
239
  case 'function':
206
- case 'other':
207
240
  return UNRESOLVED;
241
+ case 'other':
242
+ return this.resolveParameter(name, binding.declaration);
208
243
  }
209
244
  }
245
+ /** A parameter of a function read at one of its calls: the argument given there. */
246
+ resolveParameter(name, fn) {
247
+ const call = this.calls.findLast((item) => item.fn === fn);
248
+ return call?.parameters.get(name)?.() ?? UNRESOLVED;
249
+ }
210
250
  resolveConstant(init, scopes) {
211
- const cached = this.cache.get(init);
251
+ const cache = this.calls.at(-1)?.cache ?? this.cache;
252
+ const cached = cache.get(init);
212
253
  if (cached)
213
254
  return cached;
214
255
  if (this.pending.has(init))
@@ -216,7 +257,7 @@ export class Values {
216
257
  this.pending.add(init);
217
258
  const resolved = this.resolve(init, scopes);
218
259
  this.pending.delete(init);
219
- this.cache.set(init, resolved);
260
+ cache.set(init, resolved);
220
261
  return resolved;
221
262
  }
222
263
  enumMembers(node, scopes) {
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "test-inventory",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Test metadata from source code, not from test results.",
5
5
  "keywords": [
6
6
  "testing",
7
+ "playwright",
8
+ "vitest",
7
9
  "test-suite",
8
10
  "test-inventory",
9
11
  "static-analysis",
@@ -77,26 +79,27 @@
77
79
  "changeset": "changeset"
78
80
  },
79
81
  "dependencies": {
80
- "oxc-parser": "0.153.0",
82
+ "oxc-parser": "0.154.0",
81
83
  "tinyglobby": "^0.2.17"
82
84
  },
83
85
  "devDependencies": {
84
86
  "@arethetypeswrong/cli": "^0.18.5",
85
87
  "@changesets/changelog-github": "^1.0.1",
86
88
  "@changesets/cli": "^3.0.3",
87
- "@types/node": "^22.12.0",
88
- "@vitest/coverage-v8": "5.0.3",
89
+ "@types/node": "~22.12.0",
90
+ "@vitest/coverage-v8": "5.0.4",
89
91
  "ajv": "^8.20.0",
90
- "eslint": "^10.12.0",
91
- "prettier": "^3.9.9",
92
+ "eslint": "~10.12.0",
93
+ "prettier": "3.9.10",
92
94
  "publint": "^0.3.25",
93
95
  "ts-json-schema-generator": "2.9.0",
94
- "typedoc": "0.28.20",
96
+ "typedoc": "^0.28.20",
95
97
  "typedoc-plugin-markdown": "^4.13.1",
96
98
  "typedoc-vitepress-theme": "^1.1.4",
97
99
  "typescript": "~6.0.3",
98
- "typescript-eslint": "^8.71.1",
100
+ "typescript-eslint": "~8.71.2",
99
101
  "vitepress": "2.0.0-alpha.20",
100
- "vitest": "5.0.3"
102
+ "vitepress-plugin-llms": "^1.14.0",
103
+ "vitest": "5.0.4"
101
104
  }
102
105
  }
package/schema/v1.json CHANGED
@@ -268,6 +268,17 @@
268
268
  "minimum": 1,
269
269
  "description": "In UTF-16 code units."
270
270
  },
271
+ "declaredAt": {
272
+ "anyOf": [
273
+ {
274
+ "$ref": "#/definitions/DeclaredAt"
275
+ },
276
+ {
277
+ "type": "null"
278
+ }
279
+ ],
280
+ "description": "Where a helper imported from another file writes the describe; the record's location is then the call that registers it. Null for code written in the scanned file."
281
+ },
271
282
  "title": {
272
283
  "type": "string",
273
284
  "description": "As the runner shows it, tags included."
@@ -385,6 +396,7 @@
385
396
  "lineEnd",
386
397
  "columnStart",
387
398
  "columnEnd",
399
+ "declaredAt",
388
400
  "title",
389
401
  "titleWithoutTags",
390
402
  "hasDynamicTitle",
@@ -409,6 +421,29 @@
409
421
  ],
410
422
  "description": "A describe block, with what is written on the describe itself. Inherited values are on the tests."
411
423
  },
424
+ "DeclaredAt": {
425
+ "type": "object",
426
+ "properties": {
427
+ "relativeFilePath": {
428
+ "type": "string",
429
+ "description": "The helper's file, from the project root."
430
+ },
431
+ "lineStart": {
432
+ "type": "integer",
433
+ "minimum": 1
434
+ },
435
+ "columnStart": {
436
+ "type": "integer",
437
+ "minimum": 1,
438
+ "description": "In UTF-16 code units."
439
+ }
440
+ },
441
+ "required": [
442
+ "relativeFilePath",
443
+ "lineStart",
444
+ "columnStart"
445
+ ]
446
+ },
412
447
  "Annotation": {
413
448
  "type": "object",
414
449
  "properties": {
@@ -524,6 +559,17 @@
524
559
  "minimum": 1,
525
560
  "description": "In UTF-16 code units."
526
561
  },
562
+ "declaredAt": {
563
+ "anyOf": [
564
+ {
565
+ "$ref": "#/definitions/DeclaredAt"
566
+ },
567
+ {
568
+ "type": "null"
569
+ }
570
+ ],
571
+ "description": "Where a helper imported from another file writes the test; the record's location is then the call that registers it. Null for code written in the scanned file."
572
+ },
527
573
  "title": {
528
574
  "type": "string",
529
575
  "description": "As the runner shows it, tags included."
@@ -646,7 +692,7 @@
646
692
  "items": {
647
693
  "type": "string"
648
694
  },
649
- "description": "From the test and its describes. Playwright only."
695
+ "description": "From the test, its describes and the `test.describe.configure()` calls around it. Playwright only."
650
696
  },
651
697
  "meta": {
652
698
  "anyOf": [
@@ -702,6 +748,7 @@
702
748
  "lineEnd",
703
749
  "columnStart",
704
750
  "columnEnd",
751
+ "declaredAt",
705
752
  "title",
706
753
  "titleWithoutTags",
707
754
  "hasDynamicTitle",