simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->classAnalyzer = $classAnalyzer; $this->nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; $this->arrayDimFetchTypeResolver = $arrayDimFetchTypeResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->typeFactory = $typeFactory; } /** * @return array */ public function resolveFetchedPropertiesToTypesFromClass(\PhpParser\Node\Stmt\Class_ $class) : array { $fetchedLocalPropertyNameToTypes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($class->stmts, function (\PhpParser\Node $node) use(&$fetchedLocalPropertyNameToTypes) : ?int { // skip anonymous class scope $isAnonymousClass = $this->classAnalyzer->isAnonymousClass($node); if ($isAnonymousClass) { return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; } if (!$node instanceof \PhpParser\Node\Expr\PropertyFetch) { return null; } if (!$this->propertyFetchAnalyzer->isLocalPropertyFetch($node)) { return null; } if ($this->shouldSkipPropertyFetch($node)) { return null; } $propertyName = $this->nodeNameResolver->getName($node->name); if ($propertyName === null) { return null; } $parentFunctionLike = $this->betterNodeFinder->findParentType($node, \PhpParser\Node\FunctionLike::class); if (!$parentFunctionLike instanceof \PhpParser\Node\Stmt\ClassMethod) { return null; } $propertyFetchType = $this->resolvePropertyFetchType($node); $fetchedLocalPropertyNameToTypes[$propertyName][] = $propertyFetchType; return null; }); return $this->normalizeToSingleType($fetchedLocalPropertyNameToTypes); } private function shouldSkipPropertyFetch(\PhpParser\Node\Expr\PropertyFetch $propertyFetch) : bool { // special Laravel collection scope if ($this->shouldSkipForLaravelCollection($propertyFetch)) { return \true; } if ($this->isPartOfClosureBind($propertyFetch)) { return \true; } if ($propertyFetch->name instanceof \PhpParser\Node\Expr\Variable) { return \true; } return $this->isPartOfClosureBindTo($propertyFetch); } private function resolvePropertyFetchType(\PhpParser\Node\Expr\PropertyFetch $propertyFetch) : \PHPStan\Type\Type { $parentNode = $propertyFetch->getAttribute(\Rector\NodeTypeResolver\Node\AttributeKey::PARENT_NODE); // possible get type if ($parentNode instanceof \PhpParser\Node\Expr\Assign) { return $this->nodeTypeResolver->getType($parentNode->expr); } if ($parentNode instanceof \PhpParser\Node\Expr\ArrayDimFetch) { return $this->arrayDimFetchTypeResolver->resolve($parentNode); } return new \PHPStan\Type\MixedType(); } /** * @param array $propertyNameToTypes * @return array */ private function normalizeToSingleType(array $propertyNameToTypes) : array { // normalize types to union $propertyNameToType = []; foreach ($propertyNameToTypes as $name => $types) { $propertyNameToType[$name] = $this->typeFactory->createMixedPassedOrUnionType($types); } return $propertyNameToType; } private function shouldSkipForLaravelCollection(\PhpParser\Node\Expr\PropertyFetch $propertyFetch) : bool { $staticCallOrClassMethod = $this->betterNodeFinder->findParentByTypes($propertyFetch, [\PhpParser\Node\Stmt\ClassMethod::class, \PhpParser\Node\Expr\StaticCall::class]); if (!$staticCallOrClassMethod instanceof \PhpParser\Node\Expr\StaticCall) { return \false; } return $this->nodeNameResolver->isName($staticCallOrClassMethod->class, self::LARAVEL_COLLECTION_CLASS); } /** * Local property is actually not local one, but belongs to passed object * See https://ocramius.github.io/blog/accessing-private-php-class-members-without-reflection/ */ private function isPartOfClosureBind(\PhpParser\Node\Expr\PropertyFetch $propertyFetch) : bool { $parentStaticCall = $this->betterNodeFinder->findParentType($propertyFetch, \PhpParser\Node\Expr\StaticCall::class); if (!$parentStaticCall instanceof \PhpParser\Node\Expr\StaticCall) { return \false; } if (!$this->nodeNameResolver->isName($parentStaticCall->class, 'Closure')) { return \true; } return $this->nodeNameResolver->isName($parentStaticCall->name, 'bind'); } private function isPartOfClosureBindTo(\PhpParser\Node\Expr\PropertyFetch $propertyFetch) : bool { $parentMethodCall = $this->betterNodeFinder->findParentType($propertyFetch, \PhpParser\Node\Expr\MethodCall::class); if (!$parentMethodCall instanceof \PhpParser\Node\Expr\MethodCall) { return \false; } if (!$parentMethodCall->var instanceof \PhpParser\Node\Expr\Closure) { return \false; } return $this->nodeNameResolver->isName($parentMethodCall->name, 'bindTo'); } }