From f5f44c338ad1b8621d76f4ba0fcdb59e452f0350 Mon Sep 17 00:00:00 2001 From: Dextheking1 Date: Wed, 23 Sep 2026 19:28:32 +0200 Subject: [PATCH] fix: keep parens around new with member access when PHP version is unknown With the default phpVersion "auto" and no composer.json, the plugin guessed the latest PHP version and stripped parentheses from `(new X)->...`, emitting code that is a parse error on PHP < 8.4 (see #2440). resolvePhpVersion now records when the version is only a guess, and needs-parens stays conservative in that case: parentheses are kept unless the target is known to be PHP >= 8.4 (explicit option or composer.json). Fixes #2440 --- src/needs-parens.mjs | 7 ++- src/options.mjs | 9 ++- .../assign/__snapshots__/jsfmt.spec.mjs.snap | 6 +- .../__snapshots__/jsfmt.spec.mjs.snap | 6 +- .../__snapshots__/jsfmt.spec.mjs.snap | 18 +++--- tests/new/__snapshots__/jsfmt.spec.mjs.snap | 22 +++++-- tests/new/new.php | 3 + .../parens/__snapshots__/jsfmt.spec.mjs.snap | 62 +++++++++---------- .../__snapshots__/jsfmt.spec.mjs.snap | 4 +- 9 files changed, 81 insertions(+), 56 deletions(-) diff --git a/src/needs-parens.mjs b/src/needs-parens.mjs index 2bd051fc0..1fa215282 100644 --- a/src/needs-parens.mjs +++ b/src/needs-parens.mjs @@ -128,9 +128,14 @@ function needsParens(path, options) { } case "clone": case "new": { + // Parentheses around `new` with member access are only optional on + // PHP >= 8.4. When the PHP version is just a guess (default "auto" + // without a composer.json), stay conservative and keep them so the + // output stays valid on older PHP versions (see #2440). const requiresParens = node.kind === "clone" || - (node.kind === "new" && options.phpVersion < 8.4); + (node.kind === "new" && + (options.phpVersion < 8.4 || options.phpVersionIsGuessed)); switch (parent.kind) { case "propertylookup": case "nullsafepropertylookup": diff --git a/src/options.mjs b/src/options.mjs index e650f85ad..34a423ab9 100644 --- a/src/options.mjs +++ b/src/options.mjs @@ -87,8 +87,13 @@ export function resolvePhpVersion(options) { return; } if (options.phpVersion === "auto") { - options.phpVersion = - getComposerPhpVersion() ?? LATEST_SUPPORTED_PHP_VERSION; + const composerVersion = getComposerPhpVersion(); + options.phpVersion = composerVersion ?? LATEST_SUPPORTED_PHP_VERSION; + // Without a composer.json pinning a PHP version, "auto" falls back to + // the latest supported version, which is only a guess. Remember that + // so the printer stays conservative and never emits syntax that is + // invalid on older PHP versions (see #2440). + options.phpVersionIsGuessed = composerVersion === null; } else if (options.phpVersion === "composer") { const v = getComposerPhpVersion(); if (v === null) { diff --git a/tests/assign/__snapshots__/jsfmt.spec.mjs.snap b/tests/assign/__snapshots__/jsfmt.spec.mjs.snap index 78d9bd542..b9d4aef69 100644 --- a/tests/assign/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/assign/__snapshots__/jsfmt.spec.mjs.snap @@ -233,7 +233,7 @@ $veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariable = $veryVeryVeryVeryVer $obj->property = $obj->oneProperty = $obj->twoPeroperty = 0; $obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = $obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = $obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = 0; $obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = $obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = $obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = $veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongValue; -$obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = new MyClass()->call( +$obj->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongProperty = (new MyClass())->call( $arg, ); $obj->loooooooooooong->lookup = $this->getRequest()->getParam( @@ -251,7 +251,7 @@ $component = Foo::$insertReallyReallyReallyReallyReallyReallyReallyReallyLongName; $component = $a::$insertReallyReallyReallyReallyReallyReallyReallyReallyLongName; -$component = new Foo() +$component = (new Foo()) ->insertReallyReallyReallyReallyReallyReallyReallyReallyLongName; $component = (clone $a) ->insertReallyReallyReallyReallyReallyReallyReallyReallyLongName; @@ -266,7 +266,7 @@ $component = Foo::$insertReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongName; $component = $a::$insertReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongName; -$component = new Foo() +$component = (new Foo()) ->insertReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongName; $component = (clone $a) ->insertReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongName; diff --git a/tests/comments/__snapshots__/jsfmt.spec.mjs.snap b/tests/comments/__snapshots__/jsfmt.spec.mjs.snap index 5adfe5f8c..fed543709 100644 --- a/tests/comments/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/comments/__snapshots__/jsfmt.spec.mjs.snap @@ -6590,16 +6590,16 @@ $inspect = $condition $extractTextPluginOptions = $shouldUseRelativeAssetPaths ? // Making sure that the publicPath goes back to to build folder. - ["publicPath" => new Foo($cssFilename->split("/")->length)->join("../")] + ["publicPath" => (new Foo($cssFilename->split("/")->length))->join("../")] : []; $extractTextPluginOptions = $shouldUseRelativeAssetPaths ? // Making sure that the publicPath goes back to to build folder. - ["publicPath" => new Foo($cssFilename->split("/")->length)->join("../")] + ["publicPath" => (new Foo($cssFilename->split("/")->length))->join("../")] : []; $extractTextPluginOptions = $shouldUseRelativeAssetPaths // Making sure that the publicPath goes back to to build folder. - ? ["publicPath" => new Foo($cssFilename->split("/")->length)->join("../")] + ? ["publicPath" => (new Foo($cssFilename->split("/")->length))->join("../")] : []; $var = diff --git a/tests/member_chain/__snapshots__/jsfmt.spec.mjs.snap b/tests/member_chain/__snapshots__/jsfmt.spec.mjs.snap index 4d8631650..548a8e419 100644 --- a/tests/member_chain/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/member_chain/__snapshots__/jsfmt.spec.mjs.snap @@ -87,10 +87,10 @@ $superSupersuperSupersuperSupersuperSupersuperSuperLong->{$exampleOfOrderOfGette $superSupersuperSupersuperSupersuperSupersuperSuperLong::$exampleOfOrderOfGetterAndSetterReordered; $superSupersuperSupersuperSupersuperSupersuperSuperLong ::$exampleOfOrderOfGetterAndSetterReordered[0]; -$superSupersuperSupersuperSupersuperSupersuperSuperLong = new SuperSuperSuperSuperSuperSuperLongClassName() +$superSupersuperSupersuperSupersuperSupersuperSuperLong = (new SuperSuperSuperSuperSuperSuperLongClassName()) ->some_very_long_member_expression; new $superSupersuperSupersuperSupersuperSupersuperSuperLong->superSupersuperSupersuperSupersuperSupersuperSuperLong->superSupersuperSupersuperSupersuperSupersuperSuperLong(); -new $superSupersuperSupersuperSupersuperSupersuperSuperLong->superSupersuperSupersuperSupersuperSupersuperSuperLong->superSupersuperSupersuperSupersuperSupersuperSuperLong() +(new $superSupersuperSupersuperSupersuperSupersuperSuperLong->superSupersuperSupersuperSupersuperSupersuperSuperLong->superSupersuperSupersuperSupersuperSupersuperSuperLong()) ->superSupersuperSupersuperSupersuperSupersuperSuperLong; $test = $superSupersuperSupersuperSupersuperSupersuperSuperLong->exampleOfOrderOfGetterAndSetterReordered; @@ -241,7 +241,7 @@ $object[ : $helper->responseBody($defaultUser) ]->map(); -new TestClassWithReallyReallyReallyReallyReallyReallyReallyReallyReallyLongName() +(new TestClassWithReallyReallyReallyReallyReallyReallyReallyReallyReallyLongName()) ->map() ->filter(); @@ -565,14 +565,14 @@ $brian->hotel->orders()->ordered()->with("smith")->get(); $brian::$hotel->orders()->ordered()->with("smith")->get(); $brian["hotel"]->orders()->ordered()->with("smith")->get(); Foo::$hotel->orders()->ordered()->with("smith")->get(); -new Foo()->hotel->orders()->ordered()->with("smith")->get(); +(new Foo())->hotel->orders()->ordered()->with("smith")->get(); (clone $a)->hotel->orders()->ordered()->with("smith")->get(); $var = $brian->hotel->orders()->ordered()->with("smith")->get(); $var = $brian::$hotel->orders()->ordered()->with("smith")->get(); $var = $brian["hotel"]->orders()->ordered()->with("smith")->get(); $var = Foo::$hotel->orders()->ordered()->with("smith")->get(); -$var = new Foo()->hotel->orders()->ordered()->with("smith")->get(); +$var = (new Foo())->hotel->orders()->ordered()->with("smith")->get(); $var = (clone $a)->hotel->orders()->ordered()->with("smith")->get(); $var = Foo::keys($items) @@ -583,10 +583,10 @@ $var = Foo::keys($items) return $x * 2; }); -new static(func_get_args())->push($this)->each(function ($item) { +(new static(func_get_args()))->push($this)->each(function ($item) { VarDumper::dump($item); }); -new static(func_get_args()) +(new static(func_get_args())) ->offset(10) ->push($this) ->each(function ($item) { @@ -812,8 +812,8 @@ $window ->first() ->second(); -new Foo()->call($foo->bar->baz)->first()->second(); -new Foo() +(new Foo())->call($foo->bar->baz)->first()->second(); +(new Foo()) ->call($foo->bar->baz->foo()) ->first() ->second(); diff --git a/tests/new/__snapshots__/jsfmt.spec.mjs.snap b/tests/new/__snapshots__/jsfmt.spec.mjs.snap index 177c147da..51d18a506 100644 --- a/tests/new/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/new/__snapshots__/jsfmt.spec.mjs.snap @@ -17,6 +17,9 @@ $bar = 'MyClassName'; $foo = new $bar; $foo = new $bar(); +// https://github.com/prettier/plugin-php/issues/2440 +$timestamp = (new DateTimeImmutable())->format(DATE_ATOM); + abstract class A { public static function create() @@ -211,6 +214,9 @@ $bar = "MyClassName"; $foo = new $bar(); $foo = new $bar(); +// https://github.com/prettier/plugin-php/issues/2440 +$timestamp = (new DateTimeImmutable())->format(DATE_ATOM); + abstract class A { public static function create() @@ -453,6 +459,9 @@ $bar = 'MyClassName'; $foo = new $bar; $foo = new $bar(); +// https://github.com/prettier/plugin-php/issues/2440 +$timestamp = (new DateTimeImmutable())->format(DATE_ATOM); + abstract class A { public static function create() @@ -647,6 +656,9 @@ $bar = "MyClassName"; $foo = new $bar(); $foo = new $bar(); +// https://github.com/prettier/plugin-php/issues/2440 +$timestamp = (new DateTimeImmutable())->format(DATE_ATOM); + abstract class A { public static function create() @@ -656,12 +668,12 @@ abstract class A } } -$class = new Foo()->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMethod(); -$class = new Foo([ +$class = (new Foo())->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMethod(); +$class = (new Foo([ "VeryVeryVeryVeryVeryVeryVeryVeryVeryLongKey" => "VeryVeryVeryVeryVeryVeryVeryVeryVeryLongValue", -])->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMethod(); -$class = new PendingDispatch(new $this->class(...func_get_args()))->chain( +]))->veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMethod(); +$class = (new PendingDispatch(new $this->class(...func_get_args())))->chain( $this->chain, ); $dumper = in_array(PHP_SAPI, ["cli", "phpdbg"]) @@ -678,7 +690,7 @@ $class = new static( $response = new \\Illuminate\\Http\\JsonResponse( new JsonResponseTestJsonSerializeObject(), ); -$result = new Pipeline(new \\Illuminate\\Container\\Container()) +$result = (new Pipeline(new \\Illuminate\\Container\\Container())) ->send("foo") ->through([new PipelineTestPipeOne()]) ->then(function ($piped) { diff --git a/tests/new/new.php b/tests/new/new.php index a4a16e1f1..efb58c000 100644 --- a/tests/new/new.php +++ b/tests/new/new.php @@ -8,6 +8,9 @@ $foo = new $bar; $foo = new $bar(); +// https://github.com/prettier/plugin-php/issues/2440 +$timestamp = (new DateTimeImmutable())->format(DATE_ATOM); + abstract class A { public static function create() diff --git a/tests/parens/__snapshots__/jsfmt.spec.mjs.snap b/tests/parens/__snapshots__/jsfmt.spec.mjs.snap index 7a00c40a9..de7727794 100644 --- a/tests/parens/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/parens/__snapshots__/jsfmt.spec.mjs.snap @@ -4865,11 +4865,11 @@ $var = $var[0][1]::foo(); $var = $var[0]->foo()->baz; $var = $var[0]->foo()->baz; -$var = new Foo()->bar; -$var = new Foo()::bar; -$var = new Foo()->bar(); -$var = new Foo()::bar(); -$var = new Foo()[1]; +$var = (new Foo())->bar; +$var = (new Foo())::bar; +$var = (new Foo())->bar(); +$var = (new Foo())::bar(); +$var = (new Foo())[1]; $var = $var->bar()(); $var = $var->bar()(); @@ -5181,46 +5181,46 @@ new Translator( c(); -new Foo()->prop; -new Foo()->method(); -new Foo()->$var; +$var = (new Foo())->c(); +(new Foo())->prop; +(new Foo())->method(); +(new Foo())->$var; $var = new class { public function log($msg) { echo $msg; } }; -$var = new foo()->bar(); -$var = new foo()->bar()->foo(); -$var = new foo()->bar()->foo(); -$var = new foo()->bar()->foo(); -$var = new foo()->bar()->foo()[0]; -$var = new foo()->bar()->foo()[0][1]; -$var = new foo()->bar()->foo()->baz(); -$var = new $foo()->bar; -$var = new $bar->y()->x; -new SortOfLongClassName() +$var = (new foo())->bar(); +$var = (new foo())->bar()->foo(); +$var = (new foo())->bar()->foo(); +$var = (new foo())->bar()->foo(); +$var = (new foo())->bar()->foo()[0]; +$var = (new foo())->bar()->foo()[0][1]; +$var = (new foo())->bar()->foo()->baz(); +$var = (new $foo())->bar; +$var = (new $bar->y())->x; +(new SortOfLongClassName()) ->withALongMethodName() ->andAnother() ->toPushItPast80Chars(); -$asdf = new SortOfLongClassName() +$asdf = (new SortOfLongClassName()) ->withALongMethodName() ->andAnother() ->toPushItPast80Chars(); -$var = new foo()[0]; -$var = new foo()[0]["string"]; -$var = new Foo()::foo; -$var = new Foo()::$foo; +$var = (new foo())[0]; +$var = (new foo())[0]["string"]; +$var = (new Foo())::foo; +$var = (new Foo())::$foo; $var = new $a->b(); $var = new $a->b(); -$var = new $a()->b(); -$var = new $a()->b(); -new class {}->foo; -new class {}->foo(); -new class {}(); -new class {}["foo"]; -$var = new class {}->foo; +$var = (new $a())->b(); +$var = (new $a())->b(); +(new class {})->foo; +(new class {})->foo(); +(new class {})(); +(new class {})["foo"]; +$var = (new class {})->foo; ================================================================================ diff --git a/tests/staticlookup/__snapshots__/jsfmt.spec.mjs.snap b/tests/staticlookup/__snapshots__/jsfmt.spec.mjs.snap index 121cb5059..e2ff9cef4 100644 --- a/tests/staticlookup/__snapshots__/jsfmt.spec.mjs.snap +++ b/tests/staticlookup/__snapshots__/jsfmt.spec.mjs.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`single.php 1`] = ` ====================================options===================================== @@ -344,7 +344,7 @@ $var = $var[0]::foo; $var = $var[0][1]::foo; $var = $var[0][1]::foo; $var = $var[0][1]::foo; -$var = new Foo()::bar; +$var = (new Foo())::bar; $var = Foo::$bar["baz"](); $var = Foo::$bar["baz"](); $var = Foo::{$bar["baz"]}();