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
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {parse as parseBabel} from "@babel/parser"
|
|
4
|
+
import {parse as parseComment} from "comment-parser"
|
|
5
|
+
import {missingType, parseFailure, unsupportedSyntax} from "../diagnostic.js"
|
|
6
|
+
import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
|
|
7
|
+
import {withAdaptedOperation} from "../semantic/operators.js"
|
|
8
|
+
import {withParserRanges} from "../semantic/provenance.js"
|
|
9
|
+
import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
|
|
10
|
+
import {requireSourceScalarType} from "./scalars.js"
|
|
11
|
+
|
|
12
|
+
/** @typedef {NonNullable<import("@babel/parser").ParseResult<import("@babel/types").File>["tokens"]>[number]} BabelToken */
|
|
13
|
+
/** @typedef {{byStart: Map<number, BabelToken>, tokens: BabelToken[]}} BabelTokenIndex */
|
|
14
|
+
|
|
15
|
+
/** @type {WeakMap<object, BabelTokenIndex>} */
|
|
16
|
+
const nodeTokens = new WeakMap()
|
|
17
|
+
const babelBinaryOperations = new Map([
|
|
18
|
+
["+", "Add"],
|
|
19
|
+
["-", "Subtract"],
|
|
20
|
+
["*", "Multiply"],
|
|
21
|
+
["===", "Equal"],
|
|
22
|
+
["!==", "NotEqual"],
|
|
23
|
+
["<", "LessThan"],
|
|
24
|
+
["<=", "LessThanOrEqual"],
|
|
25
|
+
[">", "GreaterThan"],
|
|
26
|
+
[">=", "GreaterThanOrEqual"]
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns a source location for a Babel node.
|
|
31
|
+
* @param {import("@babel/types").Node} node - Babel node.
|
|
32
|
+
* @param {string} filename - Source filename.
|
|
33
|
+
* @param {string} source - Complete source.
|
|
34
|
+
* @returns {import("../semantic/types.js").SourceLocation} Normalized location.
|
|
35
|
+
*/
|
|
36
|
+
function nodeLocation(node, filename, source) {
|
|
37
|
+
if (typeof node.start != "number" || typeof node.end != "number") {
|
|
38
|
+
throw new Error(`Babel omitted source offsets for ${node.type}.`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return locationFromOffsets(filename, source, node.start, node.end)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the identifier spelling rather than a trailing TypeScript annotation.
|
|
46
|
+
* @param {import("@babel/types").Identifier} node - Parser identifier.
|
|
47
|
+
* @param {string} filename - Source filename.
|
|
48
|
+
* @param {string} source - Complete source.
|
|
49
|
+
* @returns {import("../semantic/types.js").SourceLocation} Identifier location.
|
|
50
|
+
*/
|
|
51
|
+
function identifierLocation(node, filename, source) {
|
|
52
|
+
if (typeof node.start != "number") throw new Error("Babel omitted an identifier start offset.")
|
|
53
|
+
const token = nodeTokens.get(node)?.byStart.get(node.start)
|
|
54
|
+
|
|
55
|
+
if (!token || token.type.label != "name") throw new Error(`Babel omitted the identifier token for '${node.name}'.`)
|
|
56
|
+
|
|
57
|
+
return locationFromOffsets(filename, source, token.start, token.end)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Locates one Babel token within parser-node boundaries.
|
|
62
|
+
* @param {import("@babel/types").Node} owner - Token-owning parser node.
|
|
63
|
+
* @param {string} value - Exact token spelling.
|
|
64
|
+
* @param {number} startOffset - Inclusive boundary.
|
|
65
|
+
* @param {number} endOffset - Exclusive boundary.
|
|
66
|
+
* @param {string} filename - Source filename.
|
|
67
|
+
* @param {string} source - Complete source.
|
|
68
|
+
* @returns {import("../semantic/types.js").SourceLocation} Exact token location.
|
|
69
|
+
*/
|
|
70
|
+
function tokenLocation(owner, value, startOffset, endOffset, filename, source) {
|
|
71
|
+
const tokens = nodeTokens.get(owner)?.tokens ?? []
|
|
72
|
+
let low = 0
|
|
73
|
+
let high = tokens.length
|
|
74
|
+
|
|
75
|
+
while (low < high) {
|
|
76
|
+
const middle = Math.floor((low + high) / 2)
|
|
77
|
+
|
|
78
|
+
if (tokens[middle].start < startOffset) low = middle + 1
|
|
79
|
+
else high = middle
|
|
80
|
+
}
|
|
81
|
+
let token
|
|
82
|
+
|
|
83
|
+
for (let index = low; index < tokens.length && tokens[index].start < endOffset; index++) {
|
|
84
|
+
const candidate = tokens[index]
|
|
85
|
+
|
|
86
|
+
if (candidate.end <= endOffset && (candidate.value == value || candidate.type.label == value)) {
|
|
87
|
+
token = candidate
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!token) throw new Error(`Babel omitted token '${value}' between ${startOffset} and ${endOffset}.`)
|
|
93
|
+
|
|
94
|
+
return locationFromOffsets(filename, source, token.start, token.end)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Locates a comment-parser token inside a Babel-owned doc comment.
|
|
99
|
+
* @param {import("@babel/types").CommentBlock} comment - Babel doc comment.
|
|
100
|
+
* @param {import("comment-parser").Spec} tag - Parsed doc tag.
|
|
101
|
+
* @param {"name" | "type"} field - Token field.
|
|
102
|
+
* @param {string} filename - Source filename.
|
|
103
|
+
* @param {string} source - Complete source.
|
|
104
|
+
* @returns {import("../semantic/types.js").SourceLocation} Exact token location.
|
|
105
|
+
*/
|
|
106
|
+
function commentTagLocation(comment, tag, field, filename, source) {
|
|
107
|
+
const line = tag.source[0]
|
|
108
|
+
|
|
109
|
+
if (typeof comment.start != "number" || !line) throw new Error(`Comment parser omitted the @${tag.tag} source token.`)
|
|
110
|
+
|
|
111
|
+
let lineStart = comment.start
|
|
112
|
+
|
|
113
|
+
for (let lineNumber = 0; lineNumber < line.number; lineNumber++) {
|
|
114
|
+
while (lineStart < source.length && source[lineStart] != "\r" && source[lineStart] != "\n") lineStart++
|
|
115
|
+
if (source[lineStart] == "\r" && source[lineStart + 1] == "\n") lineStart += 2
|
|
116
|
+
else lineStart++
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const orderedFields = ["start", "delimiter", "postDelimiter", "tag", "postTag", "type", "postType", "name"]
|
|
120
|
+
const fieldIndex = orderedFields.indexOf(field)
|
|
121
|
+
const start = lineStart + orderedFields.slice(0, fieldIndex).reduce((length, key) =>
|
|
122
|
+
length + line.tokens[/** @type {keyof typeof line.tokens} */ (key)].length, 0)
|
|
123
|
+
const token = line.tokens[field]
|
|
124
|
+
|
|
125
|
+
return locationFromOffsets(filename, source, start, start + token.length)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Converts a supported Babel expression.
|
|
130
|
+
* @param {import("@babel/types").Expression} node - Babel expression.
|
|
131
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
132
|
+
* @param {string} filename - Source filename.
|
|
133
|
+
* @param {string} source - Complete source.
|
|
134
|
+
* @returns {import("../semantic/types.js").Expression} Semantic expression.
|
|
135
|
+
*/
|
|
136
|
+
function convertExpression(node, language, filename, source) {
|
|
137
|
+
const location = nodeLocation(node, filename, source)
|
|
138
|
+
|
|
139
|
+
if (node.type == "Identifier") {
|
|
140
|
+
return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: node.name}, {
|
|
141
|
+
name: identifierLocation(node, filename, source)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (node.type == "NumericLiteral") {
|
|
146
|
+
if (!Number.isSafeInteger(node.value)) return unsupportedSyntax(language, "non-safe integer literal", location)
|
|
147
|
+
|
|
148
|
+
return withParserRanges({kind: /** @type {const} */ ("IntegerLiteral"), location, value: node.value}, {literal: location})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.type == "BooleanLiteral") {
|
|
152
|
+
if (typeof node.value != "boolean") return unsupportedSyntax(language, "invalid boolean literal value", location)
|
|
153
|
+
|
|
154
|
+
return withParserRanges({kind: /** @type {const} */ ("BooleanLiteral"), location, value: node.value}, {literal: location})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (node.type == "StringLiteral") {
|
|
158
|
+
if (typeof node.value != "string" || !hasOnlyUnicodeScalars(node.value)) {
|
|
159
|
+
return unsupportedSyntax(language, "invalid Unicode string literal", location)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: node.value}, {literal: location})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (node.type == "TemplateLiteral") {
|
|
166
|
+
if (node.expressions.length > 0) return unsupportedSyntax(language, "interpolated string", location)
|
|
167
|
+
|
|
168
|
+
const quasi = node.quasis[0]
|
|
169
|
+
|
|
170
|
+
if (typeof quasi.value.cooked != "string" || !hasOnlyUnicodeScalars(quasi.value.cooked)) {
|
|
171
|
+
return unsupportedSyntax(language, "invalid Unicode string literal", location)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: quasi.value.cooked}, {literal: location})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (node.type == "UnaryExpression" && ["!", "-"].includes(node.operator)) {
|
|
178
|
+
const operand = convertExpression(node.argument, language, filename, source)
|
|
179
|
+
const semantic = withAdaptedOperation(withParserRanges({
|
|
180
|
+
kind: "UnaryExpression",
|
|
181
|
+
location,
|
|
182
|
+
operand
|
|
183
|
+
}, {
|
|
184
|
+
operator: tokenLocation(node, node.operator, node.start ?? 0, node.argument.start ?? node.end ?? source.length, filename, source)
|
|
185
|
+
}), node.operator == "!" ? "Not" : "Negate")
|
|
186
|
+
|
|
187
|
+
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (node.type == "LogicalExpression" && ["&&", "||"].includes(node.operator)) {
|
|
191
|
+
const semantic = withAdaptedOperation(withParserRanges({
|
|
192
|
+
kind: "BinaryExpression",
|
|
193
|
+
left: convertExpression(node.left, language, filename, source),
|
|
194
|
+
location,
|
|
195
|
+
right: convertExpression(node.right, language, filename, source)
|
|
196
|
+
}, {
|
|
197
|
+
operator: tokenLocation(node, node.operator, node.left.end ?? node.start ?? 0, node.right.start ?? node.end ?? source.length, filename, source)
|
|
198
|
+
}), node.operator == "&&" ? "And" : "Or")
|
|
199
|
+
|
|
200
|
+
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (node.type == "BinaryExpression" && babelBinaryOperations.has(node.operator)) {
|
|
204
|
+
if (node.left.type == "PrivateName") unsupportedSyntax(language, node.left.type, location)
|
|
205
|
+
|
|
206
|
+
const left = convertExpression(node.left, language, filename, source)
|
|
207
|
+
const right = convertExpression(node.right, language, filename, source)
|
|
208
|
+
|
|
209
|
+
const semantic = withAdaptedOperation(withParserRanges({
|
|
210
|
+
kind: "BinaryExpression",
|
|
211
|
+
left,
|
|
212
|
+
location,
|
|
213
|
+
right
|
|
214
|
+
}, {
|
|
215
|
+
operator: tokenLocation(node, node.operator, node.left.end ?? node.start ?? 0, node.right.start ?? node.end ?? source.length, filename, source)
|
|
216
|
+
}), /** @type {import("../semantic/operators.js").AdaptedOperation} */ (babelBinaryOperations.get(node.operator)))
|
|
217
|
+
|
|
218
|
+
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (node.type == "BinaryExpression" && ["==", "!="].includes(node.operator)) {
|
|
222
|
+
return unsupportedSyntax(language, `coercive equality ${node.operator}`, location)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (node.type == "CallExpression" && node.callee.type == "Identifier") {
|
|
226
|
+
const arguments_ = node.arguments.map((argument) => {
|
|
227
|
+
if (argument.type == "SpreadElement" || argument.type == "ArgumentPlaceholder") {
|
|
228
|
+
return unsupportedSyntax(language, argument.type, nodeLocation(argument, filename, source))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return convertExpression(argument, language, filename, source)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return withParserRanges({arguments: arguments_, callee: node.callee.name, kind: /** @type {const} */ ("CallExpression"), location}, {
|
|
235
|
+
callee: identifierLocation(node.callee, filename, source)
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return unsupportedSyntax(language, node.type, location)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Converts a supported return statement.
|
|
244
|
+
* @param {import("@babel/types").Statement} node - Babel statement.
|
|
245
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
246
|
+
* @param {string} filename - Source filename.
|
|
247
|
+
* @param {string} source - Complete source.
|
|
248
|
+
* @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
|
|
249
|
+
*/
|
|
250
|
+
function convertReturn(node, language, filename, source) {
|
|
251
|
+
const location = nodeLocation(node, filename, source)
|
|
252
|
+
|
|
253
|
+
if (node.type != "ReturnStatement" || !node.argument) return unsupportedSyntax(language, node.type, location)
|
|
254
|
+
|
|
255
|
+
return {expression: convertExpression(node.argument, language, filename, source), kind: "ReturnStatement", location}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Converts one supported local declaration or assignment.
|
|
260
|
+
* @param {import("@babel/types").Statement} node - Babel statement.
|
|
261
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
262
|
+
* @param {string} filename - Source filename.
|
|
263
|
+
* @param {string} source - Complete source.
|
|
264
|
+
* @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
|
|
265
|
+
*/
|
|
266
|
+
function convertLocalStatement(node, language, filename, source) {
|
|
267
|
+
const location = nodeLocation(node, filename, source)
|
|
268
|
+
|
|
269
|
+
if (node.type == "VariableDeclaration") {
|
|
270
|
+
if (!["let", "const"].includes(node.kind) || node.declarations.length != 1) {
|
|
271
|
+
return unsupportedSyntax(language, `${node.kind} declaration`, location)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const declarator = node.declarations[0]
|
|
275
|
+
const declaratorLocation = nodeLocation(declarator, filename, source)
|
|
276
|
+
|
|
277
|
+
if (declarator.id.type != "Identifier") return unsupportedSyntax(language, declarator.id.type, declaratorLocation)
|
|
278
|
+
if (!declarator.init) return unsupportedSyntax(language, "uninitialized declaration", declaratorLocation)
|
|
279
|
+
|
|
280
|
+
const type = language == "typescript"
|
|
281
|
+
? convertTypeScriptType(declarator.id.typeAnnotation, `Local '${declarator.id.name}'`, declaratorLocation, filename, source)
|
|
282
|
+
: localJavaScriptType(node, declarator.id.name, filename, source)
|
|
283
|
+
|
|
284
|
+
return withParserRanges({
|
|
285
|
+
initializer: convertExpression(declarator.init, language, filename, source),
|
|
286
|
+
kind: "LocalDeclaration",
|
|
287
|
+
location,
|
|
288
|
+
mutable: node.kind == "let",
|
|
289
|
+
name: declarator.id.name,
|
|
290
|
+
type
|
|
291
|
+
}, {
|
|
292
|
+
name: identifierLocation(declarator.id, filename, source),
|
|
293
|
+
operator: tokenLocation(declarator, "=", declarator.id.end ?? declarator.start ?? 0, declarator.init.start ?? declarator.end ?? source.length, filename, source)
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (node.type == "ExpressionStatement" && node.expression.type == "AssignmentExpression") {
|
|
298
|
+
const assignment = node.expression
|
|
299
|
+
const targetLocation = nodeLocation(assignment.left, filename, source)
|
|
300
|
+
|
|
301
|
+
if (assignment.operator != "=") return unsupportedSyntax(language, `assignment ${assignment.operator}`, location)
|
|
302
|
+
if (assignment.left.type != "Identifier") return unsupportedSyntax(language, assignment.left.type, targetLocation)
|
|
303
|
+
|
|
304
|
+
const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: assignment.left.name}, {
|
|
305
|
+
name: identifierLocation(assignment.left, filename, source)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
return withParserRanges({
|
|
309
|
+
expression: convertExpression(assignment.right, language, filename, source),
|
|
310
|
+
kind: "AssignmentStatement",
|
|
311
|
+
location,
|
|
312
|
+
target
|
|
313
|
+
}, {
|
|
314
|
+
operator: tokenLocation(assignment, "=", assignment.left.end ?? assignment.start ?? 0, assignment.right.start ?? assignment.end ?? source.length, filename, source)
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return unsupportedSyntax(language, node.type, location)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Reads an immediately associated JavaScript local `@type` tag.
|
|
323
|
+
* @param {import("@babel/types").VariableDeclaration} node - Local declaration.
|
|
324
|
+
* @param {string} name - Local name.
|
|
325
|
+
* @param {string} filename - Source filename.
|
|
326
|
+
* @param {string} source - Complete source.
|
|
327
|
+
* @returns {import("../semantic/types.js").TypeReference} Semantic type.
|
|
328
|
+
*/
|
|
329
|
+
function localJavaScriptType(node, name, filename, source) {
|
|
330
|
+
const comment = node.leadingComments?.at(-1)
|
|
331
|
+
const location = nodeLocation(node, filename, source)
|
|
332
|
+
|
|
333
|
+
if (!comment || comment.type != "CommentBlock" || !comment.value.startsWith("*")) {
|
|
334
|
+
return missingType("javascript", `Local '${name}'`, location)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const gap = source.slice(comment.end ?? 0, node.start ?? 0)
|
|
338
|
+
|
|
339
|
+
if (!/^\s*$/u.test(gap) || (gap.match(/\n/gu)?.length ?? 0) > 1) {
|
|
340
|
+
return missingType("javascript", `Local '${name}'`, location)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const block = parseComment(`/*${comment.value}*/`)[0]
|
|
344
|
+
const tags = block?.tags.filter((tag) => tag.tag == "type") ?? []
|
|
345
|
+
|
|
346
|
+
const tag = tags.length == 1 ? tags[0] : undefined
|
|
347
|
+
|
|
348
|
+
return convertType(
|
|
349
|
+
tag?.type,
|
|
350
|
+
"javascript",
|
|
351
|
+
`Local '${name}'`,
|
|
352
|
+
location,
|
|
353
|
+
tag ? commentTagLocation(comment, tag, "type", filename, source) : location
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Converts one exhaustive Babel statement.
|
|
359
|
+
* @param {import("@babel/types").Statement} node - Babel statement.
|
|
360
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
361
|
+
* @param {string} filename - Source filename.
|
|
362
|
+
* @param {string} source - Complete source.
|
|
363
|
+
* @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
|
|
364
|
+
* @returns {import("../semantic/types.js").Statement} Semantic statement.
|
|
365
|
+
*/
|
|
366
|
+
function convertStatement(node, language, filename, source, canonicalZeroRequired) {
|
|
367
|
+
if (node.type == "ReturnStatement") return convertReturn(node, language, filename, source)
|
|
368
|
+
if (node.type == "IfStatement") return convertIf(node, language, filename, source, canonicalZeroRequired)
|
|
369
|
+
if (node.type == "VariableDeclaration" || node.type == "ExpressionStatement" && node.expression.type == "AssignmentExpression") {
|
|
370
|
+
return convertLocalStatement(node, language, filename, source)
|
|
371
|
+
}
|
|
372
|
+
if (node.type == "ExpressionStatement") return convertPrint(node, language, filename, source, canonicalZeroRequired)
|
|
373
|
+
|
|
374
|
+
return unsupportedSyntax(language, node.type, nodeLocation(node, filename, source))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Converts one Babel block or normalized single-statement body.
|
|
379
|
+
* @param {import("@babel/types").BlockStatement | import("@babel/types").Statement[]} input - Parser block or statements.
|
|
380
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
381
|
+
* @param {string} filename - Source filename.
|
|
382
|
+
* @param {string} source - Complete source.
|
|
383
|
+
* @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
|
|
384
|
+
* @param {import("../semantic/types.js").SourceLocation} [location] - Location for a statement-array block.
|
|
385
|
+
* @returns {import("../semantic/types.js").Block} Semantic block.
|
|
386
|
+
*/
|
|
387
|
+
function convertBlock(input, language, filename, source, canonicalZeroRequired, location) {
|
|
388
|
+
const statements = Array.isArray(input) ? input : input.body
|
|
389
|
+
const directives = Array.isArray(input) ? [] : input.directives
|
|
390
|
+
const blockLocation = location ?? nodeLocation(/** @type {import("@babel/types").BlockStatement} */ (input), filename, source)
|
|
391
|
+
|
|
392
|
+
if (directives.length > 0) {
|
|
393
|
+
return unsupportedSyntax(language, "directive", nodeLocation(directives[0], filename, source))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
kind: "Block",
|
|
398
|
+
location: blockLocation,
|
|
399
|
+
statements: statements.map((statement) => convertStatement(statement, language, filename, source, canonicalZeroRequired))
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Reads JavaScript JSDoc types from a function.
|
|
405
|
+
* @param {import("@babel/types").FunctionDeclaration} node - Babel function node.
|
|
406
|
+
* @param {string} filename - Source filename.
|
|
407
|
+
* @param {string} source - Complete source.
|
|
408
|
+
* @returns {{parameters: Map<string, {location: import("../semantic/types.js").SourceLocation, sourceType: string}>, returnType: {location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined}} Parsed types.
|
|
409
|
+
*/
|
|
410
|
+
function jsdocTypes(node, filename, source) {
|
|
411
|
+
const comment = node.leadingComments?.find((candidate) => candidate.type == "CommentBlock" && candidate.value.startsWith("*"))
|
|
412
|
+
|
|
413
|
+
if (!comment) missingType("javascript", `Function '${node.id?.name ?? "anonymous"}'`, nodeLocation(node, filename, source))
|
|
414
|
+
|
|
415
|
+
const commentBlock = /** @type {import("@babel/types").CommentBlock} */ (comment)
|
|
416
|
+
const block = parseComment(`/*${commentBlock.value}*/`)[0]
|
|
417
|
+
const parameters = new Map(block.tags.filter((tag) => tag.tag == "param").map((tag) => [tag.name, {
|
|
418
|
+
location: commentTagLocation(commentBlock, tag, "type", filename, source),
|
|
419
|
+
sourceType: tag.type
|
|
420
|
+
}]))
|
|
421
|
+
const returnTag = block.tags.find((tag) => tag.tag == "returns" || tag.tag == "return")
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
parameters,
|
|
425
|
+
returnType: returnTag ? {location: commentTagLocation(commentBlock, returnTag, "type", filename, source), sourceType: returnTag.type} : undefined
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Requires a supported JavaScript JSDoc scalar type spelling.
|
|
431
|
+
* @param {string | undefined} sourceType - Source-language type.
|
|
432
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
433
|
+
* @param {string} subject - Typed subject.
|
|
434
|
+
* @param {import("../semantic/types.js").SourceLocation} location - Source location.
|
|
435
|
+
* @param {import("../semantic/types.js").SourceLocation} [typeLocation] - Exact type token location.
|
|
436
|
+
* @returns {import("../semantic/types.js").TypeReference} Semantic type.
|
|
437
|
+
*/
|
|
438
|
+
function convertType(sourceType, language, subject, location, typeLocation = location) {
|
|
439
|
+
return requireSourceScalarType(language, sourceType, subject, location, typeLocation)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Converts one exact TypeScript scalar keyword annotation.
|
|
444
|
+
* @param {import("@babel/types").TypeAnnotation | import("@babel/types").TSTypeAnnotation | import("@babel/types").Noop | null | undefined} annotation - Type annotation.
|
|
445
|
+
* @param {string} subject - Typed subject.
|
|
446
|
+
* @param {import("../semantic/types.js").SourceLocation} ownerLocation - Owning declaration location.
|
|
447
|
+
* @param {string} filename - Source filename.
|
|
448
|
+
* @param {string} source - Complete source.
|
|
449
|
+
* @returns {import("../semantic/types.js").TypeReference} Semantic scalar type.
|
|
450
|
+
*/
|
|
451
|
+
function convertTypeScriptType(annotation, subject, ownerLocation, filename, source) {
|
|
452
|
+
if (!annotation) return missingType("typescript", subject, ownerLocation)
|
|
453
|
+
if (annotation.type != "TSTypeAnnotation") {
|
|
454
|
+
return unsupportedSyntax("typescript", "unsupported scalar type", nodeLocation(annotation, filename, source))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const typeNode = annotation.typeAnnotation
|
|
458
|
+
const sourceType = typeNode.type == "TSNumberKeyword"
|
|
459
|
+
? "number"
|
|
460
|
+
: typeNode.type == "TSBooleanKeyword"
|
|
461
|
+
? "boolean"
|
|
462
|
+
: typeNode.type == "TSStringKeyword" ? "string" : undefined
|
|
463
|
+
|
|
464
|
+
if (!sourceType) {
|
|
465
|
+
return unsupportedSyntax("typescript", "unsupported scalar type", nodeLocation(typeNode, filename, source))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return requireSourceScalarType("typescript", sourceType, subject, ownerLocation, nodeLocation(typeNode, filename, source))
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Converts a supported function declaration.
|
|
473
|
+
* @param {import("@babel/types").FunctionDeclaration} node - Babel function.
|
|
474
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
475
|
+
* @param {string} filename - Source filename.
|
|
476
|
+
* @param {string} source - Complete source.
|
|
477
|
+
* @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
|
|
478
|
+
* @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
|
|
479
|
+
*/
|
|
480
|
+
function convertFunction(node, language, filename, source, canonicalZeroRequired) {
|
|
481
|
+
const location = nodeLocation(node, filename, source)
|
|
482
|
+
|
|
483
|
+
if (!node.id) return unsupportedSyntax(language, "anonymous function", location)
|
|
484
|
+
if (node.async) return unsupportedSyntax(language, "async function", location)
|
|
485
|
+
if (node.generator) return unsupportedSyntax(language, "generator function", location)
|
|
486
|
+
|
|
487
|
+
const documentedTypes = language == "javascript" ? jsdocTypes(node, filename, source) : undefined
|
|
488
|
+
const parameters = node.params.map((parameter) => {
|
|
489
|
+
const parameterLocation = nodeLocation(parameter, filename, source)
|
|
490
|
+
|
|
491
|
+
if (parameter.type != "Identifier") return unsupportedSyntax(language, parameter.type, parameterLocation)
|
|
492
|
+
|
|
493
|
+
const documentedType = documentedTypes?.parameters.get(parameter.name)
|
|
494
|
+
const type = language == "javascript"
|
|
495
|
+
? convertType(documentedType?.sourceType, language, `Parameter '${parameter.name}'`, parameterLocation, documentedType?.location)
|
|
496
|
+
: convertTypeScriptType(parameter.typeAnnotation, `Parameter '${parameter.name}'`, parameterLocation, filename, source)
|
|
497
|
+
|
|
498
|
+
return withParserRanges({
|
|
499
|
+
kind: /** @type {const} */ ("Parameter"),
|
|
500
|
+
location: parameterLocation,
|
|
501
|
+
name: parameter.name,
|
|
502
|
+
type
|
|
503
|
+
}, {name: identifierLocation(parameter, filename, source)})
|
|
504
|
+
})
|
|
505
|
+
const returnAnnotation = node.returnType
|
|
506
|
+
const returnType = language == "javascript"
|
|
507
|
+
? convertType(documentedTypes?.returnType?.sourceType, language, `Function '${node.id.name}' return`, location, documentedTypes?.returnType?.location)
|
|
508
|
+
: convertTypeScriptType(returnAnnotation, `Function '${node.id.name}' return`, location, filename, source)
|
|
509
|
+
const body = convertBlock(node.body, language, filename, source, canonicalZeroRequired)
|
|
510
|
+
|
|
511
|
+
return withParserRanges({
|
|
512
|
+
body,
|
|
513
|
+
kind: "FunctionDeclaration",
|
|
514
|
+
location,
|
|
515
|
+
name: node.id.name,
|
|
516
|
+
parameters,
|
|
517
|
+
returnType
|
|
518
|
+
}, {name: identifierLocation(node.id, filename, source)})
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Converts the existing exact if/else terminal shape.
|
|
523
|
+
* @param {import("@babel/types").IfStatement} node - Babel if statement.
|
|
524
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
525
|
+
* @param {string} filename - Source filename.
|
|
526
|
+
* @param {string} source - Complete source.
|
|
527
|
+
* @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
|
|
528
|
+
* @returns {import("../semantic/types.js").IfStatement} Semantic branch.
|
|
529
|
+
*/
|
|
530
|
+
function convertIf(node, language, filename, source, canonicalZeroRequired) {
|
|
531
|
+
const location = nodeLocation(node, filename, source)
|
|
532
|
+
const consequent = node.consequent.type == "BlockStatement"
|
|
533
|
+
? convertBlock(node.consequent, language, filename, source, canonicalZeroRequired)
|
|
534
|
+
: convertBlock([node.consequent], language, filename, source, canonicalZeroRequired, nodeLocation(node.consequent, filename, source))
|
|
535
|
+
const alternate = node.alternate
|
|
536
|
+
? node.alternate.type == "BlockStatement"
|
|
537
|
+
? convertBlock(node.alternate, language, filename, source, canonicalZeroRequired)
|
|
538
|
+
: convertBlock([node.alternate], language, filename, source, canonicalZeroRequired, nodeLocation(node.alternate, filename, source))
|
|
539
|
+
: undefined
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
...(alternate ? {alternate} : {}),
|
|
543
|
+
condition: convertExpression(node.test, language, filename, source),
|
|
544
|
+
consequent,
|
|
545
|
+
kind: "IfStatement",
|
|
546
|
+
location
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Reports whether Babel parsed an operation that makes generated JavaScript-family scalar output require zero canonicalization.
|
|
552
|
+
* @param {unknown} value - Babel parser subtree.
|
|
553
|
+
* @param {WeakSet<object>} [visited] - Cycle protection.
|
|
554
|
+
* @returns {boolean} Whether the subtree contains integer negation or multiplication syntax.
|
|
555
|
+
*/
|
|
556
|
+
function parserTreeRequiresCanonicalZero(value, visited = new WeakSet()) {
|
|
557
|
+
if (!value || typeof value != "object") return false
|
|
558
|
+
if (visited.has(value)) return false
|
|
559
|
+
|
|
560
|
+
visited.add(value)
|
|
561
|
+
if (Reflect.get(value, "type") == "UnaryExpression" && Reflect.get(value, "operator") == "-") return true
|
|
562
|
+
if (Reflect.get(value, "type") == "BinaryExpression" && Reflect.get(value, "operator") == "*") return true
|
|
563
|
+
|
|
564
|
+
return Object.entries(value).some(([key, child]) =>
|
|
565
|
+
!["extra", "innerComments", "leadingComments", "loc", "trailingComments"].includes(key) &&
|
|
566
|
+
(Array.isArray(child)
|
|
567
|
+
? child.some((item) => parserTreeRequiresCanonicalZero(item, visited))
|
|
568
|
+
: parserTreeRequiresCanonicalZero(child, visited)))
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Recognizes the parser-owned receiver parentheses emitted only for canonical integer output.
|
|
573
|
+
* @param {import("@babel/types").Expression | import("@babel/types").Super} receiver - Qualified-call receiver.
|
|
574
|
+
* @param {import("@babel/types").CallExpression} call - Outer `toString` call.
|
|
575
|
+
* @returns {boolean} Whether the receiver has the generated wrapper shape.
|
|
576
|
+
*/
|
|
577
|
+
function hasCanonicalZeroReceiverWrapper(receiver, call) {
|
|
578
|
+
const extra = Reflect.get(receiver, "extra")
|
|
579
|
+
const expressionParentheses = ["BinaryExpression", "LogicalExpression", "UnaryExpression"].includes(receiver.type) ? 1 : 0
|
|
580
|
+
const wrapperParentheses = expressionParentheses + 1
|
|
581
|
+
const property = call.callee.type == "MemberExpression" ? call.callee.property : undefined
|
|
582
|
+
|
|
583
|
+
return Boolean(extra) && typeof extra == "object" && Reflect.get(extra, "parenthesized") === true &&
|
|
584
|
+
Reflect.get(extra, "parenStart") === call.start && typeof call.start == "number" && typeof call.end == "number" &&
|
|
585
|
+
typeof receiver.start == "number" && typeof receiver.end == "number" && property?.type == "Identifier" &&
|
|
586
|
+
typeof property.start == "number" && typeof property.end == "number" && call.callee.start === call.start &&
|
|
587
|
+
receiver.start == call.start + wrapperParentheses && property.start == receiver.end + wrapperParentheses + 1 &&
|
|
588
|
+
property.end == property.start + "toString".length && call.end == property.end + 2
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Converts a console.log entry-point call.
|
|
593
|
+
* @param {import("@babel/types").Statement} node - Babel statement.
|
|
594
|
+
* @param {"javascript" | "typescript"} language - Frontend language.
|
|
595
|
+
* @param {string} filename - Source filename.
|
|
596
|
+
* @param {string} source - Complete source.
|
|
597
|
+
* @param {boolean} canonicalZeroRequired - Whether this parsed module contains a sign-producing integer operation.
|
|
598
|
+
* @returns {import("../semantic/types.js").PrintStatement} Print statement.
|
|
599
|
+
*/
|
|
600
|
+
function convertPrint(node, language, filename, source, canonicalZeroRequired) {
|
|
601
|
+
const location = nodeLocation(node, filename, source)
|
|
602
|
+
|
|
603
|
+
if (node.type != "ExpressionStatement" || node.expression.type != "CallExpression") {
|
|
604
|
+
return unsupportedSyntax(language, node.type, location)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const call = node.expression
|
|
608
|
+
const callee = call.callee
|
|
609
|
+
|
|
610
|
+
if (callee.type != "MemberExpression" || callee.computed || callee.object.type != "Identifier" ||
|
|
611
|
+
callee.object.name != "console" || callee.property.type != "Identifier" || callee.property.name != "log" ||
|
|
612
|
+
call.arguments.length != 1) {
|
|
613
|
+
return unsupportedSyntax(language, "entry-point expression", location)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
let argument = call.arguments[0]
|
|
617
|
+
|
|
618
|
+
if (argument.type == "SpreadElement" || argument.type == "ArgumentPlaceholder") {
|
|
619
|
+
return unsupportedSyntax(language, argument.type, nodeLocation(argument, filename, source))
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (argument.type == "CallExpression" && argument.callee.type == "MemberExpression" && !argument.callee.computed &&
|
|
623
|
+
!argument.callee.optional && argument.callee.object.type != "Super" && argument.callee.property.type == "Identifier" &&
|
|
624
|
+
argument.callee.property.name == "toString" && !argument.optional && !argument.typeArguments &&
|
|
625
|
+
!argument.typeParameters && argument.arguments.length == 0) {
|
|
626
|
+
if (!canonicalZeroRequired || !hasCanonicalZeroReceiverWrapper(argument.callee.object, argument)) {
|
|
627
|
+
return unsupportedSyntax(language, "entry-point qualified call", nodeLocation(argument.callee.property, filename, source))
|
|
628
|
+
}
|
|
629
|
+
argument = argument.callee.object
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return {expression: convertExpression(argument, language, filename, source), kind: "PrintStatement", location}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Parses JavaScript or TypeScript into the shared semantic module.
|
|
637
|
+
* @param {object} input - Parser input.
|
|
638
|
+
* @param {string} input.filename - Source filename.
|
|
639
|
+
* @param {"javascript" | "typescript"} input.language - Frontend language.
|
|
640
|
+
* @param {string} input.source - Source text.
|
|
641
|
+
* @returns {import("../semantic/types.js").SemanticModule} Semantic module.
|
|
642
|
+
*/
|
|
643
|
+
export function parseJavaScriptTypeScript({filename, language, source}) {
|
|
644
|
+
const file = parseBabelSource({filename, language, source})
|
|
645
|
+
const tokens = file.tokens ?? []
|
|
646
|
+
const tokenIndex = {byStart: new Map(tokens.map((token) => [token.start, token])), tokens}
|
|
647
|
+
const canonicalZeroRequired = parserTreeRequiresCanonicalZero(file.program)
|
|
648
|
+
|
|
649
|
+
rememberTokens(file, tokenIndex)
|
|
650
|
+
if (file.program.directives.length > 0) {
|
|
651
|
+
return unsupportedSyntax(language, "top-level directive", nodeLocation(file.program.directives[0], filename, source))
|
|
652
|
+
}
|
|
653
|
+
const functions = file.program.body.filter((node) => node.type == "FunctionDeclaration")
|
|
654
|
+
.map((node) => convertFunction(node, language, filename, source, canonicalZeroRequired))
|
|
655
|
+
const entryNodes = file.program.body.filter((node) => node.type != "FunctionDeclaration")
|
|
656
|
+
const location = moduleLocation(filename, source)
|
|
657
|
+
|
|
658
|
+
if (functions.length == 0) return unsupportedSyntax(language, "module without a function", location)
|
|
659
|
+
const entryLocation = entryNodes.length == 0 ? location : locationFromOffsets(
|
|
660
|
+
filename,
|
|
661
|
+
source,
|
|
662
|
+
entryNodes[0].start ?? 0,
|
|
663
|
+
entryNodes.at(-1)?.end ?? source.length
|
|
664
|
+
)
|
|
665
|
+
const entryBlock = convertBlock(entryNodes, language, filename, source, canonicalZeroRequired, entryLocation)
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
entryPoint: {
|
|
669
|
+
body: entryBlock,
|
|
670
|
+
kind: "EntryPoint",
|
|
671
|
+
location: entryLocation
|
|
672
|
+
},
|
|
673
|
+
functions,
|
|
674
|
+
kind: "Module",
|
|
675
|
+
location
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Invokes Babel and normalizes syntax failures.
|
|
681
|
+
* @param {object} input - Parser input.
|
|
682
|
+
* @param {string} input.filename - Source filename.
|
|
683
|
+
* @param {"javascript" | "typescript"} input.language - Frontend language.
|
|
684
|
+
* @param {string} input.source - Source text.
|
|
685
|
+
* @returns {import("@babel/parser").ParseResult<import("@babel/types").File>} Babel file.
|
|
686
|
+
*/
|
|
687
|
+
function parseBabelSource({filename, language, source}) {
|
|
688
|
+
try {
|
|
689
|
+
return parseBabel(source, {
|
|
690
|
+
plugins: language == "typescript" ? ["typescript"] : [],
|
|
691
|
+
sourceFilename: filename,
|
|
692
|
+
sourceType: "script",
|
|
693
|
+
tokens: true
|
|
694
|
+
})
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return parseFailure(language, error)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Associates Babel nodes with their parser token stream without leaking it into semantic values.
|
|
702
|
+
* @param {object} value - Current parser value.
|
|
703
|
+
* @param {BabelTokenIndex} tokens - Indexed parser tokens.
|
|
704
|
+
* @param {WeakSet<object>} [visited] - Cycle protection.
|
|
705
|
+
* @returns {void}
|
|
706
|
+
*/
|
|
707
|
+
function rememberTokens(value, tokens, visited = new WeakSet()) {
|
|
708
|
+
if (visited.has(value)) return
|
|
709
|
+
|
|
710
|
+
visited.add(value)
|
|
711
|
+
nodeTokens.set(value, tokens)
|
|
712
|
+
|
|
713
|
+
for (const child of Object.values(value)) {
|
|
714
|
+
if (Array.isArray(child)) {
|
|
715
|
+
for (const item of child) if (item && typeof item == "object") rememberTokens(item, tokens, visited)
|
|
716
|
+
} else if (child && typeof child == "object") rememberTokens(child, tokens, visited)
|
|
717
|
+
}
|
|
718
|
+
}
|