Skip to content

Commit 8471a1c

Browse files
committed
BCFile::getMemberProperties(): sync with PHPCS 4.0 / remove parse error warning + PHP 8.4 interface properties
As of PHPCS 4.0, the `File::getMemberProperties()` method: * ... will handle properties in interfaces to support PHP 8.4, in which this is now allowed. * ... will no longer throw a parse error warning. This commit makes the necessary updates in PHPCSUtils for the same: * Adds `T_INTERFACE` to the `Collections::ooPropertyScopes()` for PHP 8.4 properties in interfaces support. * Updates the test expectations for the `Scopes::isOOProperty()` method to allow for PHP 8.4 properties in interfaces. * Updates the `BCFile::getMemberProperties()` polyfill to mirror the PHPCS 4.0 version of the method. * Updates various tests for both the `BCFile::getMemberProperties()` and the `Variables::getMemberProperties()` methods to be in line with the changes. Ref: PHPCSStandards/PHP_CodeSniffer 991
1 parent 183b458 commit 8471a1c

File tree

9 files changed

+152
-76
lines changed

9 files changed

+152
-76
lines changed

PHPCSUtils/BackCompat/BCFile.php

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ public static function getMethodProperties(File $phpcsFile, $stackPtr)
512512
*
513513
* Changelog for the PHPCS native function:
514514
* - Introduced in PHPCS 0.0.5.
515-
* - The upstream method has received no significant updates since PHPCS 3.13.0.
515+
* - PHPCS 4.0: properties in interfaces (PHP 8.4+) are accepted.
516+
* - PHPCS 4.0: will no longer throw a parse error warning.
516517
*
517518
* @see \PHP_CodeSniffer\Files\File::getMemberProperties() Original source.
518519
* @see \PHPCSUtils\Utils\Variables::getMemberProperties() PHPCSUtils native improved version.
@@ -531,7 +532,137 @@ public static function getMethodProperties(File $phpcsFile, $stackPtr)
531532
*/
532533
public static function getMemberProperties(File $phpcsFile, $stackPtr)
533534
{
534-
return $phpcsFile->getMemberProperties($stackPtr);
535+
$tokens = $phpcsFile->getTokens();
536+
537+
if ($tokens[$stackPtr]['code'] !== T_VARIABLE) {
538+
throw new RuntimeException('$stackPtr must be of type T_VARIABLE');
539+
}
540+
541+
$conditions = $tokens[$stackPtr]['conditions'];
542+
$conditions = array_keys($conditions);
543+
$ptr = array_pop($conditions);
544+
if (isset($tokens[$ptr]) === false
545+
|| isset(Tokens::$ooScopeTokens[$tokens[$ptr]['code']]) === false
546+
|| $tokens[$ptr]['code'] === T_ENUM
547+
) {
548+
throw new RuntimeException('$stackPtr is not a class member var');
549+
}
550+
551+
// Make sure it's not a method parameter.
552+
if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) {
553+
$parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']);
554+
$deepestOpen = array_pop($parenthesis);
555+
if ($deepestOpen > $ptr
556+
&& isset($tokens[$deepestOpen]['parenthesis_owner']) === true
557+
&& $tokens[$tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
558+
) {
559+
throw new RuntimeException('$stackPtr is not a class member var');
560+
}
561+
}
562+
563+
$valid = [
564+
T_PUBLIC => T_PUBLIC,
565+
T_PRIVATE => T_PRIVATE,
566+
T_PROTECTED => T_PROTECTED,
567+
T_STATIC => T_STATIC,
568+
T_VAR => T_VAR,
569+
T_READONLY => T_READONLY,
570+
T_FINAL => T_FINAL,
571+
];
572+
573+
$valid += Tokens::$emptyTokens;
574+
575+
$scope = 'public';
576+
$scopeSpecified = false;
577+
$isStatic = false;
578+
$isReadonly = false;
579+
$isFinal = false;
580+
581+
$startOfStatement = $phpcsFile->findPrevious(
582+
[
583+
T_SEMICOLON,
584+
T_OPEN_CURLY_BRACKET,
585+
T_CLOSE_CURLY_BRACKET,
586+
T_ATTRIBUTE_END,
587+
],
588+
($stackPtr - 1)
589+
);
590+
591+
for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
592+
if (isset($valid[$tokens[$i]['code']]) === false) {
593+
break;
594+
}
595+
596+
switch ($tokens[$i]['code']) {
597+
case T_PUBLIC:
598+
$scope = 'public';
599+
$scopeSpecified = true;
600+
break;
601+
case T_PRIVATE:
602+
$scope = 'private';
603+
$scopeSpecified = true;
604+
break;
605+
case T_PROTECTED:
606+
$scope = 'protected';
607+
$scopeSpecified = true;
608+
break;
609+
case T_STATIC:
610+
$isStatic = true;
611+
break;
612+
case T_READONLY:
613+
$isReadonly = true;
614+
break;
615+
case T_FINAL:
616+
$isFinal = true;
617+
break;
618+
}
619+
}
620+
621+
$type = '';
622+
$typeToken = false;
623+
$typeEndToken = false;
624+
$nullableType = false;
625+
626+
if ($i < $stackPtr) {
627+
// We've found a type.
628+
$valid = Collections::propertyTypeTokens();
629+
630+
for ($i; $i < $stackPtr; $i++) {
631+
if ($tokens[$i]['code'] === T_VARIABLE) {
632+
// Hit another variable in a group definition.
633+
break;
634+
}
635+
636+
if ($tokens[$i]['code'] === T_NULLABLE) {
637+
$nullableType = true;
638+
}
639+
640+
if (isset($valid[$tokens[$i]['code']]) === true) {
641+
$typeEndToken = $i;
642+
if ($typeToken === false) {
643+
$typeToken = $i;
644+
}
645+
646+
$type .= $tokens[$i]['content'];
647+
}
648+
}
649+
650+
if ($type !== '' && $nullableType === true) {
651+
$type = '?' . $type;
652+
}
653+
}
654+
655+
return [
656+
'scope' => $scope,
657+
'scope_specified' => $scopeSpecified,
658+
'is_static' => $isStatic,
659+
'is_readonly' => $isReadonly,
660+
'is_final' => $isFinal,
661+
'type' => $type,
662+
'type_token' => $typeToken,
663+
'type_end_token' => $typeEndToken,
664+
'nullable_type' => $nullableType,
665+
];
535666
}
536667

537668
/**

PHPCSUtils/Tokens/Collections.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ final class Collections
415415
/**
416416
* OO scopes in which properties can be declared.
417417
*
418-
* Note: interfaces can not declare properties.
418+
* - PHP 8.4 added support for properties in interfaces.
419419
*
420420
* @since 1.0.0 Use the {@see Collections::ooPropertyScopes()} method for access.
421421
*
@@ -424,6 +424,7 @@ final class Collections
424424
private static $ooPropertyScopes = [
425425
\T_CLASS => \T_CLASS,
426426
\T_ANON_CLASS => \T_ANON_CLASS,
427+
\T_INTERFACE => \T_INTERFACE,
427428
\T_TRAIT => \T_TRAIT,
428429
];
429430

PHPCSUtils/Utils/Variables.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ final class Variables
8080
* Retrieve the visibility and implementation properties of a class member variable.
8181
*
8282
* Main differences with the PHPCS version:
83-
* - Removed the parse error warning for properties in interfaces.
83+
* - Removed the parse error warning for properties in enums (PHPCS 4.0 makes the same change).
8484
* This will now throw the same _"$stackPtr is not a class member var"_ runtime exception as
8585
* other non-property variables passed to the method.
8686
* - Defensive coding against incorrect calls to this method.

Tests/BackCompat/BCFile/GetMemberPropertiesTest.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,19 @@ public static function dataGetMemberProperties()
636636
'nullable_type' => false,
637637
],
638638
],
639-
'invalid-property-in-interface' => [
639+
'property-in-interface' => [
640640
'identifier' => '/* testInterfaceProperty */',
641-
'expected' => [],
641+
'expected' => [
642+
'scope' => 'protected',
643+
'scope_specified' => true,
644+
'is_static' => false,
645+
'is_readonly' => false,
646+
'is_final' => false,
647+
'type' => '',
648+
'type_token' => false,
649+
'type_end_token' => false,
650+
'nullable_type' => false,
651+
],
642652
],
643653
'property-in-nested-class-1' => [
644654
'identifier' => '/* testNestedProperty 1 */',
@@ -1033,10 +1043,6 @@ public static function dataGetMemberProperties()
10331043
'nullable_type' => false,
10341044
],
10351045
],
1036-
'invalid-property-in-enum' => [
1037-
'identifier' => '/* testEnumProperty */',
1038-
'expected' => [],
1039-
],
10401046
'php8.1-single-intersection-type' => [
10411047
'identifier' => '/* testPHP81IntersectionTypes */',
10421048
'expected' => [
@@ -1400,6 +1406,7 @@ public static function dataNotClassProperty()
14001406
'method parameter in anon class nested in ternary' => ['/* testNestedMethodParam 1 */'],
14011407
'method parameter in anon class nested in function call' => ['/* testNestedMethodParam 2 */'],
14021408
'method parameter in enum' => ['/* testEnumMethodParamNotProperty */'],
1409+
'property in enum (parse error)' => ['/* testEnumProperty */'],
14031410
];
14041411
}
14051412

Tests/Utils/Scopes/IsOOPropertyTest.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ $a = new class {
4141
};
4242

4343
interface MyInterface {
44-
// Intentional parse error. Properties are not allowed in interfaces.
44+
// Properties are allowed in interfaces since PHP 8.4 (with a property hook).
4545
/* testInterfaceProp */
4646
public $interfaceProp = false;
4747

Tests/Utils/Scopes/IsOOPropertyTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public static function dataIsOOProperty()
125125
],
126126
'interface-property' => [
127127
'testMarker' => '/* testInterfaceProp */',
128-
'expected' => false,
128+
'expected' => true,
129129
],
130130
'interface-method-param' => [
131131
'testMarker' => '/* testInterfaceMethodParameter */',
Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
11
<?php
22

3-
interface Base
4-
{
5-
/* testInterfaceProperty */
6-
protected $anonymous;
7-
}
8-
9-
enum Suit
10-
{
11-
/* testEnumProperty */
12-
protected $anonymous;
13-
}
3+
// Placeholder file.

Tests/Utils/Variables/GetMemberPropertiesDiffTest.php

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,37 +55,4 @@ public function testNonExistentToken()
5555

5656
Variables::getMemberProperties(self::$phpcsFile, 10000);
5757
}
58-
59-
/**
60-
* Test receiving an expected exception when an (invalid) interface or enum property is passed.
61-
*
62-
* @dataProvider dataNotClassPropertyException
63-
*
64-
* @param string $testMarker Comment which precedes the test case.
65-
*
66-
* @return void
67-
*/
68-
public function testNotClassPropertyException($testMarker)
69-
{
70-
$this->expectException('PHPCSUtils\Exceptions\ValueError');
71-
$this->expectExceptionMessage('The value of argument #2 ($stackPtr) must be the pointer to a class member var');
72-
73-
$variable = $this->getTargetToken($testMarker, \T_VARIABLE);
74-
Variables::getMemberProperties(self::$phpcsFile, $variable);
75-
}
76-
77-
/**
78-
* Data provider.
79-
*
80-
* @see testNotClassPropertyException()
81-
*
82-
* @return array<string, array<string>>
83-
*/
84-
public static function dataNotClassPropertyException()
85-
{
86-
return [
87-
'interface property' => ['/* testInterfaceProperty */'],
88-
'enum property' => ['/* testEnumProperty */'],
89-
];
90-
}
9158
}

Tests/Utils/Variables/GetMemberPropertiesTest.php

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,6 @@ public function testNotAVariableException()
9090
Variables::getMemberProperties(self::$phpcsFile, $next);
9191
}
9292

93-
/**
94-
* Data provider.
95-
*
96-
* @see testGetMemberProperties()
97-
*
98-
* @return array<string, array<string|array<string, string|int|bool>>>
99-
*/
100-
public static function dataGetMemberProperties()
101-
{
102-
$data = parent::dataGetMemberProperties();
103-
104-
/*
105-
* Remove the data sets related to the invalid interface/enum properties.
106-
* These will now throw an exception instead.
107-
*/
108-
unset($data['invalid-property-in-interface'], $data['invalid-property-in-enum']);
109-
110-
return $data;
111-
}
112-
11393
/**
11494
* Verify that the build-in caching is used when caching is enabled.
11595
*

0 commit comments

Comments
 (0)