Skip to content

Commit d0dff0d

Browse files
committed
refactor: improve type safety and fix self/parent keyword handling
- Narrow return types from Node to Expr throughout expression parsing - Fix self/parent parsing to use T_STRING check instead of dedicated token IDs, matching upstream PHP-Parser behavior - Add T_SELF and T_PARENT token constants for future use
1 parent 6807f0c commit d0dff0d

2 files changed

Lines changed: 51 additions & 44 deletions

File tree

src/parser/php8.ts

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Parser } from '../parser';
22
import { Node, NodeAbstract } from '../node';
3+
import { Expr } from '../node/expr';
34
import { Token } from '../token';
45
import { Lexer } from '../lexer';
56
import { PhpParserError } from '../error';
@@ -1755,8 +1756,8 @@ export class Php8Parser implements Parser {
17551756

17561757
// ─── Expression parsing (Pratt parser / precedence climbing) ──
17571758

1758-
private parseExpressionList(): Node[] {
1759-
const exprs: Node[] = [];
1759+
private parseExpressionList(): Expr[] {
1760+
const exprs: Expr[] = [];
17601761
if (this.is(';'.charCodeAt(0)) || this.is(')'.charCodeAt(0))) return exprs;
17611762
do {
17621763
if (this.is(')'.charCodeAt(0))) break; // trailing comma
@@ -1770,22 +1771,22 @@ export class Php8Parser implements Parser {
17701771
// 5: ternary (?:) 6: ?? (coalesce) 7: pipe 8: ||
17711772
// 9: && 10: | 11: ^ 12: & 13: ==/===/!=/!==/<=>
17721773
// 14: </<=/>/>= 15: <</> 16: +/-/. 17: */% 18: instanceof 19: **
1773-
private parseExpression(minPrec: number = 0): Node {
1774+
private parseExpression(minPrec: number = 0): Expr {
17741775
let left = this.parseUnaryExpression();
17751776

17761777
while (true) {
17771778
// Check binary operators first
17781779
const op = this.getBinaryOp();
17791780
if (op !== null && op.prec >= minPrec) {
17801781
this.advance();
1781-
let right: Node;
1782+
let right: Expr | Node;
17821783
if (op.type === 'Instanceof') {
17831784
right = this.parseInstanceofRhs();
17841785
} else {
17851786
const nextMinPrec = op.rightAssoc ? op.prec : op.prec + 1;
17861787
right = this.parseExpression(nextMinPrec);
17871788
}
1788-
left = this.createBinaryOp(op.type, left, right, {
1789+
left = this.createBinaryOp(op.type, left, right as Expr, {
17891790
...left.getAttributes(),
17901791
endLine: (right as any).getEndLine?.() ?? -1,
17911792
});
@@ -1795,7 +1796,7 @@ export class Php8Parser implements Parser {
17951796
// Check ternary (?:) at precedence 5 — left-associative
17961797
if (this.is('?'.charCodeAt(0)) && minPrec <= 5) {
17971798
this.advance();
1798-
let ifTrue: Node | null = null;
1799+
let ifTrue: Expr | null = null;
17991800
if (!this.is(':'.charCodeAt(0))) {
18001801
ifTrue = this.parseExpression();
18011802
}
@@ -1889,7 +1890,7 @@ export class Php8Parser implements Parser {
18891890
}
18901891
}
18911892

1892-
private static readonly BINARY_OP_MAP: Record<string, new (left: Node, right: Node, attrs: Record<string, any>) => Node> = {
1893+
private static readonly BINARY_OP_MAP: Record<string, new (left: Expr, right: Expr, attrs: Record<string, any>) => Expr> = {
18931894
'BitwiseAnd': BinBitwiseAnd,
18941895
'BitwiseOr': BinBitwiseOr,
18951896
'BitwiseXor': BinBitwiseXor,
@@ -1933,12 +1934,12 @@ export class Php8Parser implements Parser {
19331934
this.advance();
19341935
return new Name('static', this.endAttributes(attrs));
19351936
}
1936-
if (this.is(T.T_SELF)) {
1937+
if (this.is(T.T_STRING) && this.current().text === 'self') {
19371938
const attrs = this.startAttributes();
19381939
this.advance();
19391940
return new Name('self', this.endAttributes(attrs));
19401941
}
1941-
if (this.is(T.T_PARENT)) {
1942+
if (this.is(T.T_STRING) && this.current().text === 'parent') {
19421943
const attrs = this.startAttributes();
19431944
this.advance();
19441945
return new Name('parent', this.endAttributes(attrs));
@@ -1947,7 +1948,7 @@ export class Php8Parser implements Parser {
19471948
return this.parseUnaryExpression();
19481949
}
19491950

1950-
private createBinaryOp(type: string, left: Node, right: Node, attrs: Record<string, any>): Node {
1951+
private createBinaryOp(type: string, left: Expr, right: Expr, attrs: Record<string, any>): Expr {
19511952
if (type === 'Instanceof') {
19521953
return new Instanceof_(left, right, attrs);
19531954
}
@@ -1958,7 +1959,7 @@ export class Php8Parser implements Parser {
19581959
throw new Error('Unknown binary op: ' + type);
19591960
}
19601961

1961-
private static readonly ASSIGN_OP_MAP: Record<string, new (var_: Node, expr: Node, attrs: Record<string, any>) => Node> = {
1962+
private static readonly ASSIGN_OP_MAP: Record<string, new (var_: Expr, expr: Expr, attrs: Record<string, any>) => Expr> = {
19621963
'BitwiseAnd': AssignBitwiseAnd,
19631964
'BitwiseOr': AssignBitwiseOr,
19641965
'BitwiseXor': AssignBitwiseXor,
@@ -1974,7 +1975,7 @@ export class Php8Parser implements Parser {
19741975
'ShiftRight': AssignShiftRight,
19751976
};
19761977

1977-
private arrayToList(node: Node): Node {
1978+
private arrayToList(node: Expr): Expr {
19781979
if (node instanceof ExprArray_) {
19791980
// Recursively convert nested arrays to lists
19801981
const items = node.items.map((item: any) => {
@@ -1989,7 +1990,7 @@ export class Php8Parser implements Parser {
19891990
return node;
19901991
}
19911992

1992-
private createAssignOp(type: string, left: Node, right: Node, attrs: Record<string, any>): Node {
1993+
private createAssignOp(type: string, left: Expr, right: Expr, attrs: Record<string, any>): Expr {
19931994
if (type === 'Assign') {
19941995
return new Assign(this.arrayToList(left), right, attrs);
19951996
}
@@ -2001,7 +2002,7 @@ export class Php8Parser implements Parser {
20012002
return new Assign(left, right, attrs);
20022003
}
20032004

2004-
private parseUnaryExpression(): Node {
2005+
private parseUnaryExpression(): Expr {
20052006
const attrs = this.startAttributes();
20062007
const id = this.currentId();
20072008

@@ -2156,7 +2157,7 @@ export class Php8Parser implements Parser {
21562157
return this.parsePostfixExpression();
21572158
}
21582159

2159-
private parsePostfixExpression(): Node {
2160+
private parsePostfixExpression(): Expr {
21602161
let expr = this.parsePrimaryExpression();
21612162

21622163
while (true) {
@@ -2232,7 +2233,7 @@ export class Php8Parser implements Parser {
22322233
}
22332234
} else if (this.is('['.charCodeAt(0))) {
22342235
this.advance();
2235-
let dim: Node | null = null;
2236+
let dim: Expr | null = null;
22362237
if (!this.is(']'.charCodeAt(0))) {
22372238
dim = this.parseExpression();
22382239
}
@@ -2260,7 +2261,7 @@ export class Php8Parser implements Parser {
22602261
return expr;
22612262
}
22622263

2263-
private parsePrimaryExpression(): Node {
2264+
private parsePrimaryExpression(): Expr {
22642265
const attrs = this.startAttributes();
22652266

22662267
switch (this.currentId()) {
@@ -2494,7 +2495,7 @@ export class Php8Parser implements Parser {
24942495

24952496
// ─── Helper expression parsers ─────────────────────────────────
24962497

2497-
private parseCallableVariable(): Node {
2498+
private parseCallableVariable(): Expr {
24982499
const attrs = this.startAttributes();
24992500
if (this.is('$'.charCodeAt(0))) {
25002501
this.advance(); // consume $
@@ -2514,7 +2515,7 @@ export class Php8Parser implements Parser {
25142515
return new Variable(token.text.substring(1), this.endAttributes(attrs));
25152516
}
25162517

2517-
private parseSimpleVariable(): Node {
2518+
private parseSimpleVariable(): Expr {
25182519
const attrs = this.startAttributes();
25192520
const token = this.expect(T.T_VARIABLE);
25202521
return new Variable(token.text.substring(1), this.endAttributes(attrs));
@@ -2553,7 +2554,7 @@ export class Php8Parser implements Parser {
25532554
return new Name(token.text, this.endAttributes(attrs));
25542555
}
25552556
// PHP 8.0+: keywords can be used as namespace names (e.g., namespace fn; fn\use())
2556-
if (this.isSemiReservedKeyword() || token.id === T.T_SELF || token.id === T.T_PARENT || token.id === T.T_STATIC) {
2557+
if (this.isSemiReservedKeyword() || token.id === T.T_STATIC || (token.id === T.T_STRING && (token.text === 'self' || token.text === 'parent'))) {
25572558
this.advance();
25582559
return new Name(token.text, this.endAttributes(attrs));
25592560
}
@@ -2697,7 +2698,7 @@ export class Php8Parser implements Parser {
26972698
return args;
26982699
}
26992700

2700-
private parseNew(): Node {
2701+
private parseNew(): Expr {
27012702
const attrs = this.startAttributes();
27022703
this.expect(T.T_NEW);
27032704
if (this.is(T.T_CLASS)) {
@@ -2732,7 +2733,7 @@ export class Php8Parser implements Parser {
27322733
}
27332734
if (this.is(T.T_VARIABLE)) {
27342735
// Dynamic class: new $a, new $a->b, new $a['b'], new $a::$b
2735-
let node: Node = this.parseCallableVariable();
2736+
let node: Expr = this.parseCallableVariable();
27362737
// Handle property access, array access, static property chains
27372738
while (true) {
27382739
if (this.is(T.T_OBJECT_OPERATOR)) {
@@ -2768,7 +2769,7 @@ export class Php8Parser implements Parser {
27682769
return name;
27692770
}
27702771

2771-
private parseAnonymousClass(newAttrs: Record<string, any>, flags: number = 0): Node {
2772+
private parseAnonymousClass(newAttrs: Record<string, any>, flags: number = 0): Expr {
27722773
if (flags & Modifiers.READONLY) {
27732774
this.expect(T.T_READONLY);
27742775
}
@@ -2798,7 +2799,7 @@ export class Php8Parser implements Parser {
27982799
return new New_(classNode, args, this.endAttributes(newAttrs));
27992800
}
28002801

2801-
private parseArrayLong(): Node {
2802+
private parseArrayLong(): Expr {
28022803
const attrs = this.startAttributes();
28032804
this.expect(T.T_ARRAY);
28042805
this.expect('('.charCodeAt(0));
@@ -2807,7 +2808,7 @@ export class Php8Parser implements Parser {
28072808
return new ExprArray_(items, this.endAttributes(attrs));
28082809
}
28092810

2810-
private parseArrayShort(): Node {
2811+
private parseArrayShort(): Expr {
28112812
const attrs = this.startAttributes();
28122813
this.expect('['.charCodeAt(0));
28132814
const items = this.parseArrayItemList(']'.charCodeAt(0));
@@ -2841,7 +2842,7 @@ export class Php8Parser implements Parser {
28412842
return items;
28422843
}
28432844

2844-
private parseList(): Node {
2845+
private parseList(): Expr {
28452846
const attrs = this.startAttributes();
28462847
this.expect(T.T_LIST);
28472848
this.expect('('.charCodeAt(0));
@@ -2872,7 +2873,7 @@ export class Php8Parser implements Parser {
28722873
return items;
28732874
}
28742875

2875-
private parseClosure(attrGroups: Node[] = []): Node {
2876+
private parseClosure(attrGroups: Node[] = []): Expr {
28762877
const attrs = this.startAttributes();
28772878
const isStatic = false;
28782879
this.expect(T.T_FUNCTION);
@@ -2888,7 +2889,7 @@ export class Php8Parser implements Parser {
28882889
return new Closure({ static: isStatic, byRef, params, uses, returnType, stmts, attrGroups }, this.endAttributes(attrs));
28892890
}
28902891

2891-
private parseStaticClosure(attrGroups: Node[] = []): Node {
2892+
private parseStaticClosure(attrGroups: Node[] = []): Expr {
28922893
const attrs = this.startAttributes();
28932894
this.expect(T.T_STATIC);
28942895
if (this.is(T.T_FN)) {
@@ -2907,7 +2908,7 @@ export class Php8Parser implements Parser {
29072908
return new Closure({ static: true, byRef, params, uses, returnType, stmts, attrGroups }, this.endAttributes(attrs));
29082909
}
29092910

2910-
private parseStaticRef(): Node {
2911+
private parseStaticRef(): Expr {
29112912
const attrs = this.startAttributes();
29122913
this.advance();
29132914
return new Name('static', this.endAttributes(attrs));
@@ -2928,12 +2929,12 @@ export class Php8Parser implements Parser {
29282929
return uses;
29292930
}
29302931

2931-
private parseArrowFunction(attrGroups: Node[] = []): Node {
2932+
private parseArrowFunction(attrGroups: Node[] = []): Expr {
29322933
const attrs = this.startAttributes();
29332934
return this.parseArrowFunctionInner(false, attrs, attrGroups);
29342935
}
29352936

2936-
private parseArrowFunctionInner(isStatic: boolean, attrs: Record<string, any>, attrGroups: Node[] = []): Node {
2937+
private parseArrowFunctionInner(isStatic: boolean, attrs: Record<string, any>, attrGroups: Node[] = []): Expr {
29372938
this.expect(T.T_FN);
29382939
const byRef = !!this.eat('&'.charCodeAt(0)) || !!this.eat(T.T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG) || !!this.eat(T.T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG);
29392940
this.expect('('.charCodeAt(0));
@@ -2945,11 +2946,11 @@ export class Php8Parser implements Parser {
29452946
return new ArrowFunction({ static: isStatic, byRef, params, returnType, expr: body, attrGroups }, this.endAttributes(attrs));
29462947
}
29472948

2948-
private parseYield(): Node {
2949+
private parseYield(): Expr {
29492950
const attrs = this.startAttributes();
29502951
this.expect(T.T_YIELD);
2951-
let key: Node | null = null;
2952-
let value: Node | null = null;
2952+
let key: Expr | null = null;
2953+
let value: Expr | null = null;
29532954
if (this.canStartExpression()) {
29542955
// yield binds at assignment level - doesn't consume and/or/xor
29552956
value = this.parseExpression(4);
@@ -2961,15 +2962,15 @@ export class Php8Parser implements Parser {
29612962
return new Yield_(value, key, this.endAttributes(attrs));
29622963
}
29632964

2964-
private parseYieldFrom(): Node {
2965+
private parseYieldFrom(): Expr {
29652966
const attrs = this.startAttributes();
29662967
this.advance();
29672968
// yield from binds at assignment level
29682969
const expr = this.parseExpression(4);
29692970
return new YieldFrom(expr, this.endAttributes(attrs));
29702971
}
29712972

2972-
private parseInclude(): Node {
2973+
private parseInclude(): Expr {
29732974
const attrs = this.startAttributes();
29742975
const token = this.advance();
29752976
const expr = this.parseExpression();
@@ -2984,7 +2985,7 @@ export class Php8Parser implements Parser {
29842985
return new Include_(expr, type, this.endAttributes(attrs));
29852986
}
29862987

2987-
private parseMatch(): Node {
2988+
private parseMatch(): Expr {
29882989
const attrs = this.startAttributes();
29892990
this.expect(T.T_MATCH);
29902991
this.expect('('.charCodeAt(0));
@@ -3013,7 +3014,7 @@ export class Php8Parser implements Parser {
30133014
return new Match_(cond, arms, this.endAttributes(attrs));
30143015
}
30153016

3016-
private parseShellExec(): Node {
3017+
private parseShellExec(): Expr {
30173018
const attrs = this.startAttributes();
30183019
this.expect('`'.charCodeAt(0));
30193020
const savedQuote = this.encapsedQuoteChar;
@@ -3032,7 +3033,7 @@ export class Php8Parser implements Parser {
30323033
return new ShellExec(parts, this.endAttributes(attrs));
30333034
}
30343035

3035-
private parseDollarOpenCurlyBraces(): Node {
3036+
private parseDollarOpenCurlyBraces(): Expr {
30363037
// Handle ${name} and ${expr} syntax
30373038
this.advance(); // consume T_DOLLAR_OPEN_CURLY_BRACES
30383039
if (this.is(T.T_STRING_VARNAME)) {
@@ -3108,12 +3109,12 @@ export class Php8Parser implements Parser {
31083109
return result;
31093110
}
31103111

3111-
private parseEncapsedPart(): Node | null {
3112+
private parseEncapsedPart(): Expr | null {
31123113
if (this.is(T.T_ENCAPSED_AND_WHITESPACE)) {
31133114
const token = this.advance();
31143115
return new InterpolatedStringPart(this.processDoubleQuotedEscapes(token.text, this.encapsedQuoteChar));
31153116
} else if (this.is(T.T_VARIABLE)) {
3116-
let node: Node = this.parseSimpleVariable();
3117+
let node: Expr = this.parseSimpleVariable();
31173118
// Handle $var->prop and $var?->prop in encapsed strings
31183119
if (this.is(T.T_OBJECT_OPERATOR)) {
31193120
this.advance();
@@ -3127,7 +3128,7 @@ export class Php8Parser implements Parser {
31273128
// Handle $var[expr] array access in encapsed strings
31283129
if (this.is('['.charCodeAt(0))) {
31293130
this.advance();
3130-
let dim: Node | null = null;
3131+
let dim: Expr | null = null;
31313132
if (this.is(T.T_STRING)) {
31323133
// String key (bareword)
31333134
dim = new ScalarString(this.advance().text, {});
@@ -3170,7 +3171,7 @@ export class Php8Parser implements Parser {
31703171
return null;
31713172
}
31723173

3173-
private parseInterpolatedString(): Node {
3174+
private parseInterpolatedString(): Expr {
31743175
const attrs = this.startAttributes();
31753176
this.expect('"'.charCodeAt(0));
31763177
const parts: Node[] = [];
@@ -3186,7 +3187,7 @@ export class Php8Parser implements Parser {
31863187
return new InterpolatedString(parts, this.endAttributes(attrs));
31873188
}
31883189

3189-
private parseHeredoc(): Node {
3190+
private parseHeredoc(): Expr {
31903191
const attrs = this.startAttributes();
31913192
const startToken = this.advance(); // T_START_HEREDOC
31923193
const isNowdoc = startToken.text.includes("'");

src/php-token.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ export const T_PRIVATE_SET = 406;
162162
export const T_PIPE = 407;
163163
export const T_VOID_CAST = 408;
164164

165+
// Keywords that PHP tokenizes separately
166+
export const T_SELF = 411;
167+
export const T_PARENT = 412;
168+
165169
// Increment/Decrement
166170
export const T_INC = 409;
167171
export const T_DEC = 410;
@@ -320,6 +324,8 @@ const tokenNames: Record<number, string> = {
320324
[T_VOID_CAST]: 'T_VOID_CAST',
321325
[T_INC]: 'T_INC',
322326
[T_DEC]: 'T_DEC',
327+
[T_SELF]: 'T_SELF',
328+
[T_PARENT]: 'T_PARENT',
323329
};
324330

325331
export function tokenName(id: number): string {

0 commit comments

Comments
 (0)