semantifold 0.3.0 → 0.5.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/README.md +61 -15
- package/build/index.d.ts +52 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +60 -8
- package/build/src/artifact-path.d.ts +23 -1
- package/build/src/artifact-path.d.ts.map +1 -1
- package/build/src/artifact-path.js +45 -3
- package/build/src/artifacts.d.ts.map +1 -1
- package/build/src/artifacts.js +66 -1
- package/build/src/backends/android.d.ts +49 -0
- package/build/src/backends/android.d.ts.map +1 -0
- package/build/src/backends/android.js +1274 -0
- package/build/src/backends/c-validation.d.ts.map +1 -1
- package/build/src/backends/c-validation.js +7 -3
- package/build/src/backends/cpp-validation.d.ts.map +1 -1
- package/build/src/backends/cpp-validation.js +7 -3
- package/build/src/backends/csharp.js +7 -1
- package/build/src/backends/dart-runtime.d.ts +2 -0
- package/build/src/backends/dart-runtime.d.ts.map +1 -0
- package/build/src/backends/dart-runtime.js +41 -0
- package/build/src/backends/dart.d.ts +20 -0
- package/build/src/backends/dart.d.ts.map +1 -0
- package/build/src/backends/dart.js +321 -0
- package/build/src/backends/effects.d.ts +60 -0
- package/build/src/backends/effects.d.ts.map +1 -0
- package/build/src/backends/effects.js +746 -0
- package/build/src/backends/go.js +18 -4
- package/build/src/backends/identifiers.d.ts +36 -4
- package/build/src/backends/identifiers.d.ts.map +1 -1
- package/build/src/backends/identifiers.js +160 -10
- package/build/src/backends/index.d.ts.map +1 -1
- package/build/src/backends/index.js +27 -4
- package/build/src/backends/ios.d.ts +68 -0
- package/build/src/backends/ios.d.ts.map +1 -0
- package/build/src/backends/ios.js +1503 -0
- package/build/src/backends/java.d.ts.map +1 -1
- package/build/src/backends/java.js +429 -36
- package/build/src/backends/javascript.d.ts +6 -0
- package/build/src/backends/javascript.d.ts.map +1 -1
- package/build/src/backends/javascript.js +380 -24
- package/build/src/backends/kotlin-runtime.d.ts +3 -0
- package/build/src/backends/kotlin-runtime.d.ts.map +1 -0
- package/build/src/backends/kotlin-runtime.js +29 -0
- package/build/src/backends/kotlin.d.ts +33 -0
- package/build/src/backends/kotlin.d.ts.map +1 -0
- package/build/src/backends/kotlin.js +138 -0
- package/build/src/backends/native-validation.d.ts +2 -2
- package/build/src/backends/native-validation.d.ts.map +1 -1
- package/build/src/backends/native-validation.js +2 -2
- package/build/src/backends/ordered-expressions.d.ts.map +1 -1
- package/build/src/backends/ordered-expressions.js +39 -7
- package/build/src/backends/php-stdlib-support.d.ts +19 -0
- package/build/src/backends/php-stdlib-support.d.ts.map +1 -0
- package/build/src/backends/php-stdlib-support.js +227 -0
- package/build/src/backends/php.d.ts.map +1 -1
- package/build/src/backends/php.js +448 -33
- package/build/src/backends/program.d.ts +83 -0
- package/build/src/backends/program.d.ts.map +1 -0
- package/build/src/backends/program.js +1309 -0
- package/build/src/backends/python.js +7 -1
- package/build/src/backends/ruby.d.ts.map +1 -1
- package/build/src/backends/ruby.js +310 -24
- package/build/src/backends/rust-runtime.d.ts +7 -0
- package/build/src/backends/rust-runtime.d.ts.map +1 -0
- package/build/src/backends/rust-runtime.js +32 -0
- package/build/src/backends/rust-validation.d.ts +9 -0
- package/build/src/backends/rust-validation.d.ts.map +1 -0
- package/build/src/backends/rust-validation.js +78 -0
- package/build/src/backends/rust.d.ts +24 -0
- package/build/src/backends/rust.d.ts.map +1 -0
- package/build/src/backends/rust.js +344 -0
- package/build/src/backends/scalars.d.ts +14 -6
- package/build/src/backends/scalars.d.ts.map +1 -1
- package/build/src/backends/scalars.js +167 -17
- package/build/src/backends/shared.d.ts +23 -4
- package/build/src/backends/shared.d.ts.map +1 -1
- package/build/src/backends/shared.js +2174 -62
- package/build/src/backends/stdlib-support.d.ts +14 -0
- package/build/src/backends/stdlib-support.d.ts.map +1 -0
- package/build/src/backends/stdlib-support.js +237 -0
- package/build/src/backends/swift-runtime.d.ts +3 -0
- package/build/src/backends/swift-runtime.d.ts.map +1 -0
- package/build/src/backends/swift-runtime.js +14 -0
- package/build/src/backends/swift.d.ts +8 -0
- package/build/src/backends/swift.d.ts.map +1 -0
- package/build/src/backends/swift.js +127 -0
- package/build/src/backends/typescript.d.ts.map +1 -1
- package/build/src/backends/typescript.js +344 -24
- package/build/src/backends/wasm.d.ts +43 -0
- package/build/src/backends/wasm.d.ts.map +1 -0
- package/build/src/backends/wasm.js +2164 -0
- package/build/src/backends/writer.d.ts +255 -4
- package/build/src/backends/writer.d.ts.map +1 -1
- package/build/src/backends/writer.js +491 -3
- package/build/src/diagnostic.d.ts +4 -3
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +5 -3
- package/build/src/frontends/c.d.ts.map +1 -1
- package/build/src/frontends/c.js +5 -3
- package/build/src/frontends/cpp.d.ts.map +1 -1
- package/build/src/frontends/cpp.js +5 -3
- package/build/src/frontends/csharp.d.ts.map +1 -1
- package/build/src/frontends/csharp.js +7 -3
- package/build/src/frontends/dart.d.ts +10 -0
- package/build/src/frontends/dart.d.ts.map +1 -0
- package/build/src/frontends/dart.js +913 -0
- package/build/src/frontends/index.d.ts +3 -1
- package/build/src/frontends/index.d.ts.map +1 -1
- package/build/src/frontends/index.js +15 -5
- package/build/src/frontends/java.d.ts +110 -1
- package/build/src/frontends/java.d.ts.map +1 -1
- package/build/src/frontends/java.js +1725 -70
- package/build/src/frontends/javascript-typescript.d.ts +69 -1
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
- package/build/src/frontends/javascript-typescript.js +1825 -93
- package/build/src/frontends/kotlin.d.ts +10 -0
- package/build/src/frontends/kotlin.d.ts.map +1 -0
- package/build/src/frontends/kotlin.js +685 -0
- package/build/src/frontends/php.d.ts +118 -1
- package/build/src/frontends/php.d.ts.map +1 -1
- package/build/src/frontends/php.js +1583 -76
- package/build/src/frontends/program.d.ts +74 -0
- package/build/src/frontends/program.d.ts.map +1 -0
- package/build/src/frontends/program.js +989 -0
- package/build/src/frontends/ruby.d.ts +68 -1
- package/build/src/frontends/ruby.d.ts.map +1 -1
- package/build/src/frontends/ruby.js +1446 -101
- package/build/src/frontends/rust-parser.d.ts +11 -0
- package/build/src/frontends/rust-parser.d.ts.map +1 -0
- package/build/src/frontends/rust-parser.js +15 -0
- package/build/src/frontends/rust.d.ts +14 -0
- package/build/src/frontends/rust.d.ts.map +1 -0
- package/build/src/frontends/rust.js +715 -0
- package/build/src/frontends/scalars.d.ts +10 -0
- package/build/src/frontends/scalars.d.ts.map +1 -1
- package/build/src/frontends/scalars.js +28 -2
- package/build/src/frontends/swift.d.ts +10 -0
- package/build/src/frontends/swift.d.ts.map +1 -0
- package/build/src/frontends/swift.js +860 -0
- package/build/src/frontends/types.d.ts +186 -0
- package/build/src/frontends/types.d.ts.map +1 -0
- package/build/src/frontends/types.js +634 -0
- package/build/src/language-registry.d.ts +46 -4
- package/build/src/language-registry.d.ts.map +1 -1
- package/build/src/language-registry.js +177 -13
- package/build/src/mapping.d.ts +2 -2
- package/build/src/mapping.d.ts.map +1 -1
- package/build/src/mapping.js +31 -10
- package/build/src/semantic/capabilities.d.ts +66 -0
- package/build/src/semantic/capabilities.d.ts.map +1 -0
- package/build/src/semantic/capabilities.js +649 -0
- package/build/src/semantic/effects.d.ts +26 -0
- package/build/src/semantic/effects.d.ts.map +1 -0
- package/build/src/semantic/effects.js +764 -0
- package/build/src/semantic/generics.d.ts +23 -0
- package/build/src/semantic/generics.d.ts.map +1 -0
- package/build/src/semantic/generics.js +61 -0
- package/build/src/semantic/lifetime.d.ts +37 -0
- package/build/src/semantic/lifetime.d.ts.map +1 -0
- package/build/src/semantic/lifetime.js +527 -0
- package/build/src/semantic/provenance.d.ts +9 -2
- package/build/src/semantic/provenance.d.ts.map +1 -1
- package/build/src/semantic/provenance.js +462 -20
- package/build/src/semantic/scalars.d.ts +11 -0
- package/build/src/semantic/scalars.d.ts.map +1 -1
- package/build/src/semantic/scalars.js +16 -1
- package/build/src/semantic/stdlib.d.ts +254 -0
- package/build/src/semantic/stdlib.d.ts.map +1 -0
- package/build/src/semantic/stdlib.js +880 -0
- package/build/src/semantic/types.d.ts +3108 -178
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +730 -15
- package/build/src/semantic/validate.d.ts +85 -5
- package/build/src/semantic/validate.d.ts.map +1 -1
- package/build/src/semantic/validate.js +2576 -98
- package/build/src/stdlib-facades.d.ts +304 -0
- package/build/src/stdlib-facades.d.ts.map +1 -0
- package/build/src/stdlib-facades.js +1167 -0
- package/build/src/stdlib-negotiation.d.ts +69 -0
- package/build/src/stdlib-negotiation.d.ts.map +1 -0
- package/build/src/stdlib-negotiation.js +403 -0
- package/build/src/stdlib-providers.d.ts +72 -0
- package/build/src/stdlib-providers.d.ts.map +1 -0
- package/build/src/stdlib-providers.js +339 -0
- package/build/src/toolchains.d.ts +55 -1
- package/build/src/toolchains.d.ts.map +1 -1
- package/build/src/toolchains.js +15 -4
- package/docs/android.md +79 -0
- package/docs/architecture.md +47 -13
- package/docs/dart.md +60 -0
- package/docs/goals.md +3 -3
- package/docs/ios.md +121 -0
- package/docs/kotlin.md +42 -0
- package/docs/language-support.md +238 -26
- package/docs/multi-file-modules.md +52 -0
- package/docs/parser-qualification.md +121 -8
- package/docs/plans/2026-09-05-standard-library-portability.md +10 -10
- package/docs/rust.md +88 -0
- package/docs/source-maps.md +17 -7
- package/docs/specification.md +5 -0
- package/docs/standard-library-portability.md +39 -8
- package/docs/swift.md +44 -0
- package/docs/testing.md +381 -4
- package/docs/webassembly.md +52 -0
- package/index.js +66 -8
- package/node_modules/semantifold-tree-sitter-legacy-internal/README.md +2 -2
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/LICENSE +21 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/README.md +45 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/binding.gyp +30 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding.cc +20 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding_test.js +9 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.d.ts +28 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.js +11 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/grammar.js +1630 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/package.json +64 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/highlights.scm +159 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/injections.scm +9 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/tags.scm +60 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/grammar.json +9405 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/node-types.json +5399 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/parser.c +192569 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/scanner.c +393 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/alloc.h +54 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/array.h +290 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/parser.h +266 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter-rust.wasm +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter.json +53 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/package.json +3 -2
- package/node_modules/semantifold-tree-sitter-legacy-internal/src/c.js +5 -4
- package/package.json +5 -2
- package/src/artifact-path.js +47 -2
- package/src/artifacts.js +67 -0
- package/src/backends/android.js +1370 -0
- package/src/backends/c-validation.js +6 -1
- package/src/backends/cpp-validation.js +6 -1
- package/src/backends/csharp.js +3 -0
- package/src/backends/dart-runtime.js +41 -0
- package/src/backends/dart.js +326 -0
- package/src/backends/effects.js +705 -0
- package/src/backends/go.js +14 -2
- package/src/backends/identifiers.js +166 -9
- package/src/backends/index.js +27 -2
- package/src/backends/ios.js +1612 -0
- package/src/backends/java.js +454 -36
- package/src/backends/javascript.js +389 -23
- package/src/backends/kotlin-runtime.js +29 -0
- package/src/backends/kotlin.js +144 -0
- package/src/backends/native-validation.js +1 -1
- package/src/backends/ordered-expressions.js +40 -6
- package/src/backends/php-stdlib-support.js +239 -0
- package/src/backends/php.js +469 -32
- package/src/backends/program.js +1408 -0
- package/src/backends/python.js +3 -0
- package/src/backends/ruby.js +326 -23
- package/src/backends/rust-runtime.js +34 -0
- package/src/backends/rust-validation.js +75 -0
- package/src/backends/rust.js +335 -0
- package/src/backends/scalars.js +141 -16
- package/src/backends/shared.js +2314 -115
- package/src/backends/stdlib-support.js +234 -0
- package/src/backends/swift-runtime.js +14 -0
- package/src/backends/swift.js +132 -0
- package/src/backends/typescript.js +349 -23
- package/src/backends/wasm.js +2208 -0
- package/src/backends/writer.js +545 -2
- package/src/diagnostic.js +4 -2
- package/src/frontends/c.js +4 -2
- package/src/frontends/cpp.js +4 -2
- package/src/frontends/csharp.js +4 -2
- package/src/frontends/dart.js +963 -0
- package/src/frontends/index.js +14 -4
- package/src/frontends/java.js +1977 -69
- package/src/frontends/javascript-typescript.js +2046 -102
- package/src/frontends/kotlin.js +713 -0
- package/src/frontends/php.js +1725 -79
- package/src/frontends/program.js +1062 -0
- package/src/frontends/ruby.js +1624 -98
- package/src/frontends/rust-parser.js +18 -0
- package/src/frontends/rust.js +714 -0
- package/src/frontends/scalars.js +31 -1
- package/src/frontends/swift.js +893 -0
- package/src/frontends/types.js +750 -0
- package/src/language-registry.js +182 -12
- package/src/mapping.js +33 -9
- package/src/semantic/capabilities.js +671 -0
- package/src/semantic/effects.js +737 -0
- package/src/semantic/generics.js +58 -0
- package/src/semantic/lifetime.js +517 -0
- package/src/semantic/provenance.js +412 -19
- package/src/semantic/scalars.js +17 -0
- package/src/semantic/stdlib.js +921 -0
- package/src/semantic/types.js +810 -14
- package/src/semantic/validate.js +2934 -209
- package/src/stdlib-facades.js +1211 -0
- package/src/stdlib-negotiation.js +446 -0
- package/src/stdlib-providers.js +377 -0
- package/src/toolchains.js +16 -3
package/src/frontends/php.js
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
import PhpParser from "php-parser"
|
|
4
4
|
import {parse as parseComment} from "comment-parser"
|
|
5
5
|
import {missingType, parseFailure, unsupportedSyntax} from "../diagnostic.js"
|
|
6
|
+
import {blockReferencesProtectedEntry} from "../semantic/effects.js"
|
|
6
7
|
import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
|
|
7
8
|
import {withAdaptedOperation} from "../semantic/operators.js"
|
|
8
9
|
import {withParserRanges} from "../semantic/provenance.js"
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import {isTask034ResourceProbe} from "../semantic/capabilities.js"
|
|
11
|
+
import {hasExactEffectSupport} from "../backends/effects.js"
|
|
12
|
+
import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
|
|
13
|
+
import {typeContainsAnyVariable} from "../semantic/generics.js"
|
|
14
|
+
import {requireSourceReturnType, requireSourceScalarType, sourceScalarType} from "./scalars.js"
|
|
15
|
+
import {capabilityFunctionSignatures, documentedValueType, instantiatedRecordFieldType, iterationBindingType, iterationOperandType, knownCallParameterTypes, knownCallReturnType, optionalType, orderedMapTypeFromMap, ownedResourceType, preservesGenericOptionalEvidence, recordType, referenceType, sameValueType} from "./types.js"
|
|
11
16
|
const parser = new PhpParser.Engine({
|
|
12
17
|
ast: {withPositions: true},
|
|
13
18
|
parser: {extractDoc: true, suppressErrors: false}
|
|
@@ -31,6 +36,31 @@ const phpBinaryOperations = new Map([
|
|
|
31
36
|
[".", "StringConcat"]
|
|
32
37
|
])
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @typedef PhpConversionContext
|
|
41
|
+
* @property {Map<string, import("../semantic/types.js").SemanticBindingType>} bindings - Explicitly typed visible bindings.
|
|
42
|
+
* @property {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
|
|
43
|
+
* @property {import("../semantic/types.js").ClassDeclaration} [currentClass] - Enclosing reference class.
|
|
44
|
+
* @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
|
|
45
|
+
* @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
|
|
46
|
+
* @property {Map<string, PhpFunctionSignature>} functions - Explicit module function signatures.
|
|
47
|
+
* @property {Set<object>} [orderedMapDeclarations] - Exact local declarations selected by parser-owned pair iteration.
|
|
48
|
+
* @property {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
49
|
+
* @property {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
|
|
50
|
+
* @property {import("../semantic/types.js").SemanticFunctionReturnType | undefined} returnType - Enclosing return type.
|
|
51
|
+
* @property {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef PhpFunctionSignature
|
|
56
|
+
* @property {import("../semantic/types.js").SourceLocation} location - Complete declaration location.
|
|
57
|
+
* @property {import("../semantic/types.js").SourceLocation} nameLocation - Parser-owned name location.
|
|
58
|
+
* @property {string} name - Function name.
|
|
59
|
+
* @property {import("../semantic/types.js").Parameter[]} parameters - Semantic parameters.
|
|
60
|
+
* @property {import("../semantic/types.js").SemanticFunctionReturnType} returnType - Semantic return type.
|
|
61
|
+
* @property {import("../semantic/types.js").TypeParameter[]} [typeParameters] - Declaration-scoped parameters.
|
|
62
|
+
*/
|
|
63
|
+
|
|
34
64
|
/**
|
|
35
65
|
* Returns a normalized PHP node location.
|
|
36
66
|
* @param {import("php-parser").Node} node - PHP parser node.
|
|
@@ -112,17 +142,47 @@ function tokenLocation(tokenText, startOffset, endOffset, filename, source) {
|
|
|
112
142
|
* @param {import("php-parser").Expression} node - PHP expression.
|
|
113
143
|
* @param {string} filename - Source filename.
|
|
114
144
|
* @param {string} source - Complete source.
|
|
145
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
146
|
+
* @param {import("../semantic/types.js").SemanticValueType} [expectedType] - Contextual type for empty PHP arrays.
|
|
147
|
+
* @param {boolean} [preserveOptional] - Whether an optional identifier remains wrapped.
|
|
115
148
|
* @returns {import("../semantic/types.js").Expression} Semantic expression.
|
|
116
149
|
*/
|
|
117
|
-
function convertExpression(node, filename, source) {
|
|
150
|
+
function convertExpression(node, filename, source, context, expectedType, preserveOptional = false) {
|
|
118
151
|
const location = nodeLocation(node, filename, source)
|
|
119
152
|
|
|
153
|
+
if (expectedType?.kind == "OptionalType") {
|
|
154
|
+
if (node.kind == "nullkeyword") {
|
|
155
|
+
return withParserRanges({kind: /** @type {const} */ ("OptionalNone"), location}, {absence: location})
|
|
156
|
+
}
|
|
157
|
+
if (knownExpressionType(node, context)?.kind == "OptionalType") {
|
|
158
|
+
return convertExpression(node, filename, source, context, undefined, true)
|
|
159
|
+
}
|
|
160
|
+
return withParserRanges({
|
|
161
|
+
kind: /** @type {const} */ ("OptionalSome"),
|
|
162
|
+
location,
|
|
163
|
+
value: convertExpression(node, filename, source, context, expectedType.valueType)
|
|
164
|
+
}, {some: location})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (node.kind == "nullkeyword") return unsupportedSyntax("php", "null outside an explicit optional context", location)
|
|
168
|
+
|
|
120
169
|
if (node.kind == "variable") {
|
|
121
170
|
const variable = /** @type {import("php-parser").Variable} */ (node)
|
|
122
171
|
|
|
123
172
|
if (typeof variable.name != "string") return unsupportedSyntax("php", "dynamic variable", location)
|
|
173
|
+
if (variable.name == "this") {
|
|
174
|
+
if (!context.currentClass) return unsupportedSyntax("php", "receiver outside a reference class", location)
|
|
175
|
+
return withParserRanges({classId: /** @type {string} */ (context.currentClass.id),
|
|
176
|
+
kind: /** @type {const} */ ("ReceiverExpression"), location}, {receiver: location})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const identifier = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: variable.name}, {name: location})
|
|
124
180
|
|
|
125
|
-
|
|
181
|
+
if (!preserveOptional && context.bindings.get(variable.name)?.kind == "OptionalType") {
|
|
182
|
+
return withParserRanges({kind: /** @type {const} */ ("OptionalUnwrap"), location, operand: identifier}, {unwrap: location})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return identifier
|
|
126
186
|
}
|
|
127
187
|
|
|
128
188
|
if (node.kind == "number") {
|
|
@@ -169,6 +229,175 @@ function convertExpression(node, filename, source) {
|
|
|
169
229
|
|
|
170
230
|
if (node.kind == "nowdoc") return unsupportedSyntax("php", "nowdoc string", location)
|
|
171
231
|
|
|
232
|
+
if (node.kind == "array") {
|
|
233
|
+
const literal = /** @type {import("php-parser").Array} */ (node)
|
|
234
|
+
const invalidEntry = literal.items.find((entry) => {
|
|
235
|
+
if (entry.kind != "entry") return true
|
|
236
|
+
const candidate = /** @type {import("php-parser").Entry} */ (entry)
|
|
237
|
+
|
|
238
|
+
return candidate.byRef || candidate.unpack
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
if (invalidEntry) return unsupportedSyntax("php", "array reference or unpacking", nodeLocation(invalidEntry, filename, source))
|
|
242
|
+
const entries = /** @type {import("php-parser").Entry[]} */ (literal.items)
|
|
243
|
+
const hasKeys = entries.some((entry) => entry.key != null)
|
|
244
|
+
const hasImplicit = entries.some((entry) => entry.key == null)
|
|
245
|
+
const collectionKind = entries.length == 0 ? expectedType?.kind : hasKeys && !hasImplicit
|
|
246
|
+
? expectedType?.kind == "OrderedMapType" ? "OrderedMapType" : "MapType"
|
|
247
|
+
: !hasKeys ? "ListType" : "mixed"
|
|
248
|
+
|
|
249
|
+
if (collectionKind == "mixed") return unsupportedSyntax("php", "mixed list/map array literal", location)
|
|
250
|
+
if (collectionKind == "ListType") {
|
|
251
|
+
const elementType = expectedType?.kind == "ListType" ? expectedType.elementType : undefined
|
|
252
|
+
|
|
253
|
+
return withParserRanges({
|
|
254
|
+
elements: entries.map((entry) => convertExpression(entry.value, filename, source, context, elementType)),
|
|
255
|
+
kind: /** @type {const} */ ("ListLiteral"),
|
|
256
|
+
location
|
|
257
|
+
}, {literal: location})
|
|
258
|
+
}
|
|
259
|
+
if (collectionKind == "MapType" || collectionKind == "OrderedMapType") {
|
|
260
|
+
const valueType = expectedType?.kind == "MapType" || expectedType?.kind == "OrderedMapType"
|
|
261
|
+
? expectedType.valueType : undefined
|
|
262
|
+
const semanticEntries = entries.map((entry) => {
|
|
263
|
+
if (!entry.key || entry.key.kind != "string") {
|
|
264
|
+
return unsupportedSyntax("php", "map key other than a literal string", entry.key ? nodeLocation(entry.key, filename, source) : nodeLocation(entry, filename, source))
|
|
265
|
+
}
|
|
266
|
+
const key = convertExpression(entry.key, filename, source, context)
|
|
267
|
+
|
|
268
|
+
return withParserRanges({
|
|
269
|
+
key: /** @type {import("../semantic/types.js").StringLiteral} */ (key),
|
|
270
|
+
kind: /** @type {const} */ ("MapEntry"),
|
|
271
|
+
location: nodeLocation(entry, filename, source),
|
|
272
|
+
value: convertExpression(entry.value, filename, source, context, valueType)
|
|
273
|
+
}, {operator: tokenLocation("=>", entry.key.loc?.end.offset ?? 0, entry.value.loc?.start.offset ?? source.length, filename, source)})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return withParserRanges({
|
|
277
|
+
entries: semanticEntries,
|
|
278
|
+
kind: collectionKind == "OrderedMapType"
|
|
279
|
+
? /** @type {const} */ ("OrderedMapLiteral")
|
|
280
|
+
: /** @type {const} */ ("MapLiteral"),
|
|
281
|
+
location
|
|
282
|
+
}, {literal: location})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return unsupportedSyntax("php", "empty array without a list or map type", location)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (node.kind == "new") {
|
|
289
|
+
const construction = /** @type {import("php-parser").New} */ (node)
|
|
290
|
+
const name = construction.what.kind == "name" ? /** @type {import("php-parser").Name} */ (construction.what) : undefined
|
|
291
|
+
|
|
292
|
+
if (!name || name.resolution != "uqn" || typeof name.name != "string") {
|
|
293
|
+
return unsupportedSyntax("php", "dynamic or qualified record construction", nodeLocation(construction.what, filename, source))
|
|
294
|
+
}
|
|
295
|
+
const declaration = context.recordNames.get(name.name)
|
|
296
|
+
|
|
297
|
+
if (!declaration) return unsupportedSyntax("php", "construction of an unknown nominal class", nodeLocation(construction.what, filename, source))
|
|
298
|
+
if (declaration.kind == "ClassDeclaration") {
|
|
299
|
+
const unsupportedArgument = construction.arguments.find((argument) =>
|
|
300
|
+
argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
|
|
301
|
+
|
|
302
|
+
if (unsupportedArgument) return unsupportedSyntax("php", "non-positional reference constructor argument",
|
|
303
|
+
nodeLocation(unsupportedArgument, filename, source))
|
|
304
|
+
const classLocation = nodeLocation(construction.what, filename, source)
|
|
305
|
+
|
|
306
|
+
return withParserRanges({arguments: construction.arguments.map((argument, index) =>
|
|
307
|
+
convertExpression(argument, filename, source, context, declaration.constructor.parameters[index]?.type)),
|
|
308
|
+
kind: /** @type {const} */ ("ReferenceConstruction"), location,
|
|
309
|
+
reference: referenceType(/** @type {string} */ (declaration.id), classLocation)}, {class: classLocation})
|
|
310
|
+
}
|
|
311
|
+
if (declaration.kind == "EffectResourceDeclaration") {
|
|
312
|
+
return unsupportedSyntax("php", "owned resource construction", nodeLocation(construction, filename, source))
|
|
313
|
+
}
|
|
314
|
+
const contextualTypeArguments = expectedType?.kind == "RecordType" && expectedType.declarationId == declaration.id
|
|
315
|
+
? expectedType.arguments
|
|
316
|
+
: undefined
|
|
317
|
+
const visibleTypeParameterIds = new Set([...context.typeParameters?.values() ?? []]
|
|
318
|
+
.map((parameter) => /** @type {string} */ (parameter.id)))
|
|
319
|
+
const typeArguments = contextualTypeArguments &&
|
|
320
|
+
!contextualTypeArguments.some((argument) => typeContainsAnyVariable(argument, visibleTypeParameterIds))
|
|
321
|
+
? contextualTypeArguments
|
|
322
|
+
: undefined
|
|
323
|
+
|
|
324
|
+
return withParserRanges({
|
|
325
|
+
arguments: construction.arguments.map((argument, index) =>
|
|
326
|
+
convertExpression(argument, filename, source, context,
|
|
327
|
+
instantiatedRecordFieldType(declaration, typeArguments, index))),
|
|
328
|
+
kind: /** @type {const} */ ("RecordConstruction"),
|
|
329
|
+
location,
|
|
330
|
+
record: recordType(/** @type {string} */ (declaration.id), nodeLocation(construction.what, filename, source),
|
|
331
|
+
typeArguments)
|
|
332
|
+
}, {record: nodeLocation(construction.what, filename, source)})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (node.kind == "propertylookup") {
|
|
336
|
+
const read = /** @type {import("php-parser").PropertyLookup} */ (node)
|
|
337
|
+
const offset = read.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (read.offset) : undefined
|
|
338
|
+
|
|
339
|
+
if (!offset || typeof offset.name != "string") {
|
|
340
|
+
return unsupportedSyntax("php", "dynamic or computed member access", nodeLocation(read.offset, filename, source))
|
|
341
|
+
}
|
|
342
|
+
if (read.what.kind == "variable" && /** @type {import("php-parser").Variable} */ (read.what).name == "this" &&
|
|
343
|
+
context.currentClass) {
|
|
344
|
+
const field = context.currentClass.fields.find((candidate) => candidate.name == offset.name)
|
|
345
|
+
|
|
346
|
+
if (!field) return unsupportedSyntax("php", `unknown private field '${offset.name}'`, nodeLocation(offset, filename, source))
|
|
347
|
+
return withParserRanges({field: /** @type {string} */ (field.id), kind: /** @type {const} */ ("PrivateFieldRead"), location,
|
|
348
|
+
receiver: /** @type {import("../semantic/types.js").ReceiverExpression} */ (
|
|
349
|
+
convertExpression(read.what, filename, source, context))}, {member: nodeLocation(offset, filename, source)})
|
|
350
|
+
}
|
|
351
|
+
const receiverType = knownExpressionType(read.what, context)
|
|
352
|
+
if (receiverType?.kind == "ReferenceType") {
|
|
353
|
+
return unsupportedSyntax("php", "private reference-class field access", location)
|
|
354
|
+
}
|
|
355
|
+
const receiverRecord = receiverType?.kind == "OptionalType" ? receiverType.valueType : receiverType
|
|
356
|
+
const declaration = receiverRecord?.kind == "RecordType" ? context.records.get(receiverRecord.declarationId) : undefined
|
|
357
|
+
|
|
358
|
+
if ((declaration?.typeParameters?.length ?? 0) > 0) {
|
|
359
|
+
return unsupportedSyntax("php", "generic record property access", nodeLocation(read.offset, filename, source))
|
|
360
|
+
}
|
|
361
|
+
return withParserRanges({
|
|
362
|
+
field: offset.name,
|
|
363
|
+
kind: /** @type {const} */ ("MemberRead"),
|
|
364
|
+
location,
|
|
365
|
+
receiver: convertExpression(read.what, filename, source, context)
|
|
366
|
+
}, {member: nodeLocation(read.offset, filename, source)})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (node.kind == "offsetlookup") {
|
|
370
|
+
const lookup = /** @type {import("php-parser").OffsetLookup} */ (node)
|
|
371
|
+
|
|
372
|
+
if (!lookup.offset) return unsupportedSyntax("php", "append or missing array index", location)
|
|
373
|
+
const receiver = lookup.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
|
|
374
|
+
const receiverType = receiver && typeof receiver.name == "string" ? context.bindings.get(receiver.name) : undefined
|
|
375
|
+
const accessKind = receiverType?.kind == "ListType" ? "list" :
|
|
376
|
+
receiverType?.kind == "MapType" || receiverType?.kind == "OrderedMapType" ? "map" :
|
|
377
|
+
lookup.offset.kind == "number" ? "list" : lookup.offset.kind == "string" ? "map" : undefined
|
|
378
|
+
|
|
379
|
+
if (accessKind == "list" && ["number", "string", "variable"].includes(lookup.offset.kind)) {
|
|
380
|
+
return withParserRanges({
|
|
381
|
+
collection: convertExpression(lookup.what, filename, source, context),
|
|
382
|
+
index: convertExpression(lookup.offset, filename, source, context),
|
|
383
|
+
kind: /** @type {const} */ ("ListIndexExpression"),
|
|
384
|
+
location,
|
|
385
|
+
totality: /** @type {const} */ ("proven")
|
|
386
|
+
}, {operator: tokenLocation("[", lookup.what.loc?.end.offset ?? 0, lookup.offset.loc?.start.offset ?? source.length, filename, source)})
|
|
387
|
+
}
|
|
388
|
+
if (accessKind == "map" && ["number", "string", "variable"].includes(lookup.offset.kind)) {
|
|
389
|
+
return withParserRanges({
|
|
390
|
+
collection: convertExpression(lookup.what, filename, source, context),
|
|
391
|
+
key: convertExpression(lookup.offset, filename, source, context),
|
|
392
|
+
kind: /** @type {const} */ ("MapLookupExpression"),
|
|
393
|
+
location,
|
|
394
|
+
totality: /** @type {const} */ ("proven")
|
|
395
|
+
}, {operator: tokenLocation("[", lookup.what.loc?.end.offset ?? 0, lookup.offset.loc?.start.offset ?? source.length, filename, source)})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return unsupportedSyntax("php", "nonliteral array access", nodeLocation(lookup.offset, filename, source))
|
|
399
|
+
}
|
|
400
|
+
|
|
172
401
|
if (node.kind == "unary") {
|
|
173
402
|
const unary = /** @type {import("php-parser").Unary} */ (node)
|
|
174
403
|
|
|
@@ -177,7 +406,7 @@ function convertExpression(node, filename, source) {
|
|
|
177
406
|
const semantic = withAdaptedOperation(withParserRanges({
|
|
178
407
|
kind: "UnaryExpression",
|
|
179
408
|
location,
|
|
180
|
-
operand: convertExpression(unary.what, filename, source)
|
|
409
|
+
operand: convertExpression(unary.what, filename, source, context)
|
|
181
410
|
}, {
|
|
182
411
|
operator: tokenLocation(unary.type, unary.loc?.start.offset ?? 0, unary.what.loc?.start.offset ?? unary.loc?.end.offset ?? source.length, filename, source)
|
|
183
412
|
}), unary.type == "!" ? "Not" : "Negate")
|
|
@@ -187,15 +416,24 @@ function convertExpression(node, filename, source) {
|
|
|
187
416
|
|
|
188
417
|
if (node.kind == "bin") {
|
|
189
418
|
const binary = /** @type {import("php-parser").Bin} */ (node)
|
|
419
|
+
if (binary.type == "!==" && binary.left.kind == "variable" && binary.right.kind == "nullkeyword") {
|
|
420
|
+
const operand = /** @type {import("../semantic/types.js").IdentifierExpression} */ (
|
|
421
|
+
convertExpression(binary.left, filename, source, context, undefined, true)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return withParserRanges({kind: /** @type {const} */ ("OptionalIsPresent"), location, operand}, {
|
|
425
|
+
operator: tokenLocation(binary.type, binary.left.loc?.end.offset ?? 0, binary.right.loc?.start.offset ?? source.length, filename, source)
|
|
426
|
+
})
|
|
427
|
+
}
|
|
190
428
|
if (["==", "!="].includes(binary.type)) return unsupportedSyntax("php", `coercive equality ${binary.type}`, location)
|
|
191
429
|
if (["and", "or"].includes(binary.type)) return unsupportedSyntax("php", `precedence-sensitive boolean ${binary.type}`, location)
|
|
192
430
|
if (!phpBinaryOperations.has(binary.type)) return unsupportedSyntax("php", `binary ${binary.type}`, location)
|
|
193
431
|
|
|
194
432
|
const semantic = withAdaptedOperation({
|
|
195
433
|
kind: /** @type {const} */ ("BinaryExpression"),
|
|
196
|
-
left: convertExpression(binary.left, filename, source),
|
|
434
|
+
left: convertExpression(binary.left, filename, source, context),
|
|
197
435
|
location,
|
|
198
|
-
right: convertExpression(binary.right, filename, source)
|
|
436
|
+
right: convertExpression(binary.right, filename, source, context)
|
|
199
437
|
}, /** @type {import("../semantic/operators.js").AdaptedOperation} */ (phpBinaryOperations.get(binary.type)))
|
|
200
438
|
|
|
201
439
|
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges(semantic, {
|
|
@@ -206,14 +444,88 @@ function convertExpression(node, filename, source) {
|
|
|
206
444
|
if (node.kind == "call") {
|
|
207
445
|
const call = /** @type {import("php-parser").Call} */ (node)
|
|
208
446
|
|
|
209
|
-
if (call.what.kind
|
|
447
|
+
if (call.what.kind == "propertylookup") {
|
|
448
|
+
const lookup = /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what))
|
|
449
|
+
const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
|
|
450
|
+
const receiverType = knownExpressionType(lookup.what, context)
|
|
451
|
+
|
|
452
|
+
if (member && typeof member.name == "string" && receiverType?.kind == "ReferenceType") {
|
|
453
|
+
const method = context.classes.get(receiverType.declarationId)?.methods.find((candidate) => candidate.name == member.name)
|
|
454
|
+
const unsupportedArgument = call.arguments.find((argument) =>
|
|
455
|
+
argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
|
|
456
|
+
|
|
457
|
+
if (unsupportedArgument) return unsupportedSyntax("php", "non-positional method argument",
|
|
458
|
+
nodeLocation(unsupportedArgument, filename, source))
|
|
459
|
+
return withParserRanges({arguments: call.arguments.map((argument, index) =>
|
|
460
|
+
convertExpression(argument, filename, source, context, method?.parameters[index]?.type)),
|
|
461
|
+
kind: /** @type {const} */ ("MethodCallExpression"), location, method: method?.id ?? member.name,
|
|
462
|
+
receiver: convertExpression(lookup.what, filename, source, context)}, {member: nodeLocation(member, filename, source)})
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (call.what.kind == "propertylookup" && call.arguments.length == 0) {
|
|
467
|
+
const lookup = /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what))
|
|
468
|
+
const receiver = lookup.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
|
|
469
|
+
const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
|
|
470
|
+
|
|
471
|
+
if (receiver && typeof receiver.name == "string" && member?.name == "getMessage" && context.bindings.get(receiver.name)?.kind == "ErrorType") {
|
|
472
|
+
const receiverLocation = nodeLocation(receiver, filename, source)
|
|
473
|
+
|
|
474
|
+
return withParserRanges({
|
|
475
|
+
kind: /** @type {const} */ ("ErrorMessageRead"),
|
|
476
|
+
location,
|
|
477
|
+
receiver: withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: receiverLocation, name: receiver.name}, {
|
|
478
|
+
name: receiverLocation
|
|
479
|
+
})
|
|
480
|
+
}, {member: nodeLocation(member, filename, source)})
|
|
481
|
+
}
|
|
482
|
+
if (member && typeof member.name == "string" && knownGenericRecordAccessorType(lookup, context)) {
|
|
483
|
+
return withParserRanges({
|
|
484
|
+
field: member.name,
|
|
485
|
+
kind: /** @type {const} */ ("MemberRead"),
|
|
486
|
+
location,
|
|
487
|
+
receiver: convertExpression(lookup.what, filename, source, context)
|
|
488
|
+
}, {member: nodeLocation(member, filename, source)})
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (call.what.kind == "name" && call.what.name == "count" && call.arguments.length == 1) {
|
|
493
|
+
return withParserRanges({
|
|
494
|
+
collection: convertExpression(call.arguments[0], filename, source, context),
|
|
495
|
+
kind: /** @type {const} */ ("CollectionSizeExpression"),
|
|
496
|
+
location
|
|
497
|
+
}, {operator: nodeLocation(call.what, filename, source)})
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (call.what.kind != "name") {
|
|
210
501
|
return unsupportedSyntax("php", "dynamic call", location)
|
|
211
502
|
}
|
|
503
|
+
const calledName = /** @type {import("php-parser").Name} */ (call.what)
|
|
212
504
|
|
|
213
|
-
|
|
505
|
+
if (calledName.resolution != "uqn" || calledName.name.includes("\\")) {
|
|
506
|
+
return unsupportedSyntax("php", "qualified call", nodeLocation(call.what, filename, source))
|
|
507
|
+
}
|
|
508
|
+
const unsupportedArgument = call.arguments.find((argument) =>
|
|
509
|
+
argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
|
|
510
|
+
|
|
511
|
+
if (unsupportedArgument) {
|
|
512
|
+
const detail = unsupportedArgument.kind == "namedargument" ? "named argument" :
|
|
513
|
+
unsupportedArgument.kind == "variadic" ? "unpacked argument" : "by-reference argument"
|
|
514
|
+
|
|
515
|
+
return unsupportedSyntax("php", detail, nodeLocation(unsupportedArgument, filename, source))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const callee = calledName.name
|
|
519
|
+
|
|
520
|
+
const signature = context.functions.get(callee)
|
|
521
|
+
const knownArguments = call.arguments.map((argument) => knownCallArgumentType(argument, context))
|
|
522
|
+
const parameterTypes = signature ? knownCallParameterTypes(signature, knownArguments) : undefined
|
|
214
523
|
|
|
215
524
|
return withParserRanges({
|
|
216
|
-
arguments: call.arguments.map((argument) =>
|
|
525
|
+
arguments: call.arguments.map((argument, index) =>
|
|
526
|
+
convertExpression(argument, filename, source, context,
|
|
527
|
+
parameterTypes?.[index] ?? signature?.parameters[index]?.type,
|
|
528
|
+
preservesGenericOptionalEvidence(signature, index))),
|
|
217
529
|
callee,
|
|
218
530
|
kind: "CallExpression",
|
|
219
531
|
location
|
|
@@ -223,23 +535,158 @@ function convertExpression(node, filename, source) {
|
|
|
223
535
|
return unsupportedSyntax("php", node.kind, location)
|
|
224
536
|
}
|
|
225
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Resolves only result types established by explicit signatures and bindings.
|
|
540
|
+
* @param {import("php-parser").Expression} node - Parser-owned expression.
|
|
541
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
542
|
+
* @returns {import("../semantic/types.js").SemanticFunctionReturnType | import("../semantic/types.js").ErrorType | undefined} Known result type.
|
|
543
|
+
*/
|
|
544
|
+
function knownExpressionType(node, context) {
|
|
545
|
+
if (node.kind == "variable") {
|
|
546
|
+
const variable = /** @type {import("php-parser").Variable} */ (node)
|
|
547
|
+
|
|
548
|
+
if (variable.name == "this" && context.currentClass?.id) {
|
|
549
|
+
return {declarationId: context.currentClass.id, kind: "ReferenceType"}
|
|
550
|
+
}
|
|
551
|
+
if (typeof variable.name == "string") return context.bindings.get(variable.name)
|
|
552
|
+
}
|
|
553
|
+
if (node.kind == "call") {
|
|
554
|
+
const call = /** @type {import("php-parser").Call} */ (node)
|
|
555
|
+
const name = call.what.kind == "name" && typeof call.what.name == "string" ? call.what.name : undefined
|
|
556
|
+
|
|
557
|
+
if (name) {
|
|
558
|
+
const signature = context.functions.get(name)
|
|
559
|
+
|
|
560
|
+
if (signature) return knownCallReturnType(signature, call.arguments.map((argument) => knownExpressionType(argument, context)))
|
|
561
|
+
}
|
|
562
|
+
if (call.what.kind == "propertylookup" && call.arguments.length == 0) {
|
|
563
|
+
return knownGenericRecordAccessorType(
|
|
564
|
+
/** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what)), context)
|
|
565
|
+
}
|
|
566
|
+
if (call.what.kind == "propertylookup") {
|
|
567
|
+
const lookup = /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what))
|
|
568
|
+
const receiver = knownExpressionType(lookup.what, context)
|
|
569
|
+
const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
|
|
570
|
+
|
|
571
|
+
if (receiver?.kind == "ReferenceType" && member && typeof member.name == "string") {
|
|
572
|
+
return context.classes.get(receiver.declarationId)?.methods.find((method) => method.name == member.name)?.returnType
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (node.kind == "offsetlookup") {
|
|
577
|
+
const lookup = /** @type {import("php-parser").OffsetLookup} */ (node)
|
|
578
|
+
const collectionType = knownExpressionType(lookup.what, context)
|
|
579
|
+
|
|
580
|
+
if (collectionType?.kind == "ListType") return collectionType.elementType
|
|
581
|
+
if (collectionType?.kind == "MapType" || collectionType?.kind == "OrderedMapType") return collectionType.valueType
|
|
582
|
+
}
|
|
583
|
+
if (node.kind == "new") {
|
|
584
|
+
const construction = /** @type {import("php-parser").New} */ (node)
|
|
585
|
+
const name = construction.what.kind == "name" ? /** @type {import("php-parser").Name} */ (construction.what) : undefined
|
|
586
|
+
const declaration = name && typeof name.name == "string" ? context.recordNames.get(name.name) : undefined
|
|
587
|
+
|
|
588
|
+
if (declaration?.id) return declaration.kind == "ClassDeclaration"
|
|
589
|
+
? {declarationId: declaration.id, kind: "ReferenceType"}
|
|
590
|
+
: {declarationId: declaration.id, kind: "RecordType"}
|
|
591
|
+
}
|
|
592
|
+
if (node.kind == "propertylookup") {
|
|
593
|
+
const read = /** @type {import("php-parser").PropertyLookup} */ (node)
|
|
594
|
+
const receiver = knownExpressionType(read.what, context)
|
|
595
|
+
|
|
596
|
+
if (read.what.kind == "variable" && /** @type {import("php-parser").Variable} */ (read.what).name == "this" &&
|
|
597
|
+
context.currentClass) {
|
|
598
|
+
const offset = read.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (read.offset) : undefined
|
|
599
|
+
|
|
600
|
+
return context.currentClass.fields.find((field) => field.name == offset?.name)?.type
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (receiver?.kind != "RecordType") return undefined
|
|
604
|
+
const declaration = context.records.get(receiver.declarationId)
|
|
605
|
+
const offset = read.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (read.offset) : undefined
|
|
606
|
+
|
|
607
|
+
const memberName = offset && typeof offset.name == "string" ? offset.name : undefined
|
|
608
|
+
|
|
609
|
+
if (memberName && declaration) {
|
|
610
|
+
const index = declaration.fields.findIndex((field) => field.name == memberName)
|
|
611
|
+
|
|
612
|
+
return instantiatedRecordFieldType(declaration, receiver.arguments, index)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return undefined
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Resolves exact parser-known argument evidence without changing the established
|
|
621
|
+
* expression-result classifier used for optional conversion.
|
|
622
|
+
* @param {import("php-parser").Expression} node - Parser-owned call argument.
|
|
623
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
624
|
+
* @returns {import("../semantic/types.js").SemanticFunctionReturnType | import("../semantic/types.js").ErrorType | undefined} Known argument type.
|
|
625
|
+
*/
|
|
626
|
+
function knownCallArgumentType(node, context) {
|
|
627
|
+
const known = knownExpressionType(node, context)
|
|
628
|
+
|
|
629
|
+
if (known) return known
|
|
630
|
+
if (node.kind == "number" && Number.isSafeInteger(Number(/** @type {import("php-parser").Number} */ (node).value))) {
|
|
631
|
+
return scalarType("integer")
|
|
632
|
+
}
|
|
633
|
+
if (node.kind == "boolean" && typeof /** @type {import("php-parser").Boolean} */ (node).value == "boolean") {
|
|
634
|
+
return scalarType("boolean")
|
|
635
|
+
}
|
|
636
|
+
if (node.kind == "string" && typeof /** @type {import("php-parser").String} */ (node).value == "string" &&
|
|
637
|
+
hasOnlyUnicodeScalars(/** @type {import("php-parser").String} */ (node).value)) {
|
|
638
|
+
return scalarType("string")
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return undefined
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Resolves PHP's exact zero-argument accessor carrier against its receiver's
|
|
646
|
+
* generic record declaration.
|
|
647
|
+
* @param {import("php-parser").PropertyLookup} lookup - Parser-owned method lookup.
|
|
648
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
649
|
+
* @returns {import("../semantic/types.js").SemanticValueType | undefined} Instantiated field type.
|
|
650
|
+
*/
|
|
651
|
+
function knownGenericRecordAccessorType(lookup, context) {
|
|
652
|
+
const receiver = knownExpressionType(lookup.what, context)
|
|
653
|
+
const receiverRecord = receiver?.kind == "OptionalType" ? receiver.valueType : receiver
|
|
654
|
+
|
|
655
|
+
if (receiverRecord?.kind != "RecordType") return undefined
|
|
656
|
+
const declaration = context.records.get(receiverRecord.declarationId)
|
|
657
|
+
const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
|
|
658
|
+
const index = member && typeof member.name == "string"
|
|
659
|
+
? declaration?.fields.findIndex((field) => field.name == member.name) ?? -1
|
|
660
|
+
: -1
|
|
661
|
+
|
|
662
|
+
if (!declaration || (declaration.typeParameters?.length ?? 0) == 0 || index < 0) return undefined
|
|
663
|
+
|
|
664
|
+
return instantiatedRecordFieldType(declaration, receiverRecord.arguments, index)
|
|
665
|
+
}
|
|
666
|
+
|
|
226
667
|
/**
|
|
227
668
|
* Converts one explicit PHP return.
|
|
228
669
|
* @param {import("php-parser").Node} node - PHP node.
|
|
229
670
|
* @param {string} filename - Source filename.
|
|
230
671
|
* @param {string} source - Complete source.
|
|
672
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
231
673
|
* @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
|
|
232
674
|
*/
|
|
233
|
-
function convertReturn(node, filename, source) {
|
|
675
|
+
function convertReturn(node, filename, source, context) {
|
|
234
676
|
const location = nodeLocation(node, filename, source)
|
|
235
677
|
|
|
236
678
|
if (node.kind != "return") return unsupportedSyntax("php", node.kind, location)
|
|
237
679
|
|
|
238
680
|
const returnNode = /** @type {import("php-parser").Return} */ (node)
|
|
681
|
+
const expectedType = context.returnType?.kind == "TypeReference" && context.returnType.name == "void"
|
|
682
|
+
? undefined
|
|
683
|
+
: /** @type {import("../semantic/types.js").SemanticValueType | undefined} */ (context.returnType)
|
|
239
684
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
685
|
+
return {
|
|
686
|
+
...(returnNode.expr ? {expression: convertExpression(returnNode.expr, filename, source, context, expectedType)} : {}),
|
|
687
|
+
kind: "ReturnStatement",
|
|
688
|
+
location
|
|
689
|
+
}
|
|
243
690
|
}
|
|
244
691
|
|
|
245
692
|
/**
|
|
@@ -248,9 +695,11 @@ function convertReturn(node, filename, source) {
|
|
|
248
695
|
* @param {string} name - Local name.
|
|
249
696
|
* @param {string} filename - Source filename.
|
|
250
697
|
* @param {string} source - Complete source.
|
|
251
|
-
* @
|
|
698
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
699
|
+
* @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
|
|
700
|
+
* @returns {{immutable: boolean, type: import("../semantic/types.js").SemanticValueType} | undefined} Metadata when present.
|
|
252
701
|
*/
|
|
253
|
-
function localMetadata(node, name, filename, source) {
|
|
702
|
+
function localMetadata(node, name, filename, source, recordNames, typeParameters = new Map()) {
|
|
254
703
|
const comments = node.leadingComments ?? []
|
|
255
704
|
const comment = comments.at(-1)
|
|
256
705
|
|
|
@@ -278,25 +727,28 @@ function localMetadata(node, name, filename, source) {
|
|
|
278
727
|
|
|
279
728
|
return {
|
|
280
729
|
immutable: immutable.length == 1,
|
|
281
|
-
type:
|
|
282
|
-
"php",
|
|
283
|
-
variables[0]
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
730
|
+
type: documentedValueType({
|
|
731
|
+
language: "php",
|
|
732
|
+
location: commentTagLocation(comment, variables[0], "name", filename, source),
|
|
733
|
+
ownerLocation: location,
|
|
734
|
+
records: recordNames,
|
|
735
|
+
source,
|
|
736
|
+
sourceType: variables[0].name,
|
|
737
|
+
subject: `Local '${name}'`,
|
|
738
|
+
typeParameters
|
|
739
|
+
})
|
|
288
740
|
}
|
|
289
741
|
}
|
|
290
742
|
|
|
291
743
|
/**
|
|
292
744
|
* Converts one PHP local declaration or assignment.
|
|
293
745
|
* @param {import("php-parser").Node} node - PHP statement.
|
|
294
|
-
* @param {Set<string>} visible - Names visible during adaptation.
|
|
295
746
|
* @param {string} filename - Source filename.
|
|
296
747
|
* @param {string} source - Complete source.
|
|
297
|
-
* @
|
|
748
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
749
|
+
* @returns {import("../semantic/types.js").LocalStatement | import("../semantic/types.js").PrivateFieldWriteStatement} Semantic local or private-field statement.
|
|
298
750
|
*/
|
|
299
|
-
function convertLocalStatement(node,
|
|
751
|
+
function convertLocalStatement(node, filename, source, context) {
|
|
300
752
|
const location = nodeLocation(node, filename, source)
|
|
301
753
|
|
|
302
754
|
if (node.kind != "expressionstatement") return unsupportedSyntax("php", node.kind, location)
|
|
@@ -308,38 +760,90 @@ function convertLocalStatement(node, visible, filename, source) {
|
|
|
308
760
|
const assignment = /** @type {import("php-parser").Assign} */ (expression)
|
|
309
761
|
|
|
310
762
|
if (assignment.operator != "=") return unsupportedSyntax("php", `assignment ${assignment.operator}`, location)
|
|
763
|
+
if (assignment.left.kind == "propertylookup" && context.currentClass) {
|
|
764
|
+
const lookup = /** @type {import("php-parser").PropertyLookup} */ (assignment.left)
|
|
765
|
+
const receiver = lookup.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
|
|
766
|
+
const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
|
|
767
|
+
const field = receiver?.name == "this" && member && typeof member.name == "string"
|
|
768
|
+
? context.currentClass.fields.find((candidate) => candidate.name == member.name)
|
|
769
|
+
: undefined
|
|
770
|
+
|
|
771
|
+
if (!field) return unsupportedSyntax("php", "private field write outside the declaring receiver", nodeLocation(assignment.left, filename, source))
|
|
772
|
+
return withParserRanges({expression: convertExpression(assignment.right, filename, source, context, field.type),
|
|
773
|
+
field: /** @type {string} */ (field.id), kind: /** @type {const} */ ("PrivateFieldWriteStatement"), location,
|
|
774
|
+
receiver: /** @type {import("../semantic/types.js").ReceiverExpression} */ (
|
|
775
|
+
convertExpression(lookup.what, filename, source, context))},
|
|
776
|
+
{member: nodeLocation(/** @type {import("php-parser").Identifier} */ (member), filename, source), operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0,
|
|
777
|
+
assignment.right.loc?.start.offset ?? source.length, filename, source)})
|
|
778
|
+
}
|
|
311
779
|
if (assignment.left.kind != "variable") return unsupportedSyntax("php", assignment.left.kind, nodeLocation(assignment.left, filename, source))
|
|
780
|
+
if (assignment.right.kind == "throw") {
|
|
781
|
+
return unsupportedSyntax("php", "throw expression in a value context", nodeLocation(assignment.right, filename, source))
|
|
782
|
+
}
|
|
312
783
|
|
|
313
784
|
const variable = /** @type {import("php-parser").Variable} */ (assignment.left)
|
|
314
785
|
const targetLocation = nodeLocation(variable, filename, source)
|
|
315
786
|
|
|
316
787
|
if (typeof variable.name != "string" || variable.curly) return unsupportedSyntax("php", "dynamic variable", targetLocation)
|
|
317
788
|
|
|
318
|
-
const metadata = localMetadata(node, variable.name, filename, source)
|
|
789
|
+
const metadata = localMetadata(node, variable.name, filename, source, context.recordNames, context.typeParameters)
|
|
319
790
|
|
|
320
791
|
if (metadata) {
|
|
321
|
-
|
|
792
|
+
let declaredType = metadata.type
|
|
793
|
+
let ordered = false
|
|
794
|
+
|
|
795
|
+
if (declaredType.kind == "MapType" && context.orderedMapDeclarations?.has(assignment) && assignment.right.kind == "array") {
|
|
796
|
+
declaredType = orderedMapTypeFromMap(declaredType, location)
|
|
797
|
+
ordered = true
|
|
798
|
+
}
|
|
799
|
+
const initializer = convertExpression(assignment.right, filename, source, context, declaredType)
|
|
800
|
+
|
|
801
|
+
context.bindings.set(variable.name, declaredType)
|
|
322
802
|
return withParserRanges({
|
|
323
|
-
initializer
|
|
803
|
+
initializer,
|
|
324
804
|
kind: "LocalDeclaration",
|
|
325
805
|
location,
|
|
326
|
-
mutable: !metadata.immutable,
|
|
806
|
+
mutable: ordered ? false : !metadata.immutable,
|
|
807
|
+
name: variable.name,
|
|
808
|
+
type: declaredType
|
|
809
|
+
}, {
|
|
810
|
+
name: targetLocation,
|
|
811
|
+
operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0, assignment.right.loc?.start.offset ?? source.length, filename, source)
|
|
812
|
+
})
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const inferredType = knownExpressionType(assignment.right, context)
|
|
816
|
+
|
|
817
|
+
if (inferredType?.kind == "RecordType" || inferredType?.kind == "ReferenceType" ||
|
|
818
|
+
inferredType?.kind == "OwnedResourceType") {
|
|
819
|
+
const initializer = convertExpression(assignment.right, filename, source, context, inferredType)
|
|
820
|
+
|
|
821
|
+
context.bindings.set(variable.name, inferredType)
|
|
822
|
+
return withParserRanges({
|
|
823
|
+
initializer,
|
|
824
|
+
kind: /** @type {const} */ ("LocalDeclaration"),
|
|
825
|
+
location,
|
|
826
|
+
mutable: false,
|
|
327
827
|
name: variable.name,
|
|
328
|
-
type:
|
|
828
|
+
type: inferredType
|
|
329
829
|
}, {
|
|
330
830
|
name: targetLocation,
|
|
331
831
|
operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0, assignment.right.loc?.start.offset ?? source.length, filename, source)
|
|
332
832
|
})
|
|
333
833
|
}
|
|
334
834
|
|
|
335
|
-
|
|
835
|
+
const bindingType = context.bindings.get(variable.name)
|
|
836
|
+
|
|
837
|
+
if (!bindingType) return missingType("php", `Local '${variable.name}'`, location)
|
|
336
838
|
|
|
337
839
|
const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: variable.name}, {
|
|
338
840
|
name: targetLocation
|
|
339
841
|
})
|
|
842
|
+
const value = convertExpression(assignment.right, filename, source, context,
|
|
843
|
+
bindingType.kind == "ErrorType" ? undefined : bindingType)
|
|
340
844
|
|
|
341
845
|
return withParserRanges({
|
|
342
|
-
expression:
|
|
846
|
+
expression: value,
|
|
343
847
|
kind: "AssignmentStatement",
|
|
344
848
|
location,
|
|
345
849
|
target
|
|
@@ -351,33 +855,236 @@ function convertLocalStatement(node, visible, filename, source) {
|
|
|
351
855
|
/**
|
|
352
856
|
* Converts one exhaustive PHP statement.
|
|
353
857
|
* @param {import("php-parser").Node} node - PHP statement.
|
|
354
|
-
* @param {Set<string>} visible - Names visible during adaptation.
|
|
355
858
|
* @param {string} filename - Source filename.
|
|
356
859
|
* @param {string} source - Complete source.
|
|
860
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
357
861
|
* @returns {import("../semantic/types.js").Statement} Semantic statement.
|
|
358
862
|
*/
|
|
359
|
-
function convertStatement(node,
|
|
360
|
-
if (node.kind == "return") return convertReturn(node, filename, source)
|
|
361
|
-
if (node.kind == "if") return convertIf(/** @type {import("php-parser").If} */ (node),
|
|
362
|
-
if (node.kind == "
|
|
363
|
-
if (node.kind == "
|
|
863
|
+
function convertStatement(node, filename, source, context) {
|
|
864
|
+
if (node.kind == "return") return convertReturn(node, filename, source, context)
|
|
865
|
+
if (node.kind == "if") return convertIf(/** @type {import("php-parser").If} */ (node), filename, source, context)
|
|
866
|
+
if (node.kind == "throw") return convertPhpRaise(node, filename, source, context)
|
|
867
|
+
if (node.kind == "try") return convertPhpTry(node, filename, source, context)
|
|
868
|
+
if (node.kind == "while") return convertWhile(/** @type {import("php-parser").While} */ (node), filename, source, context)
|
|
869
|
+
if (node.kind == "foreach") return convertForEach(/** @type {import("php-parser").Foreach} */ (node), filename, source, context)
|
|
870
|
+
if (node.kind == "break" || node.kind == "continue") {
|
|
871
|
+
const control = /** @type {import("php-parser").Break | import("php-parser").Continue} */ (node)
|
|
872
|
+
|
|
873
|
+
if (control.level) return unsupportedSyntax("php", `${node.kind} level`, nodeLocation(node, filename, source))
|
|
874
|
+
const location = tokenLocation(node.kind, node.loc?.start.offset ?? 0, node.loc?.end.offset ?? source.length, filename, source)
|
|
875
|
+
|
|
876
|
+
return withParserRanges({
|
|
877
|
+
kind: /** @type {"BreakStatement" | "ContinueStatement"} */ (node.kind == "break" ? "BreakStatement" : "ContinueStatement"),
|
|
878
|
+
location
|
|
879
|
+
}, {keyword: location})
|
|
880
|
+
}
|
|
881
|
+
if (node.kind == "echo") return convertPrint(/** @type {import("php-parser").Echo} */ (node), filename, source, context)
|
|
882
|
+
if (node.kind == "expressionstatement") {
|
|
883
|
+
const expression = /** @type {import("php-parser").ExpressionStatement} */ (node).expression
|
|
884
|
+
|
|
885
|
+
if (expression.kind == "call") {
|
|
886
|
+
return {
|
|
887
|
+
expression: /** @type {import("../semantic/types.js").CallExpression | import("../semantic/types.js").MethodCallExpression} */ (
|
|
888
|
+
convertExpression(expression, filename, source, context)
|
|
889
|
+
),
|
|
890
|
+
kind: "ExpressionStatement",
|
|
891
|
+
location: nodeLocation(node, filename, source)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return convertLocalStatement(node, filename, source, context)
|
|
896
|
+
}
|
|
364
897
|
|
|
365
898
|
return unsupportedSyntax("php", node.kind, nodeLocation(node, filename, source))
|
|
366
899
|
}
|
|
367
900
|
|
|
901
|
+
/**
|
|
902
|
+
* Converts exact `throw new DeclaredError(message)`.
|
|
903
|
+
* @param {import("php-parser").Node} node - PHP throw node.
|
|
904
|
+
* @param {string} filename - Source filename.
|
|
905
|
+
* @param {string} source - Complete source.
|
|
906
|
+
* @param {PhpConversionContext} context - Typed lexical context.
|
|
907
|
+
* @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
|
|
908
|
+
*/
|
|
909
|
+
function convertPhpRaise(node, filename, source, context) {
|
|
910
|
+
const location = nodeLocation(node, filename, source)
|
|
911
|
+
const construction = Reflect.get(node, "what")
|
|
912
|
+
|
|
913
|
+
if (!construction || construction.kind != "new" || construction.what?.kind != "name" ||
|
|
914
|
+
construction.what.resolution != "uqn" || !context.errorNames.has(construction.what.name) ||
|
|
915
|
+
!Array.isArray(construction.arguments) || construction.arguments.length != 1 ||
|
|
916
|
+
construction.arguments[0].kind == "namedargument" || construction.arguments[0].kind == "variadic") {
|
|
917
|
+
return unsupportedSyntax("php", "throw other than exact declared error construction", location)
|
|
918
|
+
}
|
|
919
|
+
const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(construction.what.name))
|
|
920
|
+
const typeLocation = nodeLocation(construction.what, filename, source)
|
|
921
|
+
|
|
922
|
+
return withParserRanges({
|
|
923
|
+
error: withParserRanges({
|
|
924
|
+
error: withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {type: typeLocation}),
|
|
925
|
+
kind: /** @type {const} */ ("ErrorConstruction"),
|
|
926
|
+
location: nodeLocation(construction, filename, source),
|
|
927
|
+
message: convertExpression(construction.arguments[0], filename, source, context)
|
|
928
|
+
}, {type: typeLocation}),
|
|
929
|
+
kind: /** @type {const} */ ("RaiseStatement"),
|
|
930
|
+
location
|
|
931
|
+
}, {keyword: tokenLocation("throw", node.loc?.start.offset ?? 0, construction.loc?.start.offset ?? source.length, filename, source)})
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Converts one exact typed catch with no finally.
|
|
936
|
+
* @param {import("php-parser").Node} node - PHP try node.
|
|
937
|
+
* @param {string} filename - Source filename.
|
|
938
|
+
* @param {string} source - Complete source.
|
|
939
|
+
* @param {PhpConversionContext} context - Typed lexical context.
|
|
940
|
+
* @returns {import("../semantic/types.js").TryStatement} Semantic exact handler.
|
|
941
|
+
*/
|
|
942
|
+
function convertPhpTry(node, filename, source, context) {
|
|
943
|
+
const location = nodeLocation(node, filename, source)
|
|
944
|
+
const catches = Reflect.get(node, "catches")
|
|
945
|
+
const body = Reflect.get(node, "body")
|
|
946
|
+
|
|
947
|
+
if (Reflect.get(node, "always") || !Array.isArray(catches) || catches.length != 1 || body?.kind != "block") {
|
|
948
|
+
return unsupportedSyntax("php", "try without one typed catch and no finally", location)
|
|
949
|
+
}
|
|
950
|
+
const handler = catches[0]
|
|
951
|
+
const caught = handler.what?.[0]
|
|
952
|
+
const binding = handler.variable
|
|
953
|
+
|
|
954
|
+
if (handler.what?.length != 1 || caught?.kind != "name" || caught.resolution != "uqn" ||
|
|
955
|
+
!context.errorNames.has(caught.name) || binding?.kind != "variable" || typeof binding.name != "string" ||
|
|
956
|
+
binding.curly || handler.body?.kind != "block") {
|
|
957
|
+
return unsupportedSyntax("php", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
|
|
958
|
+
}
|
|
959
|
+
const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(caught.name))
|
|
960
|
+
const caughtLocation = nodeLocation(caught, filename, source)
|
|
961
|
+
const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
|
|
962
|
+
type: caughtLocation
|
|
963
|
+
})
|
|
964
|
+
const bodyContext = {...context, bindings: new Map(context.bindings)}
|
|
965
|
+
const catchContext = {...context, bindings: new Map(context.bindings)}
|
|
966
|
+
|
|
967
|
+
catchContext.bindings.set(binding.name, catchType)
|
|
968
|
+
return withParserRanges({
|
|
969
|
+
body: convertBlock(body, filename, source, bodyContext),
|
|
970
|
+
catchBinding: withParserRanges({
|
|
971
|
+
kind: /** @type {const} */ ("CatchBinding"),
|
|
972
|
+
location: nodeLocation(binding, filename, source),
|
|
973
|
+
mutable: /** @type {const} */ (false),
|
|
974
|
+
name: binding.name,
|
|
975
|
+
type: catchType
|
|
976
|
+
}, {name: nodeLocation(binding, filename, source)}),
|
|
977
|
+
catchBody: convertBlock(handler.body, filename, source, catchContext),
|
|
978
|
+
catchType,
|
|
979
|
+
kind: /** @type {const} */ ("TryStatement"),
|
|
980
|
+
location
|
|
981
|
+
}, {catch: nodeLocation(handler, filename, source), try: tokenLocation("try", node.loc?.start.offset ?? 0, body.loc?.start.offset ?? source.length, filename, source)})
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Converts exact block-form PHP `foreach ($list as $value)`.
|
|
986
|
+
* @param {import("php-parser").Foreach} node - PHP foreach node.
|
|
987
|
+
* @param {string} filename - Source filename.
|
|
988
|
+
* @param {string} source - Complete source.
|
|
989
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
990
|
+
* @returns {import("../semantic/types.js").ForEachStatement | import("../semantic/types.js").ForEachMapStatement} Semantic loop.
|
|
991
|
+
*/
|
|
992
|
+
function convertForEach(node, filename, source, context) {
|
|
993
|
+
const location = nodeLocation(node, filename, source)
|
|
994
|
+
|
|
995
|
+
if (node.shortForm) return unsupportedSyntax("php", "alternative foreach syntax", location)
|
|
996
|
+
if (node.value.kind != "variable") return unsupportedSyntax("php", "foreach destructuring", nodeLocation(node.value, filename, source))
|
|
997
|
+
const variable = /** @type {import("php-parser").Variable} */ (node.value)
|
|
998
|
+
|
|
999
|
+
if (Reflect.get(variable, "byref") === true) return unsupportedSyntax("php", "by-reference foreach", nodeLocation(variable, filename, source))
|
|
1000
|
+
if (typeof variable.name != "string" || variable.curly) return unsupportedSyntax("php", "dynamic foreach binding", nodeLocation(variable, filename, source))
|
|
1001
|
+
if (!node.body || node.body.kind != "block") {
|
|
1002
|
+
return unsupportedSyntax("php", "foreach without block body", node.body ? nodeLocation(node.body, filename, source) : location)
|
|
1003
|
+
}
|
|
1004
|
+
const body = /** @type {import("php-parser").Block} */ (node.body)
|
|
1005
|
+
const collectionType = iterationOperandType(knownExpressionType(node.source, context))
|
|
1006
|
+
|
|
1007
|
+
if (node.key) {
|
|
1008
|
+
if (node.key.kind != "variable") return unsupportedSyntax("php", "foreach key binding shape", nodeLocation(node.key, filename, source))
|
|
1009
|
+
const keyVariable = /** @type {import("php-parser").Variable} */ (node.key)
|
|
1010
|
+
|
|
1011
|
+
if (Reflect.get(keyVariable, "byref") === true || typeof keyVariable.name != "string" || keyVariable.curly) {
|
|
1012
|
+
return unsupportedSyntax("php", "invalid foreach key binding", nodeLocation(keyVariable, filename, source))
|
|
1013
|
+
}
|
|
1014
|
+
if (collectionType?.kind == "MapType") {
|
|
1015
|
+
return unsupportedSyntax("php", "unordered map iteration", nodeLocation(node.source, filename, source))
|
|
1016
|
+
}
|
|
1017
|
+
if (!collectionType || collectionType.kind != "OrderedMapType") {
|
|
1018
|
+
return missingType("php", "Ordered map iteration collection", nodeLocation(node.source, filename, source))
|
|
1019
|
+
}
|
|
1020
|
+
const keyLocation = nodeLocation(keyVariable, filename, source)
|
|
1021
|
+
const valueLocation = nodeLocation(variable, filename, source)
|
|
1022
|
+
const keyType = iterationBindingType(collectionType.keyType, keyLocation)
|
|
1023
|
+
const valueType = iterationBindingType(collectionType.valueType, valueLocation)
|
|
1024
|
+
const keyBinding = withParserRanges({
|
|
1025
|
+
kind: /** @type {const} */ ("ValueBinding"), location: keyLocation, mutable: /** @type {const} */ (false),
|
|
1026
|
+
name: keyVariable.name, type: keyType
|
|
1027
|
+
}, {name: keyLocation})
|
|
1028
|
+
const valueBinding = withParserRanges({
|
|
1029
|
+
kind: /** @type {const} */ ("ValueBinding"), location: valueLocation, mutable: /** @type {const} */ (false),
|
|
1030
|
+
name: variable.name, type: valueType
|
|
1031
|
+
}, {name: valueLocation})
|
|
1032
|
+
const bodyContext = {...context, bindings: new Map(context.bindings)}
|
|
1033
|
+
|
|
1034
|
+
bodyContext.bindings.set(keyVariable.name, keyType)
|
|
1035
|
+
bodyContext.bindings.set(variable.name, valueType)
|
|
1036
|
+
return withParserRanges({
|
|
1037
|
+
body: convertBlock(body, filename, source, bodyContext),
|
|
1038
|
+
keyBinding,
|
|
1039
|
+
kind: /** @type {const} */ ("ForEachMapStatement"),
|
|
1040
|
+
location,
|
|
1041
|
+
map: convertExpression(node.source, filename, source, context),
|
|
1042
|
+
valueBinding
|
|
1043
|
+
}, {operator: tokenLocation("foreach", node.loc?.start.offset ?? 0, node.source.loc?.start.offset ?? source.length,
|
|
1044
|
+
filename, source)})
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (collectionType?.kind == "MapType" || collectionType?.kind == "OrderedMapType") {
|
|
1048
|
+
return unsupportedSyntax("php", "map iteration without key/value pair binding", nodeLocation(node.source, filename, source))
|
|
1049
|
+
}
|
|
1050
|
+
if (!collectionType || collectionType.kind != "ListType") {
|
|
1051
|
+
return missingType("php", "Iteration collection", nodeLocation(node.source, filename, source))
|
|
1052
|
+
}
|
|
1053
|
+
const bindingLocation = nodeLocation(variable, filename, source)
|
|
1054
|
+
const inferredType = collectionType.elementType
|
|
1055
|
+
const bindingType = iterationBindingType(inferredType, bindingLocation)
|
|
1056
|
+
const valueBinding = withParserRanges({
|
|
1057
|
+
kind: /** @type {const} */ ("ValueBinding"),
|
|
1058
|
+
location: bindingLocation,
|
|
1059
|
+
mutable: /** @type {const} */ (false),
|
|
1060
|
+
name: variable.name,
|
|
1061
|
+
type: bindingType
|
|
1062
|
+
}, {name: bindingLocation})
|
|
1063
|
+
const bodyContext = {...context, bindings: new Map(context.bindings)}
|
|
1064
|
+
|
|
1065
|
+
bodyContext.bindings.set(variable.name, bindingType)
|
|
1066
|
+
return withParserRanges({
|
|
1067
|
+
body: convertBlock(body, filename, source, bodyContext),
|
|
1068
|
+
kind: /** @type {const} */ ("ForEachStatement"),
|
|
1069
|
+
list: convertExpression(node.source, filename, source, context),
|
|
1070
|
+
location,
|
|
1071
|
+
valueBinding
|
|
1072
|
+
}, {operator: tokenLocation("foreach", node.loc?.start.offset ?? 0, node.source.loc?.start.offset ?? source.length, filename, source)})
|
|
1073
|
+
}
|
|
1074
|
+
|
|
368
1075
|
/**
|
|
369
1076
|
* Converts an ordered PHP block with its own adaptation scope.
|
|
370
1077
|
* @param {import("php-parser").Block} node - PHP parser block.
|
|
371
|
-
* @param {Set<string>} visible - Names visible on block entry.
|
|
372
1078
|
* @param {string} filename - Source filename.
|
|
373
1079
|
* @param {string} source - Complete source.
|
|
1080
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
374
1081
|
* @returns {import("../semantic/types.js").Block} Semantic block.
|
|
375
1082
|
*/
|
|
376
|
-
function convertBlock(node,
|
|
1083
|
+
function convertBlock(node, filename, source, context) {
|
|
377
1084
|
return {
|
|
378
1085
|
kind: "Block",
|
|
379
1086
|
location: nodeLocation(node, filename, source),
|
|
380
|
-
statements: node.children.map((statement) => convertStatement(statement,
|
|
1087
|
+
statements: node.children.map((statement) => convertStatement(statement, filename, source, context))
|
|
381
1088
|
}
|
|
382
1089
|
}
|
|
383
1090
|
|
|
@@ -388,9 +1095,10 @@ function convertBlock(node, visible, filename, source) {
|
|
|
388
1095
|
* @param {import("../semantic/types.js").SourceLocation} location - Source location.
|
|
389
1096
|
* @param {string} filename - Source filename.
|
|
390
1097
|
* @param {string} source - Complete source.
|
|
391
|
-
* @
|
|
1098
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
|
|
1099
|
+
* @returns {import("../semantic/types.js").SemanticValueType} Semantic type.
|
|
392
1100
|
*/
|
|
393
|
-
function convertType(sourceType, subject, location, filename, source) {
|
|
1101
|
+
function convertType(sourceType, subject, location, filename, source, recordNames = new Map()) {
|
|
394
1102
|
if (!sourceType) return requireSourceScalarType("php", undefined, subject, location)
|
|
395
1103
|
if (sourceType.kind != "typereference" && sourceType.kind != "name" && sourceType.kind != "identifier") {
|
|
396
1104
|
return unsupportedSyntax("php", "unsupported scalar type", nodeLocation(sourceType, filename, source))
|
|
@@ -398,17 +1106,161 @@ function convertType(sourceType, subject, location, filename, source) {
|
|
|
398
1106
|
|
|
399
1107
|
const typeName = /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (sourceType).name
|
|
400
1108
|
|
|
401
|
-
|
|
1109
|
+
const typeLocation = nodeLocation(sourceType, filename, source)
|
|
1110
|
+
const declaration = recordNames.get(typeName)
|
|
1111
|
+
|
|
1112
|
+
if (declaration?.id) return declaration.kind == "ClassDeclaration"
|
|
1113
|
+
? referenceType(declaration.id, typeLocation)
|
|
1114
|
+
: declaration.kind == "EffectResourceDeclaration"
|
|
1115
|
+
? ownedResourceType(declaration.id, typeLocation)
|
|
1116
|
+
: recordType(declaration.id, typeLocation)
|
|
1117
|
+
|
|
1118
|
+
return requireSourceScalarType("php", typeName, subject, location, typeLocation)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Reads the exact PHPDoc container signature attached to one function.
|
|
1123
|
+
* @param {import("php-parser").Function} node - Function declaration.
|
|
1124
|
+
* @param {string} filename - Source filename.
|
|
1125
|
+
* @param {string} source - Complete source.
|
|
1126
|
+
* @param {string} [ownerId] - Stable declaration identity.
|
|
1127
|
+
* @returns {{parameters: Map<string, {location: import("../semantic/types.js").SourceLocation, sourceType: string}>, returnType: {location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined, typeParameters?: import("../semantic/types.js").TypeParameter[]}} Documented types.
|
|
1128
|
+
*/
|
|
1129
|
+
function functionDocumentedTypes(node, filename, source, ownerId) {
|
|
1130
|
+
const comment = node.leadingComments?.at(-1)
|
|
1131
|
+
|
|
1132
|
+
if (!comment || comment.kind != "commentblock" || !comment.loc || !node.loc ||
|
|
1133
|
+
!/^\s*$/u.test(source.slice(comment.loc.end.offset, node.loc.start.offset))) return {parameters: new Map(), returnType: undefined}
|
|
1134
|
+
const block = parseComment(comment.value)[0]
|
|
1135
|
+
const tags = block?.tags ?? []
|
|
1136
|
+
const parameterTags = tags.filter((tag) => tag.tag == "param")
|
|
1137
|
+
const returnTags = tags.filter((tag) => tag.tag == "return" || tag.tag == "returns")
|
|
1138
|
+
const templateTags = tags.filter((tag) => tag.tag == "template")
|
|
1139
|
+
const malformedTemplate = tags.find((tag) => tag.tag.startsWith("template") && tag.tag != "template")
|
|
1140
|
+
const malformed = tags.some((tag) => !["param", "return", "returns", "template"].includes(tag.tag)) || returnTags.length > 1 ||
|
|
1141
|
+
parameterTags.some((tag) => tag.type != "" || !/^\$[A-Za-z_][A-Za-z0-9_]*$/u.test(tag.description)) ||
|
|
1142
|
+
returnTags.some((tag) => tag.type != "" || tag.description != "") || malformedTemplate ||
|
|
1143
|
+
templateTags.some((tag) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(tag.name) || tag.type != "" || tag.description != "") ||
|
|
1144
|
+
templateTags.length > 0 && !ownerId
|
|
1145
|
+
|
|
1146
|
+
if (malformed) return unsupportedSyntax("php", "malformed function type annotation", nodeLocation(node, filename, source))
|
|
1147
|
+
const parameters = new Map(parameterTags.map((tag) => [tag.description.slice(1), {
|
|
1148
|
+
location: commentTagLocation(comment, tag, "name", filename, source),
|
|
1149
|
+
sourceType: tag.name
|
|
1150
|
+
}]))
|
|
1151
|
+
|
|
1152
|
+
if (parameters.size != parameterTags.length) {
|
|
1153
|
+
return unsupportedSyntax("php", "duplicate parameter annotation", nodeLocation(node, filename, source))
|
|
1154
|
+
}
|
|
1155
|
+
const returnTag = returnTags[0]
|
|
1156
|
+
const typeParameters = templateTags.map((tag, index) => {
|
|
1157
|
+
const location = commentTagLocation(comment, tag, "name", filename, source)
|
|
1158
|
+
|
|
1159
|
+
return withParserRanges({id: `${ownerId}:type:${index}`, kind: /** @type {const} */ ("TypeParameter"), location, name: tag.name}, {name: location})
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
return {
|
|
1163
|
+
parameters,
|
|
1164
|
+
returnType: returnTag ? {
|
|
1165
|
+
location: commentTagLocation(comment, returnTag, "name", filename, source),
|
|
1166
|
+
sourceType: returnTag.name
|
|
1167
|
+
} : undefined,
|
|
1168
|
+
...(typeParameters.length > 0 ? {typeParameters} : {})
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Reads an optional exact class-level PHPDoc template list.
|
|
1174
|
+
* @param {import("php-parser").Class} node - Class declaration.
|
|
1175
|
+
* @param {string} ownerId - Stable record identity.
|
|
1176
|
+
* @param {string} filename - Source filename.
|
|
1177
|
+
* @param {string} source - Complete source.
|
|
1178
|
+
* @returns {import("../semantic/types.js").TypeParameter[] | undefined} Ordered parameters.
|
|
1179
|
+
*/
|
|
1180
|
+
function classDocumentedTypeParameters(node, ownerId, filename, source) {
|
|
1181
|
+
const documented = functionDocumentedTypes(
|
|
1182
|
+
/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (node)), filename, source, ownerId)
|
|
1183
|
+
|
|
1184
|
+
if (documented.parameters.size || documented.returnType) {
|
|
1185
|
+
return unsupportedSyntax("php", "function type annotation on record declaration", nodeLocation(node, filename, source))
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return documented.typeParameters
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Returns a native PHP type name from one supported parser node.
|
|
1193
|
+
* @param {import("php-parser").Node | null} type - Native type node.
|
|
1194
|
+
* @returns {string | undefined} Type name.
|
|
1195
|
+
*/
|
|
1196
|
+
function phpTypeName(type) {
|
|
1197
|
+
if (!type || !["typereference", "name", "identifier"].includes(type.kind)) return undefined
|
|
1198
|
+
|
|
1199
|
+
return /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (type).name
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Wraps one parser-declared canonical PHP nullable type.
|
|
1204
|
+
* @param {import("../semantic/types.js").SemanticValueType} valueType - Present-value semantic type.
|
|
1205
|
+
* @param {import("php-parser").Node} sourceType - Native parser type node.
|
|
1206
|
+
* @param {number} ownerStart - Earliest owning parser offset.
|
|
1207
|
+
* @param {string} filename - Source filename.
|
|
1208
|
+
* @param {string} source - Complete source.
|
|
1209
|
+
* @returns {import("../semantic/types.js").OptionalType} Optional semantic type.
|
|
1210
|
+
*/
|
|
1211
|
+
function nullableType(valueType, sourceType, ownerStart, filename, source) {
|
|
1212
|
+
if (!sourceType.loc) throw new Error("PHP parser omitted nullable type location.")
|
|
1213
|
+
const valueLocation = nodeLocation(sourceType, filename, source)
|
|
1214
|
+
const marker = tokenLocation("?", ownerStart, sourceType.loc.start.offset, filename, source)
|
|
1215
|
+
|
|
1216
|
+
if (marker.end.offset != valueLocation.start.offset) {
|
|
1217
|
+
return unsupportedSyntax("php", "noncanonical nullable type", marker)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return optionalType(
|
|
1221
|
+
valueType,
|
|
1222
|
+
locationFromOffsets(filename, source, marker.start.offset, valueLocation.end.offset),
|
|
1223
|
+
valueLocation
|
|
1224
|
+
)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Converts one explicit PHP function return type.
|
|
1229
|
+
* @param {import("php-parser").Node | null} sourceType - PHP return type node.
|
|
1230
|
+
* @param {string} subject - Typed function return.
|
|
1231
|
+
* @param {import("../semantic/types.js").SourceLocation} location - Function location.
|
|
1232
|
+
* @param {string} filename - Source filename.
|
|
1233
|
+
* @param {string} source - Complete source.
|
|
1234
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
|
|
1235
|
+
* @returns {import("../semantic/types.js").SemanticFunctionReturnType} Semantic return type.
|
|
1236
|
+
*/
|
|
1237
|
+
function convertReturnType(sourceType, subject, location, filename, source, recordNames = new Map()) {
|
|
1238
|
+
if (!sourceType) return requireSourceReturnType("php", undefined, subject, location)
|
|
1239
|
+
if (sourceType.kind != "typereference" && sourceType.kind != "name" && sourceType.kind != "identifier") {
|
|
1240
|
+
return unsupportedSyntax("php", "unsupported return type", nodeLocation(sourceType, filename, source))
|
|
1241
|
+
}
|
|
1242
|
+
const typeName = /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (sourceType).name
|
|
1243
|
+
const declaration = recordNames.get(typeName)
|
|
1244
|
+
|
|
1245
|
+
if (declaration?.id) return declaration.kind == "ClassDeclaration"
|
|
1246
|
+
? referenceType(declaration.id, nodeLocation(sourceType, filename, source))
|
|
1247
|
+
: declaration.kind == "EffectResourceDeclaration"
|
|
1248
|
+
? ownedResourceType(declaration.id, nodeLocation(sourceType, filename, source))
|
|
1249
|
+
: recordType(declaration.id, nodeLocation(sourceType, filename, source))
|
|
1250
|
+
|
|
1251
|
+
return requireSourceReturnType("php", typeName, subject, location, nodeLocation(sourceType, filename, source))
|
|
402
1252
|
}
|
|
403
1253
|
|
|
404
1254
|
/**
|
|
405
|
-
* Converts
|
|
1255
|
+
* Converts the explicit type signature of one PHP function before any body expressions.
|
|
406
1256
|
* @param {import("php-parser").Function} node - PHP function node.
|
|
407
1257
|
* @param {string} filename - Source filename.
|
|
408
1258
|
* @param {string} source - Complete source.
|
|
409
|
-
* @
|
|
1259
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
1260
|
+
* @param {string} ownerId - Stable function identity.
|
|
1261
|
+
* @returns {PhpFunctionSignature} Typed function signature.
|
|
410
1262
|
*/
|
|
411
|
-
function
|
|
1263
|
+
function convertFunctionSignature(node, filename, source, recordNames, ownerId) {
|
|
412
1264
|
const location = nodeLocation(node, filename, source)
|
|
413
1265
|
const name = typeof node.name == "string" ? node.name : node.name.name
|
|
414
1266
|
|
|
@@ -416,88 +1268,615 @@ function convertFunction(node, filename, source) {
|
|
|
416
1268
|
return unsupportedSyntax("php", "function body", location)
|
|
417
1269
|
}
|
|
418
1270
|
|
|
1271
|
+
const documented = functionDocumentedTypes(node, filename, source, ownerId)
|
|
1272
|
+
const typeParameterNames = new Map((documented.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
419
1273
|
const parameters = node.arguments.map((parameter) => {
|
|
420
1274
|
const parameterLocation = nodeLocation(parameter, filename, source)
|
|
421
1275
|
const parameterName = typeof parameter.name == "string" ? parameter.name : parameter.name.name
|
|
422
1276
|
|
|
423
|
-
if (parameter.
|
|
424
|
-
|
|
1277
|
+
if (parameter.byref) return unsupportedSyntax("php", "by-reference parameter", parameterLocation)
|
|
1278
|
+
if (parameter.variadic) return unsupportedSyntax("php", "variadic parameter", parameterLocation)
|
|
1279
|
+
if (parameter.value) return unsupportedSyntax("php", "default parameter", parameterLocation)
|
|
425
1280
|
const parameterNode = typeof parameter.name == "string" ? parameter : parameter.name
|
|
1281
|
+
const documentedType = documented.parameters.get(parameterName)
|
|
1282
|
+
const nativeType = phpTypeName(parameter.type)
|
|
1283
|
+
|
|
1284
|
+
let parameterType = documentedType
|
|
1285
|
+
? documentedValueType({language: "php", location: documentedType.location, ownerLocation: parameterLocation, source,
|
|
1286
|
+
records: recordNames, sourceType: documentedType.sourceType, subject: `Parameter '${parameterName}'`, typeParameters: typeParameterNames})
|
|
1287
|
+
: convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source, recordNames)
|
|
1288
|
+
|
|
1289
|
+
if (nativeType == "array" && !documentedType) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
|
|
1290
|
+
if (documentedType && !phpDocumentedNativeTypeMatches(nativeType, parameter.nullable, parameterType, recordNames)) {
|
|
1291
|
+
return unsupportedSyntax("php", "documented parameter type incompatible with native type", documentedType.location)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (parameter.nullable && parameterType.kind != "OptionalType") {
|
|
1295
|
+
if (!parameter.type) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
|
|
1296
|
+
parameterType = nullableType(parameterType, parameter.type, parameter.loc?.start.offset ?? 0, filename, source)
|
|
1297
|
+
} else if (!parameter.nullable && parameterType.kind == "OptionalType" &&
|
|
1298
|
+
parameterType.valueType.kind != "TypeVariableReference") {
|
|
1299
|
+
return unsupportedSyntax("php", "optional annotation without nullable native type", documentedType?.location ?? parameterLocation)
|
|
1300
|
+
}
|
|
426
1301
|
const semanticParameter = {
|
|
427
1302
|
kind: /** @type {const} */ ("Parameter"),
|
|
428
1303
|
location: parameterLocation,
|
|
429
1304
|
name: parameterName,
|
|
430
|
-
type:
|
|
1305
|
+
type: parameterType
|
|
431
1306
|
}
|
|
432
1307
|
|
|
433
1308
|
return withParserRanges(semanticParameter, {name: nodeLocation(parameterNode, filename, source)})
|
|
434
1309
|
})
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const body = convertBlock(node.body, visible, filename, source)
|
|
1310
|
+
const extraDocumentedParameter = [...documented.parameters.keys()].find((name) =>
|
|
1311
|
+
!node.arguments.some((parameter) => (typeof parameter.name == "string" ? parameter.name : parameter.name.name) == name))
|
|
438
1312
|
|
|
1313
|
+
if (extraDocumentedParameter) return unsupportedSyntax("php", `annotation for unknown parameter '${extraDocumentedParameter}'`, location)
|
|
1314
|
+
if (node.byref) return unsupportedSyntax("php", "by-reference return", location)
|
|
439
1315
|
const nameNode = typeof node.name == "string" ? node : node.name
|
|
1316
|
+
const nativeReturnType = phpTypeName(node.type)
|
|
1317
|
+
let returnType
|
|
1318
|
+
|
|
1319
|
+
if (documented.returnType) {
|
|
1320
|
+
returnType = documentedValueType({language: "php", location: documented.returnType.location, ownerLocation: location, source,
|
|
1321
|
+
records: recordNames, sourceType: documented.returnType.sourceType, subject: `Function '${name}' return`, typeParameters: typeParameterNames})
|
|
1322
|
+
if (!phpDocumentedNativeTypeMatches(nativeReturnType, node.nullable, returnType, recordNames)) {
|
|
1323
|
+
return unsupportedSyntax("php", "documented return type incompatible with native type", documented.returnType.location)
|
|
1324
|
+
}
|
|
1325
|
+
} else {
|
|
1326
|
+
if (nativeReturnType == "array") return missingType("php", `Function '${name}' return`, location)
|
|
1327
|
+
returnType = convertReturnType(node.type, `Function '${name}' return`, location, filename, source, recordNames)
|
|
1328
|
+
}
|
|
1329
|
+
if (node.nullable && returnType.kind != "OptionalType") {
|
|
1330
|
+
if (!node.type) return missingType("php", `Function '${name}' return`, location)
|
|
1331
|
+
const typeStart = node.type.loc?.start.offset ?? 0
|
|
1332
|
+
|
|
1333
|
+
returnType = nullableType(
|
|
1334
|
+
/** @type {import("../semantic/types.js").SemanticValueType} */ (returnType),
|
|
1335
|
+
node.type,
|
|
1336
|
+
Math.max(node.loc?.start.offset ?? 0, typeStart - 2),
|
|
1337
|
+
filename,
|
|
1338
|
+
source
|
|
1339
|
+
)
|
|
1340
|
+
} else if (!node.nullable && returnType.kind == "OptionalType" &&
|
|
1341
|
+
returnType.valueType.kind != "TypeVariableReference") {
|
|
1342
|
+
return unsupportedSyntax("php", "optional annotation without nullable native return type", documented.returnType?.location ?? location)
|
|
1343
|
+
}
|
|
440
1344
|
|
|
441
|
-
return
|
|
442
|
-
body,
|
|
443
|
-
kind: "FunctionDeclaration",
|
|
1345
|
+
return {
|
|
444
1346
|
location,
|
|
445
1347
|
name,
|
|
1348
|
+
nameLocation: nodeLocation(nameNode, filename, source),
|
|
446
1349
|
parameters,
|
|
447
|
-
returnType
|
|
448
|
-
|
|
1350
|
+
returnType,
|
|
1351
|
+
...(documented.typeParameters ? {typeParameters: documented.typeParameters} : {})
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Checks the exact runtime carrier permitted beside a precise PHPDoc type.
|
|
1357
|
+
* @param {string | undefined} nativeType - Native PHP type name.
|
|
1358
|
+
* @param {boolean} nativeNullable - Whether the native carrier has PHP's nullable marker.
|
|
1359
|
+
* @param {import("../semantic/types.js").SemanticValueType} documentedType - Exact semantic document type.
|
|
1360
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} records - Visible nominal declarations by name.
|
|
1361
|
+
* @returns {boolean} Whether the native carrier agrees without changing meaning.
|
|
1362
|
+
*/
|
|
1363
|
+
function phpDocumentedNativeTypeMatches(nativeType, nativeNullable, documentedType, records) {
|
|
1364
|
+
if (documentedType.kind == "OptionalType") {
|
|
1365
|
+
if (documentedType.valueType.kind == "TypeVariableReference") {
|
|
1366
|
+
return nativeType === undefined && !nativeNullable
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
return nativeNullable && phpDocumentedNativeTypeMatches(nativeType, false, documentedType.valueType, records)
|
|
1370
|
+
}
|
|
1371
|
+
if (nativeNullable) return false
|
|
1372
|
+
if (documentedType.kind == "TypeVariableReference") return nativeType === undefined
|
|
1373
|
+
if (documentedType.kind == "TypeReference") {
|
|
1374
|
+
return nativeType !== undefined && sourceScalarType("php", nativeType)?.name == documentedType.name
|
|
1375
|
+
}
|
|
1376
|
+
if (documentedType.kind == "ListType" || documentedType.kind == "MapType" || documentedType.kind == "OrderedMapType") {
|
|
1377
|
+
return nativeType == "array"
|
|
1378
|
+
}
|
|
1379
|
+
if (documentedType.kind == "RecordType") {
|
|
1380
|
+
return nativeType !== undefined && records.get(nativeType)?.id == documentedType.declarationId
|
|
1381
|
+
}
|
|
1382
|
+
if (documentedType.kind == "ReferenceType") {
|
|
1383
|
+
return nativeType !== undefined && records.get(nativeType)?.id == documentedType.declarationId
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return false
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Converts a PHP function body using the already-proved module signatures.
|
|
1391
|
+
* @param {import("php-parser").Function} node - PHP function node.
|
|
1392
|
+
* @param {PhpFunctionSignature} signature - Preconverted explicit signature.
|
|
1393
|
+
* @param {Map<string, PhpFunctionSignature>} functions - Module function signatures.
|
|
1394
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
1395
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
|
|
1396
|
+
* @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
|
|
1397
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
|
|
1398
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
|
|
1399
|
+
* @param {string} filename - Source filename.
|
|
1400
|
+
* @param {string} source - Complete source.
|
|
1401
|
+
* @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
|
|
1402
|
+
*/
|
|
1403
|
+
function convertFunction(node, signature, functions, recordNames, records, classes, errorNames, errors, filename, source) {
|
|
1404
|
+
if (!node.body) return unsupportedSyntax("php", "function body", signature.location)
|
|
1405
|
+
const context = {
|
|
1406
|
+
bindings: new Map(signature.parameters.map((parameter) => [parameter.name, parameter.type])),
|
|
1407
|
+
classes,
|
|
1408
|
+
errorNames,
|
|
1409
|
+
errors,
|
|
1410
|
+
functions,
|
|
1411
|
+
orderedMapDeclarations: orderedMapDeclarationsForPhp(node.body),
|
|
1412
|
+
recordNames,
|
|
1413
|
+
records,
|
|
1414
|
+
returnType: signature.returnType,
|
|
1415
|
+
typeParameters: new Map((signature.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return withParserRanges({
|
|
1419
|
+
body: convertBlock(node.body, filename, source, context),
|
|
1420
|
+
kind: "FunctionDeclaration",
|
|
1421
|
+
location: signature.location,
|
|
1422
|
+
name: signature.name,
|
|
1423
|
+
parameters: signature.parameters,
|
|
1424
|
+
returnType: signature.returnType,
|
|
1425
|
+
...(signature.typeParameters ? {typeParameters: signature.typeParameters} : {})
|
|
1426
|
+
}, {name: signature.nameLocation})
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Resolves parser-owned key/value loops to exact declarations inside one lexical owner.
|
|
1431
|
+
* Ambiguous same-name declarations produce no evidence and fail during loop conversion.
|
|
1432
|
+
* @param {unknown} value - Function body or entry-point subtree.
|
|
1433
|
+
* @returns {Set<object>} Exact pair-iterated parser declarations.
|
|
1434
|
+
*/
|
|
1435
|
+
function orderedMapDeclarationsForPhp(value) {
|
|
1436
|
+
/** @type {{declaration: object, name: string, offset: number}[]} */
|
|
1437
|
+
const declarations = []
|
|
1438
|
+
/** @type {{name: string, offset: number}[]} */
|
|
1439
|
+
const receivers = []
|
|
1440
|
+
const seen = new WeakSet()
|
|
1441
|
+
/**
|
|
1442
|
+
* Visits one parser candidate.
|
|
1443
|
+
* @param {unknown} candidate - Parser candidate.
|
|
1444
|
+
*/
|
|
1445
|
+
const visit = (candidate) => {
|
|
1446
|
+
if (!candidate || typeof candidate != "object" || seen.has(candidate)) return
|
|
1447
|
+
seen.add(candidate)
|
|
1448
|
+
const kind = Reflect.get(candidate, "kind")
|
|
1449
|
+
|
|
1450
|
+
if (kind == "function" || kind == "class") return
|
|
1451
|
+
if (kind == "assign") {
|
|
1452
|
+
const variable = Reflect.get(candidate, "left")
|
|
1453
|
+
|
|
1454
|
+
if (Reflect.get(variable, "kind") == "variable" && typeof Reflect.get(variable, "name") == "string") {
|
|
1455
|
+
declarations.push({
|
|
1456
|
+
declaration: candidate,
|
|
1457
|
+
name: Reflect.get(variable, "name"),
|
|
1458
|
+
offset: Reflect.get(variable, "loc")?.start.offset
|
|
1459
|
+
})
|
|
1460
|
+
}
|
|
1461
|
+
} else if (kind == "foreach") {
|
|
1462
|
+
const receiver = Reflect.get(candidate, "source")
|
|
1463
|
+
|
|
1464
|
+
if (Reflect.get(receiver, "kind") == "variable" && typeof Reflect.get(receiver, "name") == "string" &&
|
|
1465
|
+
Reflect.get(candidate, "key")) receivers.push({
|
|
1466
|
+
name: Reflect.get(receiver, "name"),
|
|
1467
|
+
offset: Reflect.get(candidate, "loc")?.start.offset
|
|
1468
|
+
})
|
|
1469
|
+
}
|
|
1470
|
+
for (const [key, child] of Object.entries(candidate)) {
|
|
1471
|
+
if (["loc", "leadingComments", "trailingComments"].includes(key)) continue
|
|
1472
|
+
if (Array.isArray(child)) {
|
|
1473
|
+
for (const item of child) visit(item)
|
|
1474
|
+
} else visit(child)
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
visit(value)
|
|
1479
|
+
const selected = new Set()
|
|
1480
|
+
for (const receiver of receivers) {
|
|
1481
|
+
const matches = declarations.filter(({name, offset}) => name == receiver.name && offset < receiver.offset)
|
|
1482
|
+
|
|
1483
|
+
if (matches.length == 1) selected.add(matches[0].declaration)
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return selected
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Converts one exact final RuntimeException subclass.
|
|
1491
|
+
* @param {import("php-parser").Class} node - PHP class declaration.
|
|
1492
|
+
* @param {import("../semantic/types.js").ErrorDeclaration} declaration - Predeclared semantic error.
|
|
1493
|
+
* @param {string} filename - Source filename.
|
|
1494
|
+
* @param {string} source - Complete source.
|
|
1495
|
+
* @returns {import("../semantic/types.js").ErrorDeclaration} Semantic error declaration.
|
|
1496
|
+
*/
|
|
1497
|
+
function convertPhpError(node, declaration, filename, source) {
|
|
1498
|
+
const location = nodeLocation(node, filename, source)
|
|
1499
|
+
const parent = node.extends
|
|
1500
|
+
const parentName = parent?.kind == "name" ? /** @type {import("php-parser").Name} */ (/** @type {unknown} */ (parent)) : undefined
|
|
1501
|
+
|
|
1502
|
+
if (node.isAnonymous || !node.isFinal || node.isReadonly || node.isAbstract || node.implements?.length ||
|
|
1503
|
+
node.attrGroups.length || node.body.length != 0 || !parentName ||
|
|
1504
|
+
!(parentName.name == "RuntimeException" && parentName.resolution == "uqn" ||
|
|
1505
|
+
parentName.name == "\\RuntimeException" && parentName.resolution == "fqn")) {
|
|
1506
|
+
return unsupportedSyntax("php", "noncanonical typed error declaration", location)
|
|
1507
|
+
}
|
|
1508
|
+
const nameNode = typeof node.name == "string" ? node : node.name
|
|
1509
|
+
|
|
1510
|
+
return withParserRanges(declaration, {name: nodeLocation(nameNode, filename, source), type: nodeLocation(parentName, filename, source)})
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Reads one exact public PHP constructor or instance-method signature.
|
|
1515
|
+
* @param {import("php-parser").Method} node - Parser-owned method.
|
|
1516
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
|
|
1517
|
+
* @param {string} ownerId - Stable callable identity.
|
|
1518
|
+
* @param {boolean} constructor - Whether this is the sole constructor.
|
|
1519
|
+
* @param {string} filename - Source filename.
|
|
1520
|
+
* @param {string} source - Complete source.
|
|
1521
|
+
* @returns {{location: import("../semantic/types.js").SourceLocation, name: string, nameLocation: import("../semantic/types.js").SourceLocation, parameters: import("../semantic/types.js").Parameter[], returnType?: import("../semantic/types.js").SemanticFunctionReturnType}} Exact signature.
|
|
1522
|
+
*/
|
|
1523
|
+
function convertPhpClassCallableSignature(node, nominalNames, ownerId, constructor, filename, source) {
|
|
1524
|
+
const location = nodeLocation(node, filename, source)
|
|
1525
|
+
const name = typeof node.name == "string" ? node.name : node.name.name
|
|
1526
|
+
|
|
1527
|
+
if (!constructor && /^__/u.test(name)) return unsupportedSyntax("php", `reserved reference method '${name}'`, location)
|
|
1528
|
+
if (node.visibility != "public" || node.isStatic || node.isAbstract || node.isFinal || Reflect.get(node, "isReadonly") || node.byref ||
|
|
1529
|
+
node.attrGroups.length || !node.body || constructor != (name == "__construct")) {
|
|
1530
|
+
return unsupportedSyntax("php", "noncanonical reference class callable", location)
|
|
1531
|
+
}
|
|
1532
|
+
const documented = functionDocumentedTypes(/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (node)),
|
|
1533
|
+
filename, source, ownerId)
|
|
1534
|
+
if (documented.typeParameters?.length) return unsupportedSyntax("php", "generic reference class callable", location)
|
|
1535
|
+
if (constructor && (node.type || node.nullable || documented.returnType)) {
|
|
1536
|
+
return unsupportedSyntax("php", "constructor return annotation", location)
|
|
1537
|
+
}
|
|
1538
|
+
const parameters = node.arguments.map((parameter) => {
|
|
1539
|
+
const parameterLocation = nodeLocation(parameter, filename, source)
|
|
1540
|
+
const parameterName = typeof parameter.name == "string" ? parameter.name : parameter.name.name
|
|
1541
|
+
|
|
1542
|
+
if (parameter.byref || parameter.variadic || parameter.value || parameter.flags || parameter.readonly ||
|
|
1543
|
+
parameter.hooks.length || parameter.attrGroups.length) {
|
|
1544
|
+
return unsupportedSyntax("php", "noncanonical reference class parameter", parameterLocation)
|
|
1545
|
+
}
|
|
1546
|
+
const documentedType = documented.parameters.get(parameterName)
|
|
1547
|
+
const nativeType = phpTypeName(parameter.type)
|
|
1548
|
+
let type = documentedType
|
|
1549
|
+
? documentedValueType({language: "php", location: documentedType.location, ownerLocation: parameterLocation,
|
|
1550
|
+
records: nominalNames, source, sourceType: documentedType.sourceType, subject: `Parameter '${parameterName}'`})
|
|
1551
|
+
: convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source, nominalNames)
|
|
1552
|
+
|
|
1553
|
+
if (nativeType == "array" && !documentedType) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
|
|
1554
|
+
if (documentedType && !phpDocumentedNativeTypeMatches(nativeType, parameter.nullable, type, nominalNames)) {
|
|
1555
|
+
return unsupportedSyntax("php", "documented parameter type incompatible with native type", documentedType.location)
|
|
1556
|
+
}
|
|
1557
|
+
if (parameter.nullable && type.kind != "OptionalType") {
|
|
1558
|
+
if (!parameter.type) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
|
|
1559
|
+
type = nullableType(type, parameter.type, parameter.loc?.start.offset ?? 0, filename, source)
|
|
1560
|
+
} else if (!parameter.nullable && type.kind == "OptionalType") {
|
|
1561
|
+
return unsupportedSyntax("php", "optional annotation without nullable native type", documentedType?.location ?? parameterLocation)
|
|
1562
|
+
}
|
|
1563
|
+
const nameNode = typeof parameter.name == "string" ? parameter : parameter.name
|
|
1564
|
+
|
|
1565
|
+
return withParserRanges({kind: /** @type {const} */ ("Parameter"), location: parameterLocation,
|
|
1566
|
+
name: parameterName, type}, {name: nodeLocation(nameNode, filename, source)})
|
|
1567
|
+
})
|
|
1568
|
+
if ([...documented.parameters.keys()].some((parameterName) =>
|
|
1569
|
+
!node.arguments.some((parameter) => (typeof parameter.name == "string" ? parameter.name : parameter.name.name) == parameterName))) {
|
|
1570
|
+
return unsupportedSyntax("php", "method PHPDoc parameter mismatch", location)
|
|
1571
|
+
}
|
|
1572
|
+
const nameNode = typeof node.name == "string" ? node : node.name
|
|
1573
|
+
|
|
1574
|
+
if (constructor) return {location, name: "constructor", nameLocation: nodeLocation(nameNode, filename, source), parameters}
|
|
1575
|
+
let returnType
|
|
1576
|
+
const nativeReturnType = phpTypeName(node.type)
|
|
1577
|
+
|
|
1578
|
+
if (documented.returnType) {
|
|
1579
|
+
returnType = documentedValueType({language: "php", location: documented.returnType.location, ownerLocation: location,
|
|
1580
|
+
records: nominalNames, source, sourceType: documented.returnType.sourceType, subject: `Method '${name}' return`})
|
|
1581
|
+
if (!phpDocumentedNativeTypeMatches(nativeReturnType, node.nullable, returnType, nominalNames)) {
|
|
1582
|
+
return unsupportedSyntax("php", "documented method return type incompatible with native type", documented.returnType.location)
|
|
1583
|
+
}
|
|
1584
|
+
} else {
|
|
1585
|
+
if (nativeReturnType == "array") return missingType("php", `Method '${name}' return`, location)
|
|
1586
|
+
returnType = convertReturnType(node.type, `Method '${name}' return`, location, filename, source, nominalNames)
|
|
1587
|
+
}
|
|
1588
|
+
if (node.nullable && returnType.kind != "OptionalType") {
|
|
1589
|
+
if (!node.type) return missingType("php", `Method '${name}' return`, location)
|
|
1590
|
+
returnType = nullableType(/** @type {import("../semantic/types.js").SemanticValueType} */ (returnType), node.type,
|
|
1591
|
+
node.type.loc?.start.offset ?? 0, filename, source)
|
|
1592
|
+
} else if (!node.nullable && returnType.kind == "OptionalType") {
|
|
1593
|
+
return unsupportedSyntax("php", "optional annotation without nullable native return type", documented.returnType?.location ?? location)
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
return {location, name, nameLocation: nodeLocation(nameNode, filename, source), parameters, returnType}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Converts the bounded final/private-field PHP reference-class profile.
|
|
1601
|
+
* @param {import("php-parser").Class} node - Parser class.
|
|
1602
|
+
* @param {import("../semantic/types.js").ClassDeclaration} declaration - Predeclared class identity.
|
|
1603
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
|
|
1604
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Records by identity.
|
|
1605
|
+
* @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Classes by identity.
|
|
1606
|
+
* @param {Map<string, PhpFunctionSignature>} functions - Top-level signatures.
|
|
1607
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Errors by name.
|
|
1608
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Errors by identity.
|
|
1609
|
+
* @param {string} filename - Source filename.
|
|
1610
|
+
* @param {string} source - Complete source.
|
|
1611
|
+
* @returns {import("../semantic/types.js").ClassDeclaration} Semantic class.
|
|
1612
|
+
*/
|
|
1613
|
+
function convertPhpReferenceClass(node, declaration, nominalNames, records, classes, functions, errorNames, errors,
|
|
1614
|
+
filename, source) {
|
|
1615
|
+
const location = nodeLocation(node, filename, source)
|
|
1616
|
+
|
|
1617
|
+
if (node.isAnonymous || !node.isFinal || node.isReadonly || node.isAbstract || node.extends || node.implements?.length ||
|
|
1618
|
+
node.attrGroups.length) return unsupportedSyntax("php", "reference class inheritance or noncanonical modifier", location)
|
|
1619
|
+
const fieldStatements = node.body.filter((member) => member.kind == "propertystatement")
|
|
1620
|
+
const methodNodes = /** @type {import("php-parser").Method[]} */ (node.body.filter((member) => member.kind == "method"))
|
|
1621
|
+
const constructors = methodNodes.filter((method) => (typeof method.name == "string" ? method.name : method.name.name) == "__construct")
|
|
1622
|
+
const ordinaryMethods = methodNodes.filter((method) => !constructors.includes(method))
|
|
1623
|
+
|
|
1624
|
+
if (constructors.length != 1 || fieldStatements.length == 0 || fieldStatements.length + methodNodes.length != node.body.length) {
|
|
1625
|
+
return unsupportedSyntax("php", "reference class requires private fields, one constructor, and ordinary methods", location)
|
|
1626
|
+
}
|
|
1627
|
+
declaration.fields = fieldStatements.map((statement, index) => {
|
|
1628
|
+
const fieldLocation = nodeLocation(statement, filename, source)
|
|
1629
|
+
const properties = Reflect.get(statement, "properties")
|
|
1630
|
+
const property = Array.isArray(properties) && properties.length == 1 ? properties[0] : undefined
|
|
1631
|
+
const nameNode = property?.name
|
|
1632
|
+
const name = typeof nameNode == "string" ? nameNode : nameNode?.name
|
|
1633
|
+
|
|
1634
|
+
if (Reflect.get(statement, "visibility") != "private" || Reflect.get(statement, "isStatic") ||
|
|
1635
|
+
Reflect.get(statement, "isAbstract") || Reflect.get(statement, "isFinal") || !property ||
|
|
1636
|
+
typeof name != "string" || property.value || property.readonly || property.nullable || property.attrGroups?.length ||
|
|
1637
|
+
property.hooks?.length) return unsupportedSyntax("php", "noncanonical private instance field", fieldLocation)
|
|
1638
|
+
const type = convertType(property.type, `Private field '${name}'`, fieldLocation, filename, source, nominalNames)
|
|
1639
|
+
|
|
1640
|
+
return withParserRanges({id: `${declaration.id}:field:${index}`, kind: /** @type {const} */ ("PrivateField"),
|
|
1641
|
+
location: fieldLocation, name, type}, {name: nodeLocation(typeof nameNode == "string" ? property : nameNode, filename, source)})
|
|
1642
|
+
})
|
|
1643
|
+
const constructorNode = constructors[0]
|
|
1644
|
+
const constructorSignature = convertPhpClassCallableSignature(constructorNode, nominalNames,
|
|
1645
|
+
`${declaration.id}:constructor`, true, filename, source)
|
|
1646
|
+
|
|
1647
|
+
declaration.constructor = withParserRanges({body: /** @type {import("../semantic/types.js").Block} */ ({}),
|
|
1648
|
+
id: `${declaration.id}:constructor`, kind: /** @type {const} */ ("ConstructorDeclaration"),
|
|
1649
|
+
location: constructorSignature.location, parameters: constructorSignature.parameters}, {constructor: constructorSignature.nameLocation})
|
|
1650
|
+
const methodSignatures = ordinaryMethods.map((method, index) => convertPhpClassCallableSignature(method, nominalNames,
|
|
1651
|
+
`${declaration.id}:method:${index}`, false, filename, source))
|
|
1652
|
+
|
|
1653
|
+
declaration.methods = methodSignatures.map((signature, index) => withParserRanges({
|
|
1654
|
+
body: /** @type {import("../semantic/types.js").Block} */ ({}), id: `${declaration.id}:method:${index}`,
|
|
1655
|
+
kind: /** @type {const} */ ("MethodDeclaration"), location: signature.location, name: signature.name,
|
|
1656
|
+
parameters: signature.parameters,
|
|
1657
|
+
returnType: /** @type {import("../semantic/types.js").SemanticFunctionReturnType} */ (signature.returnType)
|
|
1658
|
+
}, {name: signature.nameLocation}))
|
|
1659
|
+
const base = {bindings: new Map(), classes, currentClass: declaration, errorNames, errors, functions,
|
|
1660
|
+
recordNames: nominalNames, records}
|
|
1661
|
+
|
|
1662
|
+
declaration.constructor.body = convertBlock(/** @type {import("php-parser").Block} */ (constructorNode.body), filename, source,
|
|
1663
|
+
{...base, bindings: new Map(declaration.constructor.parameters.map((parameter) => [parameter.name, parameter.type])),
|
|
1664
|
+
returnType: {kind: /** @type {const} */ ("TypeReference"), name: /** @type {const} */ ("void")}})
|
|
1665
|
+
for (let index = 0; index < ordinaryMethods.length; index += 1) {
|
|
1666
|
+
const method = declaration.methods[index]
|
|
1667
|
+
|
|
1668
|
+
method.body = convertBlock(/** @type {import("php-parser").Block} */ (ordinaryMethods[index].body), filename, source,
|
|
1669
|
+
{...base, bindings: new Map(method.parameters.map((parameter) => [parameter.name, parameter.type])), returnType: method.returnType})
|
|
1670
|
+
}
|
|
1671
|
+
const nameNode = typeof node.name == "string" ? node : node.name
|
|
1672
|
+
|
|
1673
|
+
return withParserRanges(declaration, {name: nodeLocation(nameNode, filename, source)})
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Converts one exact final readonly promoted-property PHP record.
|
|
1678
|
+
* @param {import("php-parser").Class} node - PHP class declaration.
|
|
1679
|
+
* @param {import("../semantic/types.js").RecordDeclaration} declaration - Predeclared nominal identity.
|
|
1680
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration>} recordNames - Record declarations by name.
|
|
1681
|
+
* @param {string} filename - Source filename.
|
|
1682
|
+
* @param {string} source - Complete source.
|
|
1683
|
+
* @returns {import("../semantic/types.js").RecordDeclaration} Semantic record.
|
|
1684
|
+
*/
|
|
1685
|
+
function convertPhpRecord(node, declaration, recordNames, filename, source) {
|
|
1686
|
+
const location = nodeLocation(node, filename, source)
|
|
1687
|
+
const generic = (declaration.typeParameters?.length ?? 0) > 0
|
|
1688
|
+
|
|
1689
|
+
if (node.isAnonymous || !node.isFinal || node.isReadonly == generic || node.isAbstract || node.extends || node.implements?.length ||
|
|
1690
|
+
node.attrGroups.length) return unsupportedSyntax("php", "noncanonical record class modifiers", location)
|
|
1691
|
+
if (node.body.length < 1 || node.body[0].kind != "method" || !generic && node.body.length != 1) {
|
|
1692
|
+
return unsupportedSyntax("php", "record class body outside one constructor", nodeLocation(node.body[1] ?? node.body[0] ?? node, filename, source))
|
|
1693
|
+
}
|
|
1694
|
+
const constructor = /** @type {import("php-parser").Method} */ (node.body[0])
|
|
1695
|
+
const typeParameterNames = new Map((declaration.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
1696
|
+
const constructorName = typeof constructor.name == "string" ? constructor.name : constructor.name.name
|
|
1697
|
+
|
|
1698
|
+
if (constructorName != "__construct" || constructor.visibility != "public" || constructor.isStatic || constructor.isAbstract ||
|
|
1699
|
+
constructor.isFinal || Reflect.get(constructor, "isReadonly") || constructor.byref || constructor.type || constructor.nullable ||
|
|
1700
|
+
constructor.attrGroups.length || !constructor.body || constructor.body.children.length != 0) {
|
|
1701
|
+
return unsupportedSyntax("php", "noncanonical record constructor", nodeLocation(constructor, filename, source))
|
|
1702
|
+
}
|
|
1703
|
+
const documented = functionDocumentedTypes(/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (constructor)), filename, source)
|
|
1704
|
+
|
|
1705
|
+
declaration.fields = constructor.arguments.map((parameter) => {
|
|
1706
|
+
const fieldLocation = nodeLocation(parameter, filename, source)
|
|
1707
|
+
const name = typeof parameter.name == "string" ? parameter.name : parameter.name.name
|
|
1708
|
+
const documentedType = documented.parameters.get(name)
|
|
1709
|
+
const nativeType = phpTypeName(parameter.type)
|
|
1710
|
+
|
|
1711
|
+
if (parameter.flags != (generic ? 4 : 1) || parameter.byref || parameter.variadic || parameter.value || parameter.readonly ||
|
|
1712
|
+
parameter.hooks.length || parameter.attrGroups.length) {
|
|
1713
|
+
return unsupportedSyntax("php", "record field outside public promoted-property profile", fieldLocation)
|
|
1714
|
+
}
|
|
1715
|
+
if (nativeType == "array" && !documentedType) return missingType("php", `Record field '${name}'`, fieldLocation)
|
|
1716
|
+
let type = documentedType
|
|
1717
|
+
? documentedValueType({language: "php", location: documentedType.location, ownerLocation: fieldLocation, records: recordNames,
|
|
1718
|
+
source, sourceType: documentedType.sourceType, subject: `Record field '${name}'`, typeParameters: typeParameterNames})
|
|
1719
|
+
: convertType(parameter.type, `Record field '${name}'`, fieldLocation, filename, source, recordNames)
|
|
1720
|
+
|
|
1721
|
+
if (documentedType && !phpDocumentedNativeTypeMatches(nativeType, parameter.nullable, type, recordNames)) {
|
|
1722
|
+
return unsupportedSyntax("php", "documented record field type incompatible with native type", documentedType.location)
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (parameter.nullable && type.kind != "OptionalType") {
|
|
1726
|
+
if (!parameter.type) return missingType("php", `Record field '${name}'`, fieldLocation)
|
|
1727
|
+
type = nullableType(type, parameter.type, parameter.loc?.start.offset ?? 0, filename, source)
|
|
1728
|
+
} else if (!parameter.nullable && type.kind == "OptionalType" && type.valueType.kind != "TypeVariableReference") {
|
|
1729
|
+
return unsupportedSyntax("php", "optional annotation without nullable native record field", documentedType?.location ?? fieldLocation)
|
|
1730
|
+
}
|
|
1731
|
+
const field = {kind: /** @type {const} */ ("RecordField"), location: fieldLocation, name, type}
|
|
1732
|
+
const nameNode = typeof parameter.name == "string" ? parameter : parameter.name
|
|
1733
|
+
|
|
1734
|
+
return withParserRanges(field, {name: nodeLocation(nameNode, filename, source)})
|
|
1735
|
+
})
|
|
1736
|
+
const extraDocumented = [...documented.parameters.keys()].find((name) =>
|
|
1737
|
+
!constructor.arguments.some((parameter) => (typeof parameter.name == "string" ? parameter.name : parameter.name.name) == name))
|
|
1738
|
+
|
|
1739
|
+
if (extraDocumented || documented.returnType) return unsupportedSyntax("php", "invalid record constructor annotation", location)
|
|
1740
|
+
if (generic) validatePhpGenericRecordGetters(node, declaration, recordNames, filename, source)
|
|
1741
|
+
const nameNode = typeof node.name == "string" ? node : node.name
|
|
1742
|
+
|
|
1743
|
+
return withParserRanges(declaration, {name: nodeLocation(nameNode, filename, source)})
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Validates the exact accessor half of PHP's documented generic-record profile.
|
|
1748
|
+
* @param {import("php-parser").Class} node - PHP class declaration.
|
|
1749
|
+
* @param {import("../semantic/types.js").RecordDeclaration} declaration - Converted record.
|
|
1750
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration>} recordNames - Visible records.
|
|
1751
|
+
* @param {string} filename - Source filename.
|
|
1752
|
+
* @param {string} source - Complete source.
|
|
1753
|
+
* @returns {void}
|
|
1754
|
+
*/
|
|
1755
|
+
function validatePhpGenericRecordGetters(node, declaration, recordNames, filename, source) {
|
|
1756
|
+
const getters = node.body.slice(1)
|
|
1757
|
+
const typeParameters = new Map((declaration.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
1758
|
+
|
|
1759
|
+
if (getters.length != declaration.fields.length) {
|
|
1760
|
+
return unsupportedSyntax("php", "generic record getter count", nodeLocation(getters[declaration.fields.length] ?? node, filename, source))
|
|
1761
|
+
}
|
|
1762
|
+
getters.forEach((candidate, index) => {
|
|
1763
|
+
const field = declaration.fields[index]
|
|
1764
|
+
|
|
1765
|
+
if (candidate.kind != "method") return unsupportedSyntax("php", "noncanonical generic record getter", nodeLocation(candidate, filename, source))
|
|
1766
|
+
const getter = /** @type {import("php-parser").Method} */ (candidate)
|
|
1767
|
+
const name = typeof getter.name == "string" ? getter.name : getter.name.name
|
|
1768
|
+
const statement = getter.body?.children[0]
|
|
1769
|
+
const returned = statement?.kind == "return" ? /** @type {import("php-parser").Return} */ (statement).expr : undefined
|
|
1770
|
+
const lookup = returned?.kind == "propertylookup" ? /** @type {import("php-parser").PropertyLookup} */ (returned) : undefined
|
|
1771
|
+
const receiver = lookup?.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
|
|
1772
|
+
const member = lookup?.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
|
|
1773
|
+
|
|
1774
|
+
if (name != field.name || getter.visibility != "public" || getter.isStatic || getter.isAbstract || getter.isFinal ||
|
|
1775
|
+
Reflect.get(getter, "isReadonly") || getter.byref || getter.arguments.length || getter.attrGroups.length ||
|
|
1776
|
+
!getter.body || getter.body.children.length != 1 || receiver?.name != "this" || member?.name != field.name) {
|
|
1777
|
+
return unsupportedSyntax("php", "noncanonical generic record getter", nodeLocation(getter, filename, source))
|
|
1778
|
+
}
|
|
1779
|
+
const documented = functionDocumentedTypes(
|
|
1780
|
+
/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (getter)), filename, source)
|
|
1781
|
+
|
|
1782
|
+
if (documented.parameters.size || documented.typeParameters || !documented.returnType) {
|
|
1783
|
+
return unsupportedSyntax("php", "missing or invalid generic record getter type", nodeLocation(getter, filename, source))
|
|
1784
|
+
}
|
|
1785
|
+
const returnType = documentedValueType({language: "php", location: documented.returnType.location,
|
|
1786
|
+
ownerLocation: nodeLocation(getter, filename, source), records: recordNames, source,
|
|
1787
|
+
sourceType: documented.returnType.sourceType, subject: `Record getter '${name}'`, typeParameters})
|
|
1788
|
+
const nativeType = phpTypeName(getter.type)
|
|
1789
|
+
|
|
1790
|
+
if (!phpDocumentedNativeTypeMatches(nativeType, getter.nullable, returnType, recordNames) ||
|
|
1791
|
+
!sameValueType(returnType, field.type)) {
|
|
1792
|
+
return unsupportedSyntax("php", "generic record getter type mismatch", documented.returnType.location)
|
|
1793
|
+
}
|
|
1794
|
+
})
|
|
449
1795
|
}
|
|
450
1796
|
|
|
451
1797
|
/**
|
|
452
1798
|
* Converts the existing PHP if/else terminal with restricted branch prefixes.
|
|
453
1799
|
* @param {import("php-parser").If} node - PHP if node.
|
|
454
|
-
* @param {Set<string>} visible - Enclosing visible names.
|
|
455
1800
|
* @param {string} filename - Source filename.
|
|
456
1801
|
* @param {string} source - Complete source.
|
|
1802
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
457
1803
|
* @returns {import("../semantic/types.js").IfStatement} Semantic branch.
|
|
458
1804
|
*/
|
|
459
|
-
function convertIf(node,
|
|
1805
|
+
function convertIf(node, filename, source, context) {
|
|
460
1806
|
const location = nodeLocation(node, filename, source)
|
|
461
1807
|
|
|
462
1808
|
if (node.shortForm) return unsupportedSyntax("php", "alternative if syntax", location)
|
|
463
1809
|
if (node.body.kind != "block") return unsupportedSyntax("php", "if without block consequent", location)
|
|
1810
|
+
if (node.test.kind == "variable") {
|
|
1811
|
+
const tested = /** @type {import("php-parser").Variable} */ (node.test)
|
|
1812
|
+
|
|
1813
|
+
if (typeof tested.name == "string" && context.bindings.get(tested.name)?.kind == "OptionalType") {
|
|
1814
|
+
return unsupportedSyntax("php", "optional truthiness condition", nodeLocation(tested, filename, source))
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
464
1817
|
|
|
465
|
-
const
|
|
1818
|
+
const condition = convertExpression(node.test, filename, source, context)
|
|
1819
|
+
const consequentContext = {...context, bindings: new Map(context.bindings)}
|
|
1820
|
+
const alternateContext = {...context, bindings: new Map(context.bindings)}
|
|
466
1821
|
let alternate
|
|
467
1822
|
|
|
468
1823
|
if (node.alternate) {
|
|
469
|
-
const alternateVisible = new Set(visible)
|
|
470
|
-
|
|
471
1824
|
if (node.alternate.kind == "block") {
|
|
472
|
-
alternate = convertBlock(/** @type {import("php-parser").Block} */ (node.alternate),
|
|
1825
|
+
alternate = convertBlock(/** @type {import("php-parser").Block} */ (node.alternate), filename, source, alternateContext)
|
|
473
1826
|
} else if (node.alternate.kind == "if") {
|
|
474
1827
|
const nested = /** @type {import("php-parser").If} */ (node.alternate)
|
|
475
1828
|
|
|
476
1829
|
alternate = {
|
|
477
1830
|
kind: /** @type {const} */ ("Block"),
|
|
478
1831
|
location: nodeLocation(nested, filename, source),
|
|
479
|
-
statements: [convertIf(nested,
|
|
1832
|
+
statements: [convertIf(nested, filename, source, alternateContext)]
|
|
480
1833
|
}
|
|
481
1834
|
} else return unsupportedSyntax("php", `if alternate ${node.alternate.kind}`, nodeLocation(node.alternate, filename, source))
|
|
482
1835
|
}
|
|
483
1836
|
|
|
484
1837
|
return {
|
|
485
1838
|
...(alternate ? {alternate} : {}),
|
|
486
|
-
condition
|
|
487
|
-
consequent: convertBlock(node.body,
|
|
1839
|
+
condition,
|
|
1840
|
+
consequent: convertBlock(node.body, filename, source, consequentContext),
|
|
488
1841
|
kind: "IfStatement",
|
|
489
1842
|
location
|
|
490
1843
|
}
|
|
491
1844
|
}
|
|
492
1845
|
|
|
1846
|
+
/**
|
|
1847
|
+
* Converts exact braced PHP `while ($condition) { ... }` syntax.
|
|
1848
|
+
* @param {import("php-parser").While} node - PHP while node.
|
|
1849
|
+
* @param {string} filename - Source filename.
|
|
1850
|
+
* @param {string} source - Complete source.
|
|
1851
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
1852
|
+
* @returns {import("../semantic/types.js").WhileStatement} Semantic loop.
|
|
1853
|
+
*/
|
|
1854
|
+
function convertWhile(node, filename, source, context) {
|
|
1855
|
+
const location = nodeLocation(node, filename, source)
|
|
1856
|
+
|
|
1857
|
+
if (node.shortForm) return unsupportedSyntax("php", "alternative while syntax", location)
|
|
1858
|
+
if (!node.body || node.body.kind != "block") {
|
|
1859
|
+
return unsupportedSyntax("php", "while without block body", node.body ? nodeLocation(node.body, filename, source) : location)
|
|
1860
|
+
}
|
|
1861
|
+
const bodyContext = {...context, bindings: new Map(context.bindings)}
|
|
1862
|
+
|
|
1863
|
+
return withParserRanges({
|
|
1864
|
+
body: convertBlock(/** @type {import("php-parser").Block} */ (node.body), filename, source, bodyContext),
|
|
1865
|
+
condition: convertExpression(node.test, filename, source, context, undefined, true),
|
|
1866
|
+
kind: /** @type {const} */ ("WhileStatement"),
|
|
1867
|
+
location
|
|
1868
|
+
}, {keyword: tokenLocation("while", node.loc?.start.offset ?? 0, node.test.loc?.start.offset ?? source.length, filename, source)})
|
|
1869
|
+
}
|
|
1870
|
+
|
|
493
1871
|
/**
|
|
494
1872
|
* Converts a PHP echo entry point.
|
|
495
1873
|
* @param {import("php-parser").Echo} node - PHP echo node.
|
|
496
1874
|
* @param {string} filename - Source filename.
|
|
497
1875
|
* @param {string} source - Complete source.
|
|
1876
|
+
* @param {PhpConversionContext} context - Typed conversion context.
|
|
498
1877
|
* @returns {import("../semantic/types.js").PrintStatement} Print statement.
|
|
499
1878
|
*/
|
|
500
|
-
function convertPrint(node, filename, source) {
|
|
1879
|
+
function convertPrint(node, filename, source, context) {
|
|
501
1880
|
const location = nodeLocation(node, filename, source)
|
|
502
1881
|
|
|
503
1882
|
if (node.expressions.length != 2 || node.expressions[1].kind != "name" ||
|
|
@@ -505,7 +1884,7 @@ function convertPrint(node, filename, source) {
|
|
|
505
1884
|
return unsupportedSyntax("php", "echo without one expression and PHP_EOL", location)
|
|
506
1885
|
}
|
|
507
1886
|
|
|
508
|
-
return {expression: convertExpression(node.expressions[0], filename, source), kind: "PrintStatement", location}
|
|
1887
|
+
return {expression: convertExpression(node.expressions[0], filename, source, context), kind: "PrintStatement", location}
|
|
509
1888
|
}
|
|
510
1889
|
|
|
511
1890
|
/**
|
|
@@ -513,29 +1892,118 @@ function convertPrint(node, filename, source) {
|
|
|
513
1892
|
* @param {object} input - Parser input.
|
|
514
1893
|
* @param {string} input.filename - Source filename.
|
|
515
1894
|
* @param {string} input.source - Source text.
|
|
1895
|
+
* @param {readonly import("../semantic/types.js").EffectCapabilityDeclaration[]} [input.capabilities] - Compiler-authorized declarations.
|
|
1896
|
+
* @param {{isEntry: boolean, functions: Map<string, import("../semantic/types.js").FunctionDeclaration>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, errors?: Map<string, import("../semantic/types.js").ErrorDeclaration>}} [input.program] - Resolved program imports and entry role.
|
|
516
1897
|
* @returns {import("../semantic/types.js").SemanticModule} Semantic module.
|
|
517
1898
|
*/
|
|
518
|
-
export function parsePhp({filename, source}) {
|
|
1899
|
+
export function parsePhp({capabilities = [], filename, source, program: programContext}) {
|
|
519
1900
|
const program = parsePhpProgram(filename, source)
|
|
520
1901
|
validateDeclareNodes(program.children, filename, source)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1902
|
+
let body = task034PhpBody(program.children, capabilities, filename, source)
|
|
1903
|
+
|
|
1904
|
+
if (programContext) {
|
|
1905
|
+
inspectPhpModule({filename, source})
|
|
1906
|
+
const namespace = /** @type {import("php-parser").Namespace} */ (body.find((node) => node.kind == "namespace"))
|
|
1907
|
+
|
|
1908
|
+
body = namespace.children.filter((node) => node.kind != "usegroup" && !isPhpRequireOnce(node))
|
|
1909
|
+
}
|
|
1910
|
+
const classNodes = /** @type {import("php-parser").Class[]} */ (body.filter((node) => node.kind == "class"))
|
|
1911
|
+
const errorNodes = classNodes.filter((node) => node.extends?.kind == "name" &&
|
|
1912
|
+
["RuntimeException", "\\RuntimeException"].includes(node.extends.name))
|
|
1913
|
+
const referenceClassNodes = classNodes.filter((node) => !errorNodes.includes(node) &&
|
|
1914
|
+
node.body.some((member) => member.kind == "propertystatement"))
|
|
1915
|
+
const recordNodes = classNodes.filter((node) => !errorNodes.includes(node) && !referenceClassNodes.includes(node))
|
|
1916
|
+
const errorDeclarations = errorNodes.map((node, index) => ({
|
|
1917
|
+
id: `error:${index}`,
|
|
1918
|
+
kind: /** @type {const} */ ("ErrorDeclaration"),
|
|
1919
|
+
location: nodeLocation(node, filename, source),
|
|
1920
|
+
name: typeof node.name == "string" ? node.name : node.name.name
|
|
1921
|
+
}))
|
|
1922
|
+
const recordDeclarations = recordNodes.map((node, index) => {
|
|
1923
|
+
const id = `record:${index}`
|
|
1924
|
+
const typeParameters = classDocumentedTypeParameters(node, id, filename, source)
|
|
1925
|
+
|
|
1926
|
+
return {
|
|
1927
|
+
fields: [],
|
|
1928
|
+
id,
|
|
1929
|
+
kind: /** @type {const} */ ("RecordDeclaration"),
|
|
1930
|
+
location: nodeLocation(node, filename, source),
|
|
1931
|
+
name: typeof node.name == "string" ? node.name : node.name.name,
|
|
1932
|
+
...(typeParameters ? {typeParameters} : {})
|
|
1933
|
+
}
|
|
1934
|
+
})
|
|
1935
|
+
const classDeclarations = referenceClassNodes.map((node, index) => ({
|
|
1936
|
+
constructor: /** @type {import("../semantic/types.js").ConstructorDeclaration} */ ({}),
|
|
1937
|
+
fields: [], id: `class:${index}`, kind: /** @type {const} */ ("ClassDeclaration"),
|
|
1938
|
+
location: nodeLocation(node, filename, source), methods: [],
|
|
1939
|
+
name: typeof node.name == "string" ? node.name : node.name.name
|
|
1940
|
+
}))
|
|
1941
|
+
const recordNames = /** @type {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} */ (
|
|
1942
|
+
new Map(programContext?.records ?? []))
|
|
1943
|
+
for (const resource of capabilities.flatMap(({resources}) => resources)) recordNames.set(resource.name, resource)
|
|
1944
|
+
const errorNames = new Map(programContext?.errors ?? [])
|
|
1945
|
+
for (const failure of capabilities.flatMap(({failures}) => failures)) {
|
|
1946
|
+
errorNames.set(failure.name, /** @type {import("../semantic/types.js").ErrorDeclaration} */ (/** @type {unknown} */ (failure)))
|
|
1947
|
+
}
|
|
1948
|
+
for (const declaration of errorDeclarations) errorNames.set(declaration.name, declaration)
|
|
1949
|
+
const errorsById = new Map([...errorNames.values()].map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
|
|
1950
|
+
const errors = errorNodes.map((node, index) => convertPhpError(node, errorDeclarations[index], filename, source))
|
|
1951
|
+
for (const declaration of recordDeclarations) recordNames.set(declaration.name, declaration)
|
|
1952
|
+
for (const declaration of classDeclarations) recordNames.set(declaration.name, declaration)
|
|
1953
|
+
const recordsById = new Map([...recordNames.values()]
|
|
1954
|
+
.filter((declaration) => declaration.kind == "RecordDeclaration")
|
|
1955
|
+
.map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
|
|
1956
|
+
const classesById = new Map(classDeclarations.map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
|
|
1957
|
+
const records = recordNodes.map((node, index) => convertPhpRecord(node, recordDeclarations[index],
|
|
1958
|
+
/** @type {Map<string, import("../semantic/types.js").RecordDeclaration>} */ (/** @type {unknown} */ (recordNames)), filename, source))
|
|
1959
|
+
const functionNodes = /** @type {import("php-parser").Function[]} */ (body.filter((node) => node.kind == "function"))
|
|
1960
|
+
const signatures = functionNodes.map((node, index) => convertFunctionSignature(node, filename, source, recordNames, `function:${index}`))
|
|
1961
|
+
const functionSignatures = new Map([...capabilityFunctionSignatures(capabilities, moduleLocation(filename, source)),
|
|
1962
|
+
...(programContext?.functions ?? [])].map(([localName, declaration]) => [localName, {
|
|
1963
|
+
location: declaration.location,
|
|
1964
|
+
name: localName,
|
|
1965
|
+
nameLocation: declaration.location,
|
|
1966
|
+
parameters: declaration.parameters,
|
|
1967
|
+
returnType: declaration.returnType,
|
|
1968
|
+
...(declaration.typeParameters ? {typeParameters: declaration.typeParameters} : {})
|
|
1969
|
+
}]))
|
|
1970
|
+
for (const signature of signatures) functionSignatures.set(signature.name, signature)
|
|
1971
|
+
const classes = referenceClassNodes.map((node, index) => convertPhpReferenceClass(node, classDeclarations[index],
|
|
1972
|
+
recordNames, recordsById, classesById, functionSignatures, errorNames, errorsById, filename, source))
|
|
1973
|
+
const functions = functionNodes.map((node, index) =>
|
|
1974
|
+
convertFunction(node, signatures[index], functionSignatures, recordNames, recordsById, classesById, errorNames, errorsById,
|
|
1975
|
+
filename, source))
|
|
1976
|
+
const executableNodes = body.filter((node) => !["class", "function", "declare", "noop"].includes(node.kind))
|
|
524
1977
|
const location = moduleLocation(filename, source)
|
|
525
1978
|
|
|
526
|
-
if (
|
|
1979
|
+
if (programContext && !programContext.isEntry && executableNodes.length > 0) {
|
|
1980
|
+
return unsupportedSyntax("php", "top-level side effect outside the selected entry module", nodeLocation(executableNodes[0], filename, source))
|
|
1981
|
+
}
|
|
527
1982
|
const first = executableNodes[0]
|
|
528
1983
|
const last = executableNodes.at(-1) ?? first
|
|
529
1984
|
const entryLocation = first
|
|
530
1985
|
? locationFromOffsets(filename, source, first.loc?.start.offset ?? 0, last?.loc?.end.offset ?? source.length)
|
|
531
1986
|
: location
|
|
532
|
-
const
|
|
1987
|
+
const entryContext = {
|
|
1988
|
+
bindings: new Map(),
|
|
1989
|
+
classes: classesById,
|
|
1990
|
+
errorNames,
|
|
1991
|
+
errors: errorsById,
|
|
1992
|
+
functions: functionSignatures,
|
|
1993
|
+
orderedMapDeclarations: orderedMapDeclarationsForPhp(executableNodes),
|
|
1994
|
+
recordNames,
|
|
1995
|
+
records: recordsById,
|
|
1996
|
+
returnType: undefined
|
|
1997
|
+
}
|
|
533
1998
|
const entryBlock = {
|
|
534
1999
|
kind: /** @type {const} */ ("Block"),
|
|
535
2000
|
location: entryLocation,
|
|
536
|
-
statements: executableNodes.map((node) => convertStatement(node,
|
|
2001
|
+
statements: executableNodes.map((node) => convertStatement(node, filename, source, entryContext))
|
|
537
2002
|
}
|
|
538
2003
|
|
|
2004
|
+
if (!programContext && functions.length == 0 && capabilities.length == 0 && !blockReferencesProtectedEntry(entryBlock)) {
|
|
2005
|
+
return unsupportedSyntax("php", "module without a function", location)
|
|
2006
|
+
}
|
|
539
2007
|
return {
|
|
540
2008
|
entryPoint: {
|
|
541
2009
|
body: entryBlock,
|
|
@@ -544,10 +2012,156 @@ export function parsePhp({filename, source}) {
|
|
|
544
2012
|
},
|
|
545
2013
|
functions,
|
|
546
2014
|
kind: "Module",
|
|
547
|
-
location
|
|
2015
|
+
location,
|
|
2016
|
+
...(errors.length > 0 ? {errors} : {}),
|
|
2017
|
+
...(classes.length > 0 ? {classes} : {}),
|
|
2018
|
+
...(records.length > 0 ? {records} : {})
|
|
548
2019
|
}
|
|
549
2020
|
}
|
|
550
2021
|
|
|
2022
|
+
/**
|
|
2023
|
+
* Removes exact protected PHP probe support before portable conversion.
|
|
2024
|
+
* @param {import("php-parser").Statement[]} body - Parsed top-level statements.
|
|
2025
|
+
* @param {readonly import("../semantic/types.js").EffectCapabilityDeclaration[]} capabilities - Authorized declarations.
|
|
2026
|
+
* @param {string} filename - Source filename.
|
|
2027
|
+
* @param {string} source - Complete source.
|
|
2028
|
+
* @returns {import("php-parser").Statement[]} Portable statements.
|
|
2029
|
+
*/
|
|
2030
|
+
function task034PhpBody(body, capabilities, filename, source) {
|
|
2031
|
+
if (!isTask034ResourceProbe(capabilities)) return body
|
|
2032
|
+
const start = body.findIndex((node) => node.leadingComments?.some(({value}) => value == "/* semantifold-task034-support */"))
|
|
2033
|
+
|
|
2034
|
+
if (start < 0) return body
|
|
2035
|
+
if (!hasExactEffectSupport(source, "php")) {
|
|
2036
|
+
return unsupportedSyntax("php", "malformed protected Task 034 support", nodeLocation(body[start], filename, source))
|
|
2037
|
+
}
|
|
2038
|
+
const expected = [
|
|
2039
|
+
["class", "ProbeOperationFailure"], ["class", "ProbeAcquireFailure"], ["class", "ProbeReadFailure"],
|
|
2040
|
+
["class", "ProbeCloseFailure"], ["class", "ProbeResourceClosed"], ["class", "ProbeResource"],
|
|
2041
|
+
["expressionstatement", undefined], ["function", "probeEffect"], ["function", "probeAcquire"],
|
|
2042
|
+
["function", "probeRead"], ["function", "probeClose"], ["function", "probeTrace"]
|
|
2043
|
+
]
|
|
2044
|
+
const matches = expected.every(([kind, name], index) => {
|
|
2045
|
+
const node = body[start + index]
|
|
2046
|
+
const rawName = node ? Reflect.get(node, "name") : undefined
|
|
2047
|
+
const actualName = typeof rawName == "string" ? rawName : rawName && typeof rawName == "object" ? Reflect.get(rawName, "name") : undefined
|
|
2048
|
+
return node?.kind == kind && actualName == name
|
|
2049
|
+
})
|
|
2050
|
+
const next = body[start + expected.length]
|
|
2051
|
+
|
|
2052
|
+
if (!matches || next && !next.leadingComments?.some(({value}) => value == "/* semantifold-task034-support-end */")) {
|
|
2053
|
+
return unsupportedSyntax("php", "malformed protected Task 034 support", nodeLocation(body[start], filename, source))
|
|
2054
|
+
}
|
|
2055
|
+
return [...body.slice(0, start), ...body.slice(start + expected.length)]
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Reads the canonical parser-owned PHP namespace, use bindings, and literal require_once edges.
|
|
2060
|
+
* @param {object} input - Parser input.
|
|
2061
|
+
* @param {string} input.filename - Source filename.
|
|
2062
|
+
* @param {string} input.source - Source text.
|
|
2063
|
+
* @returns {{bindings: {importedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, nativeName: string, symbolKind: import("../semantic/types.js").SemanticDeclarationKind, typeOnly: false}[], imports: {importedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, namespace: true, pathLocation: import("../semantic/types.js").SourceLocation, specifier: string, typeOnly: false}[], exports: {exportedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, typeOnly: false}[], nativeName: string}} Parser-owned PHP module header.
|
|
2064
|
+
*/
|
|
2065
|
+
export function inspectPhpModule({filename, source}) {
|
|
2066
|
+
const program = parsePhpProgram(filename, source)
|
|
2067
|
+
|
|
2068
|
+
validateDeclareNodes(program.children, filename, source)
|
|
2069
|
+
const namespaces = program.children.filter((node) => node.kind == "namespace")
|
|
2070
|
+
const invalidOuter = program.children.find((node) => !["declare", "namespace", "noop"].includes(node.kind))
|
|
2071
|
+
const namespace = /** @type {import("php-parser").Namespace | undefined} */ (namespaces[0])
|
|
2072
|
+
|
|
2073
|
+
if (!namespace || namespaces.length != 1 || namespace.withBrackets || typeof namespace.name != "string" || invalidOuter) {
|
|
2074
|
+
return unsupportedSyntax("php", "one canonical unbracketed namespace",
|
|
2075
|
+
nodeLocation(invalidOuter ?? namespaces[1] ?? namespace ?? program, filename, source))
|
|
2076
|
+
}
|
|
2077
|
+
const bindings = []
|
|
2078
|
+
|
|
2079
|
+
for (const node of namespace.children.filter((child) => child.kind == "usegroup")) {
|
|
2080
|
+
const group = /** @type {import("php-parser").UseGroup} */ (node)
|
|
2081
|
+
|
|
2082
|
+
if (group.name !== null || ![null, "function"].includes(group.type) || group.items.length == 0) {
|
|
2083
|
+
return unsupportedSyntax("php", "grouped, constant, or empty use declaration", nodeLocation(group, filename, source))
|
|
2084
|
+
}
|
|
2085
|
+
for (const item of group.items) {
|
|
2086
|
+
if (item.type !== null || typeof item.name != "string" || !item.name.includes("\\")) {
|
|
2087
|
+
return unsupportedSyntax("php", "grouped or unqualified use item", nodeLocation(item, filename, source))
|
|
2088
|
+
}
|
|
2089
|
+
const importedName = item.name.split("\\").at(-1)
|
|
2090
|
+
const alias = item.alias
|
|
2091
|
+
|
|
2092
|
+
if (!importedName || alias && alias.kind != "identifier") {
|
|
2093
|
+
return unsupportedSyntax("php", "invalid use alias", nodeLocation(item, filename, source))
|
|
2094
|
+
}
|
|
2095
|
+
bindings.push({
|
|
2096
|
+
importedName,
|
|
2097
|
+
localName: alias ? alias.name : importedName,
|
|
2098
|
+
location: nodeLocation(item, filename, source),
|
|
2099
|
+
nativeName: item.name,
|
|
2100
|
+
symbolKind: /** @type {import("../semantic/types.js").SemanticDeclarationKind} */ (group.type == "function" ? "function" : "record"),
|
|
2101
|
+
typeOnly: /** @type {const} */ (false)
|
|
2102
|
+
})
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const imports = namespace.children.filter(isPhpIncludeStatement).map((node) => {
|
|
2106
|
+
if (!isPhpRequireOnce(node)) {
|
|
2107
|
+
return unsupportedSyntax("php", "include/require outside literal require_once __DIR__ profile", nodeLocation(node, filename, source))
|
|
2108
|
+
}
|
|
2109
|
+
const statement = /** @type {import("php-parser").ExpressionStatement} */ (node)
|
|
2110
|
+
const include = /** @type {import("php-parser").Include} */ (statement.expression)
|
|
2111
|
+
const target = /** @type {import("php-parser").Bin} */ (include.target)
|
|
2112
|
+
const path = /** @type {import("php-parser").String} */ (target.right)
|
|
2113
|
+
|
|
2114
|
+
if (!path.value.startsWith("/") || path.value.includes("\0")) {
|
|
2115
|
+
return unsupportedSyntax("php", "unsafe require_once path", nodeLocation(path, filename, source))
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
return {
|
|
2119
|
+
importedName: "*",
|
|
2120
|
+
localName: "",
|
|
2121
|
+
location: nodeLocation(node, filename, source),
|
|
2122
|
+
namespace: /** @type {const} */ (true),
|
|
2123
|
+
pathLocation: nodeLocation(path, filename, source),
|
|
2124
|
+
specifier: `.${path.value}`,
|
|
2125
|
+
typeOnly: /** @type {const} */ (false)
|
|
2126
|
+
}
|
|
2127
|
+
})
|
|
2128
|
+
const exports = namespace.children.flatMap((node) => {
|
|
2129
|
+
if (node.kind != "class" && node.kind != "function") return []
|
|
2130
|
+
const declaration = /** @type {import("php-parser").Class | import("php-parser").Function} */ (node)
|
|
2131
|
+
const name = typeof declaration.name == "string" ? declaration.name : declaration.name.name
|
|
2132
|
+
|
|
2133
|
+
return [{exportedName: name, localName: name, location: nodeLocation(node, filename, source), typeOnly: /** @type {const} */ (false)}]
|
|
2134
|
+
})
|
|
2135
|
+
|
|
2136
|
+
return {bindings, exports, imports, nativeName: namespace.name}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Reports whether a PHP statement is any include/require expression.
|
|
2141
|
+
* @param {import("php-parser").Node} node - Candidate statement.
|
|
2142
|
+
* @returns {boolean} Whether the node is an include expression statement.
|
|
2143
|
+
*/
|
|
2144
|
+
function isPhpIncludeStatement(node) {
|
|
2145
|
+
return node.kind == "expressionstatement" &&
|
|
2146
|
+
/** @type {import("php-parser").ExpressionStatement} */ (node).expression.kind == "include"
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Reports whether a PHP statement is the canonical literal project load edge.
|
|
2151
|
+
* @param {import("php-parser").Node} node - Candidate statement.
|
|
2152
|
+
* @returns {boolean} Whether the node is a supported require-once statement.
|
|
2153
|
+
*/
|
|
2154
|
+
function isPhpRequireOnce(node) {
|
|
2155
|
+
if (!isPhpIncludeStatement(node)) return false
|
|
2156
|
+
const statement = /** @type {import("php-parser").ExpressionStatement} */ (node)
|
|
2157
|
+
const include = /** @type {import("php-parser").Include} */ (statement.expression)
|
|
2158
|
+
const target = include.target.kind == "bin" ? /** @type {import("php-parser").Bin} */ (include.target) : undefined
|
|
2159
|
+
const left = target?.left.kind == "magic" ? /** @type {import("php-parser").Magic} */ (target.left) : undefined
|
|
2160
|
+
|
|
2161
|
+
return include.once && include.require && target?.type == "." &&
|
|
2162
|
+
left?.value == "__DIR__" && target.right.kind == "string"
|
|
2163
|
+
}
|
|
2164
|
+
|
|
551
2165
|
/**
|
|
552
2166
|
* Accepts only an optional single `declare(strict_types=1)` directive.
|
|
553
2167
|
* @param {import("php-parser").Node[]} nodes - Top-level PHP nodes.
|
|
@@ -588,6 +2202,38 @@ function parsePhpProgram(filename, source) {
|
|
|
588
2202
|
try {
|
|
589
2203
|
return parser.parseCode(source, filename)
|
|
590
2204
|
} catch (error) {
|
|
591
|
-
return parseFailure("php", error)
|
|
2205
|
+
return parseFailure("php", error, phpParserFailureLocation(error, filename, source))
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Normalizes php-parser's one-based line and zero-based column when present.
|
|
2211
|
+
* A module range remains the deterministic fallback for opaque parser errors.
|
|
2212
|
+
* @param {unknown} error - Native parser error.
|
|
2213
|
+
* @param {string} filename - Requested source filename.
|
|
2214
|
+
* @param {string} source - Complete source.
|
|
2215
|
+
* @returns {import("../semantic/types.js").SourceLocation} Failure range.
|
|
2216
|
+
*/
|
|
2217
|
+
function phpParserFailureLocation(error, filename, source) {
|
|
2218
|
+
if (!error || typeof error != "object") return moduleLocation(filename, source)
|
|
2219
|
+
const line = Reflect.get(error, "lineNumber")
|
|
2220
|
+
const column = Reflect.get(error, "columnNumber")
|
|
2221
|
+
|
|
2222
|
+
if (typeof line != "number" || !Number.isSafeInteger(line) || line < 1 ||
|
|
2223
|
+
typeof column != "number" || !Number.isSafeInteger(column) || column < 0) {
|
|
2224
|
+
return moduleLocation(filename, source)
|
|
592
2225
|
}
|
|
2226
|
+
let lineStart = 0
|
|
2227
|
+
|
|
2228
|
+
for (let current = 1; current < line; current++) {
|
|
2229
|
+
const newline = source.indexOf("\n", lineStart)
|
|
2230
|
+
|
|
2231
|
+
if (newline < 0) return moduleLocation(filename, source)
|
|
2232
|
+
lineStart = newline + 1
|
|
2233
|
+
}
|
|
2234
|
+
const lineEnd = source.indexOf("\n", lineStart)
|
|
2235
|
+
const boundedEnd = lineEnd < 0 ? source.length : lineEnd
|
|
2236
|
+
const offset = Math.min(lineStart + column, boundedEnd)
|
|
2237
|
+
|
|
2238
|
+
return locationFromOffsets(filename, source, offset, Math.min(offset + 1, source.length))
|
|
593
2239
|
}
|