/
line-numbers.js
128 lines (101 loc) · 3.31 KB
/
line-numbers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import * as fs from 'node:fs';
import {createRequire, findSourceMap} from 'node:module';
import {pathToFileURL} from 'node:url';
import callsites from 'callsites';
const require = createRequire(import.meta.url);
function parse(file) {
// Avoid loading these until we actually need to select tests by line number.
const acorn = require('acorn');
const walk = require('acorn-walk');
const ast = acorn.parse(fs.readFileSync(file, 'utf8'), {
ecmaVersion: 'latest',
locations: true,
sourceType: 'module',
});
const locations = [];
walk.simple(ast, {
CallExpression(node) {
locations.push(node.loc);
},
});
// Walking is depth-first, but we want to sort these breadth-first.
locations.sort((a, b) => {
if (a.start.line === b.start.line) {
return a.start.column - b.start.column;
}
return a.start.line - b.start.line;
});
return locations;
}
function findTest(locations, declaration) {
// Find all calls that span the test declaration.
const spans = locations.filter(loc => {
if (loc.start.line > declaration.line || loc.end.line < declaration.line) {
return false;
}
if (loc.start.line === declaration.line && loc.start.column > declaration.column) {
return false;
}
if (loc.end.line === declaration.line && loc.end.column < declaration.column) {
return false;
}
return true;
});
// Locations should be sorted by source order, so the last span must be the test.
return spans.pop();
}
const range = (start, end) => Array.from({length: end - start + 1}).fill(start).map((element, index) => element + index);
const translate = (sourceMap, pos) => {
if (sourceMap === undefined) {
return pos;
}
const entry = sourceMap.findEntry(pos.line - 1, pos.column); // Source maps are 0-based
return {
line: entry.originalLine + 1, // Readjust for Acorn.
column: entry.originalColumn,
};
};
export default function lineNumberSelection({file, lineNumbers = []}) {
if (lineNumbers.length === 0) {
return undefined;
}
const selected = new Set(lineNumbers);
let locations = parse(file);
let lookedForSourceMap = false;
let sourceMap;
return () => {
if (!lookedForSourceMap) {
lookedForSourceMap = true;
// The returned function is called *after* the file has been loaded.
// Source maps are not available before then.
sourceMap = findSourceMap(file);
if (sourceMap !== undefined) {
locations = locations.map(({start, end}) => ({
start: translate(sourceMap, start),
end: translate(sourceMap, end),
}));
}
}
// Assume this is called from a test declaration, which is located in the file.
// If not… don't select the test!
const callSite = callsites().find(callSite => {
const current = callSite.getFileName();
if (file.startsWith('file://')) {
return current.startsWith('file://') ? file === current : file === pathToFileURL(current).toString();
}
return current.startsWith('file://') ? pathToFileURL(file).toString() === current : file === current;
});
if (!callSite) {
return false;
}
const start = translate(sourceMap, {
line: callSite.getLineNumber(), // 1-based
column: callSite.getColumnNumber() - 1, // Comes out as 1-based, Acorn wants 0-based
});
const test = findTest(locations, start);
if (!test) {
return false;
}
return range(test.start.line, test.end.line).some(line => selected.has(line));
};
}