Skip to content

Commit 1651b1d

Browse files
authored
Merge branch 'main' into feat/61216
2 parents bba7060 + e9e4774 commit 1651b1d

126 files changed

Lines changed: 7882 additions & 616 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/skills/compiler-and-fourslash-tests/SKILL.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,12 @@ const x/*1*/ = 42;
320320
`
321321
```
322322

323+
#### Content mapper tests
324+
325+
When writing a test for a content-mapped file, include a comment that shows the virtual
326+
TS output of the test content mapper implementation, and a description or diagram of the
327+
mapping spans.
328+
323329
### 2.3 Verification Methods (Common API)
324330

325331
The `fourslash.FourslashTest` type (variable `f`) provides these verification methods:

‎.github/workflows/codeql.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ jobs:
2424
node-version: 24
2525
- uses: ./.github/actions/setup-go
2626
- run: npm ci
27-
- uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
27+
- uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
2828
with:
2929
languages: go, javascript-typescript
3030
config-file: ./.github/codeql/codeql-configuration.yml
3131
- run: npx hereby build
32-
- uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
32+
- uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8

‎.github/workflows/scorecard.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ jobs:
5555

5656
# Upload the results to GitHub's code scanning dashboard.
5757
- name: 'Upload to code-scanning'
58-
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
58+
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
5959
with:
6060
sarif_file: results.sarif

‎.gitignore‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ web_modules/
3838
*.tsbuildinfo
3939
*.tgz
4040
dist
41+
.eslintcache
4142
.cache
4243
.parcel-cache
4344
.rpt2_cache/
@@ -81,6 +82,48 @@ __debug_bin*
8182
tsc/testdata/baselines/local
8283
tsc/testdata/baselines/tmp
8384

85+
# Legacy Strada build and test output
86+
/.node_modules/
87+
/test-args.txt
88+
/rwc-report.html
89+
/build.json
90+
*.actual
91+
/.parallelperf.*
92+
/.failed-tests
93+
/TEST-results.xml
94+
/*v8.log
95+
/lib/
96+
/internal/
97+
/scripts/eslint/built/
98+
/scripts/debug.bat
99+
/scripts/run.bat
100+
/scripts/**/*.js
101+
/scripts/**/*.js.map
102+
/src/harness/*.js
103+
/src/compiler/diagnosticInformationMap.generated.ts
104+
/src/compiler/diagnosticMessages.generated.json
105+
/src/parser/diagnosticInformationMap.generated.ts
106+
/src/parser/diagnosticMessages.generated.json
107+
/tests/cases/rwc/
108+
/tests/cases/perf/
109+
/tests/cases/**/*.js
110+
/tests/cases/**/*.js.map
111+
/tests/baselines/local/
112+
/tests/baselines/local.old/
113+
/tests/baselines/prototyping/local/
114+
/tests/baselines/rwc/
115+
/tests/baselines/symlinks/
116+
/tests/baselines/reference/projectOutput/
117+
/tests/baselines/reference/testresults.tap
118+
/tests/baselines/reference/dt
119+
/tests/services/baselines/local/
120+
/tests/services/baselines/prototyping/local/
121+
/tests/services/browser/typescriptServices.js
122+
/tests/webTestServer.js
123+
/tests/webTestServer.js.map
124+
/tests/webhost/*.d.ts
125+
/tests/webhost/webtsc.js
126+
84127
# Generated development tools
85128
tools/custom-gcl
86129
tools/custom-gcl.exe
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
const { version } = require("../package.json");
22
exports.version = version;
3-
exports.versionMajorMinor = "7.0";
3+
exports.versionMajorMinor = "7.1";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export declare const version: string;
2-
export declare const versionMajorMinor = "7.0";
2+
export declare const versionMajorMinor = "7.1";

‎packages/typescript/src/ast/spanMap.ts‎

Lines changed: 116 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ export interface SpanMapSegment {
2020
/** Internal segment representation after omitted features have been normalized to `All`. */
2121
type NormalizedSpanMapSegment = SpanMapSegment & { readonly features: SpanMapFeature; };
2222

23+
/** Lazily built interval index for original-to-virtual lookups. */
24+
interface OriginalIndex {
25+
readonly segments: readonly NormalizedSpanMapSegment[];
26+
readonly leafCount: number;
27+
readonly maxEnds: readonly number[];
28+
}
29+
2330
/** One virtual projection of an original position and its mapping fidelity. */
2431
export interface MappedPosition {
2532
readonly position: number;
@@ -35,7 +42,7 @@ export interface MappedRange {
3542
/** Provides bidirectional span-aware mapping between virtual and original text. */
3643
export class SpanMap {
3744
readonly segments: readonly NormalizedSpanMapSegment[];
38-
private originalSegments: readonly NormalizedSpanMapSegment[] | undefined;
45+
private originalIndex: OriginalIndex | undefined;
3946

4047
/** Copies and sorts segments by virtual start, normalizing omitted features to `All`. */
4148
constructor(segments: readonly SpanMapSegment[]) {
@@ -90,7 +97,7 @@ export class SpanMap {
9097
* Results are ordered by virtual position; uncovered or disabled positions produce no results.
9198
*/
9299
originalToVirtualPositions(position: number, feature: SpanMapFeature): readonly MappedPosition[] {
93-
const groups = segmentGroupsAtOriginalPosition(this.getOriginalSegments(), position);
100+
const groups = segmentGroupsAtOriginalPosition(this.getOriginalIndex(), position);
94101
const results: MappedPosition[] = [];
95102
for (const group of groups) {
96103
for (const segment of group.segments) {
@@ -108,7 +115,7 @@ export class SpanMap {
108115

109116
/**
110117
* Returns every feature-compatible virtual projection of an original range.
111-
* A range contained by one duplicate group produces one exact or atom result per matching group member.
118+
* A range contained by one or more segments produces one exact or atom result per matching segment.
112119
*
113120
* A range that starts in one group and ends in another can have several possible virtual ranges. For
114121
* example, suppose two original segments are each copied twice into the virtual text:
@@ -131,16 +138,24 @@ export class SpanMap {
131138
originalToVirtualSpans(range: ReadonlyTextRange, feature: SpanMapFeature): readonly MappedRange[] {
132139
const start = range.pos;
133140
const end = Math.max(range.end, start);
134-
const lastCharacter = end > start ? end - 1 : end;
135-
const originalSegments = this.getOriginalSegments();
136-
const startSegments = segmentsAtOriginalPosition(originalSegments, start);
137-
const endSegments = segmentsAtOriginalPosition(originalSegments, lastCharacter);
141+
if (start === end) {
142+
return this.originalToVirtualPositions(start, feature).map(({ position, fidelity }) => ({
143+
range: { pos: position, end: position },
144+
fidelity,
145+
}));
146+
}
147+
const lastCharacter = end - 1;
148+
const originalIndex = this.getOriginalIndex();
149+
const startSegments = segmentsAtOriginalPosition(originalIndex, start);
150+
const endSegments = segmentsAtOriginalPosition(originalIndex, lastCharacter);
138151
if (!startSegments || !endSegments) return [];
139-
if (sameOriginalRange(startSegments[0], endSegments[0])) {
140-
return originalToVirtualSpansInGroup(startSegments, start, end, feature);
152+
const containing = startSegments.filter(segment => end <= segment.originalEnd);
153+
if (containing.length > 0) {
154+
const results = [...originalToVirtualSpansInSegments(containing, start, end, feature)];
155+
if (results.length > 0) return results.sort((left, right) => left.range.pos - right.range.pos);
141156
}
142-
const starts = originalStartProjections(startSegments, start, feature);
143-
const ends = originalEndProjections(endSegments, end, feature);
157+
const starts = [...originalStartProjections(startSegments, start, feature)].sort((left, right) => left - right);
158+
const ends = [...originalEndProjections(endSegments, end, feature)].sort((left, right) => left - right);
144159
if (starts.length === 0 || ends.length === 0) return [];
145160
return starts.flatMap((virtualStart, index) => {
146161
const virtualEnd = ends.find(end => end >= virtualStart);
@@ -154,8 +169,12 @@ export class SpanMap {
154169
private mapRange(range: ReadonlyTextRange, segments: readonly SpanMapSegment[], reverse: boolean): MappedRange {
155170
const start = range.pos;
156171
const end = Math.max(range.end, start);
172+
if (start === end) {
173+
const { position, fidelity } = this.mapPoint(start, segments, reverse);
174+
return { range: { pos: position, end: position }, fidelity };
175+
}
157176
const [startIndex, startInside] = segmentIndexAt(segments, start, reverse);
158-
const endProbe = end > start ? end - 1 : end;
177+
const endProbe = end - 1;
159178
const [endIndex, endInside] = segmentIndexAt(segments, endProbe, reverse);
160179

161180
if (startIndex === endIndex && startInside === endInside) {
@@ -193,13 +212,20 @@ export class SpanMap {
193212
};
194213
}
195214

196-
/** Returns the lazily built segment index ordered by original start. */
197-
private getOriginalSegments(): readonly NormalizedSpanMapSegment[] {
198-
return this.originalSegments ??= [...this.segments].sort((left, right) =>
215+
/** Returns the lazily built original-text interval index. */
216+
private getOriginalIndex(): OriginalIndex {
217+
if (this.originalIndex) return this.originalIndex;
218+
const segments = [...this.segments].sort((left, right) =>
199219
left.originalStart - right.originalStart
200220
|| left.originalEnd - right.originalEnd
201221
|| left.virtualStart - right.virtualStart
202222
);
223+
let leafCount = 1;
224+
while (leafCount < segments.length) leafCount *= 2;
225+
const maxEnds = new Array<number>(2 * leafCount).fill(0);
226+
for (let i = 0; i < segments.length; i++) maxEnds[leafCount + i] = segments[i].originalEnd;
227+
for (let i = leafCount - 1; i > 0; i--) maxEnds[i] = Math.max(maxEnds[2 * i], maxEnds[2 * i + 1]);
228+
return this.originalIndex = { segments, leafCount, maxEnds };
203229
}
204230

205231
private virtualRangeSupportsFeature(range: ReadonlyTextRange, feature: SpanMapFeature): boolean {
@@ -269,8 +295,8 @@ function originalEndProjections(segments: readonly NormalizedSpanMapSegment[], e
269295
);
270296
}
271297

272-
/** Maps a range whose boundaries are known to lie in one duplicate group. */
273-
function originalToVirtualSpansInGroup(segments: readonly NormalizedSpanMapSegment[], start: number, end: number, feature: SpanMapFeature): readonly MappedRange[] {
298+
/** Maps a range fully contained by each segment. */
299+
function originalToVirtualSpansInSegments(segments: readonly NormalizedSpanMapSegment[], start: number, end: number, feature: SpanMapFeature): readonly MappedRange[] {
274300
return segments
275301
.filter(segment => supportsFeature(segment, feature))
276302
.map(segment => {
@@ -289,30 +315,70 @@ function sameOriginalRange(left: SpanMapSegment, right: SpanMapSegment): boolean
289315
}
290316

291317
/**
292-
* Returns the complete duplicate group of mapping segments containing the original-text `position`.
293-
* Segment ends are exclusive; starts, including zero-length segment starts, are included. It finds a candidate
294-
* in O(log n), then scans only the duplicate group. `segments` must be ordered by original start, original end,
295-
* and virtual start.
318+
* Returns every mapping segment containing the original-text `position`.
319+
* Segment ends are exclusive; starts, including zero-length segment starts, are included.
296320
*/
297-
function segmentsAtOriginalPosition(segments: readonly NormalizedSpanMapSegment[], position: number): readonly NormalizedSpanMapSegment[] | undefined {
321+
function segmentsAtOriginalPosition(index: OriginalIndex, position: number): readonly NormalizedSpanMapSegment[] | undefined {
322+
// Query intervals that contain position strictly before their exclusive end. Segments starting exactly at
323+
// position are appended separately so zero-length segments are included while maxEnd <= position is pruned.
324+
const start = firstOriginalSegmentAtOrAfter(index.segments, position);
325+
const results = segmentsEndingAtOrAfter(index, start, position, false);
326+
const end = firstOriginalSegmentAfter(index.segments, position);
327+
results.push(...index.segments.slice(start, end));
328+
return results.length > 0 ? results : undefined;
329+
}
330+
331+
/** Returns segments among `[0, limit)` whose original end reaches `position`. */
332+
function segmentsEndingAtOrAfter(index: OriginalIndex, limit: number, position: number, includeEnd: boolean): NormalizedSpanMapSegment[] {
333+
const results: NormalizedSpanMapSegment[] = [];
334+
collectSegmentsEndingAtOrAfter(index, 1, 0, index.leafCount, limit, position, includeEnd, results);
335+
return results;
336+
}
337+
338+
/** Walks the flat max-end tree left-to-right, preserving original-text order. */
339+
function collectSegmentsEndingAtOrAfter(
340+
index: OriginalIndex,
341+
node: number,
342+
start: number,
343+
end: number,
344+
limit: number,
345+
position: number,
346+
includeEnd: boolean,
347+
results: NormalizedSpanMapSegment[],
348+
): void {
349+
const maxEnd = index.maxEnds[node];
350+
if (start >= limit || maxEnd < position || !includeEnd && maxEnd === position) return;
351+
if (end - start === 1) {
352+
results.push(index.segments[start]);
353+
return;
354+
}
355+
const middle = start + ((end - start) >>> 1);
356+
collectSegmentsEndingAtOrAfter(index, 2 * node, start, middle, limit, position, includeEnd, results);
357+
collectSegmentsEndingAtOrAfter(index, 2 * node + 1, middle, end, limit, position, includeEnd, results);
358+
}
359+
360+
/** Returns the first original-ordered segment whose start is greater than or equal to `position`. */
361+
function firstOriginalSegmentAtOrAfter(segments: readonly NormalizedSpanMapSegment[], position: number): number {
298362
let low = 0;
299363
let high = segments.length;
300364
while (low < high) {
301365
const middle = (low + high) >>> 1;
302366
if (segments[middle].originalStart < position) low = middle + 1;
303367
else high = middle;
304368
}
305-
let index = low < segments.length && segments[low].originalStart === position ? low : low - 1;
306-
if (
307-
index < 0 || !(
308-
segments[index].originalStart === position
309-
|| position < segments[index].originalEnd
310-
)
311-
) return undefined;
312-
while (index > 0 && sameOriginalRange(segments[index - 1], segments[index])) index--;
313-
let end = index + 1;
314-
while (end < segments.length && sameOriginalRange(segments[end], segments[index])) end++;
315-
return segments.slice(index, end);
369+
return low;
370+
}
371+
372+
/** Returns the first original-ordered segment whose start is greater than `position`. */
373+
function firstOriginalSegmentAfter(segments: readonly NormalizedSpanMapSegment[], position: number): number {
374+
let low = 0;
375+
let high = segments.length;
376+
while (low < high) {
377+
const middle = (low + high) >>> 1;
378+
if (segments[middle].originalStart <= position) low = middle + 1;
379+
else high = middle;
380+
}
381+
return low;
316382
}
317383

318384
interface SegmentGroupAtOriginalPosition {
@@ -321,8 +387,8 @@ interface SegmentGroupAtOriginalPosition {
321387
}
322388

323389
/**
324-
* Returns groups of mapping segments containing or touching the original-text `position`.
325-
* At a shared boundary, segments ending at the point and segments starting there form separate groups:
390+
* Returns every group of equal-range mapping segments containing or touching the original-text `position`.
391+
* Segment ends are included for point mapping:
326392
*
327393
* ```text
328394
* original: [--- A ---)[--- B ---)
@@ -333,31 +399,23 @@ interface SegmentGroupAtOriginalPosition {
333399
* atEnd: true atEnd: false
334400
* ```
335401
*/
336-
function segmentGroupsAtOriginalPosition(segments: readonly NormalizedSpanMapSegment[], position: number): readonly SegmentGroupAtOriginalPosition[] {
337-
let low = 0;
338-
let high = segments.length;
339-
while (low < high) {
340-
const middle = (low + high) >>> 1;
341-
if (segments[middle].originalStart < position) low = middle + 1;
342-
else high = middle;
343-
}
344-
if (low < segments.length && segments[low].originalStart === position) {
345-
const right = segmentsAtOriginalPosition(segments, position)!;
346-
const groups: SegmentGroupAtOriginalPosition[] = [];
347-
if (low > 0 && segments[low - 1].originalEnd === position) {
348-
let leftStart = low - 1;
349-
while (leftStart > 0 && sameOriginalRange(segments[leftStart - 1], segments[low - 1])) leftStart--;
350-
groups.push({ segments: segments.slice(leftStart, low), atEnd: true });
402+
function segmentGroupsAtOriginalPosition(index: OriginalIndex, position: number): readonly SegmentGroupAtOriginalPosition[] {
403+
const limit = firstOriginalSegmentAfter(index.segments, position);
404+
const segments = segmentsEndingAtOrAfter(index, limit, position, true);
405+
const groups: SegmentGroupAtOriginalPosition[] = [];
406+
for (let start = 0; start < segments.length;) {
407+
let end = start + 1;
408+
while (end < segments.length && sameOriginalRange(segments[start], segments[end])) end++;
409+
const segment = segments[start];
410+
if (position <= segment.originalEnd) {
411+
groups.push({
412+
segments: segments.slice(start, end),
413+
atEnd: position === segment.originalEnd && position !== segment.originalStart,
414+
});
351415
}
352-
groups.push({ segments: right, atEnd: false });
353-
return groups;
416+
start = end;
354417
}
355-
if (low === 0) return [];
356-
const left = segments[low - 1];
357-
if (position > left.originalEnd) return [];
358-
let start = low - 1;
359-
while (start > 0 && sameOriginalRange(segments[start - 1], left)) start--;
360-
return [{ segments: segments.slice(start, low), atEnd: position === left.originalEnd }];
418+
return groups;
361419
}
362420

363421
/** Reports whether a segment participates in an original-to-virtual query for `features`. */

0 commit comments

Comments
 (0)