diff --git a/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php b/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php index 98040f6d21f..c6931abdb5e 100644 --- a/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php +++ b/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php @@ -26,6 +26,7 @@ use Rector\BetterPhpDocParser\Contract\PhpDocParserAwareInterface; use Rector\BetterPhpDocParser\Printer\MultilineSpaceFormatPreserver; use Rector\BetterPhpDocParser\ValueObject\StartEndValueObject; use Rector\Configuration\CurrentNodeProvider; +use Rector\Exception\ShouldNotHappenException; use Symplify\PackageBuilder\Reflection\PrivatesAccessor; use Symplify\PackageBuilder\Reflection\PrivatesCaller; @@ -75,6 +76,11 @@ final class BetterPhpDocParser extends PhpDocParser */ private $classAnnotationMatcher; + /** + * @var Lexer + */ + private $lexer; + /** * @param PhpDocNodeFactoryInterface[] $phpDocNodeFactories */ @@ -85,6 +91,7 @@ final class BetterPhpDocParser extends PhpDocParser MultilineSpaceFormatPreserver $multilineSpaceFormatPreserver, CurrentNodeProvider $currentNodeProvider, ClassAnnotationMatcher $classAnnotationMatcher, + Lexer $lexer, array $phpDocNodeFactories = [] ) { parent::__construct($typeParser, $constExprParser); @@ -96,6 +103,15 @@ final class BetterPhpDocParser extends PhpDocParser $this->phpDocNodeFactories = $phpDocNodeFactories; $this->currentNodeProvider = $currentNodeProvider; $this->classAnnotationMatcher = $classAnnotationMatcher; + $this->lexer = $lexer; + } + + public function parseString(string $docBlock): PhpDocNode + { + $tokens = $this->lexer->tokenize($docBlock); + $tokenIterator = new TokenIterator($tokens); + + return parent::parse($tokenIterator); } /** @@ -157,6 +173,10 @@ final class BetterPhpDocParser extends PhpDocParser // compare regardless sensitivity $currentPhpNode = $this->currentNodeProvider->getNode(); + if ($currentPhpNode === null) { + continue; + } + if (! $this->isTagMatchingPhpDocNodeFactory($tag, $phpDocNodeFactory, $currentPhpNode)) { continue; } @@ -323,6 +343,10 @@ final class BetterPhpDocParser extends PhpDocParser private function isTagMatchedByFactories(string $tag): bool { $currentPhpNode = $this->currentNodeProvider->getNode(); + if ($currentPhpNode === null) { + throw new ShouldNotHappenException(); + } + foreach ($this->phpDocNodeFactories as $phpDocNodeFactory) { if ($this->isTagMatchingPhpDocNodeFactory($tag, $phpDocNodeFactory, $currentPhpNode)) { return true; diff --git a/packages/NodeTypeResolver/config/phpstan/better-infer.neon b/packages/NodeTypeResolver/config/phpstan/better-infer.neon new file mode 100644 index 00000000000..f21ee13bd78 --- /dev/null +++ b/packages/NodeTypeResolver/config/phpstan/better-infer.neon @@ -0,0 +1,2 @@ +parameters: + inferPrivatePropertyTypeFromConstructor: true diff --git a/packages/NodeTypeResolver/src/DependencyInjection/PHPStanServicesFactory.php b/packages/NodeTypeResolver/src/DependencyInjection/PHPStanServicesFactory.php index b63044c5200..f233a815683 100644 --- a/packages/NodeTypeResolver/src/DependencyInjection/PHPStanServicesFactory.php +++ b/packages/NodeTypeResolver/src/DependencyInjection/PHPStanServicesFactory.php @@ -58,6 +58,10 @@ final class PHPStanServicesFactory } $additionalConfigFiles[] = __DIR__ . '/../../config/phpstan/type-extensions.neon'; + + // enable type inferring from constructor + $additionalConfigFiles[] = __DIR__ . '/../../config/phpstan/better-infer.neon'; + $this->container = $containerFactory->create(sys_get_temp_dir(), $additionalConfigFiles, []); // clear bleeding edge fallback diff --git a/packages/NodeTypeResolver/src/NodeTypeResolver.php b/packages/NodeTypeResolver/src/NodeTypeResolver.php index 011cabcbcaf..818a32b8dfe 100644 --- a/packages/NodeTypeResolver/src/NodeTypeResolver.php +++ b/packages/NodeTypeResolver/src/NodeTypeResolver.php @@ -30,11 +30,13 @@ use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Interface_; +use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\PropertyProperty; use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeTraverser; use PHPStan\Analyser\Scope; use PHPStan\Broker\Broker; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -50,11 +52,14 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use Rector\BetterPhpDocParser\PhpDocParser\BetterPhpDocParser; use Rector\Exception\ShouldNotHappenException; +use Rector\NodeContainer\ParsedNodesByType; use Rector\NodeTypeResolver\Contract\NodeTypeResolverAwareInterface; use Rector\NodeTypeResolver\Contract\PerNodeTypeResolver\PerNodeTypeResolverInterface; use Rector\NodeTypeResolver\Node\AttributeKey; @@ -65,6 +70,7 @@ use Rector\PhpParser\Node\Resolver\NameResolver; use Rector\PhpParser\NodeTraverser\CallableNodeTraverser; use Rector\PhpParser\Printer\BetterStandardPrinter; use Rector\TypeDeclaration\PHPStan\Type\ObjectTypeSpecifier; +use ReflectionProperty; use Symfony\Component\Finder\SplFileInfo; final class NodeTypeResolver @@ -114,16 +120,34 @@ final class NodeTypeResolver */ private $betterNodeFinder; + /** + * @var ParsedNodesByType + */ + private $parsedNodesByType; + + /** + * @var BetterPhpDocParser + */ + private $betterPhpDocParser; + + /** + * @var StaticTypeMapper + */ + private $staticTypeMapper; + /** * @param PerNodeTypeResolverInterface[] $perNodeTypeResolvers */ public function __construct( BetterStandardPrinter $betterStandardPrinter, + BetterPhpDocParser $betterPhpDocParser, NameResolver $nameResolver, + ParsedNodesByType $parsedNodesByType, CallableNodeTraverser $callableNodeTraverser, ClassReflectionTypesResolver $classReflectionTypesResolver, Broker $broker, TypeFactory $typeFactory, + StaticTypeMapper $staticTypeMapper, ObjectTypeSpecifier $objectTypeSpecifier, BetterNodeFinder $betterNodeFinder, array $perNodeTypeResolvers @@ -141,6 +165,9 @@ final class NodeTypeResolver $this->typeFactory = $typeFactory; $this->objectTypeSpecifier = $objectTypeSpecifier; $this->betterNodeFinder = $betterNodeFinder; + $this->parsedNodesByType = $parsedNodesByType; + $this->betterPhpDocParser = $betterPhpDocParser; + $this->staticTypeMapper = $staticTypeMapper; } /** @@ -314,10 +341,6 @@ final class NodeTypeResolver public function getStaticType(Node $node): Type { - if ($node instanceof String_) { - return new ConstantStringType($node->value); - } - if ($node instanceof Arg) { throw new ShouldNotHappenException('Arg does not have a type, use $arg->value instead'); } @@ -373,6 +396,14 @@ final class NodeTypeResolver return $staticType; } + if ($node instanceof PropertyFetch) { + // compensate 3rd party non-analysed property reflection + $vendorPropertyType = $this->getVendorPropertyFetchType($node); + if ($vendorPropertyType !== null) { + return $vendorPropertyType; + } + } + return $this->objectTypeSpecifier->narrowToFullyQualifiedOrAlaisedObjectType($node, $staticType); } @@ -715,6 +746,20 @@ final class NodeTypeResolver $propertyPropertyNode = $this->getClassNodeProperty($classNode, $propertyName); if ($propertyPropertyNode === null) { + // also possible 3rd party vendor + if ($node instanceof PropertyFetch) { + $propertyOwnerStaticType = $this->getStaticType($node->var); + } else { + $propertyOwnerStaticType = $this->getStaticType($node->class); + } + + if (! $propertyOwnerStaticType instanceof ThisType && $propertyOwnerStaticType instanceof TypeWithClassName) { + if ($this->parsedNodesByType->findClass($propertyOwnerStaticType->getClassName()) === null) { + // positive assumption about 3rd party code + return true; + } + } + return false; } @@ -795,4 +840,46 @@ final class NodeTypeResolver return null; } + + private function getVendorPropertyFetchType(PropertyFetch $propertyFetch): ?Type + { + $varObjectType = $this->getStaticType($propertyFetch->var); + if (! $varObjectType instanceof TypeWithClassName) { + return null; + } + + if ($this->parsedNodesByType->findClass($varObjectType->getClassName())) { + return null; + } + + // 3rd party code + $propertyName = $this->nameResolver->getName($propertyFetch->name); + if ($propertyName === null) { + return null; + } + + if (! property_exists($varObjectType->getClassName(), $propertyName)) { + return null; + } + + // property is used + $propertyReflection = new ReflectionProperty($varObjectType->getClassName(), $propertyName); + if (! $propertyReflection->getDocComment()) { + return null; + } + + $phpDocNode = $this->betterPhpDocParser->parseString((string) $propertyReflection->getDocComment()); + $varTagValues = $phpDocNode->getVarTagValues(); + + if (! isset($varTagValues[0])) { + return null; + } + + $typeNode = $varTagValues[0]->type; + if (! $typeNode instanceof TypeNode) { + return null; + } + + return $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($typeNode, new Nop()); + } } diff --git a/packages/Php71/src/Rector/FuncCall/CountOnNullRector.php b/packages/Php71/src/Rector/FuncCall/CountOnNullRector.php index 3bc0e237555..43d2c5ec737 100644 --- a/packages/Php71/src/Rector/FuncCall/CountOnNullRector.php +++ b/packages/Php71/src/Rector/FuncCall/CountOnNullRector.php @@ -110,6 +110,7 @@ PHP if ($parentNode instanceof Ternary) { return true; } + return ! isset($funcCall->args[0]); } } diff --git a/src/Configuration/CurrentNodeProvider.php b/src/Configuration/CurrentNodeProvider.php index cff3f211033..ab585421b7a 100644 --- a/src/Configuration/CurrentNodeProvider.php +++ b/src/Configuration/CurrentNodeProvider.php @@ -9,7 +9,7 @@ use PhpParser\Node; final class CurrentNodeProvider { /** - * @var Node + * @var Node|null */ private $node; @@ -18,7 +18,7 @@ final class CurrentNodeProvider $this->node = $node; } - public function getNode(): Node + public function getNode(): ?Node { return $this->node; }