semantifold 0.1.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/LICENSE +15 -0
- package/README.md +71 -0
- package/build/index.d.ts +54 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +45 -0
- package/build/src/backends/identifiers.d.ts +19 -0
- package/build/src/backends/identifiers.d.ts.map +1 -0
- package/build/src/backends/identifiers.js +93 -0
- package/build/src/backends/index.d.ts +35 -0
- package/build/src/backends/index.d.ts.map +1 -0
- package/build/src/backends/index.js +149 -0
- package/build/src/backends/java.d.ts +8 -0
- package/build/src/backends/java.d.ts.map +1 -0
- package/build/src/backends/java.js +172 -0
- package/build/src/backends/javascript.d.ts +8 -0
- package/build/src/backends/javascript.d.ts.map +1 -0
- package/build/src/backends/javascript.js +173 -0
- package/build/src/backends/php.d.ts +8 -0
- package/build/src/backends/php.d.ts.map +1 -0
- package/build/src/backends/php.js +175 -0
- package/build/src/backends/ruby.d.ts +8 -0
- package/build/src/backends/ruby.d.ts.map +1 -0
- package/build/src/backends/ruby.js +155 -0
- package/build/src/backends/scalars.d.ts +15 -0
- package/build/src/backends/scalars.d.ts.map +1 -0
- package/build/src/backends/scalars.js +138 -0
- package/build/src/backends/shared.d.ts +26 -0
- package/build/src/backends/shared.d.ts.map +1 -0
- package/build/src/backends/shared.js +424 -0
- package/build/src/backends/typescript.d.ts +8 -0
- package/build/src/backends/typescript.d.ts.map +1 -0
- package/build/src/backends/typescript.js +160 -0
- package/build/src/backends/writer.d.ts +87 -0
- package/build/src/backends/writer.d.ts.map +1 -0
- package/build/src/backends/writer.js +207 -0
- package/build/src/diagnostic.d.ts +67 -0
- package/build/src/diagnostic.d.ts.map +1 -0
- package/build/src/diagnostic.js +96 -0
- package/build/src/frontends/index.d.ts +14 -0
- package/build/src/frontends/index.d.ts.map +1 -0
- package/build/src/frontends/index.js +36 -0
- package/build/src/frontends/java.d.ts +12 -0
- package/build/src/frontends/java.d.ts.map +1 -0
- package/build/src/frontends/java.js +630 -0
- package/build/src/frontends/javascript-typescript.d.ts +19 -0
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
- package/build/src/frontends/javascript-typescript.js +633 -0
- package/build/src/frontends/php.d.ts +12 -0
- package/build/src/frontends/php.d.ts.map +1 -0
- package/build/src/frontends/php.js +523 -0
- package/build/src/frontends/ruby.d.ts +12 -0
- package/build/src/frontends/ruby.d.ts.map +1 -0
- package/build/src/frontends/ruby.js +515 -0
- package/build/src/frontends/scalars.d.ts +19 -0
- package/build/src/frontends/scalars.d.ts.map +1 -0
- package/build/src/frontends/scalars.js +71 -0
- package/build/src/mapping.d.ts +160 -0
- package/build/src/mapping.d.ts.map +1 -0
- package/build/src/mapping.js +1120 -0
- package/build/src/semantic/location.d.ts +61 -0
- package/build/src/semantic/location.d.ts.map +1 -0
- package/build/src/semantic/location.js +142 -0
- package/build/src/semantic/operators.d.ts +16 -0
- package/build/src/semantic/operators.d.ts.map +1 -0
- package/build/src/semantic/operators.js +24 -0
- package/build/src/semantic/provenance.d.ts +74 -0
- package/build/src/semantic/provenance.d.ts.map +1 -0
- package/build/src/semantic/provenance.js +577 -0
- package/build/src/semantic/scalars.d.ts +19 -0
- package/build/src/semantic/scalars.d.ts.map +1 -0
- package/build/src/semantic/scalars.js +40 -0
- package/build/src/semantic/types.d.ts +1003 -0
- package/build/src/semantic/types.d.ts.map +1 -0
- package/build/src/semantic/types.js +279 -0
- package/build/src/semantic/validate.d.ts +60 -0
- package/build/src/semantic/validate.d.ts.map +1 -0
- package/build/src/semantic/validate.js +472 -0
- package/docs/architecture.md +39 -0
- package/docs/coding-standards.md +7 -0
- package/docs/goals.md +7 -0
- package/docs/language-support.md +69 -0
- package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
- package/docs/source-maps.md +134 -0
- package/docs/testing.md +25 -0
- package/index.js +63 -0
- package/package.json +69 -0
- package/src/backends/identifiers.js +101 -0
- package/src/backends/index.js +159 -0
- package/src/backends/java.js +182 -0
- package/src/backends/javascript.js +184 -0
- package/src/backends/php.js +184 -0
- package/src/backends/ruby.js +163 -0
- package/src/backends/scalars.js +120 -0
- package/src/backends/shared.js +456 -0
- package/src/backends/typescript.js +168 -0
- package/src/backends/writer.js +224 -0
- package/src/diagnostic.js +103 -0
- package/src/frontends/index.js +36 -0
- package/src/frontends/java.js +735 -0
- package/src/frontends/javascript-typescript.js +718 -0
- package/src/frontends/php.js +593 -0
- package/src/frontends/ruby.js +622 -0
- package/src/frontends/scalars.js +77 -0
- package/src/mapping.js +1260 -0
- package/src/semantic/location.js +154 -0
- package/src/semantic/operators.js +28 -0
- package/src/semantic/provenance.js +629 -0
- package/src/semantic/scalars.js +44 -0
- package/src/semantic/types.js +319 -0
- package/src/semantic/validate.js +529 -0
- package/tsconfig.json +16 -0
package/src/mapping.js
ADDED
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {addSegment, GenMapping, maybeAddSegment, setSourceContent, toEncodedMap} from "@jridgewell/gen-mapping"
|
|
4
|
+
import {
|
|
5
|
+
eachMapping,
|
|
6
|
+
originalPositionFor as traceOriginalPositionFor,
|
|
7
|
+
sourceContentFor,
|
|
8
|
+
TraceMap
|
|
9
|
+
} from "@jridgewell/trace-mapping"
|
|
10
|
+
import {SemantifoldDiagnostic} from "./diagnostic.js"
|
|
11
|
+
import {createCoordinateIndex, indexedOffsetAt, indexedPointAt, locationFromOffsets} from "./semantic/location.js"
|
|
12
|
+
import {primaryLocation} from "./semantic/provenance.js"
|
|
13
|
+
|
|
14
|
+
/** @typedef {ReturnType<typeof buildMappingIndex>} MappingIndex */
|
|
15
|
+
/** @type {WeakMap<import("./semantic/types.js").SemantifoldMapping, MappingIndex>} */
|
|
16
|
+
const mappingIndexes = new WeakMap()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Projects an authoritative Semantifold mapping to Source Map v3.
|
|
20
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
21
|
+
* @returns {import("@jridgewell/gen-mapping").EncodedSourceMap} Source Map v3.
|
|
22
|
+
*/
|
|
23
|
+
export function toSourceMapV3(mapping) {
|
|
24
|
+
const index = mappingIndex(mapping)
|
|
25
|
+
|
|
26
|
+
const generated = new GenMapping({file: mapping.generated.filename})
|
|
27
|
+
|
|
28
|
+
for (const source of mapping.sources) setSourceContent(generated, source.filename, source.content)
|
|
29
|
+
|
|
30
|
+
for (const span of mapping.spans) {
|
|
31
|
+
const location = primaryLocation(span.origin)
|
|
32
|
+
const line = span.generated.start.line - 1
|
|
33
|
+
const column = span.generated.start.column - 1
|
|
34
|
+
|
|
35
|
+
if (!location || span.mappingKind == "synthetic") {
|
|
36
|
+
maybeAddSegment(generated, line, column)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const registered = sourceForOrigin(mapping, span.origin, location, index.sourcesById)
|
|
41
|
+
|
|
42
|
+
if (span.name) {
|
|
43
|
+
addSegment(generated, line, column, registered.filename, location.start.line - 1, location.start.column - 1, span.name, registered.content)
|
|
44
|
+
} else {
|
|
45
|
+
addSegment(generated, line, column, registered.filename, location.start.line - 1, location.start.column - 1, null, registered.content)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return toEncodedMap(generated)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Looks up original provenance for one generated UTF-16 position.
|
|
54
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
55
|
+
* @param {{offset: number} | {line: number, column: number}} position - Generated position.
|
|
56
|
+
* @returns {{generatedLocation: import("./semantic/types.js").SourceLocation, location: import("./semantic/types.js").SourceLocation | undefined, mappingKind: import("./semantic/types.js").MappingKind, name: string | undefined, nodeId: string | undefined, role: string | undefined, symbolId: string | undefined}} Lookup result.
|
|
57
|
+
*/
|
|
58
|
+
export function originalPositionFor(mapping, position) {
|
|
59
|
+
const index = mappingIndex(mapping)
|
|
60
|
+
const offset = positionOffset(index.generatedCoordinates, position)
|
|
61
|
+
const span = spanForGenerated(mapping.spans, offset)
|
|
62
|
+
|
|
63
|
+
if (!span) throw new RangeError(`No generated mapping at offset ${offset}.`)
|
|
64
|
+
|
|
65
|
+
return mappingResult(span)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Looks up every generated span containing one original UTF-16 position.
|
|
70
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
71
|
+
* @param {({filename: string} | {sourceId: string}) & ({offset: number} | {line: number, column: number})} position - Original position.
|
|
72
|
+
* @returns {{generatedLocation: import("./semantic/types.js").SourceLocation, location: import("./semantic/types.js").SourceLocation | undefined, mappingKind: import("./semantic/types.js").MappingKind, name: string | undefined, nodeId: string | undefined, role: string | undefined, symbolId: string | undefined}[]} Generated results.
|
|
73
|
+
*/
|
|
74
|
+
export function generatedPositionFor(mapping, position) {
|
|
75
|
+
const index = mappingIndex(mapping)
|
|
76
|
+
const source = "sourceId" in position
|
|
77
|
+
? index.sourcesById.get(position.sourceId)
|
|
78
|
+
: mapping.sources.find((candidate) => candidate.filename == position.filename)
|
|
79
|
+
|
|
80
|
+
if (!source) return []
|
|
81
|
+
if (source.content == null && !("offset" in position)) {
|
|
82
|
+
throw new TypeError(`Source content is required for line/column lookup of ${source.filename}.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let offset
|
|
86
|
+
|
|
87
|
+
if ("offset" in position) {
|
|
88
|
+
if (source.content == null) {
|
|
89
|
+
if (!Number.isInteger(position.offset) || position.offset < 0) throw new RangeError(`Invalid source offset: ${position.offset}`)
|
|
90
|
+
} else indexedPointAt(/** @type {ReturnType<typeof createCoordinateIndex>} */ (index.sourceCoordinates.get(source.id)), position.offset)
|
|
91
|
+
offset = position.offset
|
|
92
|
+
} else {
|
|
93
|
+
offset = indexedOffsetAt(/** @type {ReturnType<typeof createCoordinateIndex>} */ (index.sourceCoordinates.get(source.id)),
|
|
94
|
+
position.line, position.column)
|
|
95
|
+
}
|
|
96
|
+
const entries = index.originalBySource.get(source.id) ?? []
|
|
97
|
+
let low = 0
|
|
98
|
+
let high = entries.length
|
|
99
|
+
|
|
100
|
+
while (low < high) {
|
|
101
|
+
const middle = Math.floor((low + high) / 2)
|
|
102
|
+
|
|
103
|
+
if (entries[middle].start <= offset) low = middle + 1
|
|
104
|
+
else high = middle
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @type {Set<import("./semantic/types.js").SemantifoldMappingSpan>} */
|
|
108
|
+
const matches = new Set()
|
|
109
|
+
|
|
110
|
+
for (const span of index.originalPointsBySource.get(source.id)?.get(offset) ?? []) matches.add(span)
|
|
111
|
+
|
|
112
|
+
for (let candidate = low - 1; candidate >= 0 && entries[candidate].prefixEnd > offset; candidate--) {
|
|
113
|
+
if (entries[candidate].end > offset) matches.add(entries[candidate].span)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [...matches].sort((left, right) => left.generated.start.offset - right.generated.start.offset).map(mappingResult)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns every generated span associated with one canonical semantic node.
|
|
121
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
122
|
+
* @param {string} nodeId - Canonical node identity.
|
|
123
|
+
* @returns {import("./semantic/types.js").SemantifoldMappingSpan[]} Matching spans.
|
|
124
|
+
*/
|
|
125
|
+
export function spansForNode(mapping, nodeId) {
|
|
126
|
+
const index = mappingIndex(mapping)
|
|
127
|
+
if (!index.nodeSpans.has(nodeId)) throw new RangeError(`Unknown semantic node: ${nodeId}`)
|
|
128
|
+
|
|
129
|
+
return [...(index.nodeSpans.get(nodeId) ?? [])]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns every generated span associated with one canonical semantic symbol.
|
|
134
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
135
|
+
* @param {string} symbolId - Canonical symbol identity.
|
|
136
|
+
* @returns {import("./semantic/types.js").SemantifoldMappingSpan[]} Matching spans.
|
|
137
|
+
*/
|
|
138
|
+
export function spansForSymbol(mapping, symbolId) {
|
|
139
|
+
const index = mappingIndex(mapping)
|
|
140
|
+
if (!index.symbolSpans.has(symbolId)) throw new RangeError(`Unknown semantic symbol: ${symbolId}`)
|
|
141
|
+
|
|
142
|
+
return [...(index.symbolSpans.get(symbolId) ?? [])]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Serializes a validated rich mapping deterministically.
|
|
147
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
148
|
+
* @returns {string} Canonical pretty JSON with one trailing LF.
|
|
149
|
+
*/
|
|
150
|
+
export function stringifyMapping(mapping) {
|
|
151
|
+
mappingIndex(mapping)
|
|
152
|
+
|
|
153
|
+
return `${JSON.stringify(sortJson(mapping), null, 2)}\n`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parses and validates a rich mapping.
|
|
158
|
+
* @param {string} serialized - Mapping JSON.
|
|
159
|
+
* @returns {import("./semantic/types.js").SemantifoldMapping} Parsed mapping.
|
|
160
|
+
*/
|
|
161
|
+
export function parseMapping(serialized) {
|
|
162
|
+
const mapping = /** @type {unknown} */ (JSON.parse(serialized))
|
|
163
|
+
|
|
164
|
+
return finalizeMapping(mapping)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Composes an outer rich map through an inner rich map.
|
|
169
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} outer - Final output to intermediate map.
|
|
170
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} inner - Intermediate to original map.
|
|
171
|
+
* @returns {import("./semantic/types.js").SemantifoldMapping} Composed rich map.
|
|
172
|
+
*/
|
|
173
|
+
export function composeMappings(outer, inner) {
|
|
174
|
+
const outerIndex = mappingIndex(outer)
|
|
175
|
+
|
|
176
|
+
mappingIndex(inner)
|
|
177
|
+
|
|
178
|
+
/** @type {import("./semantic/types.js").RegisteredSource[]} */
|
|
179
|
+
const sources = inner.sources.map((source) => ({...source}))
|
|
180
|
+
const sourceIds = new Set(sources.map((source) => source.id))
|
|
181
|
+
/** @type {Map<string, string>} */
|
|
182
|
+
const outerSourceIds = new Map()
|
|
183
|
+
let nextSourceIndex = 0
|
|
184
|
+
|
|
185
|
+
for (const source of outer.sources) {
|
|
186
|
+
if (sourceRepresentsGenerated(source, inner.generated)) continue
|
|
187
|
+
retainOuterSource(source.id)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const spans = outer.spans.flatMap((span) => {
|
|
191
|
+
/** @type {ReturnType<typeof traceRelatedOrigin> | undefined} */
|
|
192
|
+
let trace
|
|
193
|
+
|
|
194
|
+
for (const related of relatedOriginsForOrigin(span.origin)) {
|
|
195
|
+
trace = traceRelatedOrigin(related)
|
|
196
|
+
if (trace?.traced) break
|
|
197
|
+
}
|
|
198
|
+
const location = trace?.related.location
|
|
199
|
+
const overlapping = trace?.overlapping ?? []
|
|
200
|
+
const traced = trace?.traced
|
|
201
|
+
|
|
202
|
+
if (!traced) {
|
|
203
|
+
for (const related of relatedOriginsForOrigin(span.origin)) {
|
|
204
|
+
const relatedTrace = traceRelatedOrigin(related)
|
|
205
|
+
|
|
206
|
+
if (relatedTrace && isGeneratedEofPoint(related)) retainOuterSource(related.sourceId)
|
|
207
|
+
}
|
|
208
|
+
const unidentified = {...span}
|
|
209
|
+
|
|
210
|
+
delete unidentified.nodeId
|
|
211
|
+
delete unidentified.symbolId
|
|
212
|
+
|
|
213
|
+
return [{...unidentified, origin: remapOriginSourceIds(span.origin, outerSourceIds)}]
|
|
214
|
+
}
|
|
215
|
+
const selectedRelated = trace?.related
|
|
216
|
+
|
|
217
|
+
if (!selectedRelated) throw new RangeError("Intermediate trace selection is missing its related origin.")
|
|
218
|
+
|
|
219
|
+
if (location && overlapping.length > 1 && span.mappingKind == "exact" &&
|
|
220
|
+
span.generated.end.offset - span.generated.start.offset == location.end.offset - location.start.offset) {
|
|
221
|
+
return overlapping.map((candidate) => {
|
|
222
|
+
const overlapStart = Math.max(location.start.offset, candidate.generated.start.offset)
|
|
223
|
+
const overlapEnd = Math.min(location.end.offset, candidate.generated.end.offset)
|
|
224
|
+
const generatedStart = span.generated.start.offset + overlapStart - location.start.offset
|
|
225
|
+
const generatedEnd = span.generated.start.offset + overlapEnd - location.start.offset
|
|
226
|
+
const preservesWholeOrigin = overlapStart == candidate.generated.start.offset && overlapEnd == candidate.generated.end.offset
|
|
227
|
+
|
|
228
|
+
return composedSpan(span, candidate, locationFromOffsets(
|
|
229
|
+
outer.generated.filename,
|
|
230
|
+
outer.generated.content,
|
|
231
|
+
generatedStart,
|
|
232
|
+
generatedEnd
|
|
233
|
+
), preservesWholeOrigin, selectedRelated, true)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const preservesWholeOrigin = Boolean(location && location.start.offset == traced.generated.start.offset &&
|
|
238
|
+
location.end.offset == traced.generated.end.offset)
|
|
239
|
+
const composed = composedSpan(span, traced, span.generated, preservesWholeOrigin, selectedRelated)
|
|
240
|
+
|
|
241
|
+
if (span.origin.kind == "source" && overlapping.length > 1) {
|
|
242
|
+
const origins = composeRelatedOrigins([selectedRelated], selectedRelated, traced, false)
|
|
243
|
+
|
|
244
|
+
if (origins.length > 0) {
|
|
245
|
+
return [{...composed, mappingKind: /** @type {const} */ ("anchor"), origin: {
|
|
246
|
+
kind: /** @type {const} */ ("derived"),
|
|
247
|
+
origins
|
|
248
|
+
}}]
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return [composed]
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
return finalizeMapping({
|
|
256
|
+
coordinateSystem: "utf16",
|
|
257
|
+
generated: outer.generated,
|
|
258
|
+
nodes: inner.nodes,
|
|
259
|
+
schema: "SemantifoldMapping",
|
|
260
|
+
sources,
|
|
261
|
+
spans,
|
|
262
|
+
symbols: inner.symbols,
|
|
263
|
+
version: 1
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Composes one outer generated range with an inner range.
|
|
268
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan} outerSpan - Outer span.
|
|
269
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan} innerSpan - Inner span.
|
|
270
|
+
* @param {import("./semantic/types.js").SourceLocation} generated - Final generated subrange.
|
|
271
|
+
* @param {boolean} preservesWholeOrigin - Whether adopting the inner origin retains its complete range.
|
|
272
|
+
* @param {import("./semantic/types.js").RelatedOrigin} selectedRelated - Outer origin traced through this inner span.
|
|
273
|
+
* @param {boolean} [restrictSelectedTrace] - Whether an exact split selects only this inner span.
|
|
274
|
+
* @returns {import("./semantic/types.js").SemantifoldMappingSpan} Composed span.
|
|
275
|
+
*/
|
|
276
|
+
function composedSpan(outerSpan, innerSpan, generated, preservesWholeOrigin, selectedRelated, restrictSelectedTrace = false) {
|
|
277
|
+
if (outerSpan.mappingKind == "synthetic" && outerSpan.origin.kind == "synthetic") {
|
|
278
|
+
return definedProperties({
|
|
279
|
+
generated,
|
|
280
|
+
mappingKind: /** @type {const} */ ("synthetic"),
|
|
281
|
+
name: outerSpan.name,
|
|
282
|
+
origin: {
|
|
283
|
+
kind: /** @type {const} */ ("synthetic"),
|
|
284
|
+
reason: outerSpan.origin.reason,
|
|
285
|
+
relatedOrigins: composeRelatedOrigins(outerSpan.origin.relatedOrigins, selectedRelated, innerSpan, restrictSelectedTrace)
|
|
286
|
+
},
|
|
287
|
+
role: outerSpan.role
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const composedDerivedOrigins = outerSpan.origin.kind == "derived"
|
|
292
|
+
? composeRelatedOrigins(outerSpan.origin.origins, selectedRelated, innerSpan, restrictSelectedTrace)
|
|
293
|
+
: undefined
|
|
294
|
+
const origin = outerSpan.origin.kind == "source" ? innerSpan.origin : outerSpan.origin.kind == "derived"
|
|
295
|
+
? composedDerivedOrigins?.length == 0 && innerSpan.origin.kind == "synthetic"
|
|
296
|
+
? innerSpan.origin
|
|
297
|
+
: {kind: /** @type {const} */ ("derived"), origins: /** @type {import("./semantic/types.js").RelatedOrigin[]} */ (composedDerivedOrigins)}
|
|
298
|
+
: {
|
|
299
|
+
kind: /** @type {const} */ ("synthetic"),
|
|
300
|
+
reason: outerSpan.origin.reason,
|
|
301
|
+
relatedOrigins: composeRelatedOrigins(outerSpan.origin.relatedOrigins, selectedRelated, innerSpan, restrictSelectedTrace)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return definedProperties({
|
|
305
|
+
...outerSpan,
|
|
306
|
+
generated,
|
|
307
|
+
mappingKind: innerSpan.mappingKind == "synthetic" ? /** @type {const} */ ("synthetic") :
|
|
308
|
+
outerSpan.mappingKind == "exact" && innerSpan.mappingKind == "exact" && preservesWholeOrigin
|
|
309
|
+
? /** @type {const} */ ("exact")
|
|
310
|
+
: /** @type {const} */ ("anchor"),
|
|
311
|
+
name: outerSpan.name ?? innerSpan.name,
|
|
312
|
+
nodeId: innerSpan.nodeId,
|
|
313
|
+
origin,
|
|
314
|
+
role: innerSpan.role ?? outerSpan.role,
|
|
315
|
+
symbolId: innerSpan.symbolId
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Finds the inner span selected by one related intermediate origin.
|
|
321
|
+
* @param {import("./semantic/types.js").RelatedOrigin} related - Outer related origin.
|
|
322
|
+
* @returns {{
|
|
323
|
+
* overlapping: import("./semantic/types.js").SemantifoldMappingSpan[],
|
|
324
|
+
* related: import("./semantic/types.js").RelatedOrigin,
|
|
325
|
+
* traced: import("./semantic/types.js").SemantifoldMappingSpan | undefined
|
|
326
|
+
* } | undefined} Trace selection when the related source is the intermediate artifact.
|
|
327
|
+
*/
|
|
328
|
+
function traceRelatedOrigin(related) {
|
|
329
|
+
const source = outerIndex.sourcesById.get(related.sourceId)
|
|
330
|
+
|
|
331
|
+
if (!source || !sourceRepresentsGenerated(source, inner.generated)) return undefined
|
|
332
|
+
const overlapping = spansOverlappingGenerated(inner.spans, related.location.start.offset, related.location.end.offset)
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
overlapping,
|
|
336
|
+
related,
|
|
337
|
+
traced: overlapping[0] ?? spanForGenerated(inner.spans, related.location.start.offset)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Traces intermediate related origins and remaps unrelated source identities in order.
|
|
343
|
+
* @param {import("./semantic/types.js").RelatedOrigin[]} relatedOrigins - Outer related origins.
|
|
344
|
+
* @param {import("./semantic/types.js").RelatedOrigin} selectedRelated - Origin using the selected inner span.
|
|
345
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan} selectedSpan - Selected inner span.
|
|
346
|
+
* @param {boolean} restrictSelectedTrace - Whether an exact split selects only the supplied span.
|
|
347
|
+
* @returns {import("./semantic/types.js").RelatedOrigin[]} Composed related origins.
|
|
348
|
+
*/
|
|
349
|
+
function composeRelatedOrigins(relatedOrigins, selectedRelated, selectedSpan, restrictSelectedTrace) {
|
|
350
|
+
return relatedOrigins.flatMap((related) => {
|
|
351
|
+
const source = outerIndex.sourcesById.get(related.sourceId)
|
|
352
|
+
|
|
353
|
+
if (!source || !sourceRepresentsGenerated(source, inner.generated)) {
|
|
354
|
+
return [remapRelatedOriginSourceId(related, outerSourceIds)]
|
|
355
|
+
}
|
|
356
|
+
const trace = traceRelatedOrigin(related)
|
|
357
|
+
const tracedSpans = restrictSelectedTrace && related === selectedRelated
|
|
358
|
+
? [selectedSpan]
|
|
359
|
+
: trace?.overlapping.length
|
|
360
|
+
? trace.overlapping
|
|
361
|
+
: trace?.traced ? [trace.traced] : []
|
|
362
|
+
|
|
363
|
+
if (tracedSpans.length == 0) {
|
|
364
|
+
if (!isGeneratedEofPoint(related)) throw new RangeError(`Unable to trace intermediate origin '${related.location.filename}'.`)
|
|
365
|
+
retainOuterSource(related.sourceId)
|
|
366
|
+
|
|
367
|
+
return [remapRelatedOriginSourceId(related, outerSourceIds)]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return tracedSpans.flatMap((tracedSpan) => relatedOriginsForSpan(tracedSpan).map((tracedRelated) => definedProperties({
|
|
371
|
+
...tracedRelated,
|
|
372
|
+
nodeId: tracedRelated.nodeId ?? tracedSpan.nodeId,
|
|
373
|
+
role: related.role ?? tracedRelated.role,
|
|
374
|
+
symbolId: tracedRelated.symbolId ?? tracedSpan.symbolId
|
|
375
|
+
})))
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Retains one outer source that survives composition and assigns its composed identity.
|
|
381
|
+
* @param {string} sourceId - Outer source registry identity.
|
|
382
|
+
* @returns {void}
|
|
383
|
+
*/
|
|
384
|
+
function retainOuterSource(sourceId) {
|
|
385
|
+
if (outerSourceIds.has(sourceId)) return
|
|
386
|
+
const source = outerIndex.sourcesById.get(sourceId)
|
|
387
|
+
|
|
388
|
+
if (!source) throw new RangeError(`Unknown outer mapping source '${sourceId}'.`)
|
|
389
|
+
let composedSource = sources.find((candidate) => sameRegisteredSource(candidate, source))
|
|
390
|
+
|
|
391
|
+
if (!composedSource) {
|
|
392
|
+
while (sourceIds.has(`source:${nextSourceIndex}`)) nextSourceIndex++
|
|
393
|
+
composedSource = {...source, id: `source:${nextSourceIndex}`}
|
|
394
|
+
sourceIds.add(composedSource.id)
|
|
395
|
+
sources.push(composedSource)
|
|
396
|
+
}
|
|
397
|
+
outerSourceIds.set(source.id, composedSource.id)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Tests whether one intermediate origin is the exact point after all generated code units.
|
|
402
|
+
* @param {import("./semantic/types.js").RelatedOrigin} related - Candidate intermediate origin.
|
|
403
|
+
* @returns {boolean} Whether this is an EOF point rather than a generated half-open range.
|
|
404
|
+
*/
|
|
405
|
+
function isGeneratedEofPoint(related) {
|
|
406
|
+
return related.location.start.offset == inner.generated.content.length &&
|
|
407
|
+
related.location.end.offset == inner.generated.content.length
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Converts one mapped span's closed origin into related provenance.
|
|
413
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan} span - Traced inner span.
|
|
414
|
+
* @returns {import("./semantic/types.js").RelatedOrigin[]} Related original ranges.
|
|
415
|
+
*/
|
|
416
|
+
function relatedOriginsForSpan(span) {
|
|
417
|
+
return relatedOriginsForOrigin(span.origin)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Returns every related range in one closed origin.
|
|
422
|
+
* @param {import("./semantic/types.js").SemanticOrigin} origin - Closed origin.
|
|
423
|
+
* @returns {import("./semantic/types.js").RelatedOrigin[]} Related ranges in semantic order.
|
|
424
|
+
*/
|
|
425
|
+
function relatedOriginsForOrigin(origin) {
|
|
426
|
+
if (origin.kind == "source") return [{location: origin.location, sourceId: origin.sourceId}]
|
|
427
|
+
if (origin.kind == "derived") return origin.origins
|
|
428
|
+
|
|
429
|
+
return origin.relatedOrigins
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Determines whether a registered source is the generated artifact consumed by an inner map.
|
|
434
|
+
* @param {import("./semantic/types.js").RegisteredSource} source - Outer source registry entry.
|
|
435
|
+
* @param {import("./semantic/types.js").GeneratedSource} generated - Inner generated artifact.
|
|
436
|
+
* @returns {boolean} Whether identity-selected provenance may be traced through the inner map.
|
|
437
|
+
*/
|
|
438
|
+
function sourceRepresentsGenerated(source, generated) {
|
|
439
|
+
return source.filename == generated.filename && source.content == generated.content &&
|
|
440
|
+
(source.language == null || source.language == generated.language)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Compares complete registered-source identity fields other than registry-local ID.
|
|
445
|
+
* @param {import("./semantic/types.js").RegisteredSource} left - First registered source.
|
|
446
|
+
* @param {import("./semantic/types.js").RegisteredSource} right - Second registered source.
|
|
447
|
+
* @returns {boolean} Whether both entries represent the same source artifact.
|
|
448
|
+
*/
|
|
449
|
+
function sameRegisteredSource(left, right) {
|
|
450
|
+
return left.filename == right.filename && left.content == right.content && left.language == right.language
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Composes Source Map v3 maps with maintained tracing machinery.
|
|
455
|
+
* @param {import("@jridgewell/trace-mapping").SourceMapInput} outer - Final output to intermediate map.
|
|
456
|
+
* @param {import("@jridgewell/trace-mapping").SourceMapInput} inner - Intermediate to original map.
|
|
457
|
+
* @returns {import("@jridgewell/gen-mapping").EncodedSourceMap} Composed v3 map.
|
|
458
|
+
*/
|
|
459
|
+
export function composeSourceMaps(outer, inner) {
|
|
460
|
+
const outerMap = new TraceMap(outer)
|
|
461
|
+
const innerMap = new TraceMap(inner)
|
|
462
|
+
const composed = new GenMapping({file: outerMap.file})
|
|
463
|
+
const intermediateIndexes = typeof innerMap.file == "string"
|
|
464
|
+
? outerMap.resolvedSources.flatMap((source, index) => source == innerMap.file ? [index] : [])
|
|
465
|
+
: []
|
|
466
|
+
const intermediateIndex = intermediateIndexes.length == 1 ? intermediateIndexes[0] : undefined
|
|
467
|
+
const intermediateSource = intermediateIndex === undefined ? undefined : outerMap.resolvedSources[intermediateIndex]
|
|
468
|
+
|
|
469
|
+
innerMap.sources.forEach((rawSource, index) => {
|
|
470
|
+
const source = innerMap.resolvedSources[index] ?? rawSource
|
|
471
|
+
|
|
472
|
+
if (source != null) setSourceContent(composed, source, innerMap.sourcesContent?.[index] ?? null)
|
|
473
|
+
})
|
|
474
|
+
outerMap.sources.forEach((rawSource, index) => {
|
|
475
|
+
const source = outerMap.resolvedSources[index] ?? rawSource
|
|
476
|
+
|
|
477
|
+
if (source != null && index != intermediateIndex) {
|
|
478
|
+
setSourceContent(composed, source, outerMap.sourcesContent?.[index] ?? null)
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
eachMapping(outerMap, (mapping) => {
|
|
483
|
+
if (mapping.source == null || mapping.originalLine == null || mapping.originalColumn == null) {
|
|
484
|
+
maybeAddSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn)
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const isIntermediate = intermediateSource !== undefined && mapping.source == intermediateSource
|
|
489
|
+
const traced = isIntermediate
|
|
490
|
+
? traceOriginalPositionFor(innerMap, {column: mapping.originalColumn, line: mapping.originalLine})
|
|
491
|
+
: {column: mapping.originalColumn, line: mapping.originalLine, name: mapping.name, source: mapping.source}
|
|
492
|
+
|
|
493
|
+
if (traced.source == null || traced.line == null || traced.column == null) {
|
|
494
|
+
maybeAddSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn)
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const content = isIntermediate ? sourceContentFor(innerMap, traced.source) : sourceContentFor(outerMap, traced.source)
|
|
499
|
+
const name = traced.name ?? mapping.name
|
|
500
|
+
|
|
501
|
+
if (name == null) {
|
|
502
|
+
addSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn, traced.source, traced.line - 1, traced.column, null, content)
|
|
503
|
+
} else {
|
|
504
|
+
addSegment(composed, mapping.generatedLine - 1, mapping.generatedColumn, traced.source, traced.line - 1, traced.column, name, content)
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
return toEncodedMap(composed)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Imports a point-based Source Map v3 as a range-based Semantifold map.
|
|
513
|
+
* @param {import("@jridgewell/trace-mapping").SourceMapInput} sourceMap - Source Map v3.
|
|
514
|
+
* @param {object} generated - Generated program.
|
|
515
|
+
* @param {string} generated.content - Exact generated content.
|
|
516
|
+
* @param {string} generated.filename - Generated filename.
|
|
517
|
+
* @param {import("./semantic/types.js").SemanticLanguage} generated.language - Generated language.
|
|
518
|
+
* @param {{filename: string, content: string, language?: import("./semantic/types.js").SemanticLanguage}[]} [generated.sources] - Exact source content overrides.
|
|
519
|
+
* @returns {import("./semantic/types.js").SemantifoldMapping} Imported rich map.
|
|
520
|
+
*/
|
|
521
|
+
export function mappingFromSourceMap(sourceMap, {content, filename, language, sources: provided = []}) {
|
|
522
|
+
const trace = new TraceMap(sourceMap)
|
|
523
|
+
const generatedCoordinates = createCoordinateIndex(content)
|
|
524
|
+
/** @type {import("./semantic/types.js").RegisteredSource[]} */
|
|
525
|
+
const sources = []
|
|
526
|
+
/** @type {Map<number, {filename: string, content: string, language?: import("./semantic/types.js").SemanticLanguage}>} */
|
|
527
|
+
const overridesBySourceIndex = new Map()
|
|
528
|
+
|
|
529
|
+
for (const override of provided) {
|
|
530
|
+
const exact = trace.sources.flatMap((source, index) => {
|
|
531
|
+
const resolved = trace.resolvedSources[index]
|
|
532
|
+
|
|
533
|
+
return source == override.filename || resolved == override.filename ? [index] : []
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
if (exact.length > 0) {
|
|
537
|
+
for (const index of exact) overridesBySourceIndex.set(index, override)
|
|
538
|
+
continue
|
|
539
|
+
}
|
|
540
|
+
const suffix = trace.sources.flatMap((source, index) => {
|
|
541
|
+
const resolved = trace.resolvedSources[index]
|
|
542
|
+
|
|
543
|
+
return source?.endsWith(`/${override.filename}`) || resolved?.endsWith(`/${override.filename}`) ? [index] : []
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
if (suffix.length > 1) {
|
|
547
|
+
throw new RangeError(`Ambiguous Source Map source content override '${override.filename}' matches multiple sources.`)
|
|
548
|
+
}
|
|
549
|
+
if (suffix.length == 1 && !overridesBySourceIndex.has(suffix[0])) overridesBySourceIndex.set(suffix[0], override)
|
|
550
|
+
}
|
|
551
|
+
/** @type {Map<string, import("./semantic/types.js").RegisteredSource>} */
|
|
552
|
+
const sourcesByMapName = new Map()
|
|
553
|
+
|
|
554
|
+
trace.sources.forEach((source, index) => {
|
|
555
|
+
if (source == null) return
|
|
556
|
+
|
|
557
|
+
const resolvedSource = trace.resolvedSources[index] ?? source
|
|
558
|
+
const providedSource = overridesBySourceIndex.get(index)
|
|
559
|
+
const registered = {
|
|
560
|
+
content: providedSource?.content ?? trace.sourcesContent?.[index] ?? null,
|
|
561
|
+
filename: providedSource?.filename ?? resolvedSource,
|
|
562
|
+
id: `source:${sources.length}`,
|
|
563
|
+
language: providedSource?.language ?? null
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
sources.push(registered)
|
|
567
|
+
if (!sourcesByMapName.has(resolvedSource)) sourcesByMapName.set(resolvedSource, registered)
|
|
568
|
+
})
|
|
569
|
+
const sourceCoordinates = new Map(sources.filter((source) => source.content != null)
|
|
570
|
+
.map((source) => [source.id, createCoordinateIndex(/** @type {string} */ (source.content))]))
|
|
571
|
+
|
|
572
|
+
/** @type {{generatedOffset: number, mapping: import("@jridgewell/trace-mapping").EachMapping}[]} */
|
|
573
|
+
const points = []
|
|
574
|
+
|
|
575
|
+
eachMapping(trace, (mapping) => {
|
|
576
|
+
points.push({generatedOffset: indexedOffsetAt(generatedCoordinates, mapping.generatedLine, mapping.generatedColumn + 1), mapping})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
/** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */
|
|
580
|
+
const mappedSpans = []
|
|
581
|
+
|
|
582
|
+
points.forEach(({generatedOffset, mapping}, index) => {
|
|
583
|
+
const next = points[index + 1]
|
|
584
|
+
const end = next?.mapping.generatedLine == mapping.generatedLine
|
|
585
|
+
? next.generatedOffset
|
|
586
|
+
: lineEndOffset(content, generatedOffset)
|
|
587
|
+
|
|
588
|
+
if (end <= generatedOffset) return
|
|
589
|
+
|
|
590
|
+
if (mapping.source == null || mapping.originalLine == null || mapping.originalColumn == null) {
|
|
591
|
+
mappedSpans.push({
|
|
592
|
+
generated: locationFromOffsets(filename, content, generatedOffset, end),
|
|
593
|
+
mappingKind: "synthetic",
|
|
594
|
+
origin: {kind: "synthetic", reason: "unmapped Source Map segment", relatedOrigins: []}
|
|
595
|
+
})
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const source = sourcesByMapName.get(mapping.source)
|
|
600
|
+
|
|
601
|
+
if (!source) throw new RangeError(`Source Map references unregistered source '${mapping.source}'.`)
|
|
602
|
+
if (source.content == null) {
|
|
603
|
+
throw new RangeError(`Source content is required to import mapped positions for '${source.filename}'.`)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const originalOffset = indexedOffsetAt(
|
|
607
|
+
/** @type {ReturnType<typeof createCoordinateIndex>} */ (sourceCoordinates.get(source.id)),
|
|
608
|
+
mapping.originalLine,
|
|
609
|
+
mapping.originalColumn + 1
|
|
610
|
+
)
|
|
611
|
+
const originalEnd = Math.min(source.content.length, originalOffset + 1)
|
|
612
|
+
const originalLocation = locationFromOffsets(source.filename, source.content, originalOffset, originalEnd)
|
|
613
|
+
|
|
614
|
+
mappedSpans.push(definedProperties({
|
|
615
|
+
generated: locationFromOffsets(filename, content, generatedOffset, end),
|
|
616
|
+
mappingKind: "anchor",
|
|
617
|
+
name: mapping.name ?? undefined,
|
|
618
|
+
origin: {kind: "source", location: originalLocation, sourceId: source.id}
|
|
619
|
+
}))
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
/** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */
|
|
623
|
+
const spans = []
|
|
624
|
+
let cursor = 0
|
|
625
|
+
|
|
626
|
+
for (const span of mappedSpans) {
|
|
627
|
+
if (span.generated.start.offset > cursor) spans.push(unmappedSpan(cursor, span.generated.start.offset))
|
|
628
|
+
if (span.generated.end.offset > cursor) {
|
|
629
|
+
spans.push(span.generated.start.offset < cursor ? {
|
|
630
|
+
...span,
|
|
631
|
+
generated: locationFromOffsets(filename, content, cursor, span.generated.end.offset)
|
|
632
|
+
} : span)
|
|
633
|
+
cursor = span.generated.end.offset
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (cursor < content.length) spans.push(unmappedSpan(cursor, content.length))
|
|
637
|
+
|
|
638
|
+
return finalizeMapping({
|
|
639
|
+
coordinateSystem: "utf16",
|
|
640
|
+
generated: {content, filename, language},
|
|
641
|
+
nodes: [],
|
|
642
|
+
schema: "SemantifoldMapping",
|
|
643
|
+
sources,
|
|
644
|
+
spans,
|
|
645
|
+
symbols: [],
|
|
646
|
+
version: 1
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Creates one explicit unmapped range.
|
|
651
|
+
* @param {number} start - Generated start offset.
|
|
652
|
+
* @param {number} end - Generated end offset.
|
|
653
|
+
* @returns {import("./semantic/types.js").SemantifoldMappingSpan} Synthetic span.
|
|
654
|
+
*/
|
|
655
|
+
function unmappedSpan(start, end) {
|
|
656
|
+
return {
|
|
657
|
+
generated: locationFromOffsets(filename, content, start, end),
|
|
658
|
+
mappingKind: "synthetic",
|
|
659
|
+
origin: {kind: "synthetic", reason: "unmapped Source Map range", relatedOrigins: []}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Remaps a generated location while retaining the generated range in the result.
|
|
666
|
+
* @param {import("./semantic/types.js").SourceLocation} location - Generated location.
|
|
667
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
668
|
+
* @returns {ReturnType<typeof originalPositionFor>} Remapped lookup.
|
|
669
|
+
*/
|
|
670
|
+
export function remapLocation(location, mapping) {
|
|
671
|
+
if (location.filename != mapping.generated.filename) throw new RangeError(`Location is not in ${mapping.generated.filename}.`)
|
|
672
|
+
|
|
673
|
+
return originalPositionFor(mapping, {offset: location.start.offset})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Remaps a Semantifold diagnostic and preserves its generated location and cause.
|
|
678
|
+
* @param {SemantifoldDiagnostic} diagnostic - Generated-program diagnostic.
|
|
679
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Rich mapping.
|
|
680
|
+
* @returns {SemantifoldDiagnostic} Remapped diagnostic.
|
|
681
|
+
*/
|
|
682
|
+
export function remapDiagnostic(diagnostic, mapping) {
|
|
683
|
+
if (!diagnostic.location) return diagnostic
|
|
684
|
+
|
|
685
|
+
const remapped = remapLocation(diagnostic.location, mapping)
|
|
686
|
+
|
|
687
|
+
if (!remapped.location) return diagnostic
|
|
688
|
+
|
|
689
|
+
return new SemantifoldDiagnostic({
|
|
690
|
+
cause: diagnostic,
|
|
691
|
+
code: diagnostic.code,
|
|
692
|
+
generatedLocation: diagnostic.location,
|
|
693
|
+
language: diagnostic.language,
|
|
694
|
+
location: remapped.location,
|
|
695
|
+
message: diagnostic.detail
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Validates the public mapping envelope and ordered half-open generated ranges.
|
|
701
|
+
* @param {unknown} value - Candidate mapping.
|
|
702
|
+
* @returns {asserts value is import("./semantic/types.js").SemantifoldMapping} Nothing when valid.
|
|
703
|
+
*/
|
|
704
|
+
export function validateMapping(value) {
|
|
705
|
+
if (!value || typeof value != "object" || Reflect.get(value, "schema") != "SemantifoldMapping" ||
|
|
706
|
+
Reflect.get(value, "version") != 1 || Reflect.get(value, "coordinateSystem") != "utf16") {
|
|
707
|
+
throw new TypeError("Expected a SemantifoldMapping v1 value.")
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const mapping = /** @type {Record<string, unknown>} */ (value)
|
|
711
|
+
const generated = /** @type {Record<string, unknown>} */ (mapping.generated)
|
|
712
|
+
const spans = mapping.spans
|
|
713
|
+
|
|
714
|
+
if (!generated || typeof generated != "object" || typeof Reflect.get(generated, "filename") != "string" ||
|
|
715
|
+
typeof Reflect.get(generated, "content") != "string" || !isLanguage(Reflect.get(generated, "language")) ||
|
|
716
|
+
!Array.isArray(mapping.sources) ||
|
|
717
|
+
!Array.isArray(mapping.nodes) || !Array.isArray(mapping.symbols) || !Array.isArray(spans)) {
|
|
718
|
+
throw new TypeError("Malformed SemantifoldMapping v1 envelope.")
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const content = /** @type {string} */ (generated.content)
|
|
722
|
+
const filename = /** @type {string} */ (generated.filename)
|
|
723
|
+
|
|
724
|
+
if (content.includes("\r")) throw new TypeError("SemantifoldMapping generated content must use LF line endings.")
|
|
725
|
+
|
|
726
|
+
const sourceIds = new Set()
|
|
727
|
+
|
|
728
|
+
for (const source of mapping.sources) {
|
|
729
|
+
if (!source || typeof source != "object" || typeof source.id != "string" || source.id.length == 0 || sourceIds.has(source.id) ||
|
|
730
|
+
typeof source.filename != "string" || source.filename.length == 0 || source.content !== null && typeof source.content != "string" ||
|
|
731
|
+
source.language !== null && !isLanguage(source.language)) {
|
|
732
|
+
throw new TypeError("Malformed or duplicate SemantifoldMapping source.")
|
|
733
|
+
}
|
|
734
|
+
sourceIds.add(source.id)
|
|
735
|
+
}
|
|
736
|
+
const sourceCoordinates = new Map(mapping.sources.filter((source) => source.content != null)
|
|
737
|
+
.map((source) => [source.id, createCoordinateIndex(/** @type {string} */ (source.content))]))
|
|
738
|
+
const sourcesById = new Map(mapping.sources.map((source) => [source.id, source]))
|
|
739
|
+
/** @type {Map<string, import("./semantic/types.js").RegisteredSource[]>} */
|
|
740
|
+
const sourcesByFilename = new Map()
|
|
741
|
+
|
|
742
|
+
for (const source of mapping.sources) {
|
|
743
|
+
const sameName = sourcesByFilename.get(source.filename) ?? []
|
|
744
|
+
|
|
745
|
+
sameName.push(source)
|
|
746
|
+
sourcesByFilename.set(source.filename, sameName)
|
|
747
|
+
}
|
|
748
|
+
const generatedCoordinates = createCoordinateIndex(content)
|
|
749
|
+
|
|
750
|
+
const nodeIds = new Set()
|
|
751
|
+
const nodePaths = new Set()
|
|
752
|
+
|
|
753
|
+
for (const node of mapping.nodes) {
|
|
754
|
+
if (!node || typeof node != "object" || typeof node.id != "string" || node.id.length == 0 || nodeIds.has(node.id) ||
|
|
755
|
+
typeof node.path != "string" || nodePaths.has(node.path) ||
|
|
756
|
+
typeof node.kind != "string" || node.kind.length == 0 || !node.origin || typeof node.ranges != "object" ||
|
|
757
|
+
node.ranges === null || Array.isArray(node.ranges)) throw new TypeError("Malformed or duplicate semantic node identity.")
|
|
758
|
+
nodeIds.add(node.id)
|
|
759
|
+
nodePaths.add(node.path)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const symbolIds = new Set()
|
|
763
|
+
|
|
764
|
+
for (const symbol of mapping.symbols) {
|
|
765
|
+
if (!symbol || typeof symbol != "object" || typeof symbol.id != "string" || symbol.id.length == 0 || symbolIds.has(symbol.id) ||
|
|
766
|
+
typeof symbol.name != "string" || symbol.name.length == 0 || !["function", "parameter", "local"].includes(symbol.kind) ||
|
|
767
|
+
!nodeIds.has(symbol.declarationNodeId) || !Array.isArray(symbol.references)) {
|
|
768
|
+
throw new TypeError("Malformed or duplicate semantic symbol identity.")
|
|
769
|
+
}
|
|
770
|
+
symbolIds.add(symbol.id)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for (const node of mapping.nodes) {
|
|
774
|
+
validateOrigin(node.origin, sourcesById, sourceCoordinates, nodeIds, symbolIds)
|
|
775
|
+
for (const [role, location] of Object.entries(node.ranges)) {
|
|
776
|
+
if (role.length == 0) throw new TypeError("Semantic node range roles must be non-empty strings.")
|
|
777
|
+
validateLocationForSources(location, sourcesByFilename, sourceCoordinates)
|
|
778
|
+
}
|
|
779
|
+
if (node.symbolId !== undefined && (typeof node.symbolId != "string" || !symbolIds.has(node.symbolId))) {
|
|
780
|
+
throw new TypeError("Semantic node references an unknown symbol.")
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
for (const symbol of mapping.symbols) {
|
|
785
|
+
validateLocationForSources(symbol.location, sourcesByFilename, sourceCoordinates)
|
|
786
|
+
for (const reference of symbol.references) {
|
|
787
|
+
if (!reference || typeof reference != "object" || !nodeIds.has(reference.nodeId) ||
|
|
788
|
+
!["declaration", "read", "write", "call"].includes(reference.role)) {
|
|
789
|
+
throw new TypeError("Malformed semantic symbol reference.")
|
|
790
|
+
}
|
|
791
|
+
validateLocationForSources(reference.location, sourcesByFilename, sourceCoordinates)
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
let previousEnd = 0
|
|
796
|
+
|
|
797
|
+
for (const span of spans) {
|
|
798
|
+
if (!span || typeof span != "object" || !["exact", "anchor", "synthetic"].includes(Reflect.get(span, "mappingKind")) ||
|
|
799
|
+
!Reflect.get(span, "generated") || !Reflect.get(span, "origin")) {
|
|
800
|
+
throw new TypeError("Malformed SemantifoldMapping span.")
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const location = /** @type {import("./semantic/types.js").SourceLocation} */ (Reflect.get(span, "generated"))
|
|
804
|
+
|
|
805
|
+
if (!location.start || !location.end || location.filename != filename || location.start.offset != previousEnd ||
|
|
806
|
+
location.end.offset <= location.start.offset || location.end.offset > content.length ||
|
|
807
|
+
!samePoint(indexedPointAt(generatedCoordinates, location.start.offset), location.start) ||
|
|
808
|
+
!samePoint(indexedPointAt(generatedCoordinates, location.end.offset), location.end)) {
|
|
809
|
+
throw new TypeError("SemantifoldMapping spans must exactly cover ordered normalized generated ranges.")
|
|
810
|
+
}
|
|
811
|
+
validateOrigin(
|
|
812
|
+
/** @type {import("./semantic/types.js").SemanticOrigin} */ (Reflect.get(span, "origin")),
|
|
813
|
+
sourcesById,
|
|
814
|
+
sourceCoordinates,
|
|
815
|
+
nodeIds,
|
|
816
|
+
symbolIds
|
|
817
|
+
)
|
|
818
|
+
const nodeId = Reflect.get(span, "nodeId")
|
|
819
|
+
const symbolId = Reflect.get(span, "symbolId")
|
|
820
|
+
|
|
821
|
+
if (nodeId !== undefined && (typeof nodeId != "string" || !nodeIds.has(nodeId))) throw new TypeError("Mapping span references an unknown node.")
|
|
822
|
+
if (symbolId !== undefined && (typeof symbolId != "string" || !symbolIds.has(symbolId))) throw new TypeError("Mapping span references an unknown symbol.")
|
|
823
|
+
if (Reflect.get(span, "role") !== undefined && typeof Reflect.get(span, "role") != "string" ||
|
|
824
|
+
Reflect.get(span, "name") !== undefined && typeof Reflect.get(span, "name") != "string") {
|
|
825
|
+
throw new TypeError("Mapping span roles and names must be strings.")
|
|
826
|
+
}
|
|
827
|
+
previousEnd = location.end.offset
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (previousEnd != content.length) throw new TypeError("SemantifoldMapping spans must cover all generated content.")
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Validates, freezes, and indexes one mapping produced at a trust boundary.
|
|
835
|
+
* @param {unknown} value - Candidate mapping.
|
|
836
|
+
* @returns {import("./semantic/types.js").SemantifoldMapping} Immutable indexed mapping.
|
|
837
|
+
*/
|
|
838
|
+
export function finalizeMapping(value) {
|
|
839
|
+
// Producers may retain references to mutable semantic locations; mappings own their immutable copy.
|
|
840
|
+
const mapping = structuredClone(value)
|
|
841
|
+
|
|
842
|
+
validateMapping(mapping)
|
|
843
|
+
freezeJson(mapping)
|
|
844
|
+
mappingIndexes.set(mapping, buildMappingIndex(mapping))
|
|
845
|
+
|
|
846
|
+
return mapping
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Gets an index, validating mutable caller mappings on every trust-boundary use.
|
|
851
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Mapping.
|
|
852
|
+
* @returns {MappingIndex} Lookup index.
|
|
853
|
+
*/
|
|
854
|
+
function mappingIndex(mapping) {
|
|
855
|
+
const cached = mappingIndexes.get(mapping)
|
|
856
|
+
|
|
857
|
+
if (cached) return cached
|
|
858
|
+
|
|
859
|
+
validateMapping(mapping)
|
|
860
|
+
|
|
861
|
+
return buildMappingIndex(mapping)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Builds generated, original, node, and symbol indexes in one pass.
|
|
866
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Validated mapping.
|
|
867
|
+
* @returns {{generatedCoordinates: ReturnType<typeof createCoordinateIndex>, nodeSpans: Map<string, import("./semantic/types.js").SemantifoldMappingSpan[]>, originalBySource: Map<string, {end: number, prefixEnd: number, span: import("./semantic/types.js").SemantifoldMappingSpan, start: number}[]>, originalPointsBySource: Map<string, Map<number, import("./semantic/types.js").SemantifoldMappingSpan[]>>, sourceCoordinates: Map<string, ReturnType<typeof createCoordinateIndex>>, sourcesById: Map<string, import("./semantic/types.js").RegisteredSource>, symbolSpans: Map<string, import("./semantic/types.js").SemantifoldMappingSpan[]>}} Mapping index.
|
|
868
|
+
*/
|
|
869
|
+
function buildMappingIndex(mapping) {
|
|
870
|
+
const generatedCoordinates = createCoordinateIndex(mapping.generated.content)
|
|
871
|
+
const sourcesById = new Map(mapping.sources.map((source) => [source.id, source]))
|
|
872
|
+
const sourceCoordinates = new Map(mapping.sources.filter((source) => source.content != null)
|
|
873
|
+
.map((source) => [source.id, createCoordinateIndex(/** @type {string} */ (source.content))]))
|
|
874
|
+
const nodeSpans = new Map(mapping.nodes.map((node) => [node.id, /** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */ ([])]))
|
|
875
|
+
const symbolSpans = new Map(mapping.symbols.map((symbol) => [symbol.id, /** @type {import("./semantic/types.js").SemantifoldMappingSpan[]} */ ([])]))
|
|
876
|
+
/** @type {Map<string, {end: number, prefixEnd: number, span: import("./semantic/types.js").SemantifoldMappingSpan, start: number}[]>} */
|
|
877
|
+
const originalBySource = new Map(mapping.sources.map((source) => [source.id, []]))
|
|
878
|
+
/** @type {Map<string, Map<number, import("./semantic/types.js").SemantifoldMappingSpan[]>>} */
|
|
879
|
+
const originalPointsBySource = new Map(mapping.sources.map((source) => [source.id, new Map()]))
|
|
880
|
+
|
|
881
|
+
for (const span of mapping.spans) {
|
|
882
|
+
const spanNodeIds = new Set(span.nodeId ? [span.nodeId] : [])
|
|
883
|
+
const spanSymbolIds = new Set(span.symbolId ? [span.symbolId] : [])
|
|
884
|
+
|
|
885
|
+
for (const related of relatedOriginsForOrigin(span.origin)) {
|
|
886
|
+
if (related.nodeId) spanNodeIds.add(related.nodeId)
|
|
887
|
+
if (related.symbolId) spanSymbolIds.add(related.symbolId)
|
|
888
|
+
if (related.location.end.offset == related.location.start.offset) {
|
|
889
|
+
const points = originalPointsBySource.get(related.sourceId)
|
|
890
|
+
const pointSpans = points?.get(related.location.start.offset) ?? []
|
|
891
|
+
|
|
892
|
+
pointSpans.push(span)
|
|
893
|
+
points?.set(related.location.start.offset, pointSpans)
|
|
894
|
+
continue
|
|
895
|
+
}
|
|
896
|
+
originalBySource.get(related.sourceId)?.push({
|
|
897
|
+
end: related.location.end.offset,
|
|
898
|
+
prefixEnd: related.location.end.offset,
|
|
899
|
+
span,
|
|
900
|
+
start: related.location.start.offset
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
for (const nodeId of spanNodeIds) nodeSpans.get(nodeId)?.push(span)
|
|
904
|
+
for (const symbolId of spanSymbolIds) symbolSpans.get(symbolId)?.push(span)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
for (const entries of originalBySource.values()) {
|
|
908
|
+
entries.sort((left, right) => left.start - right.start || left.span.generated.start.offset - right.span.generated.start.offset)
|
|
909
|
+
let prefixEnd = 0
|
|
910
|
+
|
|
911
|
+
for (const entry of entries) {
|
|
912
|
+
prefixEnd = Math.max(prefixEnd, entry.end)
|
|
913
|
+
entry.prefixEnd = prefixEnd
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return {generatedCoordinates, nodeSpans, originalBySource, originalPointsBySource, sourceCoordinates, sourcesById, symbolSpans}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Finds the generated span containing an offset by binary search.
|
|
922
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan[]} spans - Ordered spans.
|
|
923
|
+
* @param {number} offset - Generated offset.
|
|
924
|
+
* @returns {import("./semantic/types.js").SemantifoldMappingSpan | undefined} Containing span.
|
|
925
|
+
*/
|
|
926
|
+
function spanForGenerated(spans, offset) {
|
|
927
|
+
let low = 0
|
|
928
|
+
let high = spans.length
|
|
929
|
+
|
|
930
|
+
while (low < high) {
|
|
931
|
+
const middle = Math.floor((low + high) / 2)
|
|
932
|
+
|
|
933
|
+
if (spans[middle].generated.start.offset <= offset) low = middle + 1
|
|
934
|
+
else high = middle
|
|
935
|
+
}
|
|
936
|
+
const candidate = spans[low - 1]
|
|
937
|
+
|
|
938
|
+
return candidate && offset < candidate.generated.end.offset ? candidate : undefined
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Finds ordered generated spans intersecting a half-open range.
|
|
943
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan[]} spans - Ordered spans.
|
|
944
|
+
* @param {number} start - Inclusive generated offset.
|
|
945
|
+
* @param {number} end - Exclusive generated offset.
|
|
946
|
+
* @returns {import("./semantic/types.js").SemantifoldMappingSpan[]} Overlapping spans.
|
|
947
|
+
*/
|
|
948
|
+
function spansOverlappingGenerated(spans, start, end) {
|
|
949
|
+
if (end <= start) {
|
|
950
|
+
const containing = spanForGenerated(spans, start)
|
|
951
|
+
|
|
952
|
+
return containing ? [containing] : []
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
let low = 0
|
|
956
|
+
let high = spans.length
|
|
957
|
+
|
|
958
|
+
while (low < high) {
|
|
959
|
+
const middle = Math.floor((low + high) / 2)
|
|
960
|
+
|
|
961
|
+
if (spans[middle].generated.end.offset <= start) low = middle + 1
|
|
962
|
+
else high = middle
|
|
963
|
+
}
|
|
964
|
+
const overlapping = []
|
|
965
|
+
|
|
966
|
+
for (let index = low; index < spans.length && spans[index].generated.start.offset < end; index++) {
|
|
967
|
+
overlapping.push(spans[index])
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return overlapping
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Deep-freezes a JSON tree.
|
|
975
|
+
* @param {unknown} value - JSON value.
|
|
976
|
+
* @param {WeakSet<object>} [visited] - Cycle protection for rejected caller extras.
|
|
977
|
+
* @returns {void}
|
|
978
|
+
*/
|
|
979
|
+
function freezeJson(value, visited = new WeakSet()) {
|
|
980
|
+
if (!value || typeof value != "object" || visited.has(value)) return
|
|
981
|
+
|
|
982
|
+
visited.add(value)
|
|
983
|
+
for (const child of Object.values(value)) freezeJson(child, visited)
|
|
984
|
+
if (!Object.isFrozen(value)) Object.freeze(value)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Validates one closed mapping origin.
|
|
989
|
+
* @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
|
|
990
|
+
* @param {Map<string, import("./semantic/types.js").RegisteredSource>} sourcesById - Sources by identity.
|
|
991
|
+
* @param {Map<string, ReturnType<typeof createCoordinateIndex>>} coordinates - Source coordinate indexes.
|
|
992
|
+
* @param {Set<unknown>} [nodeIds] - Known semantic nodes.
|
|
993
|
+
* @param {Set<unknown>} [symbolIds] - Known semantic symbols.
|
|
994
|
+
* @returns {void}
|
|
995
|
+
*/
|
|
996
|
+
function validateOrigin(origin, sourcesById, coordinates, nodeIds, symbolIds) {
|
|
997
|
+
if (!origin || typeof origin != "object") throw new TypeError("Mapping origin must be a closed provenance value.")
|
|
998
|
+
|
|
999
|
+
if (origin.kind == "source") {
|
|
1000
|
+
validateRelated({location: origin.location, sourceId: origin.sourceId}, sourcesById, coordinates, nodeIds, symbolIds)
|
|
1001
|
+
return
|
|
1002
|
+
}
|
|
1003
|
+
if (origin.kind == "derived") {
|
|
1004
|
+
if (!Array.isArray(origin.origins) || origin.origins.length == 0) throw new TypeError("Derived provenance requires origins.")
|
|
1005
|
+
origin.origins.forEach((related) => validateRelated(related, sourcesById, coordinates, nodeIds, symbolIds))
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
1008
|
+
if (origin.kind == "synthetic") {
|
|
1009
|
+
if (typeof origin.reason != "string" || !Array.isArray(origin.relatedOrigins)) throw new TypeError("Malformed synthetic provenance.")
|
|
1010
|
+
origin.relatedOrigins.forEach((related) => validateRelated(related, sourcesById, coordinates, nodeIds, symbolIds))
|
|
1011
|
+
return
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
throw new TypeError("Unknown provenance kind.")
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Validates one source-related origin.
|
|
1019
|
+
* @param {import("./semantic/types.js").RelatedOrigin} related - Related origin.
|
|
1020
|
+
* @param {Map<string, import("./semantic/types.js").RegisteredSource>} sourcesById - Sources by identity.
|
|
1021
|
+
* @param {Map<string, ReturnType<typeof createCoordinateIndex>>} coordinates - Source coordinate indexes.
|
|
1022
|
+
* @param {Set<unknown> | undefined} nodeIds - Known semantic nodes.
|
|
1023
|
+
* @param {Set<unknown> | undefined} symbolIds - Known semantic symbols.
|
|
1024
|
+
* @returns {void}
|
|
1025
|
+
*/
|
|
1026
|
+
function validateRelated(related, sourcesById, coordinates, nodeIds, symbolIds) {
|
|
1027
|
+
const source = sourcesById.get(related.sourceId)
|
|
1028
|
+
|
|
1029
|
+
if (!source || !related.location || related.location.filename != source.filename ||
|
|
1030
|
+
related.nodeId !== undefined && typeof related.nodeId != "string" ||
|
|
1031
|
+
related.symbolId !== undefined && typeof related.symbolId != "string" ||
|
|
1032
|
+
related.role !== undefined && typeof related.role != "string") throw new TypeError("Origin references an unknown source.")
|
|
1033
|
+
if (nodeIds && related.nodeId !== undefined && !nodeIds.has(related.nodeId)) throw new TypeError("Origin references an unknown node.")
|
|
1034
|
+
if (symbolIds && related.symbolId !== undefined && !symbolIds.has(related.symbolId)) {
|
|
1035
|
+
throw new TypeError("Origin references an unknown symbol.")
|
|
1036
|
+
}
|
|
1037
|
+
validateLocationForSource(related.location, source, coordinates.get(source.id))
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Validates a location against at least one same-filename source.
|
|
1042
|
+
* @param {unknown} value - Candidate location.
|
|
1043
|
+
* @param {Map<string, import("./semantic/types.js").RegisteredSource[]>} sourcesByFilename - Sources by filename.
|
|
1044
|
+
* @param {Map<string, ReturnType<typeof createCoordinateIndex>>} coordinates - Source coordinate indexes.
|
|
1045
|
+
* @returns {void}
|
|
1046
|
+
*/
|
|
1047
|
+
function validateLocationForSources(value, sourcesByFilename, coordinates) {
|
|
1048
|
+
if (!value || typeof value != "object") throw new TypeError("Malformed original location.")
|
|
1049
|
+
const location = /** @type {import("./semantic/types.js").SourceLocation} */ (value)
|
|
1050
|
+
const candidates = sourcesByFilename.get(location.filename) ?? []
|
|
1051
|
+
|
|
1052
|
+
if (candidates.length == 0 || !candidates.some((source) => locationMatchesSource(location, source, coordinates.get(source.id)))) {
|
|
1053
|
+
throw new TypeError("Stale original location.")
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Validates a location against its identified source.
|
|
1059
|
+
* @param {unknown} value - Candidate location.
|
|
1060
|
+
* @param {import("./semantic/types.js").RegisteredSource} source - Source.
|
|
1061
|
+
* @param {ReturnType<typeof createCoordinateIndex> | undefined} coordinates - Source coordinates when content exists.
|
|
1062
|
+
* @returns {void}
|
|
1063
|
+
*/
|
|
1064
|
+
function validateLocationForSource(value, source, coordinates) {
|
|
1065
|
+
if (!value || typeof value != "object" || !locationMatchesSource(
|
|
1066
|
+
/** @type {import("./semantic/types.js").SourceLocation} */ (value), source, coordinates
|
|
1067
|
+
)) throw new TypeError("Stale original location.")
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Tests a location against one source entry.
|
|
1072
|
+
* @param {import("./semantic/types.js").SourceLocation} location - Location.
|
|
1073
|
+
* @param {import("./semantic/types.js").RegisteredSource} source - Source.
|
|
1074
|
+
* @param {ReturnType<typeof createCoordinateIndex> | undefined} coordinates - Source coordinates when content exists.
|
|
1075
|
+
* @returns {boolean} Whether the range is normalized for the source.
|
|
1076
|
+
*/
|
|
1077
|
+
function locationMatchesSource(location, source, coordinates) {
|
|
1078
|
+
if (!location || location.filename != source.filename || !location.start || !location.end ||
|
|
1079
|
+
!validPoint(location.start) || !validPoint(location.end) || location.end.offset < location.start.offset) return false
|
|
1080
|
+
|
|
1081
|
+
if (source.content == null) return true
|
|
1082
|
+
|
|
1083
|
+
return location.end.offset <= source.content.length && coordinates !== undefined &&
|
|
1084
|
+
samePoint(indexedPointAt(coordinates, location.start.offset), location.start) &&
|
|
1085
|
+
samePoint(indexedPointAt(coordinates, location.end.offset), location.end)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Tests the structural validity of one source point.
|
|
1090
|
+
* @param {unknown} value - Candidate point.
|
|
1091
|
+
* @returns {value is import("./semantic/types.js").SourcePoint} Whether it is a point.
|
|
1092
|
+
*/
|
|
1093
|
+
function validPoint(value) {
|
|
1094
|
+
if (!value || typeof value != "object") return false
|
|
1095
|
+
const point = /** @type {Record<string, unknown>} */ (value)
|
|
1096
|
+
|
|
1097
|
+
return Number.isInteger(point.offset) && Number.isInteger(point.line) && Number.isInteger(point.column) &&
|
|
1098
|
+
/** @type {number} */ (point.offset) >= 0 && /** @type {number} */ (point.line) >= 1 && /** @type {number} */ (point.column) >= 1
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Tests a supported semantic language value.
|
|
1103
|
+
* @param {unknown} value - Candidate language.
|
|
1104
|
+
* @returns {value is import("./semantic/types.js").SemanticLanguage} Whether supported.
|
|
1105
|
+
*/
|
|
1106
|
+
function isLanguage(value) {
|
|
1107
|
+
return value == "php" || value == "ruby" || value == "javascript" || value == "typescript" || value == "java"
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Builds one stable lookup result.
|
|
1112
|
+
* @param {import("./semantic/types.js").SemantifoldMappingSpan} span - Mapping span.
|
|
1113
|
+
* @returns {{generatedLocation: import("./semantic/types.js").SourceLocation, location: import("./semantic/types.js").SourceLocation | undefined, mappingKind: import("./semantic/types.js").MappingKind, name: string | undefined, nodeId: string | undefined, role: string | undefined, symbolId: string | undefined}} Lookup result.
|
|
1114
|
+
*/
|
|
1115
|
+
function mappingResult(span) {
|
|
1116
|
+
return {
|
|
1117
|
+
generatedLocation: span.generated,
|
|
1118
|
+
location: primaryLocation(span.origin),
|
|
1119
|
+
mappingKind: span.mappingKind,
|
|
1120
|
+
name: span.name,
|
|
1121
|
+
nodeId: span.nodeId,
|
|
1122
|
+
role: span.role,
|
|
1123
|
+
symbolId: span.symbolId
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Resolves an offset-form or line/column-form position.
|
|
1129
|
+
* @param {ReturnType<typeof createCoordinateIndex>} coordinates - Source coordinate index.
|
|
1130
|
+
* @param {{offset: number} | {line: number, column: number}} position - Position.
|
|
1131
|
+
* @returns {number} UTF-16 offset.
|
|
1132
|
+
*/
|
|
1133
|
+
function positionOffset(coordinates, position) {
|
|
1134
|
+
if ("offset" in position) {
|
|
1135
|
+
indexedPointAt(coordinates, position.offset)
|
|
1136
|
+
|
|
1137
|
+
return position.offset
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return indexedOffsetAt(coordinates, position.line, position.column)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Finds the exclusive content offset before the next generated line break.
|
|
1145
|
+
* @param {string} content - Generated content.
|
|
1146
|
+
* @param {number} offset - Offset on the current line.
|
|
1147
|
+
* @returns {number} End of the current line or content.
|
|
1148
|
+
*/
|
|
1149
|
+
function lineEndOffset(content, offset) {
|
|
1150
|
+
const newline = content.indexOf("\n", offset)
|
|
1151
|
+
|
|
1152
|
+
return newline == -1 ? content.length : newline
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Compares complete normalized points.
|
|
1157
|
+
* @param {import("./semantic/types.js").SourcePoint} left - Left point.
|
|
1158
|
+
* @param {import("./semantic/types.js").SourcePoint} right - Right point.
|
|
1159
|
+
* @returns {boolean} Whether coordinates match.
|
|
1160
|
+
*/
|
|
1161
|
+
function samePoint(left, right) {
|
|
1162
|
+
return left.offset == right.offset && left.line == right.line && left.column == right.column
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Resolves a source registry entry for an origin.
|
|
1167
|
+
* @param {import("./semantic/types.js").SemantifoldMapping} mapping - Mapping.
|
|
1168
|
+
* @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
|
|
1169
|
+
* @param {import("./semantic/types.js").SourceLocation} location - Primary location.
|
|
1170
|
+
* @param {Map<string, import("./semantic/types.js").RegisteredSource>} sourcesById - Indexed sources.
|
|
1171
|
+
* @returns {import("./semantic/types.js").RegisteredSource} Registered source.
|
|
1172
|
+
*/
|
|
1173
|
+
function sourceForOrigin(mapping, origin, location, sourcesById) {
|
|
1174
|
+
const sourceId = sourceIdForOrigin(origin)
|
|
1175
|
+
const source = sourcesById.get(sourceId ?? "") ??
|
|
1176
|
+
mapping.sources.find((candidate) => candidate.filename == location.filename)
|
|
1177
|
+
|
|
1178
|
+
if (!source) throw new RangeError(`Unknown mapping source for ${location.filename}.`)
|
|
1179
|
+
|
|
1180
|
+
return source
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Resolves the registry identity of an origin's primary location.
|
|
1185
|
+
* @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
|
|
1186
|
+
* @returns {string | undefined} Source identity.
|
|
1187
|
+
*/
|
|
1188
|
+
function sourceIdForOrigin(origin) {
|
|
1189
|
+
if (origin.kind == "source") return origin.sourceId
|
|
1190
|
+
if (origin.kind == "derived") return origin.origins[0]?.sourceId
|
|
1191
|
+
|
|
1192
|
+
return origin.relatedOrigins[0]?.sourceId
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Rewrites closed-origin source identities against a composed registry.
|
|
1197
|
+
* @param {import("./semantic/types.js").SemanticOrigin} origin - Origin.
|
|
1198
|
+
* @param {Map<string, string>} sourceIds - Original-to-composed source identities.
|
|
1199
|
+
* @returns {import("./semantic/types.js").SemanticOrigin} Rewritten origin.
|
|
1200
|
+
*/
|
|
1201
|
+
function remapOriginSourceIds(origin, sourceIds) {
|
|
1202
|
+
if (origin.kind == "source") {
|
|
1203
|
+
const sourceId = sourceIds.get(origin.sourceId)
|
|
1204
|
+
|
|
1205
|
+
if (!sourceId) throw new RangeError(`Composed mapping omitted source '${origin.location.filename}'.`)
|
|
1206
|
+
|
|
1207
|
+
return {kind: "source", location: origin.location, sourceId}
|
|
1208
|
+
}
|
|
1209
|
+
if (origin.kind == "derived") return {kind: "derived", origins: origin.origins.map((related) => remapRelatedOriginSourceId(related, sourceIds))}
|
|
1210
|
+
|
|
1211
|
+
return {
|
|
1212
|
+
kind: "synthetic",
|
|
1213
|
+
reason: origin.reason,
|
|
1214
|
+
relatedOrigins: origin.relatedOrigins.map((related) => remapRelatedOriginSourceId(related, sourceIds))
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Rewrites one related source identity while dropping identities from a discarded node registry.
|
|
1220
|
+
* @param {import("./semantic/types.js").RelatedOrigin} related - Related origin.
|
|
1221
|
+
* @param {Map<string, string>} sourceIds - Original-to-composed source identities.
|
|
1222
|
+
* @returns {import("./semantic/types.js").RelatedOrigin} Rewritten related origin.
|
|
1223
|
+
*/
|
|
1224
|
+
function remapRelatedOriginSourceId(related, sourceIds) {
|
|
1225
|
+
const sourceId = sourceIds.get(related.sourceId)
|
|
1226
|
+
|
|
1227
|
+
if (!sourceId) throw new RangeError(`Composed mapping omitted source '${related.location.filename}'.`)
|
|
1228
|
+
|
|
1229
|
+
const unidentified = {...related}
|
|
1230
|
+
|
|
1231
|
+
delete unidentified.nodeId
|
|
1232
|
+
delete unidentified.symbolId
|
|
1233
|
+
|
|
1234
|
+
return {...unidentified, sourceId}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Sorts object keys recursively while preserving semantic array order.
|
|
1239
|
+
* @param {unknown} value - JSON value.
|
|
1240
|
+
* @returns {unknown} Deterministically keyed value.
|
|
1241
|
+
*/
|
|
1242
|
+
function sortJson(value) {
|
|
1243
|
+
if (Array.isArray(value)) return value.map(sortJson)
|
|
1244
|
+
if (value && typeof value == "object") {
|
|
1245
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
1246
|
+
.map(([key, child]) => [key, sortJson(child)]))
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return value
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Removes undefined optional properties from a public JSON value.
|
|
1254
|
+
* @template {Record<string, unknown>} Value
|
|
1255
|
+
* @param {Value} value - Candidate object.
|
|
1256
|
+
* @returns {Value} Compact JSON-safe object.
|
|
1257
|
+
*/
|
|
1258
|
+
function definedProperties(value) {
|
|
1259
|
+
return /** @type {Value} */ (Object.fromEntries(Object.entries(value).filter(([, child]) => child !== undefined)))
|
|
1260
|
+
}
|