Skip to content

Do not restrict @internal usages of anonymous class members - #6235

Open
peter17 wants to merge 1 commit into
phpstan:2.2.xfrom
peter17:patch4
Open

Do not restrict @internal usages of anonymous class members#6235
peter17 wants to merge 1 commit into
phpstan:2.2.xfrom
peter17:patch4

Conversation

@peter17

@peter17 peter17 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Anonymous classes are not part of any namespace, so the root-namespace comparison in RestrictedInternalUsageHelper always reported their members as used from outside their namespace.

Fixes phpstan/phpstan#13042 (comment)

https://phpstan.org/r/3b58b0a4-f740-4941-a7b3-d74e3564876f

Anonymous classes are not part of any namespace, so the root-namespace
comparison in RestrictedInternalUsageHelper always reported their members
as used from outside their namespace.
@staabm

staabm commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@SanderMuller please review

@SanderMuller

SanderMuller commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Reviewed at 11e3dc5 (base 0d109aa). The fix is correct and in the right place. One behaviour change deserves to be recorded before merge, because it isn't what the description says the PR does.

Fix and tests

  • shouldClassBeReported() is the right home: every class-based caller goes through it, and shouldFunctionBeReported() can never see an anonymous class.
  • Verified the tests fail without the source change: reverting only RestrictedInternalUsageHelper.php produces 5 failures, one per extension (24 errors on the fixture, vs 7 with the fix).
  • Verified the expectations are complete: with all five testBug13042() expectation arrays blanked to [], the reported set is exactly the 7 errors the PR expects, so nothing is hidden behind a partial expectation.
  • 32 tests in tests/PHPStan/Rules/InternalTag + tests/PHPStan/Rules/RestrictedUsage pass, phpcs is clean on the changed helper and test classes (the data fixture is excluded from the standard), and self-analysis (build/phpstan.neon, so with bleeding edge) reports no errors — worth checking here, since a change that only removes errors can leave an ignoreErrors entry unmatched.

The early return also fires on the referenced class

The four static-access rules re-ask the extension with a rewritten declaring class and drop the error when that second answer is null (RestrictedStaticPropertyUsageRule.php:95-101, and the same block in RestrictedStaticMethodUsageRule, RestrictedStaticMethodCallableUsageRule, RestrictedClassConstantUsageRule). So an @internal member inherited from a named class is no longer reported when it is accessed through an anonymous subclass:

namespace Vendor\Library;

class Base
{
	/** @internal */ public static $internalStatic;
	/** @internal */ public const INTERNAL = 'x';
	/** @internal */ public static function internalStaticMethod(): void {}
	/** @internal */ public function internalMethod(): void {}
}

namespace App\Consumer;

use Vendor\Library\Base;

class NamedSubclass extends Base
{
	public function f(): void
	{
		echo self::$internalStatic;      // not reported, before or after
		echo self::INTERNAL;             // not reported, before or after
		self::internalStaticMethod();    // not reported, before or after
		$this->internalMethod();         // reported, before and after
	}
}

function anonSubclass(): object
{
	return new class extends Base {
		public function f(): void
		{
			echo self::$internalStatic;      // was reported, now silent
			echo self::INTERNAL;             // was reported, now silent
			self::internalStaticMethod();    // was reported, now silent
			$this->internalMethod();         // reported, before and after
		}
	};
}

It is the same effect as lines 161, 162 and 164 of the new fixture losing their errors relative to the base.

I think this is defensible rather than a bug. NamedSubclass already gets that exemption — it is what the Rewritten* machinery is for, access through your own subclass — and an anonymous subclass declared in the same namespace is the same situation. In effect the PR makes shouldClassBeReported() model "an anonymous class belongs to the current namespace", and the current namespace is the only place its type is reachable:

  • getNativeReflection()->getNamespaceName() is '' for anonymous classes, so there is no plumbing to gate this on the declaration namespace instead.
  • The type cannot escape to another namespace. Across braced namespace blocks in one file PHPStan does not carry the variable (Variable $x might not be defined, dumped type mixed), and PHP itself rejects const FOO = new class {}; with "Cannot use anonymous class in constant expression".

Two small asks:

  1. Mention it in the PR description. It is the part a future reader will not infer from "anonymous classes are not part of any namespace".
  2. Consider adding the NamedSubclass counterpart above to bug-13042.php, so the silence on 161/162/164 reads as an exemption that deliberately matches named subclasses, rather than as an accident nobody notices when it changes back.

Other notes

  • The fixture makes a pre-existing inconsistency more visible: $this->doInternal() (163) is still reported while self::doInternalStatic() (164) is not, because only the static rules have the rewrite gate. Not this PR's problem.
  • Performance: isAnonymous() is a field null-check on a path only reached once a member or its class is @internal, and internalTag is false in the stable config. No measurable impact.
  • CI: the only red check (Benchmark, bug-11283.php at +136%) also fails on 2.2.x at this PR's base commit 0d109aab9, so it is not attributable to this change.

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.

Access to internal properties in traits causes property.internal

3 participants