diff --git a/src/AbstractPhpdocToTypeDeclarationFixer.php b/src/AbstractPhpdocToTypeDeclarationFixer.php new file mode 100644 index 00000000000..df7a7251052 --- /dev/null +++ b/src/AbstractPhpdocToTypeDeclarationFixer.php @@ -0,0 +1,203 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer; + +use PhpCsFixer\DocBlock\Annotation; +use PhpCsFixer\DocBlock\DocBlock; +use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface; +use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; +use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; +use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer; +use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer; +use PhpCsFixer\Tokenizer\CT; +use PhpCsFixer\Tokenizer\Token; +use PhpCsFixer\Tokenizer\Tokens; + +/** + * @internal + */ +abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface +{ + /** + * @var string + */ + private $classRegex = '/^\\\\?[a-zA-Z_\\x7f-\\xff](?:\\\\?[a-zA-Z0-9_\\x7f-\\xff]+)*$/'; + + /** + * @var array + */ + private $versionSpecificTypes = [ + 'void' => 70100, + 'iterable' => 70100, + 'object' => 70200, + ]; + + /** + * @var array + */ + private $scalarTypes = [ + 'bool' => true, + 'float' => true, + 'int' => true, + 'string' => true, + ]; + + /** + * @var array + */ + private $skippedTypes = [ + 'mixed' => true, + 'resource' => true, + 'null' => true, + ]; + + /** + * {@inheritdoc} + */ + public function isRisky() + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function createConfigurationDefinition() + { + return new FixerConfigurationResolver([ + (new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.')) + ->setAllowedTypes(['bool']) + ->setDefault(true) + ->getOption(), + ]); + } + + /** + * Find all the annotations of given type in the function's PHPDoc comment. + * + * @param string $name + * @param int $index The index of the function token + * + * @return Annotation[] + */ + protected function findAnnotations($name, Tokens $tokens, $index) + { + do { + $index = $tokens->getPrevNonWhitespace($index); + } while ($tokens[$index]->isGivenKind([ + T_COMMENT, + T_ABSTRACT, + T_FINAL, + T_PRIVATE, + T_PROTECTED, + T_PUBLIC, + T_STATIC, + ])); + + if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) { + return []; + } + + $namespacesAnalyzer = new NamespacesAnalyzer(); + $namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $index); + + $namespaceUsesAnalyzer = new NamespaceUsesAnalyzer(); + $namespaceUses = $namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace); + + $doc = new DocBlock( + $tokens[$index]->getContent(), + $namespace, + $namespaceUses + ); + + return $doc->getAnnotationsOfType($name); + } + + /** + * @param string $type + * @param bool $isNullable + * + * @return Token[] + */ + protected function createTypeDeclarationTokens($type, $isNullable) + { + static $specialTypes = [ + 'array' => [CT::T_ARRAY_TYPEHINT, 'array'], + 'callable' => [T_CALLABLE, 'callable'], + ]; + + $newTokens = []; + + if (true === $isNullable) { + $newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']); + } + + if (isset($specialTypes[$type])) { + $newTokens[] = new Token($specialTypes[$type]); + } else { + foreach (explode('\\', $type) as $nsIndex => $value) { + if (0 === $nsIndex && '' === $value) { + continue; + } + + if (0 < $nsIndex) { + $newTokens[] = new Token([T_NS_SEPARATOR, '\\']); + } + $newTokens[] = new Token([T_STRING, $value]); + } + } + + return $newTokens; + } + + /** + * @return null|array + */ + protected function getCommonTypeFromAnnotation(Annotation $annotation) + { + $typesExpression = $annotation->getTypeExpression(); + + $commonType = $typesExpression->getCommonType(); + $isNullable = $typesExpression->allowsNull(); + + if (null === $commonType) { + return null; + } + + if ($isNullable && (\PHP_VERSION_ID < 70100 || 'void' === $commonType)) { + return null; + } + + if ('static' === $commonType) { + $commonType = 'self'; + } + + if (isset($this->skippedTypes[$commonType])) { + return null; + } + + if (isset($this->versionSpecificTypes[$commonType]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$commonType]) { + return null; + } + + if (isset($this->scalarTypes[$commonType])) { + if (false === $this->configuration['scalar_types']) { + return null; + } + } elseif (1 !== Preg::match($this->classRegex, $commonType)) { + return null; + } + + return [$commonType, $isNullable]; + } +} diff --git a/src/DocBlock/Annotation.php b/src/DocBlock/Annotation.php index 4b0d1da94c7..1ee84f8ffe3 100644 --- a/src/DocBlock/Annotation.php +++ b/src/DocBlock/Annotation.php @@ -13,6 +13,8 @@ namespace PhpCsFixer\DocBlock; use PhpCsFixer\Preg; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; /** * This represents an entire annotation from a docblock. @@ -22,41 +24,6 @@ */ class Annotation { - /** - * Regex to match any types, shall be used with `x` modifier. - * - * @internal - */ - const REGEX_TYPES = ' - # is any non-array, non-generic, non-alternated type, eg `int` or `\Foo` - # is array of , eg `int[]` or `\Foo[]` - # is generic collection type, like `array`, `Collection` and more complex like `Collection>` - # is , or type, like `int`, `bool[]` or `Collection` - # is one or more types alternated via `|`, like `int|bool[]|Collection` - (? - (? - (? - (?&simple)(\[\])* - ) - | - (? - [@$?]?[\\\\\w]+ - ) - | - (? - (?&simple) - < - (?:(?&types),\s*)?(?:(?&types)|(?&generic)) - > - ) - ) - (?: - \| - (?:(?&simple)|(?&array)|(?&generic)) - )* - ) - '; - /** * All the annotation tag names with types. * @@ -116,14 +83,28 @@ class Annotation */ private $types; + /** + * @var null|NamespaceAnalysis + */ + private $namespace; + + /** + * @var NamespaceUseAnalysis[] + */ + private $namespaceUses; + /** * Create a new line instance. * - * @param Line[] $lines + * @param Line[] $lines + * @param null|NamespaceAnalysis $namespace + * @param NamespaceUseAnalysis[] $namespaceUses */ - public function __construct(array $lines) + public function __construct(array $lines, $namespace = null, array $namespaceUses = []) { $this->lines = array_values($lines); + $this->namespace = $namespace; + $this->namespaceUses = $namespaceUses; $keys = array_keys($lines); @@ -185,6 +166,29 @@ public function getTag() return $this->tag; } + /** + * @return TypeExpression + */ + public function getTypeExpression() + { + return new TypeExpression($this->getTypesContent(), $this->namespace, $this->namespaceUses); + } + + /** + * @return null|string + */ + public function getVariableName() + { + $type = preg_quote($this->getTypesContent(), '/'); + $regex = "/@{$this->tag->getName()}\\s+{$type}\\s+(?\\$.+?)(?:[\\s*]|$)/"; + + if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) { + return $matches['variable']; + } + + return null; + } + /** * Get the types associated with this annotation. * @@ -193,20 +197,7 @@ public function getTag() public function getTypes() { if (null === $this->types) { - $this->types = []; - - $content = $this->getTypesContent(); - - while ('' !== $content && false !== $content) { - Preg::match( - '{^'.self::REGEX_TYPES.'$}x', - $content, - $matches - ); - - $this->types[] = $matches['type']; - $content = substr($content, \strlen($matches['type']) + 1); - } + $this->types = $this->getTypeExpression()->getTypes(); } return $this->types; @@ -286,7 +277,7 @@ private function getTypesContent() } $matchingResult = Preg::match( - '{^(?:\s*\*|/\*\*)\s*@'.$name.'\s+'.self::REGEX_TYPES.'(?:[ \t].*)?$}sx', + '{^(?:\s*\*|/\*\*)\s*@'.$name.'\s+'.TypeExpression::REGEX_TYPES.'(?:[ \t].*)?$}sx', $this->lines[0]->getContent(), $matches ); diff --git a/src/DocBlock/DocBlock.php b/src/DocBlock/DocBlock.php index 3c1db83e18f..3b4b1fcd7e4 100644 --- a/src/DocBlock/DocBlock.php +++ b/src/DocBlock/DocBlock.php @@ -13,6 +13,8 @@ namespace PhpCsFixer\DocBlock; use PhpCsFixer\Preg; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; /** * This class represents a docblock. @@ -37,16 +39,31 @@ class DocBlock */ private $annotations; + /** + * @var null|NamespaceAnalysis + */ + private $namespace; + + /** + * @var NamespaceUseAnalysis[] + */ + private $namespaceUses; + /** * Create a new docblock instance. * - * @param string $content + * @param string $content + * @param null|NamespaceAnalysis $namespace + * @param NamespaceUseAnalysis[] $namespaceUses */ - public function __construct($content) + public function __construct($content, $namespace = null, array $namespaceUses = []) { foreach (Preg::split('/([^\n\r]+\R*)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $line) { $this->lines[] = new Line($line); } + + $this->namespace = $namespace; + $this->namespaceUses = $namespaceUses; } /** @@ -101,7 +118,7 @@ public function getAnnotations() if ($this->lines[$index]->containsATag()) { // get all the lines that make up the annotation $lines = \array_slice($this->lines, $index, $this->findAnnotationLength($index), true); - $annotation = new Annotation($lines); + $annotation = new Annotation($lines, $this->namespace, $this->namespaceUses); // move the index to the end of the annotation to avoid // checking it again because we know the lines inside the // current annotation cannot be part of another annotation diff --git a/src/DocBlock/TypeExpression.php b/src/DocBlock/TypeExpression.php new file mode 100644 index 00000000000..56a5c5e3988 --- /dev/null +++ b/src/DocBlock/TypeExpression.php @@ -0,0 +1,247 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\DocBlock; + +use PhpCsFixer\Preg; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; + +/** + * @internal + */ +final class TypeExpression +{ + /** + * Regex to match any types, shall be used with `x` modifier. + */ + const REGEX_TYPES = ' + # is any non-array, non-generic, non-alternated type, eg `int` or `\Foo` + # is array of , eg `int[]` or `\Foo[]` + # is generic collection type, like `array`, `Collection` and more complex like `Collection>` + # is , or type, like `int`, `bool[]` or `Collection` + # is one or more types alternated via `|`, like `int|bool[]|Collection` + (? + (? + (? + (?&simple)(\[\])* + ) + | + (? + [@$?]?[\\\\\w]+ + ) + | + (? + (?&simple) + < + (?:(?&types),\s*)?(?:(?&types)|(?&generic)) + > + ) + ) + (?: + \| + (?:(?&simple)|(?&array)|(?&generic)) + )* + ) + '; + + /** + * @var string[] + */ + private $types = []; + + /** + * @var null|NamespaceAnalysis + */ + private $namespace; + + /** + * @var NamespaceUseAnalysis[] + */ + private $namespaceUses; + + /** + * @param string $value + * @param null|NamespaceAnalysis $namespace + * @param NamespaceUseAnalysis[] $namespaceUses + */ + public function __construct($value, $namespace, array $namespaceUses) + { + while ('' !== $value && false !== $value) { + Preg::match( + '{^'.self::REGEX_TYPES.'$}x', + $value, + $matches + ); + + $this->types[] = $matches['type']; + $value = substr($value, \strlen($matches['type']) + 1); + } + + $this->namespace = $namespace; + $this->namespaceUses = $namespaceUses; + } + + /** + * @return string[] + */ + public function getTypes() + { + return $this->types; + } + + /** + * @return null|string + */ + public function getCommonType() + { + $aliases = [ + 'true' => 'bool', + 'false' => 'bool', + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float', + 'real' => 'float', + 'callback' => 'callable', + ]; + + $mainType = null; + + foreach ($this->types as $type) { + if ('null' === $type) { + continue; + } + + if (isset($aliases[$type])) { + $type = $aliases[$type]; + } elseif (1 === Preg::match('/\[\]$/', $type)) { + $type = 'array'; + } elseif (1 === Preg::match('/^(.+?)getParentType($type, $mainType); + + if (null === $mainType) { + return null; + } + } + + return $mainType; + } + + /** + * @return bool + */ + public function allowsNull() + { + foreach ($this->types as $type) { + if (\in_array($type, ['null', 'mixed'], true)) { + return true; + } + } + + return false; + } + + private function getParentType($type1, $type2) + { + $types = [ + $this->normalize($type1), + $this->normalize($type2), + ]; + natcasesort($types); + $types = implode('|', $types); + + $parents = [ + 'array|iterable' => 'iterable', + 'array|Traversable' => 'iterable', + 'iterable|Traversable' => 'iterable', + 'self|static' => 'self', + ]; + + if (isset($parents[$types])) { + return $parents[$types]; + } + + return null; + } + + /** + * @param string $type + * + * @return string + */ + private function normalize($type) + { + $aliases = [ + 'true' => 'bool', + 'false' => 'bool', + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float', + 'real' => 'float', + 'callback' => 'callable', + ]; + + if (isset($aliases[$type])) { + return $aliases[$type]; + } + + if (\in_array($type, [ + 'void', + 'null', + 'bool', + 'int', + 'float', + 'string', + 'array', + 'iterable', + 'object', + 'callable', + 'resource', + 'mixed', + ], true)) { + return $type; + } + + if (1 === Preg::match('/\[\]$/', $type)) { + return 'array'; + } + + if (1 === Preg::match('/^(.+?)namespaceUses as $namespaceUse) { + if ($namespaceUse->getShortName() === $type) { + return $namespaceUse->getFullName(); + } + } + + if (null === $this->namespace || '' === $this->namespace->getShortName()) { + return $type; + } + + return "{$this->namespace->getFullName()}\\{$type}"; + } +} diff --git a/src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php b/src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php index 6f0e2c2370d..bdb4cd44c89 100644 --- a/src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php +++ b/src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php @@ -12,31 +12,19 @@ namespace PhpCsFixer\Fixer\FunctionNotation; -use PhpCsFixer\AbstractFixer; +use PhpCsFixer\AbstractPhpdocToTypeDeclarationFixer; use PhpCsFixer\DocBlock\Annotation; -use PhpCsFixer\DocBlock\DocBlock; -use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface; -use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; -use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\VersionSpecification; use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample; -use PhpCsFixer\Preg; -use PhpCsFixer\Tokenizer\CT; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; /** * @author Jan Gantzert */ -final class PhpdocToParamTypeFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface +final class PhpdocToParamTypeFixer extends AbstractPhpdocToTypeDeclarationFixer { - /** @internal */ - const CLASS_REGEX = '/^\\\\?[a-zA-Z_\\x7f-\\xff](?:\\\\?[a-zA-Z0-9_\\x7f-\\xff]+)*(?\[\])*$/'; - - /** @internal */ - const MINIMUM_PHP_VERSION = 70000; - /** * @var array */ @@ -45,15 +33,6 @@ final class PhpdocToParamTypeFixer extends AbstractFixer implements Configuratio [T_STRING, '__destruct'], ]; - /** - * @var array - */ - private $skippedTypes = [ - 'mixed' => true, - 'resource' => true, - 'static' => true, - ]; - /** * {@inheritdoc} */ @@ -91,7 +70,7 @@ function my_foo($bar) */ public function isCandidate(Tokens $tokens) { - return \PHP_VERSION_ID >= self::MINIMUM_PHP_VERSION && $tokens->isTokenKindFound(T_FUNCTION); + return \PHP_VERSION_ID >= 70000 && $tokens->isTokenKindFound(T_FUNCTION); } /** @@ -103,27 +82,6 @@ public function getPriority() return 8; } - /** - * {@inheritdoc} - */ - public function isRisky() - { - return true; - } - - /** - * {@inheritdoc} - */ - protected function createConfigurationDefinition() - { - return new FixerConfigurationResolver([ - (new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.')) - ->setAllowedTypes(['bool']) - ->setDefault(true) - ->getOption(), - ]); - } - /** * {@inheritdoc} */ @@ -139,111 +97,16 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens) continue; } - $paramTypeAnnotations = $this->findParamAnnotations($tokens, $index); + $paramTypeAnnotations = $this->findAnnotations('param', $tokens, $index); foreach ($paramTypeAnnotations as $paramTypeAnnotation) { - if (\PHP_VERSION_ID < self::MINIMUM_PHP_VERSION) { - continue; - } - - $types = array_values($paramTypeAnnotation->getTypes()); - $paramType = current($types); - - if (isset($this->skippedTypes[$paramType])) { - continue; - } - - $hasIterable = false; - $hasNull = false; - $hasVoid = false; - $hasArray = false; - $hasString = false; - $hasInt = false; - $hasFloat = false; - $hasBool = false; - $hasCallable = false; - $hasObject = false; - $minimumTokenPhpVersion = self::MINIMUM_PHP_VERSION; - - foreach ($types as $key => $type) { - if (1 !== Preg::match(self::CLASS_REGEX, $type, $matches)) { - continue; - } - - if (isset($matches['array'])) { - $hasArray = true; - unset($types[$key]); - } - - if ('iterable' === $type) { - $hasIterable = true; - unset($types[$key]); - $minimumTokenPhpVersion = 70100; - } - - if ('null' === $type) { - $hasNull = true; - unset($types[$key]); - $minimumTokenPhpVersion = 70100; - } - - if ('void' === $type) { - $hasVoid = true; - unset($types[$key]); - } - - if ('string' === $type) { - $hasString = true; - unset($types[$key]); - } - - if ('int' === $type) { - $hasInt = true; - unset($types[$key]); - } - - if ('float' === $type) { - $hasFloat = true; - unset($types[$key]); - } - - if ('bool' === $type) { - $hasBool = true; - unset($types[$key]); - } - - if ('callable' === $type) { - $hasCallable = true; - unset($types[$key]); - } + $typeInfo = $this->getCommonTypeFromAnnotation($paramTypeAnnotation); - if ('array' === $type) { - $hasArray = true; - unset($types[$key]); - } - - if ('object' === $type) { - $hasObject = true; - unset($types[$key]); - $minimumTokenPhpVersion = 70200; - } - } - - if (\PHP_VERSION_ID < $minimumTokenPhpVersion) { + if (null === $typeInfo) { continue; } - $typesCount = \count($types); - - if (1 < $typesCount) { - continue; - } - - if (0 === $typesCount) { - $paramType = ''; - } elseif (1 === $typesCount) { - $paramType = array_shift($types); - } + list($paramType, $isNullable) = $typeInfo; $startIndex = $tokens->getNextTokenOfKind($index, ['(']) + 1; $variableIndex = $this->findCorrectVariable($tokens, $startIndex - 1, $paramTypeAnnotation); @@ -257,59 +120,18 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens) $variableIndex = $byRefIndex; } - if (!('(' === $tokens[$variableIndex - 1]->getContent()) && $this->hasParamTypeHint($tokens, $variableIndex - 2)) { + if ($this->hasParamTypeHint($tokens, $variableIndex)) { continue; } - $this->fixFunctionDefinition( - $paramType, - $tokens, - $variableIndex, - $hasNull, - $hasArray, - $hasIterable, - $hasVoid, - $hasString, - $hasInt, - $hasFloat, - $hasBool, - $hasCallable, - $hasObject - ); + $tokens->insertAt($variableIndex, array_merge( + $this->createTypeDeclarationTokens($paramType, $isNullable), + [new Token([T_WHITESPACE, ' '])] + )); } } } - /** - * Find all the param annotations in the function's PHPDoc comment. - * - * @param int $index The index of the function token - * - * @return Annotation[] - */ - private function findParamAnnotations(Tokens $tokens, $index) - { - do { - $index = $tokens->getPrevNonWhitespace($index); - } while ($tokens[$index]->isGivenKind([ - T_COMMENT, - T_ABSTRACT, - T_FINAL, - T_PRIVATE, - T_PROTECTED, - T_PUBLIC, - T_STATIC, - ])); - - if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) { - return []; - } - - $doc = new DocBlock($tokens[$index]->getContent()); - - return $doc->getAnnotationsOfType('param'); - } - /** * @param int $index * @param Annotation $paramTypeAnnotation @@ -330,8 +152,7 @@ private function findCorrectVariable(Tokens $tokens, $index, $paramTypeAnnotatio } $variableToken = $tokens[$variableIndex]->getContent(); - Preg::match('/@param\s*[^\s!<]+\s*([^\s]+)/', $paramTypeAnnotation->getContent(), $paramVariable); - if (isset($paramVariable[1]) && $paramVariable[1] === $variableToken) { + if ($paramTypeAnnotation->getVariableName() === $variableToken) { return $variableIndex; } @@ -347,82 +168,8 @@ private function findCorrectVariable(Tokens $tokens, $index, $paramTypeAnnotatio */ private function hasParamTypeHint(Tokens $tokens, $index) { - return $tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR, CT::T_ARRAY_TYPEHINT, T_CALLABLE, CT::T_NULLABLE_TYPE]); - } - - /** - * @param string $paramType - * @param int $index The index of the end of the function definition line, EG at { or ; - * @param bool $hasNull - * @param bool $hasArray - * @param bool $hasIterable - * @param bool $hasVoid - * @param bool $hasString - * @param bool $hasInt - * @param bool $hasFloat - * @param bool $hasBool - * @param bool $hasCallable - * @param bool $hasObject - */ - private function fixFunctionDefinition( - $paramType, - Tokens $tokens, - $index, - $hasNull, - $hasArray, - $hasIterable, - $hasVoid, - $hasString, - $hasInt, - $hasFloat, - $hasBool, - $hasCallable, - $hasObject - ) { - $newTokens = []; - - if (true === $hasVoid) { - $newTokens[] = new Token('void'); - } elseif (true === $hasIterable && true === $hasArray) { - $newTokens[] = new Token([CT::T_ARRAY_TYPEHINT, 'array']); - } elseif (true === $hasIterable) { - $newTokens[] = new Token([T_STRING, 'iterable']); - } elseif (true === $hasArray) { - $newTokens[] = new Token([CT::T_ARRAY_TYPEHINT, 'array']); - } elseif (true === $hasString) { - $newTokens[] = new Token([T_STRING, 'string']); - } elseif (true === $hasInt) { - $newTokens[] = new Token([T_STRING, 'int']); - } elseif (true === $hasFloat) { - $newTokens[] = new Token([T_STRING, 'float']); - } elseif (true === $hasBool) { - $newTokens[] = new Token([T_STRING, 'bool']); - } elseif (true === $hasCallable) { - $newTokens[] = new Token([T_CALLABLE, 'callable']); - } elseif (true === $hasObject) { - $newTokens[] = new Token([T_STRING, 'object']); - } - - if ('' !== $paramType && [] !== $newTokens) { - return; - } - - foreach (explode('\\', $paramType) as $nsIndex => $value) { - if (0 === $nsIndex && '' === $value) { - continue; - } - - if (0 < $nsIndex) { - $newTokens[] = new Token([T_NS_SEPARATOR, '\\']); - } - $newTokens[] = new Token([T_STRING, $value]); - } - - if (true === $hasNull) { - array_unshift($newTokens, new Token([CT::T_NULLABLE_TYPE, '?'])); - } + $prevIndex = $tokens->getPrevMeaningfulToken($index); - $newTokens[] = new Token([T_WHITESPACE, ' ']); - $tokens->insertAt($index, $newTokens); + return !$tokens[$prevIndex]->equalsAny([',', '(']); } } diff --git a/src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php b/src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php index df5f3e22419..2c90b971469 100644 --- a/src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php +++ b/src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php @@ -12,16 +12,10 @@ namespace PhpCsFixer\Fixer\FunctionNotation; -use PhpCsFixer\AbstractFixer; -use PhpCsFixer\DocBlock\Annotation; -use PhpCsFixer\DocBlock\DocBlock; -use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface; -use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; -use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; +use PhpCsFixer\AbstractPhpdocToTypeDeclarationFixer; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\VersionSpecification; use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample; -use PhpCsFixer\Preg; use PhpCsFixer\Tokenizer\CT; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; @@ -29,7 +23,7 @@ /** * @author Filippo Tessarotto */ -final class PhpdocToReturnTypeFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface +final class PhpdocToReturnTypeFixer extends AbstractPhpdocToTypeDeclarationFixer { /** * @var array> @@ -40,41 +34,6 @@ final class PhpdocToReturnTypeFixer extends AbstractFixer implements Configurati [T_STRING, '__clone'], ]; - /** - * @var array - */ - private $versionSpecificTypes = [ - 'void' => 70100, - 'iterable' => 70100, - 'object' => 70200, - ]; - - /** - * @var array - */ - private $scalarTypes = [ - 'bool' => 'bool', - 'true' => 'bool', - 'false' => 'bool', - 'float' => 'float', - 'int' => 'int', - 'string' => 'string', - ]; - - /** - * @var array - */ - private $skippedTypes = [ - 'mixed' => true, - 'resource' => true, - 'null' => true, - ]; - - /** - * @var string - */ - private $classRegex = '/^\\\\?[a-zA-Z_\\x7f-\\xff](?:\\\\?[a-zA-Z0-9_\\x7f-\\xff]+)*(?\[\])*$/'; - /** * {@inheritdoc} */ @@ -138,27 +97,6 @@ public function getPriority() return 13; } - /** - * {@inheritdoc} - */ - public function isRisky() - { - return true; - } - - /** - * {@inheritdoc} - */ - protected function createConfigurationDefinition() - { - return new FixerConfigurationResolver([ - (new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.')) - ->setAllowedTypes(['bool']) - ->setDefault(true) - ->getOption(), - ]); - } - /** * {@inheritdoc} */ @@ -177,72 +115,18 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens) continue; } - $returnTypeAnnotation = $this->findReturnAnnotations($tokens, $index); + $returnTypeAnnotation = $this->findAnnotations('return', $tokens, $index); if (1 !== \count($returnTypeAnnotation)) { continue; } - $returnTypeAnnotation = current($returnTypeAnnotation); - $types = array_values($returnTypeAnnotation->getTypes()); - $typesCount = \count($types); - - if (1 > $typesCount || 2 < $typesCount) { - continue; - } - - $isNullable = false; - $returnType = current($types); - - if (2 === $typesCount) { - $null = $types[0]; - $returnType = $types[1]; - if ('null' !== $null) { - $null = $types[1]; - $returnType = $types[0]; - } - - if ('null' !== $null) { - continue; - } - - $isNullable = true; - - if (\PHP_VERSION_ID < 70100) { - continue; - } - - if ('void' === $returnType) { - continue; - } - } - - if ('static' === $returnType) { - $returnType = 'self'; - } + $typeInfo = $this->getCommonTypeFromAnnotation(current($returnTypeAnnotation)); - if (isset($this->skippedTypes[$returnType])) { + if (null === $typeInfo) { continue; } - if (isset($this->versionSpecificTypes[$returnType]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$returnType]) { - continue; - } - - if (isset($this->scalarTypes[$returnType])) { - if (false === $this->configuration['scalar_types']) { - continue; - } - - $returnType = $this->scalarTypes[$returnType]; - } else { - if (1 !== Preg::match($this->classRegex, $returnType, $matches)) { - continue; - } - - if (isset($matches['array'])) { - $returnType = 'array'; - } - } + list($returnType, $isNullable) = $typeInfo; $startIndex = $tokens->getNextTokenOfKind($index, ['{', ';']); @@ -250,7 +134,18 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens) continue; } - $this->fixFunctionDefinition($tokens, $startIndex, $isNullable, $returnType); + $endFuncIndex = $tokens->getPrevTokenOfKind($startIndex, [')']); + + $tokens->insertAt( + $endFuncIndex + 1, + array_merge( + [ + new Token([CT::T_TYPE_COLON, ':']), + new Token([T_WHITESPACE, ' ']), + ], + $this->createTypeDeclarationTokens($returnType, $isNullable) + ) + ); } } @@ -268,72 +163,4 @@ private function hasReturnTypeHint(Tokens $tokens, $index) return $tokens[$nextIndex]->isGivenKind(CT::T_TYPE_COLON); } - - /** - * @param int $index The index of the end of the function definition line, EG at { or ; - * @param bool $isNullable - * @param string $returnType - */ - private function fixFunctionDefinition(Tokens $tokens, $index, $isNullable, $returnType) - { - static $specialTypes = [ - 'array' => [CT::T_ARRAY_TYPEHINT, 'array'], - 'callable' => [T_CALLABLE, 'callable'], - ]; - $newTokens = [ - new Token([CT::T_TYPE_COLON, ':']), - new Token([T_WHITESPACE, ' ']), - ]; - if (true === $isNullable) { - $newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']); - } - - if (isset($specialTypes[$returnType])) { - $newTokens[] = new Token($specialTypes[$returnType]); - } else { - foreach (explode('\\', $returnType) as $nsIndex => $value) { - if (0 === $nsIndex && '' === $value) { - continue; - } - - if (0 < $nsIndex) { - $newTokens[] = new Token([T_NS_SEPARATOR, '\\']); - } - $newTokens[] = new Token([T_STRING, $value]); - } - } - - $endFuncIndex = $tokens->getPrevTokenOfKind($index, [')']); - $tokens->insertAt($endFuncIndex + 1, $newTokens); - } - - /** - * Find all the return annotations in the function's PHPDoc comment. - * - * @param int $index The index of the function token - * - * @return Annotation[] - */ - private function findReturnAnnotations(Tokens $tokens, $index) - { - do { - $index = $tokens->getPrevNonWhitespace($index); - } while ($tokens[$index]->isGivenKind([ - T_COMMENT, - T_ABSTRACT, - T_FINAL, - T_PRIVATE, - T_PROTECTED, - T_PUBLIC, - T_STATIC, - ])); - - if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) { - return []; - } - - $doc = new DocBlock($tokens[$index]->getContent()); - - return $doc->getAnnotationsOfType('return'); - } } diff --git a/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php b/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php index 3e62cd14449..9c4deaabd42 100644 --- a/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php +++ b/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php @@ -224,12 +224,12 @@ private function fixFunctionDocComment($content, Tokens $tokens, $functionIndex, ); foreach ($docBlock->getAnnotationsOfType('param') as $annotation) { - if (0 === Preg::match('/@param(?:\s+[^\$]\S+)?\s+(\$\S+)/', $annotation->getContent(), $matches)) { + $argumentName = $annotation->getVariableName(); + + if (null === $argumentName) { continue; } - $argumentName = $matches[1]; - if (!isset($argumentsInfo[$argumentName]) && $this->configuration['allow_unused_params']) { continue; } diff --git a/src/Tokenizer/Analyzer/NamespaceUsesAnalyzer.php b/src/Tokenizer/Analyzer/NamespaceUsesAnalyzer.php index 3149f114335..63ffc6493f2 100644 --- a/src/Tokenizer/Analyzer/NamespaceUsesAnalyzer.php +++ b/src/Tokenizer/Analyzer/NamespaceUsesAnalyzer.php @@ -12,6 +12,7 @@ namespace PhpCsFixer\Tokenizer\Analyzer; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; use PhpCsFixer\Tokenizer\CT; use PhpCsFixer\Tokenizer\Tokens; @@ -33,6 +34,22 @@ public function getDeclarationsFromTokens(Tokens $tokens) return $this->getDeclarations($tokens, $useIndexes); } + /** + * @return NamespaceUseAnalysis[] + */ + public function getDeclarationsInNamespace(Tokens $tokens, NamespaceAnalysis $namespace) + { + $namespaceUses = []; + + foreach ($this->getDeclarationsFromTokens($tokens) as $namespaceUse) { + if ($namespaceUse->getStartIndex() >= $namespace->getScopeStartIndex() && $namespaceUse->getStartIndex() <= $namespace->getScopeEndIndex()) { + $namespaceUses[] = $namespaceUse; + } + } + + return $namespaceUses; + } + /** * @return NamespaceUseAnalysis[] */ diff --git a/src/Tokenizer/Analyzer/NamespacesAnalyzer.php b/src/Tokenizer/Analyzer/NamespacesAnalyzer.php index 36fd71413dc..fc8f414cb60 100644 --- a/src/Tokenizer/Analyzer/NamespacesAnalyzer.php +++ b/src/Tokenizer/Analyzer/NamespacesAnalyzer.php @@ -68,4 +68,24 @@ public function getDeclarations(Tokens $tokens) return $namespaces; } + + /** + * @param int $index + * + * @return NamespaceAnalysis + */ + public function getNamespaceAt(Tokens $tokens, $index) + { + if (!$tokens->offsetExists($index)) { + throw new \InvalidArgumentException("Token index {$index} does not exist."); + } + + foreach ($this->getDeclarations($tokens) as $namespace) { + if ($namespace->getScopeStartIndex() <= $index && $namespace->getScopeEndIndex() >= $index) { + return $namespace; + } + } + + throw new \LogicException("Unable to get the namespace at index {$index}."); + } } diff --git a/tests/DocBlock/AnnotationTest.php b/tests/DocBlock/AnnotationTest.php index d4886a618ab..f83389c9a39 100644 --- a/tests/DocBlock/AnnotationTest.php +++ b/tests/DocBlock/AnnotationTest.php @@ -15,7 +15,10 @@ use PhpCsFixer\DocBlock\Annotation; use PhpCsFixer\DocBlock\DocBlock; use PhpCsFixer\DocBlock\Line; +use PhpCsFixer\DocBlock\TypeExpression; use PhpCsFixer\Tests\TestCase; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; /** * @author Graham Campbell @@ -398,4 +401,55 @@ public function testGetTagsWithTypes() static::assertNotEmpty($tag); } } + + /** + * @param Line[] $lines + * @param null|NamespaceAnalysis $namespace + * @param NamespaceUseAnalysis[] $namespaceUses + * @param null|string $expectedCommonType + * + * @dataProvider provideTypeExpressionCases + */ + public function testGetTypeExpression(array $lines, $namespace, array $namespaceUses, $expectedCommonType) + { + $annotation = new Annotation($lines, $namespace, $namespaceUses); + $result = $annotation->getTypeExpression(); + + static::assertInstanceOf(TypeExpression::class, $result); + static::assertSame($expectedCommonType, $result->getCommonType()); + } + + public function provideTypeExpressionCases() + { + $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999); + $useTraversable = new NamespaceUseAnalysis('Traversable', 'Traversable', false, 0, 999, NamespaceUseAnalysis::TYPE_CLASS); + + yield [[new Line('* @param array|Traversable $foo')], null, [], 'iterable']; + yield [[new Line('* @param array|Traversable $foo')], $appNamespace, [], null]; + yield [[new Line('* @param array|Traversable $foo')], $appNamespace, [$useTraversable], 'iterable']; + } + + /** + * @param Line[] $lines + * @param null|string $expectedVariableName + * + * @dataProvider provideGetVariableCases + */ + public function testGetVariableName(array $lines, $expectedVariableName) + { + $annotation = new Annotation($lines); + static::assertSame($expectedVariableName, $annotation->getVariableName()); + } + + public function provideGetVariableCases() + { + yield [[new Line('* @param int $foo')], '$foo']; + yield [[new Line('* @param int $foo some description')], '$foo']; + yield [[new Line('/** @param int $foo*/')], '$foo']; + yield [[new Line('* @param int')], null]; + yield [[new Line('* @var int $foo')], '$foo']; + yield [[new Line('* @var int $foo some description')], '$foo']; + yield [[new Line('/** @var int $foo*/')], '$foo']; + yield [[new Line('* @var int')], null]; + } } diff --git a/tests/DocBlock/TypeExpressionTest.php b/tests/DocBlock/TypeExpressionTest.php new file mode 100644 index 00000000000..7eac187d20f --- /dev/null +++ b/tests/DocBlock/TypeExpressionTest.php @@ -0,0 +1,160 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Tests\DocBlock; + +use PhpCsFixer\DocBlock\TypeExpression; +use PhpCsFixer\Tests\TestCase; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; + +/** + * @covers \PhpCsFixer\DocBlock\TypeExpression + * + * @internal + */ +final class TypeExpressionTest extends TestCase +{ + /** + * @param string $typesExpression + * @param string[] $expectedTypes + * + * @dataProvider provideGetTypesCases + */ + public function testGetTypes($typesExpression, $expectedTypes) + { + $expression = new TypeExpression($typesExpression, null, []); + static::assertSame($expectedTypes, $expression->getTypes()); + } + + public function provideGetTypesCases() + { + yield ['int', ['int']]; + yield ['Foo[][]', ['Foo[][]']]; + yield ['int[]', ['int[]']]; + yield ['int[]|null', ['int[]', 'null']]; + yield ['int[]|null|?int|array', ['int[]', 'null', '?int', 'array']]; + yield ['null|Foo\Bar|\Baz\Bax|int[]', ['null', 'Foo\Bar', '\Baz\Bax', 'int[]']]; + yield ['gen', ['gen']]; + yield ['int|gen', ['int', 'gen']]; + yield ['\int|\gen<\int, \bool>', ['\int', '\gen<\int, \bool>']]; + yield ['gen', ['gen']]; + yield ['gen', ['gen']]; + yield ['gen', ['gen']]; + yield ['gen>', ['gen>']]; + yield ['gen>', ['gen>']]; + yield ['null|gen>|int|string[]', ['null', 'gen>', 'int', 'string[]']]; + yield ['null|gen>|int|array|string[]', ['null', 'gen>', 'int', 'array', 'string[]']]; + yield ['this', ['this']]; + yield ['@this', ['@this']]; + yield ['$SELF|int', ['$SELF', 'int']]; + yield ['array', ['array']]; + } + + /** + * @param string $typesExpression + * @param null|string $expectedCommonType + * @param NamespaceUseAnalysis[] $namespaceUses + * + * @dataProvider provideCommonTypeCases + */ + public function testGetCommonType($typesExpression, $expectedCommonType, NamespaceAnalysis $namespace = null, array $namespaceUses = []) + { + $expression = new TypeExpression($typesExpression, $namespace, $namespaceUses); + static::assertSame($expectedCommonType, $expression->getCommonType()); + } + + public function provideCommonTypeCases() + { + $globalNamespace = new NamespaceAnalysis('', '', 0, 999, 0, 999); + $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999); + + $useTraversable = new NamespaceUseAnalysis('Traversable', 'Traversable', false, 0, 0, NamespaceUseAnalysis::TYPE_CLASS); + $useObjectAsTraversable = new NamespaceUseAnalysis('Foo', 'Traversable', false, 0, 0, NamespaceUseAnalysis::TYPE_CLASS); + + yield ['true', 'bool']; + yield ['false', 'bool']; + yield ['bool', 'bool']; + yield ['int', 'int']; + yield ['float', 'float']; + yield ['string', 'string']; + yield ['array', 'array']; + yield ['object', 'object']; + yield ['self', 'self']; + yield ['static', 'static']; + yield ['bool[]', 'array']; + yield ['int[]', 'array']; + yield ['float[]', 'array']; + yield ['string[]', 'array']; + yield ['array[]', 'array']; + yield ['bool[][]', 'array']; + yield ['int[][]', 'array']; + yield ['float[][]', 'array']; + yield ['string[][]', 'array']; + yield ['array[][]', 'array']; + yield ['array|iterable', 'iterable']; + yield ['iterable|array', 'iterable']; + yield ['array|Traversable', 'iterable']; + yield ['array|\Traversable', 'iterable']; + yield ['array|Traversable', 'iterable', $globalNamespace]; + yield ['iterable|Traversable', 'iterable']; + yield ['array', 'array']; + yield ['array', 'array']; + yield ['iterable', 'iterable']; + yield ['iterable', 'iterable']; + yield ['\Traversable', '\Traversable']; + yield ['Traversable', 'Traversable']; + yield ['Collection', 'Collection']; + yield ['Collection', 'Collection']; + yield ['array|iterable', 'iterable']; + yield ['int[]|string[]', 'array']; + yield ['int|null', 'int']; + yield ['null|int', 'int']; + yield ['void', 'void']; + yield ['array|Traversable', 'iterable', null, [$useTraversable]]; + yield ['array|Traversable', 'iterable', $globalNamespace, [$useTraversable]]; + yield ['array|Traversable', 'iterable', $appNamespace, [$useTraversable]]; + yield ['self|static', 'self']; + + yield ['array|Traversable', null, null, [$useObjectAsTraversable]]; + yield ['array|Traversable', null, $globalNamespace, [$useObjectAsTraversable]]; + yield ['array|Traversable', null, $appNamespace, [$useObjectAsTraversable]]; + yield ['bool|int', null]; + yield ['string|bool', null]; + yield ['array|Collection', null]; + } + + /** + * @param string $typesExpression + * @param bool $expectNullAllowed + * + * @dataProvider provideAllowsNullCases + */ + public function testAllowsNull($typesExpression, $expectNullAllowed) + { + $expression = new TypeExpression($typesExpression, null, []); + static::assertSame($expectNullAllowed, $expression->allowsNull()); + } + + public function provideAllowsNullCases() + { + yield ['null', true]; + yield ['mixed', true]; + yield ['null|mixed', true]; + yield ['int|bool|null', true]; + yield ['int|bool|mixed', true]; + + yield ['int', false]; + yield ['bool', false]; + yield ['string', false]; + } +} diff --git a/tests/Fixer/FunctionNotation/PhpdocToParamTypeFixerTest.php b/tests/Fixer/FunctionNotation/PhpdocToParamTypeFixerTest.php index de0db950586..98c28eb4d17 100644 --- a/tests/Fixer/FunctionNotation/PhpdocToParamTypeFixerTest.php +++ b/tests/Fixer/FunctionNotation/PhpdocToParamTypeFixerTest.php @@ -34,8 +34,11 @@ final class PhpdocToParamTypeFixerTest extends AbstractFixerTestCase public function testFix($expected, $input = null, $versionSpecificFix = null, $config = null) { if ( - (null !== $input && \PHP_VERSION_ID < 70000) - || (null !== $versionSpecificFix && \PHP_VERSION_ID < $versionSpecificFix) + null !== $input + && ( + \PHP_VERSION_ID < 70000 + || (null !== $versionSpecificFix && \PHP_VERSION_ID < $versionSpecificFix) + ) ) { $expected = $input; $input = null; @@ -214,7 +217,12 @@ class Foo { } ', ], - 'static is skipped' => [ + 'report static as self' => [ + ' [ - ' [ + 'generics with single type' => [ + ' $foo */ function my_foo(array $foo) {}', ' $foo */ function my_foo($foo) {}', ], - 'generics with multiple types are not supported' => [ + 'generics with multiple types' => [ + ' $foo */ function my_foo(array $foo) {}', ' $foo */ function my_foo($foo) {}', ], 'stop searching last token' => [ @@ -328,6 +338,75 @@ class Foo { ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ ' [ + 'generics' => [ + ' */ function my_foo(): array {}', ' */ function my_foo() {}', ], 'array of types' => [ @@ -247,6 +251,75 @@ class A } ', ], + 'array and traversable' => [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + 'getDeclarationsFromTokens($tokens))); + static::assertSame( + serialize($expected), + serialize($analyzer->getDeclarationsFromTokens($tokens)) + ); } public function provideNamespaceUsesCases() @@ -152,4 +156,70 @@ public function provideNamespaceUsesCases() // use const some\namespace\{ConstA, ConstB, ConstC}; ]; } + + /** + * @param string $code + * @param NamespaceUseAnalysis[] $expected + * + * @dataProvider provideGetDeclarationsInNamespaceCases + */ + public function testGetDeclarationsInNamespace($code, NamespaceAnalysis $namespace, array $expected) + { + $tokens = Tokens::fromCode($code); + $analyzer = new NamespaceUsesAnalyzer(); + + static::assertSame( + serialize($expected), + serialize($analyzer->getDeclarationsInNamespace($tokens, $namespace)) + ); + } + + public function provideGetDeclarationsInNamespaceCases() + { + return [ + [ + 'getDeclarations($tokens)))); + static::assertSame( + serialize($expected), + serialize($analyzer->getDeclarations($tokens)) + ); } public function provideNamespacesCases() @@ -83,4 +86,75 @@ public function provideNamespacesCases() ]], ]; } + + /** + * @param string $code + * @param int $index + * + * @dataProvider provideGetNamespaceAtCases + */ + public function testGetNamespaceAt($code, $index, NamespaceAnalysis $expected) + { + $tokens = Tokens::fromCode($code); + $analyzer = new NamespacesAnalyzer(); + + static::assertSame( + serialize($expected), + serialize($analyzer->getNamespaceAt($tokens, $index)) + ); + } + + public function provideGetNamespaceAtCases() + { + return [ + [ + '