Skip to content

Commit ba4956b

Browse files
committed
Merge branch 'feature/support-union-types' of https://github.com/jrfnl/PHP_CodeSniffer
2 parents f117e81 + 25a2e44 commit ba4956b

15 files changed

+1261
-11
lines changed

package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
126126
<file baseinstalldir="" name="BackfillFnTokenTest.php" role="test" />
127127
<file baseinstalldir="" name="BackfillNumericSeparatorTest.inc" role="test" />
128128
<file baseinstalldir="" name="BackfillNumericSeparatorTest.php" role="test" />
129+
<file baseinstalldir="" name="BitwiseOrTest.inc" role="test" />
130+
<file baseinstalldir="" name="BitwiseOrTest.php" role="test" />
129131
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
130132
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
131133
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.inc" role="test" />
@@ -2011,6 +2013,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20112013
<install as="CodeSniffer/Core/Tokenizer/BackfillFnTokenTest.inc" name="tests/Core/Tokenizer/BackfillFnTokenTest.inc" />
20122014
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.php" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.php" />
20132015
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.inc" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc" />
2016+
<install as="CodeSniffer/Core/Tokenizer/BitwiseOrTest.php" name="tests/Core/Tokenizer/BitwiseOrTest.php" />
2017+
<install as="CodeSniffer/Core/Tokenizer/BitwiseOrTest.inc" name="tests/Core/Tokenizer/BitwiseOrTest.inc" />
20142018
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
20152019
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
20162020
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
@@ -2081,6 +2085,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20812085
<install as="CodeSniffer/Core/Tokenizer/BackfillFnTokenTest.inc" name="tests/Core/Tokenizer/BackfillFnTokenTest.inc" />
20822086
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.php" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.php" />
20832087
<install as="CodeSniffer/Core/Tokenizer/BackfillNumericSeparatorTest.inc" name="tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc" />
2088+
<install as="CodeSniffer/Core/Tokenizer/BitwiseOrTest.php" name="tests/Core/Tokenizer/BitwiseOrTest.php" />
2089+
<install as="CodeSniffer/Core/Tokenizer/BitwiseOrTest.inc" name="tests/Core/Tokenizer/BitwiseOrTest.inc" />
20842090
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
20852091
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
20862092
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />

src/Files/File.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,7 +1294,8 @@ public function getDeclarationName($stackPtr)
12941294
* // or FALSE if there is no type hint.
12951295
* 'type_hint_end_token' => integer, // The stack pointer to the end of the type hint
12961296
* // or FALSE if there is no type hint.
1297-
* 'nullable_type' => boolean, // TRUE if the var type is nullable.
1297+
* 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability
1298+
* // operator.
12981299
* 'comma_token' => integer, // The stack pointer to the comma after the param
12991300
* // or FALSE if this is the last param.
13001301
* )
@@ -1449,6 +1450,9 @@ public function getMethodParameters($stackPtr)
14491450
break;
14501451
case T_NAMESPACE:
14511452
case T_NS_SEPARATOR:
1453+
case T_TYPE_UNION:
1454+
case T_FALSE:
1455+
case T_NULL:
14521456
// Part of a type hint or default value.
14531457
if ($defaultStart === null) {
14541458
if ($typeHintToken === false) {
@@ -1539,7 +1543,8 @@ public function getMethodParameters($stackPtr)
15391543
* 'return_type' => '', // The return type of the method.
15401544
* 'return_type_token' => integer, // The stack pointer to the start of the return type
15411545
* // or FALSE if there is no return type.
1542-
* 'nullable_return_type' => false, // TRUE if the return type is nullable.
1546+
* 'nullable_return_type' => false, // TRUE if the return type is preceded by the
1547+
* // nullability operator.
15431548
* 'is_abstract' => false, // TRUE if the abstract keyword was found.
15441549
* 'is_final' => false, // TRUE if the final keyword was found.
15451550
* 'is_static' => false, // TRUE if the static keyword was found.
@@ -1637,8 +1642,11 @@ public function getMethodProperties($stackPtr)
16371642
T_SELF => T_SELF,
16381643
T_PARENT => T_PARENT,
16391644
T_STATIC => T_STATIC,
1645+
T_FALSE => T_FALSE,
1646+
T_NULL => T_NULL,
16401647
T_NAMESPACE => T_NAMESPACE,
16411648
T_NS_SEPARATOR => T_NS_SEPARATOR,
1649+
T_TYPE_UNION => T_TYPE_UNION,
16421650
];
16431651

16441652
for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
@@ -1706,7 +1714,8 @@ public function getMethodProperties($stackPtr)
17061714
* // or FALSE if there is no type.
17071715
* 'type_end_token' => integer, // The stack pointer to the end of the type
17081716
* // or FALSE if there is no type.
1709-
* 'nullable_type' => boolean, // TRUE if the type is nullable.
1717+
* 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability
1718+
* // operator.
17101719
* );
17111720
* </code>
17121721
*
@@ -1821,8 +1830,11 @@ public function getMemberProperties($stackPtr)
18211830
T_CALLABLE => T_CALLABLE,
18221831
T_SELF => T_SELF,
18231832
T_PARENT => T_PARENT,
1833+
T_FALSE => T_FALSE,
1834+
T_NULL => T_NULL,
18241835
T_NAMESPACE => T_NAMESPACE,
18251836
T_NS_SEPARATOR => T_NS_SEPARATOR,
1837+
T_TYPE_UNION => T_TYPE_UNION,
18261838
];
18271839

18281840
for ($i; $i < $stackPtr; $i++) {

src/Tokenizers/PHP.php

Lines changed: 179 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ class PHP extends Tokenizer
441441
T_BACKTICK => 1,
442442
T_OPEN_SHORT_ARRAY => 1,
443443
T_CLOSE_SHORT_ARRAY => 1,
444+
T_TYPE_UNION => 1,
444445
];
445446

446447
/**
@@ -1477,6 +1478,7 @@ function return types. We want to keep the parenthesis map clean,
14771478
T_SELF => T_SELF,
14781479
T_PARENT => T_PARENT,
14791480
T_NAMESPACE => T_NAMESPACE,
1481+
T_STATIC => T_STATIC,
14801482
T_NS_SEPARATOR => T_NS_SEPARATOR,
14811483
];
14821484

@@ -1511,12 +1513,14 @@ function return types. We want to keep the parenthesis map clean,
15111513
}//end for
15121514

15131515
// Any T_ARRAY tokens we find between here and the next
1514-
// token that can't be part of the return type need to be
1516+
// token that can't be part of the return type, need to be
15151517
// converted to T_STRING tokens.
15161518
for ($x; $x < $numTokens; $x++) {
1517-
if (is_array($tokens[$x]) === false || isset($allowed[$tokens[$x][0]]) === false) {
1519+
if ((is_array($tokens[$x]) === false && $tokens[$x] !== '|')
1520+
|| (is_array($tokens[$x]) === true && isset($allowed[$tokens[$x][0]]) === false)
1521+
) {
15181522
break;
1519-
} else if ($tokens[$x][0] === T_ARRAY) {
1523+
} else if (is_array($tokens[$x]) === true && $tokens[$x][0] === T_ARRAY) {
15201524
$tokens[$x][0] = T_STRING;
15211525

15221526
if (PHP_CODESNIFFER_VERBOSITY > 1) {
@@ -1998,6 +2002,7 @@ protected function processAdditional()
19982002
T_PARENT => T_PARENT,
19992003
T_SELF => T_SELF,
20002004
T_STATIC => T_STATIC,
2005+
T_TYPE_UNION => T_TYPE_UNION,
20012006
];
20022007

20032008
$closer = $this->tokens[$x]['parenthesis_closer'];
@@ -2178,6 +2183,177 @@ protected function processAdditional()
21782183
}
21792184
}
21802185

2186+
continue;
2187+
} else if ($this->tokens[$i]['code'] === T_BITWISE_OR) {
2188+
/*
2189+
Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR.
2190+
*/
2191+
2192+
$allowed = [
2193+
T_STRING => T_STRING,
2194+
T_CALLABLE => T_CALLABLE,
2195+
T_SELF => T_SELF,
2196+
T_PARENT => T_PARENT,
2197+
T_STATIC => T_STATIC,
2198+
T_FALSE => T_FALSE,
2199+
T_NULL => T_NULL,
2200+
T_NS_SEPARATOR => T_NS_SEPARATOR,
2201+
];
2202+
2203+
$suspectedType = null;
2204+
$typeTokenCount = 0;
2205+
2206+
for ($x = ($i + 1); $x < $numTokens; $x++) {
2207+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
2208+
continue;
2209+
}
2210+
2211+
if (isset($allowed[$this->tokens[$x]['code']]) === true) {
2212+
++$typeTokenCount;
2213+
continue;
2214+
}
2215+
2216+
if ($typeTokenCount > 0
2217+
&& ($this->tokens[$x]['code'] === T_BITWISE_AND
2218+
|| $this->tokens[$x]['code'] === T_ELLIPSIS)
2219+
) {
2220+
// Skip past reference and variadic indicators for parameter types.
2221+
++$x;
2222+
continue;
2223+
}
2224+
2225+
if ($this->tokens[$x]['code'] === T_VARIABLE) {
2226+
// Parameter/Property defaults can not contain variables, so this could be a type.
2227+
$suspectedType = 'property or parameter';
2228+
break;
2229+
}
2230+
2231+
if ($this->tokens[$x]['code'] === T_DOUBLE_ARROW) {
2232+
// Possible arrow function.
2233+
$suspectedType = 'return';
2234+
break;
2235+
}
2236+
2237+
if ($this->tokens[$x]['code'] === T_SEMICOLON) {
2238+
// Possible abstract method or interface method.
2239+
$suspectedType = 'return';
2240+
break;
2241+
}
2242+
2243+
if ($this->tokens[$x]['code'] === T_OPEN_CURLY_BRACKET
2244+
&& isset($this->tokens[$x]['scope_condition']) === true
2245+
&& $this->tokens[$this->tokens[$x]['scope_condition']]['code'] === T_FUNCTION
2246+
) {
2247+
$suspectedType = 'return';
2248+
}
2249+
2250+
break;
2251+
}//end for
2252+
2253+
if ($typeTokenCount === 0 || isset($suspectedType) === false) {
2254+
// Definitely not a union type, move on.
2255+
continue;
2256+
}
2257+
2258+
$typeTokenCount = 0;
2259+
$unionOperators = [$i];
2260+
$confirmed = false;
2261+
2262+
for ($x = ($i - 1); $x >= 0; $x--) {
2263+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
2264+
continue;
2265+
}
2266+
2267+
if (isset($allowed[$this->tokens[$x]['code']]) === true) {
2268+
++$typeTokenCount;
2269+
continue;
2270+
}
2271+
2272+
// Union types can't use the nullable operator, but be tolerant to parse errors.
2273+
if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) {
2274+
continue;
2275+
}
2276+
2277+
if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
2278+
$unionOperators[] = $x;
2279+
continue;
2280+
}
2281+
2282+
if ($suspectedType === 'return' && $this->tokens[$x]['code'] === T_COLON) {
2283+
$confirmed = true;
2284+
break;
2285+
}
2286+
2287+
if ($suspectedType === 'property or parameter'
2288+
&& (isset(Util\Tokens::$scopeModifiers[$this->tokens[$x]['code']]) === true
2289+
|| $this->tokens[$x]['code'] === T_VAR)
2290+
) {
2291+
// This will also confirm constructor property promotion parameters, but that's fine.
2292+
$confirmed = true;
2293+
}
2294+
2295+
break;
2296+
}//end for
2297+
2298+
if ($confirmed === false
2299+
&& $suspectedType === 'property or parameter'
2300+
&& isset($this->tokens[$i]['nested_parenthesis']) === true
2301+
) {
2302+
$parens = $this->tokens[$i]['nested_parenthesis'];
2303+
$last = end($parens);
2304+
2305+
if (isset($this->tokens[$last]['parenthesis_owner']) === true
2306+
&& $this->tokens[$this->tokens[$last]['parenthesis_owner']]['code'] === T_FUNCTION
2307+
) {
2308+
$confirmed = true;
2309+
} else {
2310+
// No parenthesis owner set, this may be an arrow function which has not yet
2311+
// had additional processing done.
2312+
if (isset($this->tokens[$last]['parenthesis_opener']) === true) {
2313+
for ($x = ($this->tokens[$last]['parenthesis_opener'] - 1); $x >= 0; $x--) {
2314+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
2315+
continue;
2316+
}
2317+
2318+
break;
2319+
}
2320+
2321+
if ($this->tokens[$x]['code'] === T_FN) {
2322+
for (--$x; $x >= 0; $x--) {
2323+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true
2324+
|| $this->tokens[$x]['code'] === T_BITWISE_AND
2325+
) {
2326+
continue;
2327+
}
2328+
2329+
break;
2330+
}
2331+
2332+
if ($this->tokens[$x]['code'] !== T_FUNCTION) {
2333+
$confirmed = true;
2334+
}
2335+
}
2336+
}//end if
2337+
}//end if
2338+
2339+
unset($parens, $last);
2340+
}//end if
2341+
2342+
if ($confirmed === false) {
2343+
// Not a union type after all, move on.
2344+
continue;
2345+
}
2346+
2347+
foreach ($unionOperators as $x) {
2348+
$this->tokens[$x]['code'] = T_TYPE_UNION;
2349+
$this->tokens[$x]['type'] = 'T_TYPE_UNION';
2350+
2351+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2352+
$line = $this->tokens[$x]['line'];
2353+
echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
2354+
}
2355+
}
2356+
21812357
continue;
21822358
} else if ($this->tokens[$i]['code'] === T_STATIC) {
21832359
for ($x = ($i - 1); $x > 0; $x--) {

src/Util/Tokens.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
define('T_ZSR', 'PHPCS_T_ZSR');
7676
define('T_ZSR_EQUAL', 'PHPCS_T_ZSR_EQUAL');
7777
define('T_FN_ARROW', 'T_FN_ARROW');
78+
define('T_TYPE_UNION', 'T_TYPE_UNION');
7879

7980
// Some PHP 5.5 tokens, replicated for lower versions.
8081
if (defined('T_FINALLY') === false) {

tests/Core/File/GetMemberPropertiesTest.inc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,50 @@ class NSOperatorInType {
193193
/* testNamespaceOperatorTypeHint */
194194
public ?namespace\Name $prop;
195195
}
196+
197+
$anon = class() {
198+
/* testPHP8UnionTypesSimple */
199+
public int|float $unionTypeSimple;
200+
201+
/* testPHP8UnionTypesTwoClasses */
202+
private MyClassA|\Package\MyClassB $unionTypesTwoClasses;
203+
204+
/* testPHP8UnionTypesAllBaseTypes */
205+
protected array|bool|int|float|NULL|object|string $unionTypesAllBaseTypes;
206+
207+
/* testPHP8UnionTypesAllPseudoTypes */
208+
// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
209+
var false|mixed|self|parent|iterable|Resource $unionTypesAllPseudoTypes;
210+
211+
/* testPHP8UnionTypesIllegalTypes */
212+
// Intentional fatal error - types which are not allowed for properties, but that's not the concern of the method.
213+
public callable|static|void $unionTypesIllegalTypes;
214+
215+
/* testPHP8UnionTypesNullable */
216+
// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
217+
public ?int|float $unionTypesNullable;
218+
219+
/* testPHP8PseudoTypeNull */
220+
// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
221+
public null $pseudoTypeNull;
222+
223+
/* testPHP8PseudoTypeFalse */
224+
// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
225+
public false $pseudoTypeFalse;
226+
227+
/* testPHP8PseudoTypeFalseAndBool */
228+
// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
229+
public bool|FALSE $pseudoTypeFalseAndBool;
230+
231+
/* testPHP8ObjectAndClass */
232+
// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
233+
public object|ClassName $objectAndClass;
234+
235+
/* testPHP8PseudoTypeIterableAndArray */
236+
// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
237+
public iterable|array|Traversable $pseudoTypeIterableAndArray;
238+
239+
/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
240+
// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
241+
public int |string| /*comment*/ INT $duplicateTypeInUnion;
242+
};

0 commit comments

Comments
 (0)