Skip to content

Commit 459521c

Browse files
jorgsowagenintho
andcommitted
feat: asymmetric visibility support (PHP 8.4+)
Add parsing support for PHP 8.4 asymmetric visibility modifiers on class properties and constructor promotion parameters. - Parse `public private(set)`, `protected private(set)`, `private(set)`, `protected(set)` modifiers in class body via `read_member_flags` - Parse asymmetric visibility on constructor promoted parameters via `read_promoted` in function.js - Add `visibilitySet` field to `Declaration` and `PropertyStatement` AST nodes - Add `flagsSet` field to `Parameter` AST node - Update `types.d.ts` with new fields - All (set) parsing is gated behind `version >= 804` Co-authored-by: Thomas Genin <664857+genintho@users.noreply.github.com>
1 parent 1d8ba83 commit 459521c

38 files changed

Lines changed: 1142 additions & 56 deletions

src/ast/declaration.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ const IS_PUBLIC = "public";
1313
const IS_PROTECTED = "protected";
1414
const IS_PRIVATE = "private";
1515

16+
const SET_VISIBILITY_MAP = {
17+
0: IS_PUBLIC,
18+
1: IS_PROTECTED,
19+
2: IS_PRIVATE,
20+
};
21+
1622
/**
1723
* A declaration statement (function, class, interface...)
1824
* @constructor Declaration
@@ -54,6 +60,10 @@ Declaration.prototype.parseFlags = function (flags) {
5460
this.visibility = IS_PRIVATE;
5561
}
5662
this.isStatic = flags[1] === 1;
63+
this.visibilitySet =
64+
flags[4] !== undefined && flags[4] !== -1
65+
? SET_VISIBILITY_MAP[flags[4]]
66+
: null;
5767
}
5868
};
5969

src/ast/parameter.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const KIND = "parameter";
3434
* @property {AttrGroup[]} attrGroups
3535
* @property {MODIFIER_PUBLIC|MODIFIER_PROTECTED|MODIFIER_PRIVATE} flags
3636
* @property {PropertyHook[]} hooks
37+
* @property {MODIFIER_PUBLIC|MODIFIER_PROTECTED|MODIFIER_PRIVATE} flagsSet
3738
*/
3839
module.exports = Declaration.extends(
3940
KIND,
@@ -47,6 +48,7 @@ module.exports = Declaration.extends(
4748
nullable,
4849
flags,
4950
hooks,
51+
flagsSet,
5052
docs,
5153
location,
5254
) {
@@ -59,6 +61,7 @@ module.exports = Declaration.extends(
5961
this.nullable = nullable;
6062
this.flags = flags || 0;
6163
this.hooks = hooks || [];
64+
this.flagsSet = flagsSet || 0;
6265
this.attrGroups = [];
6366
},
6467
);

src/ast/propertystatement.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,20 @@ const IS_PUBLIC = "public";
1313
const IS_PROTECTED = "protected";
1414
const IS_PRIVATE = "private";
1515

16+
const SET_VISIBILITY_MAP = {
17+
0: IS_PUBLIC,
18+
1: IS_PROTECTED,
19+
2: IS_PRIVATE,
20+
};
21+
1622
/**
1723
* Declares a properties into the current scope
1824
* @constructor PropertyStatement
1925
* @memberOf module:php-parser
2026
* @extends {Statement}
2127
* @property {Property[]} properties
2228
* @property {string|null} visibility
29+
* @property {string|null} visibilitySet
2330
* @property {boolean} isStatic
2431
* @property {boolean} isAbstract
2532
* @property {boolean} isFinal
@@ -56,6 +63,10 @@ PropertyStatement.prototype.parseFlags = function (flags) {
5663
this.isStatic = flags[1] === 1;
5764
this.isAbstract = flags[2] === 1;
5865
this.isFinal = flags[2] === 2;
66+
this.visibilitySet =
67+
flags[4] !== undefined && flags[4] !== -1
68+
? SET_VISIBILITY_MAP[flags[4]]
69+
: null;
5970
};
6071

6172
module.exports = PropertyStatement;

src/parser/class.js

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -388,69 +388,94 @@ module.exports = {
388388
/*
389389
* Read member flags
390390
* @return array
391-
* 1st index : 0 => public, 1 => protected, 2 => private
391+
* 1st index : -1 => no visibility, 0 => public, 1 => protected, 2 => private
392392
* 2nd index : 0 => instance member, 1 => static member
393393
* 3rd index : 0 => normal, 1 => abstract member, 2 => final member
394+
* 4th index : 0 => no readonly, 1 => readonly
395+
* 5th index : -1 => no set modifier, 0 => public(set), 1 => protected(set), 2 => private(set)
394396
*/
395397
read_member_flags(asInterface) {
396-
const result = [-1, -1, -1, -1];
397-
if (this.is("T_MEMBER_FLAGS")) {
398-
let idx = 0,
399-
val = 0;
400-
do {
401-
switch (this.token) {
402-
case this.tok.T_PUBLIC:
403-
idx = 0;
404-
val = 0;
405-
break;
406-
case this.tok.T_PROTECTED:
407-
idx = 0;
408-
val = 1;
409-
break;
410-
case this.tok.T_PRIVATE:
411-
idx = 0;
412-
val = 2;
413-
break;
414-
case this.tok.T_STATIC:
415-
idx = 1;
416-
val = 1;
417-
break;
418-
case this.tok.T_ABSTRACT:
419-
idx = 2;
420-
val = 1;
421-
break;
422-
case this.tok.T_FINAL:
423-
idx = 2;
424-
val = 2;
425-
break;
426-
case this.tok.T_READ_ONLY:
427-
idx = 3;
428-
val = 1;
429-
break;
430-
}
431-
if (asInterface) {
432-
if (idx === 0 && val === 2) {
398+
const result = [-1, 0, 0, 0, -1];
399+
const seen = new Set();
400+
while (this.is("T_MEMBER_FLAGS")) {
401+
let idx = -1,
402+
val = -1;
403+
switch (this.token) {
404+
case this.tok.T_PUBLIC:
405+
case this.tok.T_PROTECTED:
406+
case this.tok.T_PRIVATE: {
407+
idx = 0;
408+
val =
409+
this.token === this.tok.T_PUBLIC
410+
? 0
411+
: this.token === this.tok.T_PROTECTED
412+
? 1
413+
: 2;
414+
if (asInterface && val === 2) {
433415
// an interface can't be private
434416
this.expect([this.tok.T_PUBLIC, this.tok.T_PROTECTED]);
435417
val = -1;
436-
} else if (idx === 2 && val === 1) {
437-
// an interface cant be abstract
418+
}
419+
this.next(); // consume the visibility keyword
420+
if (this.version >= 804 && this.token === "(") {
421+
// visibility(set) modifier: e.g. private(set)
422+
this.next(); // consume '('
423+
if (this.token !== this.tok.T_STRING || this.text() !== "set") {
424+
this.error("set");
425+
} else {
426+
this.next(); // consume 'set'
427+
}
428+
if (this.expect(")")) {
429+
this.next(); // consume ')'
430+
}
431+
if (seen.has(4)) {
432+
this.error(); // set modifier already defined
433+
} else if (val !== -1) {
434+
seen.add(4);
435+
result[4] = val;
436+
}
437+
continue;
438+
}
439+
if (seen.has(idx)) {
438440
this.error();
439-
val = -1;
441+
} else if (val !== -1) {
442+
seen.add(idx);
443+
result[idx] = val;
440444
}
445+
continue;
441446
}
442-
if (result[idx] !== -1) {
443-
// already defined flag
444-
this.error();
445-
} else if (val !== -1) {
446-
result[idx] = val;
447-
}
448-
} while (this.next().is("T_MEMBER_FLAGS"));
447+
case this.tok.T_STATIC:
448+
idx = 1;
449+
val = 1;
450+
break;
451+
case this.tok.T_ABSTRACT:
452+
idx = 2;
453+
val = 1;
454+
break;
455+
case this.tok.T_FINAL:
456+
idx = 2;
457+
val = 2;
458+
break;
459+
case this.tok.T_READ_ONLY:
460+
idx = 3;
461+
val = 1;
462+
break;
463+
}
464+
if (asInterface && idx === 2 && val === 1) {
465+
// an interface can't be abstract
466+
this.error();
467+
val = -1;
468+
}
469+
if (seen.has(idx)) {
470+
// already defined flag
471+
this.error();
472+
} else if (val !== -1) {
473+
seen.add(idx);
474+
result[idx] = val;
475+
}
476+
this.next();
449477
}
450478

451-
if (result[1] === -1) result[1] = 0;
452-
if (result[2] === -1) result[2] = 0;
453-
if (result[3] === -1) result[3] = 0;
454479
return result;
455480
},
456481

src/parser/function.js

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ module.exports = {
268268
}
269269
}
270270

271-
const flags = this.read_promoted();
271+
const [flags, flagsSet] = this.read_promoted();
272272

273273
if (
274274
!readonly &&
@@ -320,6 +320,7 @@ module.exports = {
320320
nullable,
321321
flags,
322322
hooks,
323+
flagsSet,
323324
);
324325
if (attrs) result.attrGroups = attrs;
325326
return result;
@@ -387,17 +388,67 @@ module.exports = {
387388
const MODIFIER_PUBLIC = 1;
388389
const MODIFIER_PROTECTED = 2;
389390
const MODIFIER_PRIVATE = 4;
391+
392+
let firstModifier;
390393
if (this.token === this.tok.T_PUBLIC) {
391394
this.next();
392-
return MODIFIER_PUBLIC;
395+
firstModifier = MODIFIER_PUBLIC;
393396
} else if (this.token === this.tok.T_PROTECTED) {
394397
this.next();
395-
return MODIFIER_PROTECTED;
398+
firstModifier = MODIFIER_PROTECTED;
396399
} else if (this.token === this.tok.T_PRIVATE) {
397400
this.next();
398-
return MODIFIER_PRIVATE;
401+
firstModifier = MODIFIER_PRIVATE;
402+
} else {
403+
return [0, 0];
404+
}
405+
406+
// PHP 8.4+ asymmetric visibility
407+
if (this.version >= 804) {
408+
if (this.token === "(") {
409+
// shorthand: visibility(set) — firstModifier is the set modifier, read visibility is implicit
410+
this.next(); // consume '('
411+
if (this.token !== this.tok.T_STRING || this.text() !== "set") {
412+
this.error("set");
413+
} else {
414+
this.next(); // consume 'set'
415+
}
416+
if (this.expect(")")) {
417+
this.next(); // consume ')'
418+
}
419+
return [0, firstModifier]; // no explicit read visibility, set = firstModifier
420+
}
421+
422+
// Check for explicit form: readVisibility setVisibility(set)
423+
let setModifier = 0;
424+
if (this.token === this.tok.T_PUBLIC) {
425+
this.next();
426+
setModifier = MODIFIER_PUBLIC;
427+
} else if (this.token === this.tok.T_PROTECTED) {
428+
this.next();
429+
setModifier = MODIFIER_PROTECTED;
430+
} else if (this.token === this.tok.T_PRIVATE) {
431+
this.next();
432+
setModifier = MODIFIER_PRIVATE;
433+
}
434+
435+
if (setModifier > 0) {
436+
if (this.expect("(")) {
437+
this.next(); // consume '('
438+
}
439+
if (this.token !== this.tok.T_STRING || this.text() !== "set") {
440+
this.error("set");
441+
} else {
442+
this.next(); // consume 'set'
443+
}
444+
if (this.expect(")")) {
445+
this.next(); // consume ')'
446+
}
447+
return [firstModifier, setModifier];
448+
}
399449
}
400-
return 0;
450+
451+
return [firstModifier, 0];
401452
},
402453
/*
403454
* Reads a list of arguments

test/snapshot/__snapshots__/acid.test.js.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,7 @@ Program {
10331033
},
10341034
],
10351035
"visibility": "protected",
1036+
"visibilitySet": null,
10361037
},
10371038
Method {
10381039
"arguments": [],
@@ -1606,6 +1607,7 @@ Program {
16061607
"nullable": false,
16071608
"type": null,
16081609
"visibility": "public",
1610+
"visibilitySet": null,
16091611
},
16101612
],
16111613
"extends": null,
@@ -1774,6 +1776,7 @@ Program {
17741776
"raw": "bool",
17751777
},
17761778
"visibility": "public",
1779+
"visibilitySet": null,
17771780
},
17781781
Method {
17791782
"arguments": [],
@@ -1835,6 +1838,7 @@ Program {
18351838
"raw": "bool",
18361839
},
18371840
"visibility": "protected",
1841+
"visibilitySet": null,
18381842
},
18391843
Method {
18401844
"arguments": [],
@@ -1896,6 +1900,7 @@ Program {
18961900
"raw": "bool",
18971901
},
18981902
"visibility": "protected",
1903+
"visibilitySet": null,
18991904
},
19001905
],
19011906
"extends": [
@@ -2177,6 +2182,7 @@ Program {
21772182
"attrGroups": [],
21782183
"byref": false,
21792184
"flags": 0,
2185+
"flagsSet": 0,
21802186
"hooks": [],
21812187
"kind": "parameter",
21822188
"loc": Location {
@@ -2889,6 +2895,7 @@ Program {
28892895
"raw": "string",
28902896
},
28912897
"visibility": "public",
2898+
"visibilitySet": null,
28922899
},
28932900
Method {
28942901
"arguments": [],
@@ -2986,6 +2993,7 @@ Program {
29862993
"nullable": false,
29872994
"type": null,
29882995
"visibility": "private",
2996+
"visibilitySet": null,
29892997
},
29902998
],
29912999
"kind": "trait",
@@ -4140,6 +4148,7 @@ next:
41404148
"attrGroups": [],
41414149
"byref": false,
41424150
"flags": 0,
4151+
"flagsSet": 0,
41434152
"hooks": [],
41444153
"kind": "parameter",
41454154
"loc": Location {
@@ -5336,6 +5345,7 @@ next:
53365345
"nullable": false,
53375346
"type": null,
53385347
"visibility": "",
5348+
"visibilitySet": null,
53395349
},
53405350
],
53415351
"extends": Name {

0 commit comments

Comments
 (0)