@descryy/runtime-adapter-php 0.0.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/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/laravel-frame-parsing.d.ts +114 -0
- package/dist/laravel-frame-parsing.d.ts.map +1 -0
- package/dist/laravel-frame-parsing.js +145 -0
- package/dist/laravel-frame-parsing.js.map +1 -0
- package/dist/laravel-stack-trace-parser.d.ts +19 -0
- package/dist/laravel-stack-trace-parser.d.ts.map +1 -0
- package/dist/laravel-stack-trace-parser.js +55 -0
- package/dist/laravel-stack-trace-parser.js.map +1 -0
- package/dist/php-frame-parsing.d.ts +109 -0
- package/dist/php-frame-parsing.d.ts.map +1 -0
- package/dist/php-frame-parsing.js +200 -0
- package/dist/php-frame-parsing.js.map +1 -0
- package/dist/php-runtime-adapter.d.ts +35 -0
- package/dist/php-runtime-adapter.d.ts.map +1 -0
- package/dist/php-runtime-adapter.js +42 -0
- package/dist/php-runtime-adapter.js.map +1 -0
- package/dist/php-source-location-resolver.d.ts +30 -0
- package/dist/php-source-location-resolver.d.ts.map +1 -0
- package/dist/php-source-location-resolver.js +45 -0
- package/dist/php-source-location-resolver.js.map +1 -0
- package/dist/php-stack-trace-parser.d.ts +20 -0
- package/dist/php-stack-trace-parser.d.ts.map +1 -0
- package/dist/php-stack-trace-parser.js +66 -0
- package/dist/php-stack-trace-parser.js.map +1 -0
- package/package.json +30 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createLaravelLineClassifier, decodeLaravelCallee, isLaravelHeaderLine, parseLaravelHeader, } from "./laravel-frame-parsing.ts";
|
|
2
|
+
export { createLaravelStackTraceParser } from "./laravel-stack-trace-parser.ts";
|
|
3
|
+
export type { RawPhpFrame } from "./php-frame-parsing.ts";
|
|
4
|
+
export { createPhpLineClassifier, isPhpHeaderLine, isPhpFrameLine, isPhpTrailerLine, parsePhpHeaderFrame, parsePhpFrame, } from "./php-frame-parsing.ts";
|
|
5
|
+
export { createPhpSourceLocationResolver } from "./php-source-location-resolver.ts";
|
|
6
|
+
export { createPhpStackTraceParser } from "./php-stack-trace-parser.ts";
|
|
7
|
+
export type { PhpRuntimeAdapterOptions } from "./php-runtime-adapter.ts";
|
|
8
|
+
export { createPhpRuntimeAdapter } from "./php-runtime-adapter.ts";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createLaravelLineClassifier, decodeLaravelCallee, isLaravelHeaderLine, parseLaravelHeader, } from "./laravel-frame-parsing.js";
|
|
2
|
+
export { createLaravelStackTraceParser } from "./laravel-stack-trace-parser.js";
|
|
3
|
+
export { createPhpLineClassifier, isPhpHeaderLine, isPhpFrameLine, isPhpTrailerLine, parsePhpHeaderFrame, parsePhpFrame, } from "./php-frame-parsing.js";
|
|
4
|
+
export { createPhpSourceLocationResolver } from "./php-source-location-resolver.js";
|
|
5
|
+
export { createPhpStackTraceParser } from "./php-stack-trace-parser.js";
|
|
6
|
+
export { createPhpRuntimeAdapter } from "./php-runtime-adapter.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAExE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laravel's exception-log grammar, which is **not PHP's** (RT-112).
|
|
3
|
+
*
|
|
4
|
+
* ## The measurement
|
|
5
|
+
*
|
|
6
|
+
* A real Laravel 12.67.0 application on PHP 8.4.4, spawned through the
|
|
7
|
+
* managed lifecycle, failing through its own request pipeline with
|
|
8
|
+
* `LOG_CHANNEL=stderr`:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* 64 lines in -> 0 exceptions, 53 lines in a bare STACK_TRACE, 11 logs
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The **mirror image of Rails' defect** (RT-111), which lost everything.
|
|
15
|
+
* Laravel loses only the identity — and that is arguably worse to receive,
|
|
16
|
+
* because the result looks like working evidence: fifty correct frames with
|
|
17
|
+
* no exception class and no message attached to them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why PHP's classifier half-sees it
|
|
20
|
+
*
|
|
21
|
+
* Laravel logs through Monolog, not through PHP's own
|
|
22
|
+
* `Throwable::__toString()`:
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* [2026-08-19 03:45:19] production.ERROR: Trying to access array offset on null {"exception":"[object] (ErrorException(code: 0): Trying to access array offset on null at /app/routes/web.php:9)
|
|
26
|
+
* [stacktrace]
|
|
27
|
+
* #0 /app/vendor/.../HandleExceptions.php(262): Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, '...')
|
|
28
|
+
* #1 /app/routes/web.php(9): ...
|
|
29
|
+
* #11 {main}
|
|
30
|
+
* "}
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Three differences, of which two are fatal and one is silent:
|
|
34
|
+
*
|
|
35
|
+
* 1. **The header says `at file:line)` where PHP says `in file:line`.**
|
|
36
|
+
* PHP's `HEADER_PATTERN` is `$`-anchored on ` in <file>:<line>`, so
|
|
37
|
+
* Laravel's — with the word `at` and a trailing `)` — matches nothing.
|
|
38
|
+
* 2. **The bridge is `[stacktrace]`, not `Stack trace:`.**
|
|
39
|
+
* 3. **Class names are JSON-escaped.** The trace is written inside a JSON
|
|
40
|
+
* string, so `Illuminate\Routing\Router` arrives as
|
|
41
|
+
* `Illuminate\\Routing\\Router`. The frames still match PHP's
|
|
42
|
+
* `FRAME_PATTERN` — which is why the evidence is not lost — and every
|
|
43
|
+
* class name in them is wrong by a backslash, which resolves to no graph
|
|
44
|
+
* node while looking entirely correct. RT-103's file-path corruption,
|
|
45
|
+
* one field over.
|
|
46
|
+
*
|
|
47
|
+
* ## Decoding, not rewriting
|
|
48
|
+
*
|
|
49
|
+
* `\\` is recovered to `\` because the text **is** JSON-escaped and decoding
|
|
50
|
+
* it recovers what the process meant. That is the opposite of RT-105's
|
|
51
|
+
* decision to leave an `eval` frame's synthetic path alone: there, no
|
|
52
|
+
* decoding exists and any "fix" would invent a location; here, the encoding
|
|
53
|
+
* is documented and reversible.
|
|
54
|
+
*
|
|
55
|
+
* Only the callee is decoded. POSIX file paths carry no backslashes, so
|
|
56
|
+
* decoding them would be a no-op with a Windows-shaped hazard attached.
|
|
57
|
+
*
|
|
58
|
+
* ## Laravel's default is worse than any of this, and no parser can fix it
|
|
59
|
+
*
|
|
60
|
+
* `LOG_CHANNEL` defaults to `stack` -> `single`, which writes
|
|
61
|
+
* `storage/logs/laravel.log`. **A collector watching process output sees
|
|
62
|
+
* nothing at all** — not a parse failure, no evidence. Setting
|
|
63
|
+
* `LOG_CHANNEL=stderr` is a first-class Laravel option and it is a
|
|
64
|
+
* *configuration requirement this adapter cannot satisfy on the
|
|
65
|
+
* application's behalf*. It belongs in the disclosure to the user, not in a
|
|
66
|
+
* pattern.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Recovers a JSON-escaped backslash.
|
|
70
|
+
*
|
|
71
|
+
* `Illuminate\\Routing\\Router` -> `Illuminate\Routing\Router`. Applied only
|
|
72
|
+
* to a callee: this is a decode of a known encoding, not a guess, and the
|
|
73
|
+
* file paths in the same line are not escaped.
|
|
74
|
+
*/
|
|
75
|
+
export declare function decodeLaravelCallee(callee: string): string;
|
|
76
|
+
export declare function isLaravelHeaderLine(line: string): boolean;
|
|
77
|
+
/** `null` when the line is not Laravel's `(Class(code: N): message at file:line)` shape — never fabricated. */
|
|
78
|
+
export declare function parseLaravelHeader(line: string): {
|
|
79
|
+
readonly exceptionClass: string;
|
|
80
|
+
readonly message: string;
|
|
81
|
+
readonly file: string;
|
|
82
|
+
readonly line: number;
|
|
83
|
+
} | null;
|
|
84
|
+
/**
|
|
85
|
+
* Laravel's classifier: its own header and bridge, PHP's own frames and
|
|
86
|
+
* trailer.
|
|
87
|
+
*
|
|
88
|
+
* The frame and trailer halves are imported rather than restated, because
|
|
89
|
+
* the `#N file(line): callee(args)` shape genuinely is PHP's — Monolog
|
|
90
|
+
* writes `Throwable::getTraceAsString()` verbatim. Only the framing around
|
|
91
|
+
* it is Laravel's. Restating them would be a second copy to keep in step
|
|
92
|
+
* with RT-105's closure-name fix.
|
|
93
|
+
*
|
|
94
|
+
* `isTrailer` is PHP's `#N {main}`, which closes the block and leaves
|
|
95
|
+
* Monolog's own `"}` terminator to fall through as a log line — the same
|
|
96
|
+
* choice PHP's grammar already makes for its `thrown in ...` trailer, and
|
|
97
|
+
* for the same reason: attach what identifies the exception, not everything
|
|
98
|
+
* printed after it.
|
|
99
|
+
*
|
|
100
|
+
* **`isBridge` is mutation-inert, and that is recorded rather than hidden.**
|
|
101
|
+
* Replacing `[stacktrace]` with PHP's `Stack trace:` kills no test: RT-082's
|
|
102
|
+
* held-message absorption picks the line up instead, and the block still
|
|
103
|
+
* forms. It is kept because the line genuinely *is* a bridge and absorbing
|
|
104
|
+
* it spends one of `MAX_HELD_LINES` on transport punctuation — but no test
|
|
105
|
+
* distinguishes the two, so "every guard is mutation-tested" would be the
|
|
106
|
+
* wrong summary of this file.
|
|
107
|
+
*/
|
|
108
|
+
export declare function createLaravelLineClassifier(): {
|
|
109
|
+
isHeader(line: string): boolean;
|
|
110
|
+
isFrame(line: string): boolean;
|
|
111
|
+
isBridge(line: string): boolean;
|
|
112
|
+
isTrailer(line: string): boolean;
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=laravel-frame-parsing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laravel-frame-parsing.d.ts","sourceRoot":"","sources":["../src/laravel-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAqBH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,GACX;IAAE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAapH;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,2BAA2B,IAAI;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC,CAOA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laravel's exception-log grammar, which is **not PHP's** (RT-112).
|
|
3
|
+
*
|
|
4
|
+
* ## The measurement
|
|
5
|
+
*
|
|
6
|
+
* A real Laravel 12.67.0 application on PHP 8.4.4, spawned through the
|
|
7
|
+
* managed lifecycle, failing through its own request pipeline with
|
|
8
|
+
* `LOG_CHANNEL=stderr`:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* 64 lines in -> 0 exceptions, 53 lines in a bare STACK_TRACE, 11 logs
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The **mirror image of Rails' defect** (RT-111), which lost everything.
|
|
15
|
+
* Laravel loses only the identity — and that is arguably worse to receive,
|
|
16
|
+
* because the result looks like working evidence: fifty correct frames with
|
|
17
|
+
* no exception class and no message attached to them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why PHP's classifier half-sees it
|
|
20
|
+
*
|
|
21
|
+
* Laravel logs through Monolog, not through PHP's own
|
|
22
|
+
* `Throwable::__toString()`:
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* [2026-08-19 03:45:19] production.ERROR: Trying to access array offset on null {"exception":"[object] (ErrorException(code: 0): Trying to access array offset on null at /app/routes/web.php:9)
|
|
26
|
+
* [stacktrace]
|
|
27
|
+
* #0 /app/vendor/.../HandleExceptions.php(262): Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, '...')
|
|
28
|
+
* #1 /app/routes/web.php(9): ...
|
|
29
|
+
* #11 {main}
|
|
30
|
+
* "}
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Three differences, of which two are fatal and one is silent:
|
|
34
|
+
*
|
|
35
|
+
* 1. **The header says `at file:line)` where PHP says `in file:line`.**
|
|
36
|
+
* PHP's `HEADER_PATTERN` is `$`-anchored on ` in <file>:<line>`, so
|
|
37
|
+
* Laravel's — with the word `at` and a trailing `)` — matches nothing.
|
|
38
|
+
* 2. **The bridge is `[stacktrace]`, not `Stack trace:`.**
|
|
39
|
+
* 3. **Class names are JSON-escaped.** The trace is written inside a JSON
|
|
40
|
+
* string, so `Illuminate\Routing\Router` arrives as
|
|
41
|
+
* `Illuminate\\Routing\\Router`. The frames still match PHP's
|
|
42
|
+
* `FRAME_PATTERN` — which is why the evidence is not lost — and every
|
|
43
|
+
* class name in them is wrong by a backslash, which resolves to no graph
|
|
44
|
+
* node while looking entirely correct. RT-103's file-path corruption,
|
|
45
|
+
* one field over.
|
|
46
|
+
*
|
|
47
|
+
* ## Decoding, not rewriting
|
|
48
|
+
*
|
|
49
|
+
* `\\` is recovered to `\` because the text **is** JSON-escaped and decoding
|
|
50
|
+
* it recovers what the process meant. That is the opposite of RT-105's
|
|
51
|
+
* decision to leave an `eval` frame's synthetic path alone: there, no
|
|
52
|
+
* decoding exists and any "fix" would invent a location; here, the encoding
|
|
53
|
+
* is documented and reversible.
|
|
54
|
+
*
|
|
55
|
+
* Only the callee is decoded. POSIX file paths carry no backslashes, so
|
|
56
|
+
* decoding them would be a no-op with a Windows-shaped hazard attached.
|
|
57
|
+
*
|
|
58
|
+
* ## Laravel's default is worse than any of this, and no parser can fix it
|
|
59
|
+
*
|
|
60
|
+
* `LOG_CHANNEL` defaults to `stack` -> `single`, which writes
|
|
61
|
+
* `storage/logs/laravel.log`. **A collector watching process output sees
|
|
62
|
+
* nothing at all** — not a parse failure, no evidence. Setting
|
|
63
|
+
* `LOG_CHANNEL=stderr` is a first-class Laravel option and it is a
|
|
64
|
+
* *configuration requirement this adapter cannot satisfy on the
|
|
65
|
+
* application's behalf*. It belongs in the disclosure to the user, not in a
|
|
66
|
+
* pattern.
|
|
67
|
+
*/
|
|
68
|
+
import { isPhpFrameLine, isPhpTrailerLine } from "./php-frame-parsing.js";
|
|
69
|
+
/**
|
|
70
|
+
* `(ErrorException(code: 0): Trying to access array offset on null at /app/routes/web.php:9)`
|
|
71
|
+
*
|
|
72
|
+
* Composed by `Illuminate\Log\Logger`'s exception context. The class group
|
|
73
|
+
* accepts a namespaced name (`Illuminate\Database\QueryException`) in either
|
|
74
|
+
* escaping, since the same shape appears outside a JSON string in some
|
|
75
|
+
* channels.
|
|
76
|
+
*
|
|
77
|
+
* Not anchored at the start: the Monolog line prefix (`[timestamp]
|
|
78
|
+
* env.LEVEL: message `) always precedes it, and matching from anywhere is
|
|
79
|
+
* how PHP's own header pattern already handles its SAPI prefix.
|
|
80
|
+
*/
|
|
81
|
+
const LARAVEL_HEADER = /\((?<class>[A-Za-z_\x7f-\xff][\w\\]*)\(code: -?\d+\): (?<message>.*?) at (?<file>[^\s]+):(?<line>\d+)\)/;
|
|
82
|
+
const LARAVEL_BRIDGE = "[stacktrace]";
|
|
83
|
+
/**
|
|
84
|
+
* Recovers a JSON-escaped backslash.
|
|
85
|
+
*
|
|
86
|
+
* `Illuminate\\Routing\\Router` -> `Illuminate\Routing\Router`. Applied only
|
|
87
|
+
* to a callee: this is a decode of a known encoding, not a guess, and the
|
|
88
|
+
* file paths in the same line are not escaped.
|
|
89
|
+
*/
|
|
90
|
+
export function decodeLaravelCallee(callee) {
|
|
91
|
+
return callee.replace(/\\\\/g, "\\");
|
|
92
|
+
}
|
|
93
|
+
export function isLaravelHeaderLine(line) {
|
|
94
|
+
return LARAVEL_HEADER.test(line);
|
|
95
|
+
}
|
|
96
|
+
/** `null` when the line is not Laravel's `(Class(code: N): message at file:line)` shape — never fabricated. */
|
|
97
|
+
export function parseLaravelHeader(line) {
|
|
98
|
+
const match = LARAVEL_HEADER.exec(line);
|
|
99
|
+
if (!match?.groups)
|
|
100
|
+
return null;
|
|
101
|
+
const file = match.groups["file"];
|
|
102
|
+
const lineNumber = Number(match.groups["line"]);
|
|
103
|
+
const exceptionClass = match.groups["class"];
|
|
104
|
+
if (file === undefined || exceptionClass === undefined || !Number.isInteger(lineNumber))
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
exceptionClass: decodeLaravelCallee(exceptionClass),
|
|
108
|
+
message: match.groups["message"] ?? "",
|
|
109
|
+
file,
|
|
110
|
+
line: lineNumber,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Laravel's classifier: its own header and bridge, PHP's own frames and
|
|
115
|
+
* trailer.
|
|
116
|
+
*
|
|
117
|
+
* The frame and trailer halves are imported rather than restated, because
|
|
118
|
+
* the `#N file(line): callee(args)` shape genuinely is PHP's — Monolog
|
|
119
|
+
* writes `Throwable::getTraceAsString()` verbatim. Only the framing around
|
|
120
|
+
* it is Laravel's. Restating them would be a second copy to keep in step
|
|
121
|
+
* with RT-105's closure-name fix.
|
|
122
|
+
*
|
|
123
|
+
* `isTrailer` is PHP's `#N {main}`, which closes the block and leaves
|
|
124
|
+
* Monolog's own `"}` terminator to fall through as a log line — the same
|
|
125
|
+
* choice PHP's grammar already makes for its `thrown in ...` trailer, and
|
|
126
|
+
* for the same reason: attach what identifies the exception, not everything
|
|
127
|
+
* printed after it.
|
|
128
|
+
*
|
|
129
|
+
* **`isBridge` is mutation-inert, and that is recorded rather than hidden.**
|
|
130
|
+
* Replacing `[stacktrace]` with PHP's `Stack trace:` kills no test: RT-082's
|
|
131
|
+
* held-message absorption picks the line up instead, and the block still
|
|
132
|
+
* forms. It is kept because the line genuinely *is* a bridge and absorbing
|
|
133
|
+
* it spends one of `MAX_HELD_LINES` on transport punctuation — but no test
|
|
134
|
+
* distinguishes the two, so "every guard is mutation-tested" would be the
|
|
135
|
+
* wrong summary of this file.
|
|
136
|
+
*/
|
|
137
|
+
export function createLaravelLineClassifier() {
|
|
138
|
+
return {
|
|
139
|
+
isHeader: isLaravelHeaderLine,
|
|
140
|
+
isFrame: isPhpFrameLine,
|
|
141
|
+
isBridge: (line) => line.trim() === LARAVEL_BRIDGE,
|
|
142
|
+
isTrailer: isPhpTrailerLine,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=laravel-frame-parsing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laravel-frame-parsing.js","sourceRoot":"","sources":["../src/laravel-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,MAAM,cAAc,GAClB,yGAAyG,CAAC;AAE5G,MAAM,cAAc,GAAG,cAAc,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,kBAAkB,CAChC,IAAY;IAEZ,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,SAAS,IAAI,cAAc,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrG,OAAO;QACL,cAAc,EAAE,mBAAmB,CAAC,cAAc,CAAC;QACnD,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE;QACtC,IAAI;QACJ,IAAI,EAAE,UAAU;KACjB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,2BAA2B;IAMzC,OAAO;QACL,QAAQ,EAAE,mBAAmB;QAC7B,OAAO,EAAE,cAAc;QACvB,QAAQ,EAAE,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,cAAc;QACnE,SAAS,EAAE,gBAAgB;KAC5B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes Laravel's header with PHP's own frames (RT-112).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a second parser and not a branch in the PHP one
|
|
5
|
+
*
|
|
6
|
+
* Same reason RT-111 gave for Rails: the two grammars disagree about the
|
|
7
|
+
* shape of the thing `primaryFrameIndex` indexes. They happen to agree on
|
|
8
|
+
* the *answer* — Laravel's header carries `at file:line`, the exception's
|
|
9
|
+
* construction site, exactly as PHP's `in file:line` does, so index 0 is the
|
|
10
|
+
* header-derived frame in both — and agreeing by coincidence is precisely
|
|
11
|
+
* what RT-083 was about. Two parsers each state one answer.
|
|
12
|
+
*
|
|
13
|
+
* The frames themselves are PHP's, parsed by PHP's parser, because Monolog
|
|
14
|
+
* writes `Throwable::getTraceAsString()` verbatim. What differs is the
|
|
15
|
+
* framing and the JSON escaping of class names, and both are handled here.
|
|
16
|
+
*/
|
|
17
|
+
import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
|
|
18
|
+
export declare function createLaravelStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
|
|
19
|
+
//# sourceMappingURL=laravel-stack-trace-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laravel-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/laravel-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAKrG,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAqChG"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes Laravel's header with PHP's own frames (RT-112).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a second parser and not a branch in the PHP one
|
|
5
|
+
*
|
|
6
|
+
* Same reason RT-111 gave for Rails: the two grammars disagree about the
|
|
7
|
+
* shape of the thing `primaryFrameIndex` indexes. They happen to agree on
|
|
8
|
+
* the *answer* — Laravel's header carries `at file:line`, the exception's
|
|
9
|
+
* construction site, exactly as PHP's `in file:line` does, so index 0 is the
|
|
10
|
+
* header-derived frame in both — and agreeing by coincidence is precisely
|
|
11
|
+
* what RT-083 was about. Two parsers each state one answer.
|
|
12
|
+
*
|
|
13
|
+
* The frames themselves are PHP's, parsed by PHP's parser, because Monolog
|
|
14
|
+
* writes `Throwable::getTraceAsString()` verbatim. What differs is the
|
|
15
|
+
* framing and the JSON escaping of class names, and both are handled here.
|
|
16
|
+
*/
|
|
17
|
+
import { decodeLaravelCallee, parseLaravelHeader } from "./laravel-frame-parsing.js";
|
|
18
|
+
import { parsePhpFrame } from "./php-frame-parsing.js";
|
|
19
|
+
export function createLaravelStackTraceParser(resolver) {
|
|
20
|
+
return {
|
|
21
|
+
async parse(rawText) {
|
|
22
|
+
const lines = rawText.split(/\r\n|\r|\n/);
|
|
23
|
+
const frames = [];
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const header = parseLaravelHeader(line);
|
|
26
|
+
if (header !== null) {
|
|
27
|
+
// The construction site. `functionName` is null because Laravel,
|
|
28
|
+
// like PHP itself, names no function for it — not because it could
|
|
29
|
+
// not be parsed.
|
|
30
|
+
const location = await resolver.resolve({ file: header.file, line: header.line, column: null, functionName: null });
|
|
31
|
+
frames.push({ location, raw: line });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const raw = parsePhpFrame(line);
|
|
35
|
+
if (raw === null)
|
|
36
|
+
continue;
|
|
37
|
+
const location = await resolver.resolve({
|
|
38
|
+
file: raw.file,
|
|
39
|
+
line: raw.line,
|
|
40
|
+
column: null,
|
|
41
|
+
// Decoded, not rewritten: the trace lives inside a JSON string, so
|
|
42
|
+
// `Illuminate\\Routing\\Router` IS `Illuminate\Routing\Router` and
|
|
43
|
+
// recovering it is reading rather than guessing. Left encoded it
|
|
44
|
+
// resolves to no graph node while looking correct.
|
|
45
|
+
functionName: raw.functionName === null ? null : decodeLaravelCallee(raw.functionName),
|
|
46
|
+
});
|
|
47
|
+
frames.push({ location, raw: line });
|
|
48
|
+
}
|
|
49
|
+
if (frames.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=laravel-stack-trace-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laravel-stack-trace-parser.js","sourceRoot":"","sources":["../src/laravel-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,MAAM,UAAU,6BAA6B,CAAC,QAAgC;IAC5E,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAe;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC1C,MAAM,MAAM,GAAiB,EAAE,CAAC;YAEhC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;oBACpB,iEAAiE;oBACjE,mEAAmE;oBACnE,iBAAiB;oBACjB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;oBACpH,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;oBACrC,SAAS;gBACX,CAAC;gBAED,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,GAAG,KAAK,IAAI;oBAAE,SAAS;gBAE3B,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC;oBACtC,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,MAAM,EAAE,IAAI;oBACZ,mEAAmE;oBACnE,mEAAmE;oBACnE,iEAAiE;oBACjE,mDAAmD;oBACnD,YAAY,EAAE,GAAG,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,YAAY,CAAC;iBACvF,CAAC,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,CAAC;YAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACrC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP uncaught-`Throwable` trace line shapes — the `LineClassifier` for
|
|
3
|
+
* `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
|
|
4
|
+
* feeds `SourceLocationResolver`.
|
|
5
|
+
*
|
|
6
|
+
* Grammar verified against `php-src`'s own `Zend/zend_exceptions.c` — the C
|
|
7
|
+
* source that generates this text (`build_trace_string`,
|
|
8
|
+
* `zend_trace_to_string`, and the default `Throwable::__toString`
|
|
9
|
+
* composition around line 780) — and against `php-src`'s own `.phpt`
|
|
10
|
+
* conformance suite, the same category of interpreter-verified source
|
|
11
|
+
* `ruby/spec` was for RT-038. See `test/fixtures/PROVENANCE.md` for the
|
|
12
|
+
* specific citations.
|
|
13
|
+
*
|
|
14
|
+
* **A real `php` runs against this now** — `php-call-shapes.test.ts` spawns
|
|
15
|
+
* 8.4.4 and asserts against its stdout. That upgrade is what found RT-105:
|
|
16
|
+
* the engine-source reading above was correct about the grammar it read and
|
|
17
|
+
* was reading the *older* grammar, and PHP 8.4 had since put parentheses
|
|
18
|
+
* inside a function name. A format read from the generator is only as
|
|
19
|
+
* current as the generator you read.
|
|
20
|
+
*
|
|
21
|
+
* PHP's shape is its own, distinct from V8, CPython and Ruby alike:
|
|
22
|
+
*
|
|
23
|
+
* 1. **The header carries a real location, but it is a separate line from
|
|
24
|
+
* the first frame, with `Stack trace:` sitting between them.**
|
|
25
|
+
* `Fatal error: Uncaught RuntimeException: invoice 3 is corrupt in
|
|
26
|
+
* /app/InvoiceService.php:15` names the exception AND its construction
|
|
27
|
+
* site (`Throwable::getFile()`/`getLine()` — the near-universal `throw
|
|
28
|
+
* new X(...)` pattern makes this the throw site, though strictly it is
|
|
29
|
+
* where the exception object was `new`-ed, which can differ if a caller
|
|
30
|
+
* holds a `Throwable` across lines before throwing it). That makes it a
|
|
31
|
+
* genuine, un-fabricated frame — same reasoning as RT-037's header-only
|
|
32
|
+
* Rust panic, `functionName: null` because PHP names no function for
|
|
33
|
+
* this specific location — but the literal `Stack trace:` line always
|
|
34
|
+
* sits between the header and the first real `#N` frame.
|
|
35
|
+
* `createPhpLineClassifier` reports this via `LineClassifier.isBridge`
|
|
36
|
+
* (RT-048): a lone `Stack trace:` line is never even offered to
|
|
37
|
+
* `isBridge` — `ExceptionBlockDetector` only consults it while a header
|
|
38
|
+
* is already being held — so an application log line that happens to
|
|
39
|
+
* print the same phrase for an unrelated reason cannot open anything on
|
|
40
|
+
* its own, and this classifier no longer needs to track that itself.
|
|
41
|
+
* 2. **A real frame carries a call-type marker PHP itself distinguishes:**
|
|
42
|
+
* `Class->method()` for an instance call, `Class::method()` for a
|
|
43
|
+
* static one (verified verbatim against `Zend/tests/bug65911.phpt`'s
|
|
44
|
+
* `B->go()` and `Zend/tests/inheritance/bug44141.phpt`'s `Y::cheat(5)`).
|
|
45
|
+
* Both are kept exactly as printed, never normalised to one form.
|
|
46
|
+
* 3. **A frame can have no location at all.** `#N [internal function]:
|
|
47
|
+
* Class::method(args)` — a call made through `array_map`,
|
|
48
|
+
* `call_user_func` or similar, where PHP genuinely has no file/line to
|
|
49
|
+
* report — confirmed against `Zend/tests/bug74164.phpt`. This is exactly
|
|
50
|
+
* the case RT-037 widened `RawFrameLocation.file`/`.line` to `| null`
|
|
51
|
+
* for; both are `null` here rather than a fabricated location.
|
|
52
|
+
* 4. **`#N {main}` ends the trace and is a trailer, not a frame** — it has
|
|
53
|
+
* no trailing `(args)`, so `FRAME_PATTERN` never matches it, and
|
|
54
|
+
* `isTrailer` closes the block on it. What follows (` thrown in FILE
|
|
55
|
+
* on line N`, present on the CLI/log-formatted output) is real but
|
|
56
|
+
* entirely redundant with the header's own file:line, so it is left to
|
|
57
|
+
* fall through as a plain log line rather than special-cased into the
|
|
58
|
+
* block — same choice CPython's `isTrailer` design already established
|
|
59
|
+
* for "attach only what identifies the exception, not everything after
|
|
60
|
+
* it."
|
|
61
|
+
* 5. **Chained exceptions (`Throwable::getPrevious()`) need no
|
|
62
|
+
* connective-exclusion logic**, unlike CPython's. `#N {main}` closes the
|
|
63
|
+
* first block cleanly; the next line is a blank line (an honest, empty
|
|
64
|
+
* log event) and then `Next Class: message in file:line` — which
|
|
65
|
+
* `HEADER_PATTERN`'s lack of start-of-line anchoring matches without
|
|
66
|
+
* the literal word `Next` needing any special handling at all, the same
|
|
67
|
+
* way V8's and Ruby's non-anchored header patterns handle a prefix they
|
|
68
|
+
* were never written to expect. Verified against
|
|
69
|
+
* `Zend/tests/throwable_001.phpt`, including the detail that only the
|
|
70
|
+
* **outermost** exception's `thrown in ... on line N` trailer appears —
|
|
71
|
+
* once, at the very end, not once per link.
|
|
72
|
+
*/
|
|
73
|
+
export interface RawPhpFrame {
|
|
74
|
+
/** `Class->method`, `Class::method`, a bare function name, or `{closure:file:line}` — kept verbatim, never rewritten. */
|
|
75
|
+
readonly functionName: string | null;
|
|
76
|
+
/** Null for an `[internal function]` frame — no file was ever printed, never fabricated. */
|
|
77
|
+
readonly file: string | null;
|
|
78
|
+
/** Null exactly when `file` is — the two are only ever known or unknown together in this grammar. */
|
|
79
|
+
readonly line: number | null;
|
|
80
|
+
}
|
|
81
|
+
export declare function isPhpHeaderLine(line: string): boolean;
|
|
82
|
+
export declare function isPhpFrameLine(line: string): boolean;
|
|
83
|
+
export declare function isPhpTrailerLine(line: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Stateless (RT-048): the single `Stack trace:` line that separates a
|
|
86
|
+
* header from its first real frame is reported via `isBridge`, which
|
|
87
|
+
* `ExceptionBlockDetector` only ever consults while a header is already
|
|
88
|
+
* being held — never from idle. A `Stack trace:` line with no preceding
|
|
89
|
+
* held header therefore can't reach this method at all, so recognising it
|
|
90
|
+
* unconditionally here is exactly as safe as PHP's previous hand-gated
|
|
91
|
+
* boolean, without the classifier having to track "did I just hold a
|
|
92
|
+
* header" itself.
|
|
93
|
+
*
|
|
94
|
+
* Before RT-048, this correctness property depended on a boolean this
|
|
95
|
+
* classifier tracked and reset itself (`awaitingStackTraceMarker`); it is
|
|
96
|
+
* now a structural property of `ExceptionBlockDetector`'s own state
|
|
97
|
+
* machine instead — see `LineClassifier.isBridge`'s doc comment.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createPhpLineClassifier(): {
|
|
100
|
+
isHeader(line: string): boolean;
|
|
101
|
+
isFrame(line: string): boolean;
|
|
102
|
+
isBridge(line: string): boolean;
|
|
103
|
+
isTrailer(line: string): boolean;
|
|
104
|
+
};
|
|
105
|
+
/** `null` when the line doesn't match PHP's `Class[: message] in file:line` header shape — never fabricated. */
|
|
106
|
+
export declare function parsePhpHeaderFrame(line: string): RawPhpFrame | null;
|
|
107
|
+
/** `null` for anything not shaped like a `#N (file(line)|[internal function]): callee(...)` frame — never fabricated. */
|
|
108
|
+
export declare function parsePhpFrame(line: string): RawPhpFrame | null;
|
|
109
|
+
//# sourceMappingURL=php-frame-parsing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-frame-parsing.d.ts","sourceRoot":"","sources":["../src/php-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,MAAM,WAAW,WAAW;IAC1B,yHAAyH;IACzH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qGAAqG;IACrG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AA+DD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,IAAI;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC,CAkBA;AAED,gHAAgH;AAChH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAQpE;AAED,yHAAyH;AACzH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAe9D"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP uncaught-`Throwable` trace line shapes — the `LineClassifier` for
|
|
3
|
+
* `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
|
|
4
|
+
* feeds `SourceLocationResolver`.
|
|
5
|
+
*
|
|
6
|
+
* Grammar verified against `php-src`'s own `Zend/zend_exceptions.c` — the C
|
|
7
|
+
* source that generates this text (`build_trace_string`,
|
|
8
|
+
* `zend_trace_to_string`, and the default `Throwable::__toString`
|
|
9
|
+
* composition around line 780) — and against `php-src`'s own `.phpt`
|
|
10
|
+
* conformance suite, the same category of interpreter-verified source
|
|
11
|
+
* `ruby/spec` was for RT-038. See `test/fixtures/PROVENANCE.md` for the
|
|
12
|
+
* specific citations.
|
|
13
|
+
*
|
|
14
|
+
* **A real `php` runs against this now** — `php-call-shapes.test.ts` spawns
|
|
15
|
+
* 8.4.4 and asserts against its stdout. That upgrade is what found RT-105:
|
|
16
|
+
* the engine-source reading above was correct about the grammar it read and
|
|
17
|
+
* was reading the *older* grammar, and PHP 8.4 had since put parentheses
|
|
18
|
+
* inside a function name. A format read from the generator is only as
|
|
19
|
+
* current as the generator you read.
|
|
20
|
+
*
|
|
21
|
+
* PHP's shape is its own, distinct from V8, CPython and Ruby alike:
|
|
22
|
+
*
|
|
23
|
+
* 1. **The header carries a real location, but it is a separate line from
|
|
24
|
+
* the first frame, with `Stack trace:` sitting between them.**
|
|
25
|
+
* `Fatal error: Uncaught RuntimeException: invoice 3 is corrupt in
|
|
26
|
+
* /app/InvoiceService.php:15` names the exception AND its construction
|
|
27
|
+
* site (`Throwable::getFile()`/`getLine()` — the near-universal `throw
|
|
28
|
+
* new X(...)` pattern makes this the throw site, though strictly it is
|
|
29
|
+
* where the exception object was `new`-ed, which can differ if a caller
|
|
30
|
+
* holds a `Throwable` across lines before throwing it). That makes it a
|
|
31
|
+
* genuine, un-fabricated frame — same reasoning as RT-037's header-only
|
|
32
|
+
* Rust panic, `functionName: null` because PHP names no function for
|
|
33
|
+
* this specific location — but the literal `Stack trace:` line always
|
|
34
|
+
* sits between the header and the first real `#N` frame.
|
|
35
|
+
* `createPhpLineClassifier` reports this via `LineClassifier.isBridge`
|
|
36
|
+
* (RT-048): a lone `Stack trace:` line is never even offered to
|
|
37
|
+
* `isBridge` — `ExceptionBlockDetector` only consults it while a header
|
|
38
|
+
* is already being held — so an application log line that happens to
|
|
39
|
+
* print the same phrase for an unrelated reason cannot open anything on
|
|
40
|
+
* its own, and this classifier no longer needs to track that itself.
|
|
41
|
+
* 2. **A real frame carries a call-type marker PHP itself distinguishes:**
|
|
42
|
+
* `Class->method()` for an instance call, `Class::method()` for a
|
|
43
|
+
* static one (verified verbatim against `Zend/tests/bug65911.phpt`'s
|
|
44
|
+
* `B->go()` and `Zend/tests/inheritance/bug44141.phpt`'s `Y::cheat(5)`).
|
|
45
|
+
* Both are kept exactly as printed, never normalised to one form.
|
|
46
|
+
* 3. **A frame can have no location at all.** `#N [internal function]:
|
|
47
|
+
* Class::method(args)` — a call made through `array_map`,
|
|
48
|
+
* `call_user_func` or similar, where PHP genuinely has no file/line to
|
|
49
|
+
* report — confirmed against `Zend/tests/bug74164.phpt`. This is exactly
|
|
50
|
+
* the case RT-037 widened `RawFrameLocation.file`/`.line` to `| null`
|
|
51
|
+
* for; both are `null` here rather than a fabricated location.
|
|
52
|
+
* 4. **`#N {main}` ends the trace and is a trailer, not a frame** — it has
|
|
53
|
+
* no trailing `(args)`, so `FRAME_PATTERN` never matches it, and
|
|
54
|
+
* `isTrailer` closes the block on it. What follows (` thrown in FILE
|
|
55
|
+
* on line N`, present on the CLI/log-formatted output) is real but
|
|
56
|
+
* entirely redundant with the header's own file:line, so it is left to
|
|
57
|
+
* fall through as a plain log line rather than special-cased into the
|
|
58
|
+
* block — same choice CPython's `isTrailer` design already established
|
|
59
|
+
* for "attach only what identifies the exception, not everything after
|
|
60
|
+
* it."
|
|
61
|
+
* 5. **Chained exceptions (`Throwable::getPrevious()`) need no
|
|
62
|
+
* connective-exclusion logic**, unlike CPython's. `#N {main}` closes the
|
|
63
|
+
* first block cleanly; the next line is a blank line (an honest, empty
|
|
64
|
+
* log event) and then `Next Class: message in file:line` — which
|
|
65
|
+
* `HEADER_PATTERN`'s lack of start-of-line anchoring matches without
|
|
66
|
+
* the literal word `Next` needing any special handling at all, the same
|
|
67
|
+
* way V8's and Ruby's non-anchored header patterns handle a prefix they
|
|
68
|
+
* were never written to expect. Verified against
|
|
69
|
+
* `Zend/tests/throwable_001.phpt`, including the detail that only the
|
|
70
|
+
* **outermost** exception's `thrown in ... on line N` trailer appears —
|
|
71
|
+
* once, at the very end, not once per link.
|
|
72
|
+
*/
|
|
73
|
+
const STACK_TRACE_MARKER = "Stack trace:";
|
|
74
|
+
/**
|
|
75
|
+
* Not anchored to the start of the line, for the same reason V8's and
|
|
76
|
+
* Ruby's header patterns aren't: PHP's own "Fatal error: " / "PHP Fatal
|
|
77
|
+
* error: " SAPI prefix, an application logger's own prefix, and the
|
|
78
|
+
* chained-exception "Next " marker are all real prefixes this must see
|
|
79
|
+
* through. The greedy `message` group combined with the `$`-anchored
|
|
80
|
+
* `in file:line` suffix correctly resolves to the LAST such suffix on the
|
|
81
|
+
* line even when the message text itself contains the substring " in ".
|
|
82
|
+
*/
|
|
83
|
+
const HEADER_PATTERN = /(?<class>[A-Za-z_\x7f-\xff][\w\\]*)(?::\s(?<message>.*))?\s+in\s+(?<file>.+):(?<line>\d+)$/;
|
|
84
|
+
/**
|
|
85
|
+
* `#N file(line): [Class-><or::>]function(args)` or
|
|
86
|
+
* `#N [internal function]: [Class-><or::>]function(args)`. Requiring a
|
|
87
|
+
* trailing `(...)` is what excludes `#N {main}` from this pattern, so no
|
|
88
|
+
* separate exclusion is needed. Args can themselves contain parens
|
|
89
|
+
* (`Object(Closure)`, a nested call's own printed form) and are not
|
|
90
|
+
* otherwise parsed; they stay in `raw` for audit.
|
|
91
|
+
*
|
|
92
|
+
* ## Why the callee is not simply `[^(]+` (RT-105)
|
|
93
|
+
*
|
|
94
|
+
* It was, and **PHP 8.4 put parentheses inside a function name.** The
|
|
95
|
+
* "Closures in stack traces" change (php-src #12178, shipped 2024-11-21)
|
|
96
|
+
* renamed a closure declared inside a method from `{closure}` to
|
|
97
|
+
* `{closure:Class::method():LINE}`. Measured against real `php 8.4.4`:
|
|
98
|
+
*
|
|
99
|
+
* ```
|
|
100
|
+
* #0 /app/Repo.php(2): Repo->{closure:Repo::load():5}()
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* `[^(]+` stops at the `(` in `load()`, so the callee parsed as
|
|
104
|
+
* `Repo->{closure:Repo::load` — the line still matched `isFrame`, still
|
|
105
|
+
* carried the right file and line, and reported a **truncated, malformed
|
|
106
|
+
* function name with full confidence.** Not lost evidence: wrong evidence,
|
|
107
|
+
* which is the class this adapter's cross-language controls exist to ban.
|
|
108
|
+
* PHP < 8.4 printed a bare `{closure}` and could not reach it.
|
|
109
|
+
*
|
|
110
|
+
* The fix is a grammar, not a looser pattern. A `{closure:...}` block is
|
|
111
|
+
* the one place a *name* may legitimately contain a paren, so it is matched
|
|
112
|
+
* whole and everything else stays `[^(]`. It must come first in the
|
|
113
|
+
* alternation: `[^(]` would otherwise consume the `{` and leave the engine
|
|
114
|
+
* mid-token when it reached the inner paren.
|
|
115
|
+
*
|
|
116
|
+
* Greedy `.+` was the obvious alternative and is wrong — measured, not
|
|
117
|
+
* assumed: on `handler(Object(Closure))` it backtracks to the *last* `(`
|
|
118
|
+
* and yields the callee `handler(Object`, breaking the nested-args case
|
|
119
|
+
* `[^(]+` was written for in the first place.
|
|
120
|
+
*
|
|
121
|
+
* **Not changed, because it was measured and holds:** the greedy `file`
|
|
122
|
+
* group looks like it should mis-split on a string argument containing
|
|
123
|
+
* `(N): ` — `audit('order(12): x')` — and does not. The engine tries that
|
|
124
|
+
* split first, fails to satisfy the callee-plus-args tail, and backtracks
|
|
125
|
+
* to the real one. Recorded because the next reader will suspect it too.
|
|
126
|
+
*/
|
|
127
|
+
const FRAME_PATTERN = /^#\d+\s+(?:(?<file>.+)\((?<line>\d+)\):\s|\[internal function\]:\s)(?<callee>(?:\{closure:[^}]*\}|[^(])+)\(.*\)\s*$/;
|
|
128
|
+
const TRAILER_PATTERN = /^#\d+\s+\{main\}\s*$/;
|
|
129
|
+
export function isPhpHeaderLine(line) {
|
|
130
|
+
return HEADER_PATTERN.test(line);
|
|
131
|
+
}
|
|
132
|
+
export function isPhpFrameLine(line) {
|
|
133
|
+
return FRAME_PATTERN.test(line);
|
|
134
|
+
}
|
|
135
|
+
export function isPhpTrailerLine(line) {
|
|
136
|
+
return TRAILER_PATTERN.test(line);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Stateless (RT-048): the single `Stack trace:` line that separates a
|
|
140
|
+
* header from its first real frame is reported via `isBridge`, which
|
|
141
|
+
* `ExceptionBlockDetector` only ever consults while a header is already
|
|
142
|
+
* being held — never from idle. A `Stack trace:` line with no preceding
|
|
143
|
+
* held header therefore can't reach this method at all, so recognising it
|
|
144
|
+
* unconditionally here is exactly as safe as PHP's previous hand-gated
|
|
145
|
+
* boolean, without the classifier having to track "did I just hold a
|
|
146
|
+
* header" itself.
|
|
147
|
+
*
|
|
148
|
+
* Before RT-048, this correctness property depended on a boolean this
|
|
149
|
+
* classifier tracked and reset itself (`awaitingStackTraceMarker`); it is
|
|
150
|
+
* now a structural property of `ExceptionBlockDetector`'s own state
|
|
151
|
+
* machine instead — see `LineClassifier.isBridge`'s doc comment.
|
|
152
|
+
*/
|
|
153
|
+
export function createPhpLineClassifier() {
|
|
154
|
+
return {
|
|
155
|
+
isHeader(line) {
|
|
156
|
+
return isPhpHeaderLine(line);
|
|
157
|
+
},
|
|
158
|
+
isFrame(line) {
|
|
159
|
+
return isPhpFrameLine(line);
|
|
160
|
+
},
|
|
161
|
+
isBridge(line) {
|
|
162
|
+
return line.trim() === STACK_TRACE_MARKER;
|
|
163
|
+
},
|
|
164
|
+
isTrailer(line) {
|
|
165
|
+
return isPhpTrailerLine(line);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** `null` when the line doesn't match PHP's `Class[: message] in file:line` header shape — never fabricated. */
|
|
170
|
+
export function parsePhpHeaderFrame(line) {
|
|
171
|
+
const match = HEADER_PATTERN.exec(line);
|
|
172
|
+
if (!match?.groups)
|
|
173
|
+
return null;
|
|
174
|
+
const file = match.groups["file"];
|
|
175
|
+
const lineNumber = Number(match.groups["line"]);
|
|
176
|
+
if (file === undefined || !Number.isInteger(lineNumber))
|
|
177
|
+
return null;
|
|
178
|
+
// PHP names no function for the exception's own construction site.
|
|
179
|
+
return { functionName: null, file, line: lineNumber };
|
|
180
|
+
}
|
|
181
|
+
/** `null` for anything not shaped like a `#N (file(line)|[internal function]): callee(...)` frame — never fabricated. */
|
|
182
|
+
export function parsePhpFrame(line) {
|
|
183
|
+
const match = FRAME_PATTERN.exec(line);
|
|
184
|
+
if (!match?.groups)
|
|
185
|
+
return null;
|
|
186
|
+
const callee = match.groups["callee"];
|
|
187
|
+
if (callee === undefined)
|
|
188
|
+
return null;
|
|
189
|
+
const functionName = callee.trim();
|
|
190
|
+
const file = match.groups["file"];
|
|
191
|
+
if (file === undefined) {
|
|
192
|
+
// The `[internal function]:` branch matched — no location was ever printed.
|
|
193
|
+
return { functionName, file: null, line: null };
|
|
194
|
+
}
|
|
195
|
+
const lineNumber = Number(match.groups["line"]);
|
|
196
|
+
if (!Number.isInteger(lineNumber))
|
|
197
|
+
return null;
|
|
198
|
+
return { functionName, file, line: lineNumber };
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=php-frame-parsing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-frame-parsing.js","sourceRoot":"","sources":["../src/php-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAWH,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,4FAA4F,CAAC;AAEpH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,aAAa,GACjB,qHAAqH,CAAC;AAExH,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAE/C,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB;IAMrC,OAAO;QACL,QAAQ,CAAC,IAAY;YACnB,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QAED,OAAO,CAAC,IAAY;YAClB,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,QAAQ,CAAC,IAAY;YACnB,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,kBAAkB,CAAC;QAC5C,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,mEAAmE;IACnE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AACxD,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAEnC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,4EAA4E;QAC5E,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAClD,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The concrete `RuntimeAdapter` for PHP.
|
|
3
|
+
*
|
|
4
|
+
* PHP's grammar needed no seam changes beyond RT-037's already-nullable
|
|
5
|
+
* `SourceLocation`/`RawFrameLocation` fields, which an `[internal function]`
|
|
6
|
+
* frame needs but did not itself force — that widening already existed by
|
|
7
|
+
* the time this adapter was built. See `php-frame-parsing.ts` for the one
|
|
8
|
+
* behaviour this language DID need from the classifier: recognising the
|
|
9
|
+
* single `Stack trace:` line that separates PHP's header from its first
|
|
10
|
+
* real frame, and only the one that follows a header just held.
|
|
11
|
+
*/
|
|
12
|
+
import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
|
|
13
|
+
export interface PhpRuntimeAdapterOptions {
|
|
14
|
+
/** Attributed on every emitted Evidence's `service` field (plan §16.7). Omit when not applicable. */
|
|
15
|
+
readonly service?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The framework whose exception logging this process uses (§16, RT-112).
|
|
18
|
+
*
|
|
19
|
+
* **Declared, never sniffed**, the same rule ASP.NET's and Rails' options
|
|
20
|
+
* follow. Laravel logs through Monolog rather than through PHP's own
|
|
21
|
+
* `Throwable::__toString()`: the header reads `(Class(code: 0): message at
|
|
22
|
+
* file:line)` where PHP's reads `... in file:line`, the bridge is
|
|
23
|
+
* `[stacktrace]` rather than `Stack trace:`, and class names arrive
|
|
24
|
+
* JSON-escaped. Measured live: **0 exceptions, and a bare 53-line
|
|
25
|
+
* STACK_TRACE with no identity attached to it.**
|
|
26
|
+
*
|
|
27
|
+
* **This option cannot fix Laravel's default log destination**, which is a
|
|
28
|
+
* file (`storage/logs/laravel.log`). A collector watching process output
|
|
29
|
+
* sees nothing at all unless the application sets `LOG_CHANNEL=stderr`.
|
|
30
|
+
* That is a disclosure to the user, not something an adapter can decide.
|
|
31
|
+
*/
|
|
32
|
+
readonly framework?: "laravel";
|
|
33
|
+
}
|
|
34
|
+
export declare function createPhpRuntimeAdapter(options?: PhpRuntimeAdapterOptions): RuntimeAdapter;
|
|
35
|
+
//# sourceMappingURL=php-runtime-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/php-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sCAAsC,CAAC;AAUhG,MAAM,WAAW,wBAAwB;IACvC,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,wBAA6B,GAAG,cAAc,CA2B9F"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The concrete `RuntimeAdapter` for PHP.
|
|
3
|
+
*
|
|
4
|
+
* PHP's grammar needed no seam changes beyond RT-037's already-nullable
|
|
5
|
+
* `SourceLocation`/`RawFrameLocation` fields, which an `[internal function]`
|
|
6
|
+
* frame needs but did not itself force — that widening already existed by
|
|
7
|
+
* the time this adapter was built. See `php-frame-parsing.ts` for the one
|
|
8
|
+
* behaviour this language DID need from the classifier: recognising the
|
|
9
|
+
* single `Stack trace:` line that separates PHP's header from its first
|
|
10
|
+
* real frame, and only the one that follows a header just held.
|
|
11
|
+
*/
|
|
12
|
+
import { createLogCollector } from "@descryy/runtime-backend-observation";
|
|
13
|
+
import { createLaravelLineClassifier } from "./laravel-frame-parsing.js";
|
|
14
|
+
import { createLaravelStackTraceParser } from "./laravel-stack-trace-parser.js";
|
|
15
|
+
import { createPhpLineClassifier } from "./php-frame-parsing.js";
|
|
16
|
+
import { createPhpSourceLocationResolver } from "./php-source-location-resolver.js";
|
|
17
|
+
import { createPhpStackTraceParser } from "./php-stack-trace-parser.js";
|
|
18
|
+
export function createPhpRuntimeAdapter(options = {}) {
|
|
19
|
+
const sourceLocationResolver = createPhpSourceLocationResolver();
|
|
20
|
+
const isLaravel = options.framework === "laravel";
|
|
21
|
+
const stackTraceParser = isLaravel
|
|
22
|
+
? createLaravelStackTraceParser(sourceLocationResolver)
|
|
23
|
+
: createPhpStackTraceParser(sourceLocationResolver);
|
|
24
|
+
function createBackendCollectors(sources) {
|
|
25
|
+
return sources.map((source) => createLogCollector({
|
|
26
|
+
source,
|
|
27
|
+
// One classifier per collector, never shared: it carries the
|
|
28
|
+
// "just saw a header, expecting Stack trace: next" bit, and two
|
|
29
|
+
// processes' output must not prime each other.
|
|
30
|
+
classifier: isLaravel ? createLaravelLineClassifier() : createPhpLineClassifier(),
|
|
31
|
+
stackTraceParser,
|
|
32
|
+
...(options.service !== undefined ? { service: options.service } : {}),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
language: "php",
|
|
37
|
+
stackTraceParser,
|
|
38
|
+
sourceLocationResolver,
|
|
39
|
+
createBackendCollectors,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=php-runtime-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-runtime-adapter.js","sourceRoot":"","sources":["../src/php-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAwBxE,MAAM,UAAU,uBAAuB,CAAC,UAAoC,EAAE;IAC5E,MAAM,sBAAsB,GAAG,+BAA+B,EAAE,CAAC;IACjE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;IAClD,MAAM,gBAAgB,GAAG,SAAS;QAChC,CAAC,CAAC,6BAA6B,CAAC,sBAAsB,CAAC;QACvD,CAAC,CAAC,yBAAyB,CAAC,sBAAsB,CAAC,CAAC;IAEtD,SAAS,uBAAuB,CAAC,OAAuC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5B,kBAAkB,CAAC;YACjB,MAAM;YACN,6DAA6D;YAC7D,gEAAgE;YAChE,+CAA+C;YAC/C,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC,uBAAuB,EAAE;YACjF,gBAAgB;YAChB,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC,CACH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,gBAAgB;QAChB,sBAAsB;QACtB,uBAAuB;KACxB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP `SourceLocationResolver`.
|
|
3
|
+
*
|
|
4
|
+
* **Every location is `self-contained`, and that is a positive claim rather
|
|
5
|
+
* than a fallback** — the same category RT-003 already assigns PHP traces
|
|
6
|
+
* alongside Python tracebacks and Ruby backtraces. A real frame names the
|
|
7
|
+
* `.php` file the engine was actually executing; there is no compilation
|
|
8
|
+
* step and no separate map that can drift or go missing. The frame IS the
|
|
9
|
+
* answer, when the engine printed one at all.
|
|
10
|
+
*
|
|
11
|
+
* **A frame can have no location.** `#N [internal function]: ...` means
|
|
12
|
+
* PHP genuinely captured no file or line for that call — not that this
|
|
13
|
+
* resolver failed to look. `reliability` stays `self-contained` even then:
|
|
14
|
+
* this is not a side artifact that went missing (there was never a PDB or
|
|
15
|
+
* source map to lose), it is the engine's own honest report that it has
|
|
16
|
+
* nothing to offer for that frame. `file`/`line` pass through as the `null`
|
|
17
|
+
* they arrived as, per RT-037's widening of `SourceLocation` for exactly
|
|
18
|
+
* this shape (a stripped JVM frame first forced it; a PHP internal-function
|
|
19
|
+
* frame is the same shape for a different reason).
|
|
20
|
+
*
|
|
21
|
+
* **What this deliberately does not do:** check that the file exists on
|
|
22
|
+
* disk, or that its contents still match what ran. `self-contained` is a
|
|
23
|
+
* statement about the mechanism, not about whether the working tree has
|
|
24
|
+
* since been edited.
|
|
25
|
+
*
|
|
26
|
+
* `column` is always null. PHP traces never print one.
|
|
27
|
+
*/
|
|
28
|
+
import type { SourceLocationResolver } from "@descryy/runtime-backend-observation";
|
|
29
|
+
export declare function createPhpSourceLocationResolver(): SourceLocationResolver;
|
|
30
|
+
//# sourceMappingURL=php-source-location-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/php-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAErG,wBAAgB,+BAA+B,IAAI,sBAAsB,CAgBxE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP `SourceLocationResolver`.
|
|
3
|
+
*
|
|
4
|
+
* **Every location is `self-contained`, and that is a positive claim rather
|
|
5
|
+
* than a fallback** — the same category RT-003 already assigns PHP traces
|
|
6
|
+
* alongside Python tracebacks and Ruby backtraces. A real frame names the
|
|
7
|
+
* `.php` file the engine was actually executing; there is no compilation
|
|
8
|
+
* step and no separate map that can drift or go missing. The frame IS the
|
|
9
|
+
* answer, when the engine printed one at all.
|
|
10
|
+
*
|
|
11
|
+
* **A frame can have no location.** `#N [internal function]: ...` means
|
|
12
|
+
* PHP genuinely captured no file or line for that call — not that this
|
|
13
|
+
* resolver failed to look. `reliability` stays `self-contained` even then:
|
|
14
|
+
* this is not a side artifact that went missing (there was never a PDB or
|
|
15
|
+
* source map to lose), it is the engine's own honest report that it has
|
|
16
|
+
* nothing to offer for that frame. `file`/`line` pass through as the `null`
|
|
17
|
+
* they arrived as, per RT-037's widening of `SourceLocation` for exactly
|
|
18
|
+
* this shape (a stripped JVM frame first forced it; a PHP internal-function
|
|
19
|
+
* frame is the same shape for a different reason).
|
|
20
|
+
*
|
|
21
|
+
* **What this deliberately does not do:** check that the file exists on
|
|
22
|
+
* disk, or that its contents still match what ran. `self-contained` is a
|
|
23
|
+
* statement about the mechanism, not about whether the working tree has
|
|
24
|
+
* since been edited.
|
|
25
|
+
*
|
|
26
|
+
* `column` is always null. PHP traces never print one.
|
|
27
|
+
*/
|
|
28
|
+
export function createPhpSourceLocationResolver() {
|
|
29
|
+
return {
|
|
30
|
+
// Async to satisfy the interface; PHP needs no I/O to answer, so this
|
|
31
|
+
// never awaits — same as the Python and Ruby resolvers, for the same
|
|
32
|
+
// reason.
|
|
33
|
+
resolve(frame) {
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
file: frame.file,
|
|
36
|
+
line: frame.line,
|
|
37
|
+
column: frame.column,
|
|
38
|
+
functionName: frame.functionName,
|
|
39
|
+
reliability: "self-contained",
|
|
40
|
+
resolvedVia: null,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=php-source-location-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-source-location-resolver.js","sourceRoot":"","sources":["../src/php-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,MAAM,UAAU,+BAA+B;IAC7C,OAAO;QACL,sEAAsE;QACtE,qEAAqE;QACrE,UAAU;QACV,OAAO,CAAC,KAAuB;YAC7B,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,WAAW,EAAE,gBAAgB;gBAC7B,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes the pure PHP line-recognition functions (`php-frame-parsing.ts`)
|
|
3
|
+
* with a `SourceLocationResolver` to implement the full async
|
|
4
|
+
* `StackTraceParser` contract.
|
|
5
|
+
*
|
|
6
|
+
* **No reversal.** The header's own file:line is the exception's
|
|
7
|
+
* construction site — the innermost real location — and each `#N` frame
|
|
8
|
+
* walks outward toward `{main}`. `StackTrace.frames` is contractually
|
|
9
|
+
* most-recent-first, and that is the order this text already prints in.
|
|
10
|
+
*
|
|
11
|
+
* `fidelity` is always reported `"synchronous"`. A PHP trace is built by
|
|
12
|
+
* walking the actual call frames live at the moment the `Throwable` was
|
|
13
|
+
* constructed — every location shown is a genuine link in the chain that
|
|
14
|
+
* was executing. It cannot show a logical caller across a `Fiber` (PHP
|
|
15
|
+
* 8.1+) suspension boundary, the same class of limit every other adapter
|
|
16
|
+
* in this codebase shares and none claims to see through by text alone.
|
|
17
|
+
*/
|
|
18
|
+
import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
|
|
19
|
+
export declare function createPhpStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
|
|
20
|
+
//# sourceMappingURL=php-stack-trace-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/php-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAiD5F"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes the pure PHP line-recognition functions (`php-frame-parsing.ts`)
|
|
3
|
+
* with a `SourceLocationResolver` to implement the full async
|
|
4
|
+
* `StackTraceParser` contract.
|
|
5
|
+
*
|
|
6
|
+
* **No reversal.** The header's own file:line is the exception's
|
|
7
|
+
* construction site — the innermost real location — and each `#N` frame
|
|
8
|
+
* walks outward toward `{main}`. `StackTrace.frames` is contractually
|
|
9
|
+
* most-recent-first, and that is the order this text already prints in.
|
|
10
|
+
*
|
|
11
|
+
* `fidelity` is always reported `"synchronous"`. A PHP trace is built by
|
|
12
|
+
* walking the actual call frames live at the moment the `Throwable` was
|
|
13
|
+
* constructed — every location shown is a genuine link in the chain that
|
|
14
|
+
* was executing. It cannot show a logical caller across a `Fiber` (PHP
|
|
15
|
+
* 8.1+) suspension boundary, the same class of limit every other adapter
|
|
16
|
+
* in this codebase shares and none claims to see through by text alone.
|
|
17
|
+
*/
|
|
18
|
+
import { parsePhpHeaderFrame, parsePhpFrame } from "./php-frame-parsing.js";
|
|
19
|
+
export function createPhpStackTraceParser(resolver) {
|
|
20
|
+
return {
|
|
21
|
+
async parse(rawText) {
|
|
22
|
+
const lines = rawText.split(/\r\n|\r|\n/);
|
|
23
|
+
const frames = [];
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
// Structurally disjoint: a real frame line always starts with `#N`,
|
|
26
|
+
// the header never does, so at most one of these ever matches a
|
|
27
|
+
// given line. `Stack trace:` and the `#N {main}` trailer match
|
|
28
|
+
// neither and are real parts of the block that are simply not
|
|
29
|
+
// frames — nothing is fabricated for them.
|
|
30
|
+
const raw = parsePhpFrame(line) ?? parsePhpHeaderFrame(line);
|
|
31
|
+
if (raw === null)
|
|
32
|
+
continue;
|
|
33
|
+
const location = await resolver.resolve({
|
|
34
|
+
file: raw.file,
|
|
35
|
+
line: raw.line,
|
|
36
|
+
column: null,
|
|
37
|
+
functionName: raw.functionName,
|
|
38
|
+
});
|
|
39
|
+
frames.push({ location, raw: line });
|
|
40
|
+
}
|
|
41
|
+
if (frames.length === 0) {
|
|
42
|
+
// Nothing frame-shaped at all — not recognisable as a PHP
|
|
43
|
+
// uncaught-Throwable trace, per this interface's contract: null,
|
|
44
|
+
// never an empty-but-structured StackTrace standing in for
|
|
45
|
+
// "couldn't parse".
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// `primaryFrameIndex` is required so that every adapter states an
|
|
49
|
+
// answer rather than inheriting one (RT-083). PHP's is 0 because the
|
|
50
|
+
// **header carries a location no `#N` frame does** — `getFile()` /
|
|
51
|
+
// `getLine()`, the site where the `Throwable` was constructed — and
|
|
52
|
+
// it is the first line of the block, so it parses to frame 0.
|
|
53
|
+
//
|
|
54
|
+
// This is emphatically not "index 0 happens to work". PHP's `#N`
|
|
55
|
+
// frames name the **callee at the caller's location**, so `#0` is the
|
|
56
|
+
// call site one level out; an adapter that pointed at `#0` would
|
|
57
|
+
// report the caller's line as the failure site and look correct doing
|
|
58
|
+
// it. See `php-frame-parsing.ts` for the one caveat this inherits:
|
|
59
|
+
// strictly the header is where the exception was `new`-ed, which
|
|
60
|
+
// differs from the throw site only if a caller holds a `Throwable`
|
|
61
|
+
// across lines before throwing it.
|
|
62
|
+
return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=php-stack-trace-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"php-stack-trace-parser.js","sourceRoot":"","sources":["../src/php-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5E,MAAM,UAAU,yBAAyB,CAAC,QAAgC;IACxE,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAe;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAE1C,MAAM,MAAM,GAAiB,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,oEAAoE;gBACpE,gEAAgE;gBAChE,+DAA+D;gBAC/D,8DAA8D;gBAC9D,2CAA2C;gBAC3C,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC;gBAC7D,IAAI,GAAG,KAAK,IAAI;oBAAE,SAAS;gBAE3B,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC;oBACtC,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,MAAM,EAAE,IAAI;oBACZ,YAAY,EAAE,GAAG,CAAC,YAAY;iBAC/B,CAAC,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,CAAC;YAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,0DAA0D;gBAC1D,iEAAiE;gBACjE,2DAA2D;gBAC3D,oBAAoB;gBACpB,OAAO,IAAI,CAAC;YACd,CAAC;YAED,kEAAkE;YAClE,qEAAqE;YACrE,mEAAmE;YACnE,oEAAoE;YACpE,8DAA8D;YAC9D,EAAE;YACF,iEAAiE;YACjE,sEAAsE;YACtE,iEAAiE;YACjE,sEAAsE;YACtE,mEAAmE;YACnE,iEAAiE;YACjE,mEAAmE;YACnE,mCAAmC;YACnC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descryy/runtime-adapter-php",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "PHP runtime adapter: uncaught-Throwable trace parsing, self-contained source locations, the concrete RuntimeAdapter for PHP.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.5"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"registry": "https://registry.npmjs.org",
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@descryy/runtime-contracts": "*",
|
|
28
|
+
"@descryy/runtime-backend-observation": "*"
|
|
29
|
+
}
|
|
30
|
+
}
|