ucn 5.6.0 → 5.6.1

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.
@@ -2,10 +2,10 @@
2
2
  * core/test-units.js — what a test is, where it is, and how to run it.
3
3
  *
4
4
  * One identity for a test, shared by `tests`, the affected-test selection
5
- * and `check`: a UNIT is (file, suite chain, name, line), read from the
5
+ * and `check`: a UNIT is (file, suite chain, name, source extent), read from the
6
6
  * declaration the language's runner collects (the language module's
7
- * `testUnits`). A line of test code belongs to the innermost unit whose
8
- * source range holds it; code outside every unit belongs to the file.
7
+ * `testUnits`). A site of test code belongs to the innermost unit whose
8
+ * source range holds it; an ambiguous line alone does not select a sibling.
9
9
  *
10
10
  * A RUNNER is named only on evidence: the test file imports the runner's
11
11
  * own module, or the nearest build manifest names it. A SELECTOR (the
@@ -1556,6 +1556,44 @@ function topLevelTestClasses(model, filePath) {
1556
1556
  * @param {object} index - ProjectIndex
1557
1557
  * @returns {object} per-operation test model
1558
1558
  */
1559
+ // Positions are query facts, never persisted test-reader state. Caller
1560
+ // provenance identifies the physical token even when its displayed line
1561
+ // holds several calls or tests.
1562
+ function testSite(site) {
1563
+ if (typeof site === 'number') return { line: site };
1564
+ const point = site.provenance?.facts?.site || {};
1565
+ return {
1566
+ line: site.line,
1567
+ ...(Number.isInteger(site.column ?? point.column) && { column: site.column ?? point.column }),
1568
+ ...(Number.isInteger(site.callStart ?? point.start) && { callStart: site.callStart ?? point.start }),
1569
+ ...(Number.isInteger(site.callEnd ?? point.end) && { callEnd: site.callEnd ?? point.end }),
1570
+ };
1571
+ }
1572
+
1573
+ function testSiteKey(site) {
1574
+ const point = testSite(site);
1575
+ return `${point.line}:${point.column ?? ''}:${point.callStart ?? ''}`;
1576
+ }
1577
+
1578
+ function containsTestSite(row, site) {
1579
+ const point = testSite(site);
1580
+ if (row.startLine > point.line || row.endLine < point.line) return false;
1581
+ return require('./callable-scope').containsCall(row, point) !== false;
1582
+ }
1583
+
1584
+ function containsTestUnit(outer, inner) {
1585
+ if (outer.sourceSpan && inner.sourceSpan) {
1586
+ return outer.sourceSpan.start <= inner.sourceSpan.start && outer.sourceSpan.end >= inner.sourceSpan.end;
1587
+ }
1588
+ return outer.startLine <= inner.startLine && outer.endLine >= inner.endLine;
1589
+ }
1590
+
1591
+ function innermostTestRows(rows, site) {
1592
+ const candidates = rows.filter(row => containsTestSite(row, site));
1593
+ return candidates.filter(row => !candidates.some(other => other !== row &&
1594
+ containsTestUnit(row, other) && !containsTestUnit(other, row)));
1595
+ }
1596
+
1559
1597
  function createTestModel(index, options = {}) {
1560
1598
  const model = {
1561
1599
  manifests: createManifests(index),
@@ -1663,6 +1701,11 @@ function createTestModel(index, options = {}) {
1663
1701
  const out = [];
1664
1702
  for (const other of symbols) {
1665
1703
  if (other === symbol) continue;
1704
+ if (other.sourceSpan && symbol.sourceSpan) {
1705
+ if (!containsTestUnit(other, symbol) || containsTestUnit(symbol, other)) continue;
1706
+ if (CONTAINER_KINDS.has(other.type) || CALLABLE_KINDS.has(other.type)) out.push(other);
1707
+ continue;
1708
+ }
1666
1709
  const end = other.endLine ?? other.startLine;
1667
1710
  const ownEnd = symbol.endLine ?? symbol.startLine;
1668
1711
  if (other.startLine > symbol.startLine || end < ownEnd) continue;
@@ -2697,6 +2740,7 @@ function createTestModel(index, options = {}) {
2697
2740
  (byFixture && states.some(each => each.regime === 'unittest'));
2698
2741
  const unit = {
2699
2742
  ...base,
2743
+ ...(!base.inherited && rowFile === filePath && row.sourceSpan && { sourceSpan: row.sourceSpan }),
2700
2744
  ...(skipped && { skipped: true }),
2701
2745
  ...(sometimes && { conditional: true }),
2702
2746
  ...(dynamic && { dynamic }),
@@ -2777,14 +2821,14 @@ function createTestModel(index, options = {}) {
2777
2821
  inherited: {
2778
2822
  file: relativeOf(base.filePath), filePath: base.filePath, owner: base.cls.name,
2779
2823
  ownerLine: base.cls.startLine, line: test.alias ? test.of : test.line,
2780
- startLine: test.startLine, endLine: test.endLine,
2824
+ startLine: test.startLine, endLine: test.endLine, sourceSpan: test.sourceSpan,
2781
2825
  },
2782
2826
  }, test, around, reopened(test) || (standing.regime === 'unittest' ? null : byForce(test)), base.filePath);
2783
2827
  }
2784
2828
  if (units.length > before) {
2785
2829
  units.push({
2786
2830
  kind: 'suite', name: cls.name, suite: cls.suite,
2787
- line: cls.line, startLine: cls.startLine, endLine: cls.endLine,
2831
+ line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
2788
2832
  });
2789
2833
  }
2790
2834
  }
@@ -2808,7 +2852,7 @@ function createTestModel(index, options = {}) {
2808
2852
  kind: 'test', name: entry.name, suite: [], line: entry.line, startLine: entry.line, endLine: entry.line,
2809
2853
  inherited: {
2810
2854
  file: relativeOf(target), filePath: target, owner: null, ownerLine: 0,
2811
- line: fn.line, startLine: fn.startLine, endLine: fn.endLine,
2855
+ line: fn.line, startLine: fn.startLine, endLine: fn.endLine, sourceSpan: fn.sourceSpan,
2812
2856
  },
2813
2857
  }, row, [], (on === 'unknown' ? 'collection-switch' : null) || byForce(row), target);
2814
2858
  }
@@ -2875,7 +2919,7 @@ function createTestModel(index, options = {}) {
2875
2919
  const relativePath = index.files.get(filePath).relativePath;
2876
2920
  const runner = model.runnerOf(filePath) || {};
2877
2921
  const classesHere = [...(found.classes || [])];
2878
- const within = (unit, cls) => unit.startLine >= cls.startLine && unit.endLine <= cls.endLine;
2922
+ const within = (unit, cls) => containsTestUnit(cls, unit);
2879
2923
  // A setup that can stop a test, in a class the test's class
2880
2924
  // derives from: the test runs only sometimes.
2881
2925
  const guardedAbove = cls => cls.bases.length > 0 &&
@@ -2943,7 +2987,7 @@ function createTestModel(index, options = {}) {
2943
2987
  if (inherited.tests.some(({ test }) => !test.needsClassMark || cls.marked) && !own) {
2944
2988
  units.push({
2945
2989
  kind: 'suite', name: cls.name, suite: cls.suite,
2946
- line: cls.line, startLine: cls.startLine, endLine: cls.endLine,
2990
+ line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
2947
2991
  });
2948
2992
  }
2949
2993
  for (const { test, base, body, uncertain, maybe, again } of inherited.tests) {
@@ -2969,7 +3013,7 @@ function createTestModel(index, options = {}) {
2969
3013
  alternatives: maybe.map(other => ({
2970
3014
  file: index.files.get(other.at.filePath).relativePath,
2971
3015
  filePath: other.at.filePath, ownerLine: other.at.cls.startLine,
2972
- startLine: other.row.startLine, endLine: other.row.endLine,
3016
+ startLine: other.row.startLine, endLine: other.row.endLine, sourceSpan: other.row.sourceSpan,
2973
3017
  })),
2974
3018
  }),
2975
3019
  };
@@ -2979,7 +3023,7 @@ function createTestModel(index, options = {}) {
2979
3023
  if (body && body.at.cls === cls && !uncertain && !again) {
2980
3024
  units.push({
2981
3025
  kind: 'test', name: test.name, suite, ...(cls.namespace && { namespace: cls.namespace }),
2982
- line: body.row.line, startLine: body.row.startLine, endLine: body.row.endLine, ...state,
3026
+ line: body.row.line, startLine: body.row.startLine, endLine: body.row.endLine, sourceSpan: body.row.sourceSpan, ...state,
2983
3027
  });
2984
3028
  continue;
2985
3029
  }
@@ -2994,7 +3038,7 @@ function createTestModel(index, options = {}) {
2994
3038
  file: index.files.get(at.filePath).relativePath,
2995
3039
  filePath: at.filePath, owner: at.cls.name,
2996
3040
  ownerLine: at.cls.startLine, line: where.line,
2997
- startLine: where.startLine, endLine: where.endLine,
3041
+ startLine: where.startLine, endLine: where.endLine, sourceSpan: where.sourceSpan,
2998
3042
  },
2999
3043
  });
3000
3044
  }
@@ -3010,7 +3054,7 @@ function createTestModel(index, options = {}) {
3010
3054
  units.push({
3011
3055
  kind: 'test', name: row.name, suite: [...cls.suite, cls.name],
3012
3056
  ...(cls.namespace && { namespace: cls.namespace }),
3013
- line: row.line, startLine: row.startLine, endLine: row.endLine,
3057
+ line: row.line, startLine: row.startLine, endLine: row.endLine, sourceSpan: row.sourceSpan,
3014
3058
  ...(cls.skipped && { skipped: true }),
3015
3059
  ...((cls.sometimes || cls.guard || guardedAbove(cls)) && { conditional: true }),
3016
3060
  dynamic: 'declared-in-a-base-class',
@@ -3019,7 +3063,7 @@ function createTestModel(index, options = {}) {
3019
3063
  if (added && !own && inherited.tests.length === 0) {
3020
3064
  units.push({
3021
3065
  kind: 'suite', name: cls.name, suite: cls.suite,
3022
- line: cls.line, startLine: cls.startLine, endLine: cls.endLine,
3066
+ line: cls.line, startLine: cls.startLine, endLine: cls.endLine, sourceSpan: cls.sourceSpan,
3023
3067
  });
3024
3068
  }
3025
3069
  }
@@ -3089,8 +3133,9 @@ function createTestModel(index, options = {}) {
3089
3133
  * empty answer: the line belongs to no unit (file-level code).
3090
3134
  * @returns {Array<{filePath: string, unit: object}>}
3091
3135
  */
3092
- model.runsAt = (filePath, line) => {
3136
+ model.runsAt = (filePath, site) => {
3093
3137
  const out = [];
3138
+ const point = testSite(site);
3094
3139
  // The units of a file that may run this code. A module its runner
3095
3140
  // does not collect by its name declares none: it is not read for
3096
3141
  // the answer.
@@ -3101,19 +3146,22 @@ function createTestModel(index, options = {}) {
3101
3146
  }
3102
3147
  return model.unitsOf(file).units;
3103
3148
  };
3104
- const unit = model.unitAt(filePath, line);
3149
+ const unit = model.unitAt(filePath, point);
3105
3150
  if (unit && unit.kind === 'test') out.push({ filePath, unit });
3106
- const holder = (model.declared(filePath)?.classes || [])
3107
- .filter(cls => cls.startLine <= line && cls.endLine >= line)
3108
- .sort((a, b) => b.startLine - a.startLine)[0];
3151
+ const holders = innermostTestRows(model.declared(filePath)?.classes || [], point);
3152
+ const holder = holders.length === 1 ? holders[0] : null;
3109
3153
  // The method that holds the line: a test's declaration, or a method
3110
3154
  // a deriving class's test takes its body from.
3111
- const template = holder && [...holder.tests, ...(holder.memberRows || [])]
3112
- .find(row => row.startLine <= line && row.endLine >= line);
3155
+ const templates = holder ? innermostTestRows([...holder.tests, ...(holder.memberRows || [])], point) : [];
3156
+ // Test/member rows can describe the same body; distinct spans must
3157
+ // not be collapsed merely because their declaration lines match.
3158
+ const template = templates.length > 0 && templates.every(row => row.name === templates[0].name &&
3159
+ row.sourceSpan?.start === templates[0].sourceSpan?.start) ? templates[0] : null;
3113
3160
  const importers = [...(index.exportGraph?.get(filePath) || [])];
3114
3161
  if (template) {
3115
3162
  const held = from => !!from && from.filePath === filePath && from.ownerLine === holder.startLine &&
3116
- from.startLine === template.startLine && from.endLine === template.endLine;
3163
+ from.startLine === template.startLine && from.endLine === template.endLine &&
3164
+ (!from.sourceSpan || !template.sourceSpan || from.sourceSpan.start === template.sourceSpan.start);
3117
3165
  for (const file of [...new Set([...descendantFiles(holder.name), ...importers, filePath])].sort(codeUnitCompare)) {
3118
3166
  for (const candidate of unitsIn(file)) {
3119
3167
  // Every test whose body this is: the one a deriving
@@ -3127,13 +3175,14 @@ function createTestModel(index, options = {}) {
3127
3175
  }
3128
3176
  }
3129
3177
  // A function another module imports under a test's name runs there.
3130
- const fn = holder ? null : (model.declared(filePath)?.functions || [])
3131
- .find(other => other.startLine <= line && other.endLine >= line);
3178
+ const functions = holder ? [] : innermostTestRows(model.declared(filePath)?.functions || [], point);
3179
+ const fn = functions.length === 1 ? functions[0] : null;
3132
3180
  if (fn) {
3133
3181
  for (const file of [...new Set([...importers, filePath])].sort(codeUnitCompare)) {
3134
3182
  for (const candidate of unitsIn(file)) {
3135
3183
  const from = candidate.inherited;
3136
- if (from && from.filePath === filePath && from.owner == null && from.startLine === fn.startLine) {
3184
+ if (from && from.filePath === filePath && from.owner == null && from.startLine === fn.startLine &&
3185
+ (!from.sourceSpan || !fn.sourceSpan || from.sourceSpan.start === fn.sourceSpan.start)) {
3137
3186
  out.push({ filePath: file, unit: candidate });
3138
3187
  }
3139
3188
  }
@@ -3228,23 +3277,27 @@ function createTestModel(index, options = {}) {
3228
3277
  rows.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
3229
3278
  for (const row of rows) {
3230
3279
  if (row.startLine == null || row.endLine == null || covered(row)) continue;
3231
- if (model.runsAt(filePath, row.startLine).length > 0) ranges.push([row.startLine, row.endLine]);
3280
+ if (model.runsAt(filePath, { line: row.startLine,
3281
+ column: row.sourceSpan?.startColumn, callStart: row.sourceSpan?.start }).length > 0) {
3282
+ ranges.push([row.startLine, row.endLine]);
3283
+ }
3232
3284
  }
3233
3285
  }
3234
3286
  model.ranges.set(filePath, ranges);
3235
3287
  return ranges;
3236
3288
  };
3237
3289
 
3238
- /** The innermost unit holding a line: a test before a suite. */
3239
- model.unitAt = (filePath, line) => {
3240
- let best = null;
3241
- for (const unit of model.unitsOf(filePath).units) {
3242
- if (unit.startLine > line) break;
3243
- if (unit.endLine < line || unit.inherited) continue;
3244
- if (!best || (unit.kind === 'test' && best.kind !== 'test') ||
3245
- (unit.kind === best.kind && unit.line >= best.line)) best = unit;
3246
- }
3247
- return best;
3290
+ /** Innermost units at a physical site. A line alone may be ambiguous. */
3291
+ model.unitsAt = (filePath, site) => {
3292
+ const candidates = model.unitsOf(filePath).units.filter(unit =>
3293
+ !unit.inherited && containsTestSite(unit, site));
3294
+ return candidates.filter(unit => !candidates.some(other => other !== unit &&
3295
+ containsTestUnit(unit, other) &&
3296
+ (!containsTestUnit(other, unit) || (other.kind === 'test' && unit.kind !== 'test'))));
3297
+ };
3298
+ model.unitAt = (filePath, site) => {
3299
+ const units = model.unitsAt(filePath, site);
3300
+ return units.length === 1 ? units[0] : null;
3248
3301
  };
3249
3302
 
3250
3303
  /** Tests a unit stands for: itself, or every test a suite holds. */
@@ -3253,7 +3306,7 @@ function createTestModel(index, options = {}) {
3253
3306
  if (!unit) return units.filter(candidate => candidate.kind === 'test');
3254
3307
  if (unit.kind === 'test') return [unit];
3255
3308
  return units.filter(candidate => candidate.kind === 'test' &&
3256
- candidate.startLine >= unit.startLine && candidate.endLine <= unit.endLine);
3309
+ containsTestUnit(unit, candidate));
3257
3310
  };
3258
3311
 
3259
3312
  /**
@@ -3372,6 +3425,7 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
3372
3425
  taken.add(unit.startLine);
3373
3426
  added.push({
3374
3427
  line: unit.startLine,
3428
+ ...(unit.sourceSpan && { column: unit.sourceSpan.startColumn, callStart: unit.sourceSpan.start }),
3375
3429
  content: (index.getLineContent(filePath, unit.startLine) || '').trim(),
3376
3430
  matchType: 'test-case',
3377
3431
  ...(options.extra || {}),
@@ -3399,7 +3453,7 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
3399
3453
  return;
3400
3454
  }
3401
3455
  for (const match of counted) {
3402
- const unit = model.unitAt(filePath, match.line);
3456
+ const unit = model.unitAt(filePath, match);
3403
3457
  if (unit && unit.kind === 'test') add(unit);
3404
3458
  }
3405
3459
  if (options.titles !== false && term) {
@@ -3408,7 +3462,7 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
3408
3462
  for (const unit of model.unitsOf(filePath).units) {
3409
3463
  if (!unit.titled || unit.name == null || !named.test(unit.name)) continue;
3410
3464
  if (options.titleNeedsMatch &&
3411
- !counted.some(match => match.line >= unit.startLine && match.line <= unit.endLine)) continue;
3465
+ !counted.some(match => containsTestSite(unit, match))) continue;
3412
3466
  add(unit);
3413
3467
  }
3414
3468
  }
@@ -3422,7 +3476,8 @@ function addTestCaseRows(model, index, filePath, term, matches, options) {
3422
3476
  */
3423
3477
  function testKey(relativePath, unit) {
3424
3478
  return [relativePath, ...unit.suite, unit.name == null ? '' : unit.name, `@${unit.startLine}`,
3425
- ...(unit.inherited ? [`<${unit.inherited.file}:${unit.inherited.startLine}`] : [])].join('\0');
3479
+ ...(unit.sourceSpan ? [`#${unit.sourceSpan.start}`] : []),
3480
+ ...(unit.inherited ? [`<${unit.inherited.file}:${unit.inherited.startLine}:${unit.inherited.sourceSpan?.start ?? ''}`] : [])].join('\0');
3426
3481
  }
3427
3482
 
3428
3483
  /**
@@ -3462,9 +3517,28 @@ function linkFile(model, index, filePath, lines, extra = []) {
3462
3517
  for (const test of model.testsWithin(filePath, unit)) count(test);
3463
3518
  };
3464
3519
  let fileLevel = false;
3465
- for (const line of [...new Set(lines)].sort((a, b) => a - b)) {
3466
- const runs = model.runsAt(filePath, line);
3520
+ const sites = new Map(lines.map(site => [testSiteKey(site), {
3521
+ ...testSite(site), wholeLine: typeof site === 'number',
3522
+ }]));
3523
+ for (const site of [...sites.values()].sort((a, b) => a.line - b.line || (a.column || 0) - (b.column || 0))) {
3524
+ if (site.wholeLine) {
3525
+ // Legacy numeric inputs deliberately select the whole source
3526
+ // line (the matrix uses every line of a file). Precise callers
3527
+ // pass objects, so a missing position never implies this scope.
3528
+ const units = model.unitsAt(filePath, site);
3529
+ if (units.length > 1) {
3530
+ for (const unit of units) take(unit);
3531
+ continue;
3532
+ }
3533
+ }
3534
+ const runs = model.runsAt(filePath, site);
3467
3535
  if (runs.length === 0) {
3536
+ if (model.unitsAt(filePath, site).length > 1) {
3537
+ // A line without a physical position cannot select one of
3538
+ // several test bodies. Keep the file, withhold exact units.
3539
+ more = true;
3540
+ continue;
3541
+ }
3468
3542
  fileLevel = true;
3469
3543
  continue;
3470
3544
  }
@@ -3616,7 +3690,7 @@ function unitLabel(unit) {
3616
3690
 
3617
3691
  module.exports = {
3618
3692
  createTestModel, selectorText, unitLabel, shellWord, globMatches, testCountText, standsForSeveral,
3619
- addTestCaseRows, linkFile, linkFiles, commandsFor,
3693
+ addTestCaseRows, linkFile, linkFiles, commandsFor, testSite, testSiteKey,
3620
3694
  // Exposed for tests.
3621
3695
  scriptRunners, nodeTestOptions, configWords, tableText, namesRequirement, focusedOut,
3622
3696
  shellSplit, configArguments, pytestOptions, markSelects, pathGlob, nodeTestPaths, nodeRunsFile,