Skip to content

Commit 89aec73

Browse files
SONARPHP-1584 Support Asymmetric Property Visibility (#1335)
1 parent fd46c0a commit 89aec73

File tree

9 files changed

+63
-6
lines changed

9 files changed

+63
-6
lines changed

php-frontend/src/main/java/org/sonar/php/parser/LexicalConstant.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ public class LexicalConstant {
142142
*/
143143
public static final String NUMERIC_LITERAL = EXPONENT_DNUM + "|" + DNUM + "|" + INTEGER_LITERAL;
144144

145+
/**
146+
* Other
147+
*/
148+
public static final String ASYMMETRIC_VISIBILITY_MODIFIER = "(?i)(?:public|protected|private)\\(set\\)";
149+
145150
private LexicalConstant() {
146151
}
147152

php-frontend/src/main/java/org/sonar/php/parser/PHPGrammar.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ public SyntaxToken VISIBILITY_MODIFIER() {
440440
public SyntaxToken MEMBER_MODIFIER() {
441441
return b.<SyntaxToken>nonterminal(PHPLexicalGrammar.MEMBER_MODIFIER).is(
442442
b.firstOf(
443+
b.token(PHPLexicalGrammar.ASYMMETRIC_VISIBILITY_MODIFIER),
443444
VISIBILITY_MODIFIER(),
444445
b.token(PHPKeyword.READONLY),
445446
b.token(PHPKeyword.STATIC),
@@ -500,7 +501,7 @@ public ParameterTree PARAMETER() {
500501
f.parameter(
501502
b.zeroOrMore(ATTRIBUTE_GROUP()),
502503
b.zeroOrMore(
503-
b.firstOf(VISIBILITY_MODIFIER(), b.token(PHPKeyword.READONLY))),
504+
b.firstOf(b.token(PHPLexicalGrammar.ASYMMETRIC_VISIBILITY_MODIFIER), VISIBILITY_MODIFIER(), b.token(PHPKeyword.READONLY))),
504505
b.optional(DECLARED_TYPE()),
505506
b.optional(b.token(PHPPunctuator.AMPERSAND)),
506507
b.optional(b.token(PHPPunctuator.ELLIPSIS)),

php-frontend/src/main/java/org/sonar/php/parser/PHPLexicalGrammar.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public enum PHPLexicalGrammar implements GrammarRuleKey {
5757
MEMBER_MODIFIER,
5858
CLASS_CONST_MODIFIER,
5959
VISIBILITY_MODIFIER,
60+
ASYMMETRIC_VISIBILITY_MODIFIER,
6061
MEMBER_CONST_DECLARATION,
6162
FUNCTION_CALL_ARGUMENT,
6263

@@ -312,6 +313,7 @@ public static void lexical(LexerlessGrammarBuilder b) {
312313
b.rule(BACKTICK).is("`");
313314
// FIXME: this recovery is introduce in order to parse ${var}, as expression cannot match keywords.
314315
b.rule(SEMI_COMPLEX_RECOVERY_EXPRESSION).is(b.regexp("[^}]++"));
316+
b.rule(ASYMMETRIC_VISIBILITY_MODIFIER).is(SPACING, b.regexp(LexicalConstant.ASYMMETRIC_VISIBILITY_MODIFIER));
315317

316318
// Identifier
317319
b.rule(WHITESPACES).is(b.regexp("[" + LexicalConstant.WHITESPACE + "]*+"));

php-frontend/src/test/java/org/sonar/php/parser/PHPParserTest.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import org.sonar.plugins.php.api.tree.expression.BinaryExpressionTree;
2424
import org.sonar.plugins.php.api.tree.statement.ForEachStatementTree;
2525

26+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
27+
2628
class PHPParserTest {
2729

2830
@Test
29-
void expression() {
31+
void shouldParseExpression() {
3032
ActionParser<Tree> parser = PHPParserBuilder.createParser(PHPLexicalGrammar.EXPRESSION);
3133
BinaryExpressionTree tree = (BinaryExpressionTree) parser.parse("$a + $b + $c");
3234
Assertions.assertThat(tree.getParent()).isNull();
@@ -39,10 +41,24 @@ void expression() {
3941
}
4042

4143
@Test
42-
void forEach() {
44+
void shouldParseForEach() {
4345
ActionParser<Tree> parser = PHPParserBuilder.createParser(PHPLexicalGrammar.FOREACH_STATEMENT);
4446
ForEachStatementTree tree = (ForEachStatementTree) parser.parse("foreach ($arr as &$value) { }");
4547
Assertions.assertThat(tree.expression().getParent()).isSameAs(tree);
4648
}
4749

50+
@Test
51+
void shouldParseAsymmetricVisibilityInConstructor() {
52+
ActionParser<Tree> parser = PHPParserBuilder.createParser(PHPLexicalGrammar.COMPILATION_UNIT);
53+
assertDoesNotThrow(() -> parser.parse("""
54+
<?php
55+
class Book
56+
{
57+
public function __construct(
58+
public private(set) string $title,
59+
public protected(set) string $author,
60+
protected private(set) int $pubYear,
61+
) {}
62+
}"""));
63+
}
4864
}

php-frontend/src/test/java/org/sonar/php/parser/declaration/ClassMemberTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ void test() {
3030
.matches("const A;")
3131
.matches("private function f() {}")
3232
.matches("public readonly string $prop;")
33-
.matches("public int|null|(A&B) $a;");
33+
.matches("public int|null|(A&B) $a;")
34+
.matches("public(set) string $a;")
35+
.matches("protected(set) string $a;")
36+
.matches("private(set) string $a;")
37+
.notMatches("public( set) string $a;") // spaces are not accepted
38+
.notMatches("public(set ) string $a;")
39+
.notMatches("public( set ) string $a;");
3440
}
3541
}

php-frontend/src/test/java/org/sonar/php/parser/declaration/MethodDeclarationTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ void construct() {
5353
.matches("public function __construct(protected readonly $prop) {}")
5454
.matches("public function __construct(protected readonly string $prop) {}")
5555
.matches("public function __construct(readonly protected $prop) {}")
56+
.matches("public function __construct(readonly public protected(set) $prop) {}")
57+
.matches("public function __construct(protected(set) $prop) {}")
5658
.matches("public function __construct(readonly protected string $prop) {}")
5759
.matches("public function __construct(readonly $prop) {}")
5860
.matches("public function __construct(public $p { get; }) {}")

php-frontend/src/test/java/org/sonar/php/tree/impl/declaration/ClassPropertyDeclarationTreeTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,31 @@ void shouldParseVariable() {
5555
.matches("readonly $a;")
5656
.matches("var $a;")
5757

58+
// all possible asymmetric visibility
59+
.matches("public(set) string $a;")
60+
.matches("protected(set) string $a;")
61+
.matches("private(set) string $a;")
62+
63+
// any order is possible for modifiers, including asymmetric visibility
64+
.matches("readonly public protected(set) final string $prop;")
65+
.matches("public readonly protected(set) final string $prop;")
66+
.matches("readonly protected(set) public final string $prop;")
67+
.matches("final readonly public protected(set) string $prop;")
68+
.matches("readonly public final protected(set) string $prop;")
69+
70+
// not valid php, but we still parse these
5871
.matches("final $a;")
5972
.matches("public private $a;")
73+
.matches("private public(set) string $a;")
6074

6175
.matches("public string $a { get; }")
6276
.matches("public string $a { get { return $this-> a + 1; } }")
6377
.matches("public string $a { final set($value) => $value - 1; }")
78+
.matches("public protected(set) string $a { get; }")
6479

6580
.notMatches("public final A;")
66-
.notMatches("$a;");
81+
.notMatches("$a;")
82+
.notMatches("public( set ) string $a;");
6783
}
6884

6985
@Test
@@ -255,6 +271,14 @@ void shouldSupportPropertyHook() {
255271
assertThat(tree.eosToken()).isNull();
256272
}
257273

274+
@Test
275+
void shouldSupportAsymmetricVisibilityModifier() {
276+
ClassPropertyDeclarationTree tree = parse("protected(set) string $staticProp;", PHPLexicalGrammar.CLASS_VARIABLE_DECLARATION);
277+
assertThat(tree.is(Kind.CLASS_PROPERTY_DECLARATION)).isTrue();
278+
assertThat(tree.modifierTokens()).extracting(SyntaxToken::text).containsExactly("protected(set)");
279+
assertThat(builtinType(tree)).isEqualTo("string");
280+
}
281+
258282
/**
259283
* Get list of all modifier token texts
260284
*/

php-frontend/src/test/java/org/sonar/php/tree/symbols/SymbolTableImplTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ void testClassFields() {
130130

131131
assertThat(field.name()).isEqualTo(fieldName);
132132
assertThat(field.hasModifier("public")).isTrue();
133+
assertThat(field.hasModifier("private(set)")).isTrue();
133134
assertThat(field.is(Symbol.Kind.FIELD)).isTrue();
134135

135136
assertThat(constantField.name()).isEqualTo(constantName);

php-frontend/src/test/resources/symbols/symbolTable.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function f() { // f
1717
}
1818

1919
class A { // A
20-
public $fieldOne, $field2 = 1; // $field1, $field2
20+
public private(set) string $fieldOne, $field2 = 1; // $field1, $field2
2121
const CONSTANT_FIELD; // $constantField
2222

2323
public function f($p = 12) { // f, $p

0 commit comments

Comments
 (0)