Skip to content

Report new on an expression whose type is not string|object via new.nonObject#5866

Open
phpstan-bot wants to merge 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-9u2kw5i
Open

Report new on an expression whose type is not string|object via new.nonObject#5866
phpstan-bot wants to merge 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-9u2kw5i

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

PHPStan did not detect when the new operator is used with an expression whose type can never be a valid class name. For example new $class where $class is int was silently accepted, even though it always fails at runtime. This PR makes InstantiationRule report such cases with a new new.nonObject error, mirroring the existing checks for the analogous dynamic-class-name constructs.

Changes

  • src/Rules/Classes/InstantiationRule.php
    • Injected RuleLevelHelper and added checkClassNameExprType(), run for new $expr (when the class is an expression).
    • It uses findTypeToCheck() against the accepted type string|object (new UnionType([new StringType(), new ObjectWithoutClassType()])) and reports Cannot instantiate class using <type>. with identifier new.nonObject when the type is not a supertype-compatible string/object.
    • ErrorType results (mixed/never/unknown class) are intentionally skipped so the existing class.notFound ("Instantiated class X not found.") path keeps working for object values of unknown classes.
  • src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php
    • Constant-fold base64_decode() when the input is a constant string, returning the decoded ConstantStringType (or false in strict mode for invalid base64). The decode always runs in strict mode internally; for a non-strict call on invalid input the extension falls back to the generic string rather than guessing the lenient result.
  • Test updates:
    • New tests/PHPStan/Rules/Classes/data/instantiation-non-object.php + testInstantiationWithNonObjectType() in InstantiationRuleTest.
    • Updated both InstantiationRule test factories (InstantiationRuleTest, ForbiddenNameCheckExtensionRuleTest) for the new constructor argument.
    • Extended tests/PHPStan/Analyser/nsrt/base64_decode.php with constant-input cases and adjusted the constant '' expectations in tests/PHPStan/Analyser/nsrt/functions.php.

Root cause

InstantiationRule::getClassNames() only produced class names from constant strings and object types; for any other expression type it returned an empty list, so no error was ever emitted for new $wrongType. The fix adds an explicit type check for the expression form of new, using the same string|object acceptance criterion that the parallel rules already use.

Parallel-construct audit (dynamic class name in a class-name context): static method call (staticMethod.nonObject), static property access (staticProperty.nonObject), class constant fetch (classConstant.nonObject) and instanceof (instanceof.invalidExprType) already report wrong dynamic class types. new was the single missing member of that family, now covered.

The base64_decode() constant-folding is required because PHPStan's own source instantiates a base64-obfuscated adapter class name (new $enumAdapter(...) in NodeScopeResolver and BetterReflectionProvider). Without folding, that value is string|false, which the new rule correctly flags. Folding resolves it to the precise class-string, which both satisfies the new rule and lets the surrounding ClassReflectionFactory::create() call stay well-typed — i.e. teaching inference rather than rewriting the source.

Test

  • InstantiationRuleTest::testInstantiationWithNonObjectType() asserts new.nonObject is reported for new $int, new $float, new $bool, new $intOrString and new $array, and that valid forms (string, class-string, object, an instance) produce no error. Verified the data file produces no errors before the rule change.
  • tests/PHPStan/Analyser/nsrt/base64_decode.php asserts the new constant-folded return types ('Hello world', false, string).
  • Full suite green (make tests: 17383 tests) and make phpstan shows no new errors (only pre-existing shipmonk.deadMethod findings unrelated to this change).

Fixes phpstan/phpstan#4922

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-9u2kw5i branch from e237c69 to 57a00e8 Compare June 14, 2026 08:14
@VincentLanglet VincentLanglet self-assigned this Jun 14, 2026
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-9u2kw5i branch from 57a00e8 to d391efd Compare June 14, 2026 08:59
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-9u2kw5i branch from d391efd to 9def6e9 Compare June 14, 2026 17:26
Comment thread tests/PHPStan/Rules/Classes/data/instantiation-non-object.php Outdated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be behind a flag which is enabled by bleedingEdge

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. I addressed VincentLanglet's review.

What I changed

The reviewer asked that the new new.nonObject check be behind a flag enabled by bleeding edge rather than always on. I introduced a newOnNonObject feature toggle:

  • conf/parametersSchema.neon — declared newOnNonObject: bool() in the featureToggles structure.
  • conf/config.neon — default newOnNonObject: false (off in stable).
  • conf/bleedingEdge.neonnewOnNonObject: true (on under bleeding edge).
  • src/Rules/Classes/InstantiationRule.php — injected the toggle via #[AutowiredParameter(ref: '%featureToggles.newOnNonObject%')] and gated the checkClassNameExprType() call on $this->newOnNonObject.
  • Both test factories (InstantiationRuleTest, ForbiddenNameCheckExtensionRuleTest) — pass newOnNonObject: true so the existing assertions still exercise the rule.

One gotcha worth noting: after adding a new #[AutowiredParameter], the DI container couldn't resolve it until I regenerated vendor/attributes.php via composer dump-autoload (the olvlvl attribute-collector cache). That file is gitignored, so it isn't part of the commit — CI regenerates it.

Verification

  • InstantiationRuleTest + ForbiddenNameCheckExtensionRuleTest: 46 tests pass (2 environmental skips).
  • make phpstan: no errors (PHPStan analyses itself with bleeding edge on, so the toggle is active there — the base64_decode constant-folding from the original PR is still required and confirmed working).

Committed as d17ff9dc3 and pushed to create-pull-request/patch-9u2kw5i.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect wrong type used with new

2 participants