Skip to content

AddTypeToConst: allow adding type to parent class if child does not override - #5979

Closed
ndench wants to merge 8 commits into
rectorphp:mainfrom
ndench:feat/const-type-inheritance
Closed

ndench wants to merge 8 commits into
rectorphp:mainfrom
ndench:feat/const-type-inheritance

Conversation

@ndench

@ndench ndench commented Jun 19, 2024

Copy link
Copy Markdown
Contributor

I'm aware of the discussion in #8373 with the conclusion being that adding types to constants that could have inheritance applied will create a risky rule.

However, many codebases will have sufficient static analysis and test coverage to counteract this. We could easily support inheritance as a non-default configuration option. This would mean the rule is non-risky by default and provide users the ability to opt in to the risky part.

Comment thread Dockerfile Resolved
* @api
* @var string
*/
final public const IGNORE_INHERITANCE = 'ignore_inheritance';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I prefer more verbose config, like allow_has_child, then, add check FamilyRelationsAnalyzer->getChildrenOfClassReflection() that collect children on the paths() provided in rector.php config, and check if it has constant:

// resolve current class Reflection ....
$classReflection = $this->reflectionResolver->resolveClassReflection($node);
if (! $classReflection instanceof \PHPStan\Reflection\ClassReflection)
    return null;
}

// verify has children with constant
if ($this->allow_has_child) {
    $childClasses = $this->familyRelationsAnalyzer->getChildrenOfClassReflection($classReflection);
    $constName = $this->getName($classConst->name);
    foreach ($childClasses as $childClass) {
        if ($childClass->hasConstant($constName)) {
             continue 2; // don't change, continue to inner loop...
        }
    }
}

Above is pseudo code, but I think you have the idea ;)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the quick feedback @samsonasik!

I'm not entirely sure what you're getting at. Do you want it to only type the constant if a child class does not define the same constant?

@samsonasik samsonasik Jun 19, 2024 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, that means constant overridden, which will make fatal error. After some thinking:

$childClass->hasConstant($constName);

seems will be always true on public or protected constant as it derived from parent, so I think you need to get real Class_ via AstResolver

foreach ($childClasses as $childClass) {
        $classLike = $this->astResolver->resolveClassFromClassReflection($childClass);
        if ($childClassLike instanceof \PhpParser\Node\Stmt\ClassLike) {
            continue;
        }
        
        foreach ($childClassLike->getConstants() as $constant) {
             foreach ($constant->const as $childConst) {
                    if ($this->nodeComparator->areNodesEquals($childConst, $constNode)) {
                          continue 3; // don't change, continue to inner loop...
                    }
             } 
        }
    }

When allow_has_child enabled:

For note: the allow_has_child should be configurable with default false.


On personal note, I prefer the original behaviour for this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another way that might work is by use its ClassConstantReflection object itself, you can add fixture test with both parent and child in same fixture file, then you can check something like this:

foreach ($childClasses as $childClass) {
        if (! $childClass->hasConstant($constName)) {
            continue;
        }
          
        $constantChild =   $childClass->getConstant($constName);
        // debug here, check whether it own by child class or only deriving...
        if ($constantChild->getFileName() !== $classReflection->getFileName()) {
            continue 2; // don't change, continue to inner loop...
      } 
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the help @samsonasik, I've implemented these changes now.

It will now only add the type if:

  • there are no child classes
  • the child classes do not override the constant

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you @ndench, I will check when I have a chance, and verify the possible improvement if needed.

This may not be our priority for now, so this may be here for a while for some thinking and/or discussion with @TomasVotruba in our next meeting, so you can create your custom rule for your needs for now.

@ndench ndench changed the title AddTypeToConst allow ignoring inheritance AddTypeToConst: allow adding type to parent class if child does not override Jun 20, 2024
@ndench
ndench requested a review from samsonasik June 20, 2024 12:35
@samsonasik

Copy link
Copy Markdown
Member

@ndench FYI, the children class detection maybe dropped by PR:

due to performance regression, so this feature won't work ...

@TomasVotruba

Copy link
Copy Markdown
Member

Just looking into this...


$currentClassReflection = $this->reflectionProvider->getClass($className);

return $this->familyRelationsAnalyzer->getChildrenOfClassReflection($currentClassReflection);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the method removed at PR:

so this change no longer valid,

Please rebase. You may can revert to original ignore_inheritance config ;)

@TomasVotruba

Copy link
Copy Markdown
Member

First, thank you for your work on this PR.

Rector has currently access only to classes it has processed, so there is no safe way to decide if any class has child.
See rectorphp/rector#8439 for better description.

Another approach that allowed this had performance issues, rectorphp/rector#8638 (comment), so we had to remove it.

Saying that, we cannot safely detect child classes. It's known drawback based on parallel run, that we use here.
Your proposed feature is not possible to deliver now in reliable and performant way. Closing then.

Also, such situations would be better solved PHPStan. Having a rule to check type consistency across child + parent classes 👍

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically locked because it has been closed for 150 days. Please open a new PR if you want to continue the work.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Oct 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants