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,593 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import PhpParser from "php-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
|
+
const parser = new PhpParser.Engine({
|
|
12
|
+
ast: {withPositions: true},
|
|
13
|
+
parser: {extractDoc: true, suppressErrors: false}
|
|
14
|
+
})
|
|
15
|
+
/** @type {string | undefined} */
|
|
16
|
+
let cachedTokenSource
|
|
17
|
+
/** @type {{end: number, start: number, text: string}[]} */
|
|
18
|
+
let cachedTokenRanges = []
|
|
19
|
+
const phpBinaryOperations = new Map([
|
|
20
|
+
["+", "PhpAdd"],
|
|
21
|
+
["-", "Subtract"],
|
|
22
|
+
["*", "Multiply"],
|
|
23
|
+
["&&", "And"],
|
|
24
|
+
["||", "Or"],
|
|
25
|
+
["===", "Equal"],
|
|
26
|
+
["!==", "NotEqual"],
|
|
27
|
+
["<", "LessThan"],
|
|
28
|
+
["<=", "LessThanOrEqual"],
|
|
29
|
+
[">", "GreaterThan"],
|
|
30
|
+
[">=", "GreaterThanOrEqual"],
|
|
31
|
+
[".", "StringConcat"]
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns a normalized PHP node location.
|
|
36
|
+
* @param {import("php-parser").Node} node - PHP parser node.
|
|
37
|
+
* @param {string} filename - Source filename.
|
|
38
|
+
* @param {string} source - Complete source.
|
|
39
|
+
* @returns {import("../semantic/types.js").SourceLocation} Source location.
|
|
40
|
+
*/
|
|
41
|
+
function nodeLocation(node, filename, source) {
|
|
42
|
+
if (!node.loc) throw new Error(`PHP parser omitted source location for ${node.kind}.`)
|
|
43
|
+
|
|
44
|
+
return locationFromOffsets(filename, source, node.loc.start.offset, node.loc.end.offset)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Locates a comment-parser token inside a PHP parser-owned doc comment.
|
|
49
|
+
* @param {import("php-parser").CommentBlock} comment - PHP doc comment.
|
|
50
|
+
* @param {import("comment-parser").Spec} tag - Parsed doc tag.
|
|
51
|
+
* @param {"name" | "type"} field - Token field.
|
|
52
|
+
* @param {string} filename - Source filename.
|
|
53
|
+
* @param {string} source - Complete source.
|
|
54
|
+
* @returns {import("../semantic/types.js").SourceLocation} Exact token location.
|
|
55
|
+
*/
|
|
56
|
+
function commentTagLocation(comment, tag, field, filename, source) {
|
|
57
|
+
const line = tag.source[0]
|
|
58
|
+
|
|
59
|
+
if (!comment.loc || !line) throw new Error(`Comment parser omitted the @${tag.tag} source token.`)
|
|
60
|
+
|
|
61
|
+
let lineStart = comment.loc.start.offset
|
|
62
|
+
|
|
63
|
+
for (let lineNumber = 0; lineNumber < line.number; lineNumber++) {
|
|
64
|
+
while (lineStart < source.length && source[lineStart] != "\r" && source[lineStart] != "\n") lineStart++
|
|
65
|
+
if (source[lineStart] == "\r" && source[lineStart + 1] == "\n") lineStart += 2
|
|
66
|
+
else lineStart++
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const orderedFields = ["start", "delimiter", "postDelimiter", "tag", "postTag", "name", "postName", "type"]
|
|
70
|
+
const fieldIndex = orderedFields.indexOf(field)
|
|
71
|
+
const start = lineStart + orderedFields.slice(0, fieldIndex).reduce((length, key) =>
|
|
72
|
+
length + line.tokens[/** @type {keyof typeof line.tokens} */ (key)].length, 0)
|
|
73
|
+
const value = line.tokens[field]
|
|
74
|
+
|
|
75
|
+
return locationFromOffsets(filename, source, start, start + value.length)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Finds one parser token between parser-owned node boundaries.
|
|
80
|
+
* @param {string} tokenText - Exact token text.
|
|
81
|
+
* @param {number} startOffset - Inclusive search boundary.
|
|
82
|
+
* @param {number} endOffset - Exclusive search boundary.
|
|
83
|
+
* @param {string} filename - Source filename.
|
|
84
|
+
* @param {string} source - Complete source.
|
|
85
|
+
* @returns {import("../semantic/types.js").SourceLocation} Token location.
|
|
86
|
+
*/
|
|
87
|
+
function tokenLocation(tokenText, startOffset, endOffset, filename, source) {
|
|
88
|
+
if (cachedTokenSource != source) {
|
|
89
|
+
let offset = 0
|
|
90
|
+
|
|
91
|
+
cachedTokenRanges = parser.tokenGetAll(source).map((token) => {
|
|
92
|
+
const text = typeof token == "string" ? token : token[1]
|
|
93
|
+
const range = {end: offset + text.length, start: offset, text}
|
|
94
|
+
|
|
95
|
+
offset = range.end
|
|
96
|
+
|
|
97
|
+
return range
|
|
98
|
+
})
|
|
99
|
+
cachedTokenSource = source
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const token = cachedTokenRanges.find((candidate) => candidate.text == tokenText && candidate.start >= startOffset &&
|
|
103
|
+
candidate.end <= endOffset)
|
|
104
|
+
|
|
105
|
+
if (token) return locationFromOffsets(filename, source, token.start, token.end)
|
|
106
|
+
|
|
107
|
+
throw new Error(`PHP parser omitted token '${tokenText}' between ${startOffset} and ${endOffset}.`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Converts a supported PHP expression.
|
|
112
|
+
* @param {import("php-parser").Expression} node - PHP expression.
|
|
113
|
+
* @param {string} filename - Source filename.
|
|
114
|
+
* @param {string} source - Complete source.
|
|
115
|
+
* @returns {import("../semantic/types.js").Expression} Semantic expression.
|
|
116
|
+
*/
|
|
117
|
+
function convertExpression(node, filename, source) {
|
|
118
|
+
const location = nodeLocation(node, filename, source)
|
|
119
|
+
|
|
120
|
+
if (node.kind == "variable") {
|
|
121
|
+
const variable = /** @type {import("php-parser").Variable} */ (node)
|
|
122
|
+
|
|
123
|
+
if (typeof variable.name != "string") return unsupportedSyntax("php", "dynamic variable", location)
|
|
124
|
+
|
|
125
|
+
return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: variable.name}, {name: location})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (node.kind == "number") {
|
|
129
|
+
const literal = /** @type {import("php-parser").Number} */ (node)
|
|
130
|
+
const value = Number(literal.value)
|
|
131
|
+
|
|
132
|
+
if (!Number.isSafeInteger(value)) return unsupportedSyntax("php", "non-safe integer literal", location)
|
|
133
|
+
|
|
134
|
+
return withParserRanges({kind: /** @type {const} */ ("IntegerLiteral"), location, value}, {literal: location})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node.kind == "boolean") {
|
|
138
|
+
const literal = /** @type {import("php-parser").Boolean} */ (node)
|
|
139
|
+
|
|
140
|
+
if (typeof literal.value != "boolean") return unsupportedSyntax("php", "invalid boolean literal value", location)
|
|
141
|
+
|
|
142
|
+
return withParserRanges({kind: /** @type {const} */ ("BooleanLiteral"), location, value: literal.value}, {literal: location})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (node.kind == "string") {
|
|
146
|
+
const literal = /** @type {import("php-parser").String} */ (node)
|
|
147
|
+
|
|
148
|
+
if (typeof literal.raw == "string" && literal.raw.startsWith("<<<")) {
|
|
149
|
+
const detail = literal.raw.startsWith("<<<'") ? "nowdoc string" : "heredoc string"
|
|
150
|
+
|
|
151
|
+
return unsupportedSyntax("php", detail, location)
|
|
152
|
+
}
|
|
153
|
+
if (typeof literal.raw == "string" && ["b\"", "B\"", "b'", "B'"].some((prefix) => literal.raw.startsWith(prefix))) {
|
|
154
|
+
return unsupportedSyntax("php", "binary string literal", location)
|
|
155
|
+
}
|
|
156
|
+
if (typeof literal.value != "string" || !hasOnlyUnicodeScalars(literal.value)) {
|
|
157
|
+
return unsupportedSyntax("php", "invalid Unicode string literal", location)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: literal.value}, {literal: location})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (node.kind == "encapsed") {
|
|
164
|
+
const literal = /** @type {import("php-parser").Encapsed} */ (node)
|
|
165
|
+
const detail = literal.type == "string" ? "interpolated string" : `${literal.type} string`
|
|
166
|
+
|
|
167
|
+
return unsupportedSyntax("php", detail, location)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (node.kind == "nowdoc") return unsupportedSyntax("php", "nowdoc string", location)
|
|
171
|
+
|
|
172
|
+
if (node.kind == "unary") {
|
|
173
|
+
const unary = /** @type {import("php-parser").Unary} */ (node)
|
|
174
|
+
|
|
175
|
+
if (!["!", "-"].includes(unary.type)) return unsupportedSyntax("php", `unary ${unary.type}`, location)
|
|
176
|
+
|
|
177
|
+
const semantic = withAdaptedOperation(withParserRanges({
|
|
178
|
+
kind: "UnaryExpression",
|
|
179
|
+
location,
|
|
180
|
+
operand: convertExpression(unary.what, filename, source)
|
|
181
|
+
}, {
|
|
182
|
+
operator: tokenLocation(unary.type, unary.loc?.start.offset ?? 0, unary.what.loc?.start.offset ?? unary.loc?.end.offset ?? source.length, filename, source)
|
|
183
|
+
}), unary.type == "!" ? "Not" : "Negate")
|
|
184
|
+
|
|
185
|
+
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (node.kind == "bin") {
|
|
189
|
+
const binary = /** @type {import("php-parser").Bin} */ (node)
|
|
190
|
+
if (["==", "!="].includes(binary.type)) return unsupportedSyntax("php", `coercive equality ${binary.type}`, location)
|
|
191
|
+
if (["and", "or"].includes(binary.type)) return unsupportedSyntax("php", `precedence-sensitive boolean ${binary.type}`, location)
|
|
192
|
+
if (!phpBinaryOperations.has(binary.type)) return unsupportedSyntax("php", `binary ${binary.type}`, location)
|
|
193
|
+
|
|
194
|
+
const semantic = withAdaptedOperation({
|
|
195
|
+
kind: /** @type {const} */ ("BinaryExpression"),
|
|
196
|
+
left: convertExpression(binary.left, filename, source),
|
|
197
|
+
location,
|
|
198
|
+
right: convertExpression(binary.right, filename, source)
|
|
199
|
+
}, /** @type {import("../semantic/operators.js").AdaptedOperation} */ (phpBinaryOperations.get(binary.type)))
|
|
200
|
+
|
|
201
|
+
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges(semantic, {
|
|
202
|
+
operator: tokenLocation(binary.type, binary.left.loc?.end.offset ?? 0, binary.right.loc?.start.offset ?? source.length, filename, source)
|
|
203
|
+
})))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (node.kind == "call") {
|
|
207
|
+
const call = /** @type {import("php-parser").Call} */ (node)
|
|
208
|
+
|
|
209
|
+
if (call.what.kind != "name" && call.what.kind != "identifier") {
|
|
210
|
+
return unsupportedSyntax("php", "dynamic call", location)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const callee = /** @type {import("php-parser").Name | import("php-parser").Identifier} */ (call.what).name
|
|
214
|
+
|
|
215
|
+
return withParserRanges({
|
|
216
|
+
arguments: call.arguments.map((argument) => convertExpression(argument, filename, source)),
|
|
217
|
+
callee,
|
|
218
|
+
kind: "CallExpression",
|
|
219
|
+
location
|
|
220
|
+
}, {callee: nodeLocation(call.what, filename, source)})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return unsupportedSyntax("php", node.kind, location)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Converts one explicit PHP return.
|
|
228
|
+
* @param {import("php-parser").Node} node - PHP node.
|
|
229
|
+
* @param {string} filename - Source filename.
|
|
230
|
+
* @param {string} source - Complete source.
|
|
231
|
+
* @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
|
|
232
|
+
*/
|
|
233
|
+
function convertReturn(node, filename, source) {
|
|
234
|
+
const location = nodeLocation(node, filename, source)
|
|
235
|
+
|
|
236
|
+
if (node.kind != "return") return unsupportedSyntax("php", node.kind, location)
|
|
237
|
+
|
|
238
|
+
const returnNode = /** @type {import("php-parser").Return} */ (node)
|
|
239
|
+
|
|
240
|
+
if (!returnNode.expr) return unsupportedSyntax("php", "empty return", location)
|
|
241
|
+
|
|
242
|
+
return {expression: convertExpression(returnNode.expr, filename, source), kind: "ReturnStatement", location}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Reads one exact PHP local `@var` carrier and immutability marker.
|
|
247
|
+
* @param {import("php-parser").Node} node - Assignment-owning statement.
|
|
248
|
+
* @param {string} name - Local name.
|
|
249
|
+
* @param {string} filename - Source filename.
|
|
250
|
+
* @param {string} source - Complete source.
|
|
251
|
+
* @returns {{immutable: boolean, type: import("../semantic/types.js").TypeReference} | undefined} Metadata when present.
|
|
252
|
+
*/
|
|
253
|
+
function localMetadata(node, name, filename, source) {
|
|
254
|
+
const comments = node.leadingComments ?? []
|
|
255
|
+
const comment = comments.at(-1)
|
|
256
|
+
|
|
257
|
+
if (!comment || comment.kind != "commentblock" || !comment.loc || !node.loc) return undefined
|
|
258
|
+
|
|
259
|
+
const gap = source.slice(comment.loc.end.offset, node.loc.start.offset)
|
|
260
|
+
|
|
261
|
+
if (!/^\s*$/u.test(gap) || (gap.match(/\n/gu)?.length ?? 0) > 1) return undefined
|
|
262
|
+
|
|
263
|
+
const block = parseComment(comment.value)[0]
|
|
264
|
+
const tags = block?.tags ?? []
|
|
265
|
+
const variables = tags.filter((tag) => tag.tag == "var")
|
|
266
|
+
const immutable = tags.filter((tag) => tag.tag == "semantifold-immutable")
|
|
267
|
+
const location = nodeLocation(node, filename, source)
|
|
268
|
+
|
|
269
|
+
if (variables.length == 0 && immutable.length == 0) return undefined
|
|
270
|
+
|
|
271
|
+
const exactVariable = variables.length == 1 && variables[0].type == "" && variables[0].description == `$${name}`
|
|
272
|
+
const exactImmutable = immutable.length <= 1 && immutable.every((tag) => tag.name == "" && tag.type == "" && tag.description == "")
|
|
273
|
+
const knownTags = tags.every((tag) => tag.tag == "var" || tag.tag == "semantifold-immutable")
|
|
274
|
+
|
|
275
|
+
if (!exactVariable || !exactImmutable || !knownTags) {
|
|
276
|
+
return unsupportedSyntax("php", "malformed local type metadata", location)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
immutable: immutable.length == 1,
|
|
281
|
+
type: requireSourceScalarType(
|
|
282
|
+
"php",
|
|
283
|
+
variables[0].name,
|
|
284
|
+
`Local '${name}'`,
|
|
285
|
+
location,
|
|
286
|
+
commentTagLocation(comment, variables[0], "name", filename, source)
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Converts one PHP local declaration or assignment.
|
|
293
|
+
* @param {import("php-parser").Node} node - PHP statement.
|
|
294
|
+
* @param {Set<string>} visible - Names visible during adaptation.
|
|
295
|
+
* @param {string} filename - Source filename.
|
|
296
|
+
* @param {string} source - Complete source.
|
|
297
|
+
* @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
|
|
298
|
+
*/
|
|
299
|
+
function convertLocalStatement(node, visible, filename, source) {
|
|
300
|
+
const location = nodeLocation(node, filename, source)
|
|
301
|
+
|
|
302
|
+
if (node.kind != "expressionstatement") return unsupportedSyntax("php", node.kind, location)
|
|
303
|
+
|
|
304
|
+
const expression = /** @type {import("php-parser").ExpressionStatement} */ (node).expression
|
|
305
|
+
|
|
306
|
+
if (expression.kind != "assign") return unsupportedSyntax("php", expression.kind, nodeLocation(expression, filename, source))
|
|
307
|
+
|
|
308
|
+
const assignment = /** @type {import("php-parser").Assign} */ (expression)
|
|
309
|
+
|
|
310
|
+
if (assignment.operator != "=") return unsupportedSyntax("php", `assignment ${assignment.operator}`, location)
|
|
311
|
+
if (assignment.left.kind != "variable") return unsupportedSyntax("php", assignment.left.kind, nodeLocation(assignment.left, filename, source))
|
|
312
|
+
|
|
313
|
+
const variable = /** @type {import("php-parser").Variable} */ (assignment.left)
|
|
314
|
+
const targetLocation = nodeLocation(variable, filename, source)
|
|
315
|
+
|
|
316
|
+
if (typeof variable.name != "string" || variable.curly) return unsupportedSyntax("php", "dynamic variable", targetLocation)
|
|
317
|
+
|
|
318
|
+
const metadata = localMetadata(node, variable.name, filename, source)
|
|
319
|
+
|
|
320
|
+
if (metadata) {
|
|
321
|
+
visible.add(variable.name)
|
|
322
|
+
return withParserRanges({
|
|
323
|
+
initializer: convertExpression(assignment.right, filename, source),
|
|
324
|
+
kind: "LocalDeclaration",
|
|
325
|
+
location,
|
|
326
|
+
mutable: !metadata.immutable,
|
|
327
|
+
name: variable.name,
|
|
328
|
+
type: metadata.type
|
|
329
|
+
}, {
|
|
330
|
+
name: targetLocation,
|
|
331
|
+
operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0, assignment.right.loc?.start.offset ?? source.length, filename, source)
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!visible.has(variable.name)) return missingType("php", `Local '${variable.name}'`, location)
|
|
336
|
+
|
|
337
|
+
const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: variable.name}, {
|
|
338
|
+
name: targetLocation
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
return withParserRanges({
|
|
342
|
+
expression: convertExpression(assignment.right, filename, source),
|
|
343
|
+
kind: "AssignmentStatement",
|
|
344
|
+
location,
|
|
345
|
+
target
|
|
346
|
+
}, {
|
|
347
|
+
operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0, assignment.right.loc?.start.offset ?? source.length, filename, source)
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Converts one exhaustive PHP statement.
|
|
353
|
+
* @param {import("php-parser").Node} node - PHP statement.
|
|
354
|
+
* @param {Set<string>} visible - Names visible during adaptation.
|
|
355
|
+
* @param {string} filename - Source filename.
|
|
356
|
+
* @param {string} source - Complete source.
|
|
357
|
+
* @returns {import("../semantic/types.js").Statement} Semantic statement.
|
|
358
|
+
*/
|
|
359
|
+
function convertStatement(node, visible, filename, source) {
|
|
360
|
+
if (node.kind == "return") return convertReturn(node, filename, source)
|
|
361
|
+
if (node.kind == "if") return convertIf(/** @type {import("php-parser").If} */ (node), visible, filename, source)
|
|
362
|
+
if (node.kind == "echo") return convertPrint(/** @type {import("php-parser").Echo} */ (node), filename, source)
|
|
363
|
+
if (node.kind == "expressionstatement") return convertLocalStatement(node, visible, filename, source)
|
|
364
|
+
|
|
365
|
+
return unsupportedSyntax("php", node.kind, nodeLocation(node, filename, source))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Converts an ordered PHP block with its own adaptation scope.
|
|
370
|
+
* @param {import("php-parser").Block} node - PHP parser block.
|
|
371
|
+
* @param {Set<string>} visible - Names visible on block entry.
|
|
372
|
+
* @param {string} filename - Source filename.
|
|
373
|
+
* @param {string} source - Complete source.
|
|
374
|
+
* @returns {import("../semantic/types.js").Block} Semantic block.
|
|
375
|
+
*/
|
|
376
|
+
function convertBlock(node, visible, filename, source) {
|
|
377
|
+
return {
|
|
378
|
+
kind: "Block",
|
|
379
|
+
location: nodeLocation(node, filename, source),
|
|
380
|
+
statements: node.children.map((statement) => convertStatement(statement, visible, filename, source))
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Requires an exact PHP scalar declaration type.
|
|
386
|
+
* @param {import("php-parser").Node | null} sourceType - PHP type node.
|
|
387
|
+
* @param {string} subject - Typed subject.
|
|
388
|
+
* @param {import("../semantic/types.js").SourceLocation} location - Source location.
|
|
389
|
+
* @param {string} filename - Source filename.
|
|
390
|
+
* @param {string} source - Complete source.
|
|
391
|
+
* @returns {import("../semantic/types.js").TypeReference} Semantic type.
|
|
392
|
+
*/
|
|
393
|
+
function convertType(sourceType, subject, location, filename, source) {
|
|
394
|
+
if (!sourceType) return requireSourceScalarType("php", undefined, subject, location)
|
|
395
|
+
if (sourceType.kind != "typereference" && sourceType.kind != "name" && sourceType.kind != "identifier") {
|
|
396
|
+
return unsupportedSyntax("php", "unsupported scalar type", nodeLocation(sourceType, filename, source))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const typeName = /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (sourceType).name
|
|
400
|
+
|
|
401
|
+
return requireSourceScalarType("php", typeName, subject, location, nodeLocation(sourceType, filename, source))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Converts a PHP function.
|
|
406
|
+
* @param {import("php-parser").Function} node - PHP function node.
|
|
407
|
+
* @param {string} filename - Source filename.
|
|
408
|
+
* @param {string} source - Complete source.
|
|
409
|
+
* @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
|
|
410
|
+
*/
|
|
411
|
+
function convertFunction(node, filename, source) {
|
|
412
|
+
const location = nodeLocation(node, filename, source)
|
|
413
|
+
const name = typeof node.name == "string" ? node.name : node.name.name
|
|
414
|
+
|
|
415
|
+
if (!node.body) {
|
|
416
|
+
return unsupportedSyntax("php", "function body", location)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const parameters = node.arguments.map((parameter) => {
|
|
420
|
+
const parameterLocation = nodeLocation(parameter, filename, source)
|
|
421
|
+
const parameterName = typeof parameter.name == "string" ? parameter.name : parameter.name.name
|
|
422
|
+
|
|
423
|
+
if (parameter.nullable) return unsupportedSyntax("php", "unsupported scalar type", parameterLocation)
|
|
424
|
+
|
|
425
|
+
const parameterNode = typeof parameter.name == "string" ? parameter : parameter.name
|
|
426
|
+
const semanticParameter = {
|
|
427
|
+
kind: /** @type {const} */ ("Parameter"),
|
|
428
|
+
location: parameterLocation,
|
|
429
|
+
name: parameterName,
|
|
430
|
+
type: convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return withParserRanges(semanticParameter, {name: nodeLocation(parameterNode, filename, source)})
|
|
434
|
+
})
|
|
435
|
+
if (node.nullable) return unsupportedSyntax("php", "unsupported scalar type", location)
|
|
436
|
+
const visible = new Set(parameters.map((parameter) => parameter.name))
|
|
437
|
+
const body = convertBlock(node.body, visible, filename, source)
|
|
438
|
+
|
|
439
|
+
const nameNode = typeof node.name == "string" ? node : node.name
|
|
440
|
+
|
|
441
|
+
return withParserRanges({
|
|
442
|
+
body,
|
|
443
|
+
kind: "FunctionDeclaration",
|
|
444
|
+
location,
|
|
445
|
+
name,
|
|
446
|
+
parameters,
|
|
447
|
+
returnType: convertType(node.type, `Function '${name}' return`, location, filename, source)
|
|
448
|
+
}, {name: nodeLocation(nameNode, filename, source)})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Converts the existing PHP if/else terminal with restricted branch prefixes.
|
|
453
|
+
* @param {import("php-parser").If} node - PHP if node.
|
|
454
|
+
* @param {Set<string>} visible - Enclosing visible names.
|
|
455
|
+
* @param {string} filename - Source filename.
|
|
456
|
+
* @param {string} source - Complete source.
|
|
457
|
+
* @returns {import("../semantic/types.js").IfStatement} Semantic branch.
|
|
458
|
+
*/
|
|
459
|
+
function convertIf(node, visible, filename, source) {
|
|
460
|
+
const location = nodeLocation(node, filename, source)
|
|
461
|
+
|
|
462
|
+
if (node.shortForm) return unsupportedSyntax("php", "alternative if syntax", location)
|
|
463
|
+
if (node.body.kind != "block") return unsupportedSyntax("php", "if without block consequent", location)
|
|
464
|
+
|
|
465
|
+
const consequentVisible = new Set(visible)
|
|
466
|
+
let alternate
|
|
467
|
+
|
|
468
|
+
if (node.alternate) {
|
|
469
|
+
const alternateVisible = new Set(visible)
|
|
470
|
+
|
|
471
|
+
if (node.alternate.kind == "block") {
|
|
472
|
+
alternate = convertBlock(/** @type {import("php-parser").Block} */ (node.alternate), alternateVisible, filename, source)
|
|
473
|
+
} else if (node.alternate.kind == "if") {
|
|
474
|
+
const nested = /** @type {import("php-parser").If} */ (node.alternate)
|
|
475
|
+
|
|
476
|
+
alternate = {
|
|
477
|
+
kind: /** @type {const} */ ("Block"),
|
|
478
|
+
location: nodeLocation(nested, filename, source),
|
|
479
|
+
statements: [convertIf(nested, alternateVisible, filename, source)]
|
|
480
|
+
}
|
|
481
|
+
} else return unsupportedSyntax("php", `if alternate ${node.alternate.kind}`, nodeLocation(node.alternate, filename, source))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
...(alternate ? {alternate} : {}),
|
|
486
|
+
condition: convertExpression(node.test, filename, source),
|
|
487
|
+
consequent: convertBlock(node.body, consequentVisible, filename, source),
|
|
488
|
+
kind: "IfStatement",
|
|
489
|
+
location
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Converts a PHP echo entry point.
|
|
495
|
+
* @param {import("php-parser").Echo} node - PHP echo node.
|
|
496
|
+
* @param {string} filename - Source filename.
|
|
497
|
+
* @param {string} source - Complete source.
|
|
498
|
+
* @returns {import("../semantic/types.js").PrintStatement} Print statement.
|
|
499
|
+
*/
|
|
500
|
+
function convertPrint(node, filename, source) {
|
|
501
|
+
const location = nodeLocation(node, filename, source)
|
|
502
|
+
|
|
503
|
+
if (node.expressions.length != 2 || node.expressions[1].kind != "name" ||
|
|
504
|
+
/** @type {import("php-parser").Name} */ (node.expressions[1]).name != "PHP_EOL") {
|
|
505
|
+
return unsupportedSyntax("php", "echo without one expression and PHP_EOL", location)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {expression: convertExpression(node.expressions[0], filename, source), kind: "PrintStatement", location}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Parses PHP into the shared semantic module.
|
|
513
|
+
* @param {object} input - Parser input.
|
|
514
|
+
* @param {string} input.filename - Source filename.
|
|
515
|
+
* @param {string} input.source - Source text.
|
|
516
|
+
* @returns {import("../semantic/types.js").SemanticModule} Semantic module.
|
|
517
|
+
*/
|
|
518
|
+
export function parsePhp({filename, source}) {
|
|
519
|
+
const program = parsePhpProgram(filename, source)
|
|
520
|
+
validateDeclareNodes(program.children, filename, source)
|
|
521
|
+
const functions = program.children.filter((node) => node.kind == "function")
|
|
522
|
+
.map((node) => convertFunction(/** @type {import("php-parser").Function} */ (node), filename, source))
|
|
523
|
+
const executableNodes = program.children.filter((node) => node.kind != "function" && node.kind != "declare" && node.kind != "noop")
|
|
524
|
+
const location = moduleLocation(filename, source)
|
|
525
|
+
|
|
526
|
+
if (functions.length == 0) return unsupportedSyntax("php", "module without a function", location)
|
|
527
|
+
const first = executableNodes[0]
|
|
528
|
+
const last = executableNodes.at(-1) ?? first
|
|
529
|
+
const entryLocation = first
|
|
530
|
+
? locationFromOffsets(filename, source, first.loc?.start.offset ?? 0, last?.loc?.end.offset ?? source.length)
|
|
531
|
+
: location
|
|
532
|
+
const entryVisible = new Set()
|
|
533
|
+
const entryBlock = {
|
|
534
|
+
kind: /** @type {const} */ ("Block"),
|
|
535
|
+
location: entryLocation,
|
|
536
|
+
statements: executableNodes.map((node) => convertStatement(node, entryVisible, filename, source))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
entryPoint: {
|
|
541
|
+
body: entryBlock,
|
|
542
|
+
kind: "EntryPoint",
|
|
543
|
+
location: entryLocation
|
|
544
|
+
},
|
|
545
|
+
functions,
|
|
546
|
+
kind: "Module",
|
|
547
|
+
location
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Accepts only an optional single `declare(strict_types=1)` directive.
|
|
553
|
+
* @param {import("php-parser").Node[]} nodes - Top-level PHP nodes.
|
|
554
|
+
* @param {string} filename - Source filename.
|
|
555
|
+
* @param {string} source - Complete source.
|
|
556
|
+
* @returns {void}
|
|
557
|
+
*/
|
|
558
|
+
function validateDeclareNodes(nodes, filename, source) {
|
|
559
|
+
const declarations = nodes.filter((node) => node.kind == "declare")
|
|
560
|
+
|
|
561
|
+
if (declarations.length > 1) {
|
|
562
|
+
unsupportedSyntax("php", "multiple declare directives", nodeLocation(declarations[1], filename, source))
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (const declarationNode of declarations) {
|
|
566
|
+
const declaration = /** @type {import("php-parser").Declare} */ (declarationNode)
|
|
567
|
+
const directive = declaration.directives[0]
|
|
568
|
+
const key = directive?.key
|
|
569
|
+
const value = directive?.value
|
|
570
|
+
const numericValue = value && typeof value == "object" && value.kind == "number"
|
|
571
|
+
? /** @type {import("php-parser").Number} */ (value)
|
|
572
|
+
: undefined
|
|
573
|
+
const valueIsOne = numericValue?.loc && source.slice(numericValue.loc.start.offset, numericValue.loc.end.offset) == "1"
|
|
574
|
+
const valid = declaration.mode == "none" && declaration.children.length == 0 && declaration.directives.length == 1 &&
|
|
575
|
+
key.kind == "identifier" && key.name == "strict_types" && valueIsOne
|
|
576
|
+
|
|
577
|
+
if (!valid) unsupportedSyntax("php", "declare other than strict_types=1", nodeLocation(declaration, filename, source))
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Invokes php-parser and normalizes syntax failures.
|
|
583
|
+
* @param {string} filename - Source filename.
|
|
584
|
+
* @param {string} source - Source text.
|
|
585
|
+
* @returns {import("php-parser").Program} PHP program.
|
|
586
|
+
*/
|
|
587
|
+
function parsePhpProgram(filename, source) {
|
|
588
|
+
try {
|
|
589
|
+
return parser.parseCode(source, filename)
|
|
590
|
+
} catch (error) {
|
|
591
|
+
return parseFailure("php", error)
|
|
592
|
+
}
|
|
593
|
+
}
|