Skip to content

Commit c758819

Browse files
committed
✨ PHP 8.1: New PHPCompatibility.InitialValue.NewNewInInitializers sniff
> `new` in Initializers > > It is now possible to use `new ClassName()` expressions as the default value of a parameter, static variable, global constant initializers, and as attribute arguments. Refs: * https://wiki.php.net/rfc/new_in_initializers * https://www.php.net/manual/en/migration81.new-features.php#migration81.new-features.core.new-in-initializer * php/php-src#7153 * php/php-src@52d3d0d Includes unit tests.
1 parent 2d98eee commit c758819

File tree

3 files changed

+460
-0
lines changed

3 files changed

+460
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
/**
3+
* PHPCompatibility, an external standard for PHP_CodeSniffer.
4+
*
5+
* @package PHPCompatibility
6+
* @copyright 2012-2020 PHPCompatibility Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCompatibility/PHPCompatibility
9+
*/
10+
11+
namespace PHPCompatibility\Sniffs\InitialValue;
12+
13+
use PHPCompatibility\AbstractInitialValueSniff;
14+
use PHP_CodeSniffer\Files\File;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
use PHPCSUtils\Tokens\Collections;
17+
use PHPCSUtils\Utils\MessageHelper;
18+
use PHPCSUtils\Utils\Parentheses;
19+
use PHPCSUtils\Utils\Scopes;
20+
21+
/**
22+
* Detect object instantiation in initial values as allowed per PHP 8.1.
23+
*
24+
* As of PHP 8.1, `new` expressions are allowed in parameter default values,
25+
* attribute arguments, static variable initializers and global class constant initializers.
26+
* Parameter default values also include defaults for promoted properties.
27+
*
28+
* The use of a dynamic or non-string class name or an anonymous class is not allowed.
29+
* The use of argument unpacking is not allowed. The use of unsupported expressions as arguments is not allowed.
30+
*
31+
* PHP version 8.1
32+
*
33+
* @link https://wiki.php.net/rfc/new_in_initializers
34+
* @link https://www.php.net/manual/en/migration81.new-features.php#migration81.new-features.core.new-in-initializer
35+
*
36+
* @since 10.0.0
37+
*/
38+
final class NewNewInInitializersSniff extends AbstractInitialValueSniff
39+
{
40+
41+
/**
42+
* Error message.
43+
*
44+
* @since 10.0.0
45+
*
46+
* @var string
47+
*/
48+
const ERROR_PHRASE = 'New in initializers is not supported in PHP 8.0 or earlier for %s.';
49+
50+
/**
51+
* Partial error phrases to be used in combination with the error message constant.
52+
*
53+
* @since 10.0.0.
54+
*
55+
* @var array
56+
*/
57+
protected $initialValueTypes = [
58+
'const' => 'global/namespaced constants declared using the const keyword',
59+
'property' => '', // Not supported.
60+
'staticvar' => 'static variables',
61+
'default' => 'default parameter values',
62+
];
63+
64+
/**
65+
* Do a version check to determine if this sniff needs to run at all.
66+
*
67+
* @since 10.0.0
68+
*
69+
* @return bool
70+
*/
71+
protected function bowOutEarly()
72+
{
73+
return ($this->supportsBelow('8.0') === false);
74+
}
75+
76+
/**
77+
* Process a token which has an initial value.
78+
*
79+
* @since 10.0.0
80+
*
81+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
82+
* @param int $stackPtr The position of the variable/constant name token
83+
* in the stack passed in $tokens.
84+
* @param int $start The stackPtr to the start of the initial value.
85+
* @param int $end The stackPtr to the end of the initial value.
86+
* This will normally be a comma or semi-colon.
87+
* @param string $type The "type" of initial value declaration being examined.
88+
* The type will match one of the keys in the
89+
* `AbstractInitialValueSniff::$initialValueTypes` property.
90+
*
91+
* @return void
92+
*/
93+
protected function processInitialValue(File $phpcsFile, $stackPtr, $start, $end, $type)
94+
{
95+
if ($type === 'property'
96+
|| ($type === 'const'
97+
&& Scopes::validDirectScope($phpcsFile, $stackPtr, Collections::ooConstantScopes()) !== false)
98+
) {
99+
// New is (still) not allowed in OO constants or properties.
100+
return;
101+
}
102+
103+
$tokens = $phpcsFile->getTokens();
104+
105+
$targetNestingLevel = 0;
106+
if (isset($tokens[$start]['nested_parenthesis'])) {
107+
$targetNestingLevel = \count($tokens[$start]['nested_parenthesis']);
108+
}
109+
110+
$error = self::ERROR_PHRASE;
111+
$errorCode = 'Found';
112+
$phrase = '';
113+
114+
if (isset($this->initialValueTypes[$type]) === true) {
115+
$errorCode = MessageHelper::stringToErrorCode($type) . 'Found';
116+
$phrase = $this->initialValueTypes[$type];
117+
}
118+
119+
$data = [$phrase];
120+
121+
$allowedNameTokens = Collections::namespacedNameTokens();
122+
$allowedNameTokens[\T_SELF] = \T_SELF;
123+
$allowedNameTokens[\T_PARENT] = \T_PARENT;
124+
125+
$current = $start;
126+
while (($hasNew = $phpcsFile->findNext(\T_NEW, $current, $end)) !== false) {
127+
// Handle nesting within arrays.
128+
$currentNestingLevel = 0;
129+
if (isset($tokens[$hasNew]['nested_parenthesis'])) {
130+
foreach ($tokens[$hasNew]['nested_parenthesis'] as $opener => $closer) {
131+
// Always count outer parentheses.
132+
if ($opener < $start) {
133+
++$currentNestingLevel;
134+
continue;
135+
}
136+
137+
// Only count inner parentheses when they are not for an array.
138+
$owner = Parentheses::getOwner($phpcsFile, $opener);
139+
if ($owner === false || $tokens[$owner]['code'] !== \T_ARRAY) {
140+
++$currentNestingLevel;
141+
}
142+
}
143+
}
144+
145+
$current = ($hasNew + 1);
146+
147+
if ($currentNestingLevel !== $targetNestingLevel) {
148+
continue;
149+
}
150+
151+
// Only throw an error if this is a non-dynamic object instantiation. Dynamic is still not supported.
152+
$isNameInvalid = $phpcsFile->findNext($allowedNameTokens + Tokens::$emptyTokens, ($hasNew + 1), $end, true);
153+
$hasValidNameToken = $phpcsFile->findNext($allowedNameTokens, ($hasNew + 1), $end);
154+
155+
if ($hasValidNameToken !== false
156+
&& ($isNameInvalid === false
157+
|| ($tokens[$isNameInvalid]['code'] === \T_OPEN_PARENTHESIS && $hasValidNameToken < $isNameInvalid))
158+
) {
159+
$phpcsFile->addError($error, $hasNew, $errorCode, $data);
160+
return;
161+
}
162+
}
163+
}
164+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
/*
4+
* Valid cross-version.
5+
*/
6+
const NON_OO_CONST = Foo::CONST_NAME;
7+
8+
function containsStaticVars() {
9+
static $var = Foo::class, $bar = 'static value';
10+
static $boo = [];
11+
}
12+
13+
function bar($a, $b = (10 + 5)) {}
14+
$cl = function ($a = true) {};
15+
$ar = fn (Foo $a = null) => $a;
16+
17+
18+
/*
19+
* Not our targets.
20+
*/
21+
$closure = static function() { return new Foo; };
22+
$fn = static fn() => new Foo;
23+
24+
class Something {
25+
public function foo() {
26+
$bool = new Foo() instanceof static;
27+
}
28+
}
29+
30+
$var = new Foo();
31+
32+
33+
/*
34+
* PHP 8.1 new in initializers.
35+
*/
36+
37+
// Non-OO constants declared using the `const` keyword.
38+
const NON_OO_CONST = new Foo();
39+
const NON_OO_CONST_LONG_ARRAY = [new Foo()];
40+
const NON_OO_CONST_SHORT_ARRAY = array(new Foo());
41+
42+
// Static variable declarations.
43+
function containsStaticVars() {
44+
static $var = new Foo(1), $bar = new \Fully\Qualified('static value'); // x2.
45+
static $boo = [new Partially\Qualified()];
46+
}
47+
48+
// Default values for function declarations in all forms.
49+
function bar($a, $b = new Foo()) {}
50+
$array = array(
51+
'k1' => function ($a, $b = new Foo()) {},
52+
'k2' => function ($a, $b = array(new Foo())) {},
53+
'k3' => function ($a, $b = [new Foo()]) {},
54+
'k4' => function ($a, $b = array(array(new Foo()))) {},
55+
);
56+
$ar = fn (Foo $a = new Foo()) => $a;
57+
58+
class ClassMethodsCanUseNew extends Something {
59+
public function __construct(
60+
public Foo $constructorProp = new Foo(),
61+
$normalVar = new self(x: 2),
62+
) {}
63+
64+
private function bar($a = new parent()) {}
65+
}
66+
67+
$anon = new class {
68+
public function __construct(
69+
public Foo $constructorProp = new Foo(),
70+
$normalVar = new Bar(),
71+
) {}
72+
73+
private function bar($a = new Foo()) {}
74+
};
75+
76+
interface InterfaceMethodsCanUseNew {
77+
public function __construct(
78+
$normalVar = new Bar,
79+
);
80+
81+
public function bar($a = new Foo());
82+
}
83+
84+
trait TraitMethodsCanUseNew {
85+
public function __construct(
86+
public Foo $constructorProp = new Foo,
87+
$normalVar = new Bar(),
88+
) {}
89+
90+
private function bar($a = new Foo()) {}
91+
}
92+
93+
enum EnumMethodsCanUseNew: string {
94+
case Example = 'test';
95+
private function bar($a = new Foo) {}
96+
}
97+
98+
99+
/*
100+
* Still not supported. Flag anyway.
101+
*/
102+
function bar($a, $b = new Foo($bar)) {}
103+
$cl = function ($a = new namespace\Foo(function_call())) {};
104+
105+
function NewUsingUnsupportedArguments(
106+
$c = new A(...[]), // argument unpacking
107+
$d = new B($abc), // unsupported constant expression
108+
) {}
109+
110+
111+
/*
112+
* Still not supported. Ignore.
113+
*/
114+
// `new` is not supported in OO constants or properties.
115+
class StillNotSupported {
116+
const MINE = new Foo();
117+
public $prop = new Foo();
118+
}
119+
120+
$anon = new class {
121+
const MINE = new Foo();
122+
public $prop = new Foo();
123+
};
124+
125+
interface InterfaceStillNotSupported {
126+
const MINE = new Foo();
127+
}
128+
129+
trait TraitStillNotSupported {
130+
const MINE = new Foo;
131+
public $prop = new Foo();
132+
}
133+
134+
enum EnumStillNotSupported: string {
135+
const MINE = new Foo;
136+
}
137+
138+
// `new` using a dynamic of non-string class name or anonymous class is not allowed.
139+
function DynamicClassNameNotAllowed(
140+
$a = new (CLASS_NAME_CONSTANT)(), // dynamic class name
141+
$b = new $className(), // dynamic class name
142+
$c = new class {}, // anonymous class
143+
$d = new static() {}, // dynamic class name
144+
) {}
145+
146+
// New at wrong nesting level, but function calls are not supported, so ignore.
147+
$cl = function ($a = function_call(new namespace\Foo)) {};

0 commit comments

Comments
 (0)