simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; $this->arrayDimFetchTypeResolver = $arrayDimFetchTypeResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->typeFactory = $typeFactory; } /** * @return array */ public function resolveFetchedPropertiesToTypesFromClass(Class_ $class) : array { $fetchedLocalPropertyNameToTypes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($class->getMethods(), function (Node $node) use(&$fetchedLocalPropertyNameToTypes) : ?int { if ($this->shouldSkip($node)) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Assign && ($node->var instanceof PropertyFetch || $node->var instanceof ArrayDimFetch)) { $propertyFetch = $node->var; $propertyName = $this->resolvePropertyName($propertyFetch instanceof ArrayDimFetch ? $propertyFetch->var : $propertyFetch); if ($propertyName === null) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($propertyFetch instanceof ArrayDimFetch) { $fetchedLocalPropertyNameToTypes[$propertyName][] = $this->arrayDimFetchTypeResolver->resolve($propertyFetch, $node); return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $fetchedLocalPropertyNameToTypes[$propertyName][] = $this->nodeTypeResolver->getType($node->expr); return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $propertyName = $this->resolvePropertyName($node); if ($propertyName === null) { return null; } $fetchedLocalPropertyNameToTypes[$propertyName][] = new MixedType(); return null; }); return $this->normalizeToSingleType($fetchedLocalPropertyNameToTypes); } private function shouldSkip(Node $node) : bool { // skip anonymous classes and inner function if ($node instanceof Class_ || $node instanceof Function_) { return \true; } // skip closure call if ($node instanceof MethodCall && $node->var instanceof Closure) { return \true; } if ($node instanceof StaticCall) { return $this->nodeNameResolver->isName($node->class, self::LARAVEL_COLLECTION_CLASS); } return \false; } private function resolvePropertyName(Node $node) : ?string { if (!$node instanceof PropertyFetch) { return null; } if (!$this->propertyFetchAnalyzer->isLocalPropertyFetch($node)) { return null; } if ($this->shouldSkipPropertyFetch($node)) { return null; } return $this->nodeNameResolver->getName($node->name); } private function shouldSkipPropertyFetch(PropertyFetch $propertyFetch) : bool { if ($this->isPartOfClosureBind($propertyFetch)) { return \true; } return $propertyFetch->name instanceof Variable; } /** * @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; } /** * 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(PropertyFetch $propertyFetch) : bool { $scope = $propertyFetch->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return \false; } return $scope->isInClosureBind(); } }