Skip to content

Commit f70e324

Browse files
authored
Merge pull request #2 from magento-tsg/develop-sync
Sync develop branch with magento/magento-semver
2 parents 5663771 + 751a5ce commit f70e324

File tree

26 files changed

+732
-197
lines changed

26 files changed

+732
-197
lines changed

composer.lock

Lines changed: 118 additions & 171 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Analyzer/ClassAnalyzer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace Magento\SemanticVersionChecker\Analyzer;
1111

12+
use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph;
1213
use PhpParser\Node\Stmt\Class_;
1314
use PHPSemVerChecker\Operation\ClassAdded;
1415
use PHPSemVerChecker\Operation\ClassRemoved;

src/Analyzer/ClassMethodAnalyzer.php

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
use Magento\SemanticVersionChecker\Operation\Visibility\MethodDecreased as VisibilityMethodDecreased;
2525
use Magento\SemanticVersionChecker\Operation\Visibility\MethodIncreased as VisibilityMethodIncreased;
2626
use PhpParser\Node\NullableType;
27-
use PhpParser\Node\Stmt;
27+
use PhpParser\Node\Name;
28+
use PhpParser\Node\Stmt\Class_;
2829
use PhpParser\Node\Stmt\ClassLike;
2930
use PhpParser\Node\Stmt\ClassMethod;
3031
use PHPSemVerChecker\Comparator\Implementation;
@@ -38,12 +39,6 @@
3839
use PHPSemVerChecker\Operation\ClassMethodParameterTypingRemoved;
3940
use PHPSemVerChecker\Operation\ClassMethodRemoved;
4041
use PHPSemVerChecker\Report\Report;
41-
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
42-
use PHPStan\PhpDocParser\Lexer\Lexer;
43-
use PHPStan\PhpDocParser\Parser\ConstExprParser;
44-
use PHPStan\PhpDocParser\Parser\PhpDocParser;
45-
use PHPStan\PhpDocParser\Parser\TokenIterator;
46-
use PHPStan\PhpDocParser\Parser\TypeParser;
4742

4843
/**
4944
* Class method analyzer.
@@ -422,20 +417,62 @@ private function isReturnsEqualByNullability(ClassMethod $before, ClassMethod $a
422417
*/
423418
private function getDocReturnDeclaration(ClassMethod $method)
424419
{
425-
if ($method->getDocComment() !== null) {
426-
$lexer = new Lexer();
427-
$typeParser = new TypeParser();
428-
$constExprParser = new ConstExprParser();
429-
$phpDocParser = new PhpDocParser($typeParser, $constExprParser);
430-
431-
$tokens = $lexer->tokenize((string)$method->getDocComment());
432-
$tokenIterator = new TokenIterator($tokens);
433-
$phpDocNode = $phpDocParser->parse($tokenIterator);
434-
$tags = $phpDocNode->getTagsByName('@return');
435-
/** @var PhpDocTagNode $tag */
436-
$tag = array_shift($tags);
420+
if (
421+
($parsedComment = $method->getAttribute('docCommentParsed'))
422+
&& isset($parsedComment['return'])
423+
) {
424+
$result = implode('|', $parsedComment['return']);
425+
426+
return $result;
427+
} elseif ($this->dependencyGraph !== null) {
428+
/** @var Class_ $methodClass */
429+
$methodClass = $method->getAttribute('parent');
430+
if ($methodClass) {
431+
$ancestors = [];
432+
if (!empty($methodClass->extends)) {
433+
$ancestors = $this->addAncestorsToArray($ancestors, $methodClass->extends);
434+
}
435+
if (!empty($methodClass->implements)) {
436+
$ancestors = $this->addAncestorsToArray($ancestors, $methodClass->implements);
437+
}
438+
/** @var Name $ancestor */
439+
foreach ($ancestors as $ancestor) {
440+
$ancestorClass = $this->dependencyGraph->findEntityByName($ancestor->toString());
441+
if ($ancestorClass) {
442+
foreach ($ancestorClass->getMethodList() as $methodItem) {
443+
if ($method->name->toString() == $methodItem->name->toString()) {
444+
$result = $this->getDocReturnDeclaration($methodItem);
445+
if (!empty(trim($result))) {
446+
return $result;
447+
}
448+
}
449+
}
450+
}
451+
}
452+
}
437453
}
438-
return isset($tag) ? (string)$tag->value : ' ';
454+
455+
return ' ';
456+
}
457+
458+
/**
459+
* Add ancestors to array
460+
*
461+
* @param array $ancestors
462+
* @param array|Name $toAdd
463+
* @return array
464+
*/
465+
private function addAncestorsToArray(array $ancestors, $toAdd)
466+
{
467+
if (!empty($toAdd)) {
468+
if (is_array($toAdd)) {
469+
$ancestors = array_merge($ancestors, $toAdd);
470+
} else {
471+
$ancestors[] = $toAdd;
472+
}
473+
}
474+
475+
return $ancestors;
439476
}
440477

441478
/**

src/Analyzer/Factory/AnalyzerFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterfa
3030
{
3131
$analyzers = [
3232
new ClassAnalyzer(null, null, null, $dependencyGraph),
33-
new InterfaceAnalyzer(),
33+
new InterfaceAnalyzer(null, null, null, $dependencyGraph),
3434
new TraitAnalyzer(),
3535
];
3636

src/Analyzer/Factory/NonApiAnalyzerFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ class NonApiAnalyzerFactory implements AnalyzerFactoryInterface
2828
public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface
2929
{
3030
$analyzers = [
31-
new ClassAnalyzer(),
32-
new InterfaceAnalyzer(),
31+
new ClassAnalyzer(null, null, null, $dependencyGraph),
32+
new InterfaceAnalyzer(null, null, null, $dependencyGraph),
3333
new TraitAnalyzer(),
3434
];
3535

src/Analyzer/InterfaceAnalyzer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe
139139
protected function getContentAnalyzers($context, $fileBefore, $fileAfter)
140140
{
141141
return [
142-
new ClassMethodAnalyzer($context, $fileBefore, $fileAfter),
142+
new ClassMethodAnalyzer($context, $fileBefore, $fileAfter, $this->dependencyGraph),
143143
new ClassConstantAnalyzer($context, $fileBefore, $fileAfter),
144144
new ClassMethodExceptionAnalyzer($context, $fileBefore, $fileAfter),
145145
new InterfaceExtendsAnalyzer($context, $fileBefore, $fileAfter)

src/Analyzer/MethodDocBlockAnalyzer.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
* - method param typehint moved from in-line to doc block
3636
* - method return typehint moved from doc block to in-line
3737
* - method return typehint moved from in-line to doc block
38+
*
39+
* TODO: this class should be rewritten using new possibility added by
40+
* Magento\SemanticVersionChecker\Visitor\NameResolver
41+
* Now all information (and resolved typed) about DocBlock params and return type exists in
42+
* method node 'docCommentParsed' attribute
3843
*/
3944
class MethodDocBlockAnalyzer
4045
{

src/ClassHierarchy/StaticAnalyzerFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
use Magento\SemanticVersionChecker\Helper\Node as NodeHelper;
1313
use PhpParser\NodeTraverser;
14-
use PhpParser\NodeVisitor\NameResolver;
14+
use Magento\SemanticVersionChecker\Visitor\NameResolver;
15+
use PhpParser\NodeVisitor\ParentConnectingVisitor;
1516
use PhpParser\ParserFactory;
1617

1718
/**
@@ -31,6 +32,7 @@ public function create(): StaticAnalyzer
3132
);
3233
$nodeTraverser = new NodeTraverser();
3334

35+
$nodeTraverser->addVisitor(new ParentConnectingVisitor());
3436
$nodeTraverser->addVisitor(new NameResolver());
3537

3638
return new StaticAnalyzer($parser, $dependencyInspectionVisitor, $nodeTraverser);

src/Scanner/ScannerRegistryFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
use Magento\SemanticVersionChecker\Visitor\ApiTraitVisitor;
1919
use PhpParser\Lexer\Emulative;
2020
use PhpParser\NodeTraverser;
21-
use PhpParser\NodeVisitor\NameResolver;
21+
use Magento\SemanticVersionChecker\Visitor\NameResolver;
22+
use PhpParser\NodeVisitor\ParentConnectingVisitor;
2223
use PhpParser\Parser\Php7 as Parser;
2324
use PHPSemVerChecker\Registry\Registry;
2425
use PHPSemVerChecker\Visitor\ClassVisitor;
@@ -38,6 +39,7 @@ private function buildFullScanner()
3839
$traverser = new NodeTraverser();
3940
$apiVisitors = [
4041
new NameResolver(),
42+
new ParentConnectingVisitor(),
4143
new ClassVisitor($registry),
4244
new InterfaceVisitor($registry),
4345
new FunctionVisitor($registry),
@@ -59,6 +61,7 @@ private function buildApiScanner(DependencyGraph $dependencyGraph = null)
5961
$nodeHelper = new NodeHelper();
6062
$apiVisitors = [
6163
new NameResolver(),
64+
new ParentConnectingVisitor(),
6265
new ApiClassVisitor($registry, $nodeHelper, $dependencyGraph),
6366
new ApiInterfaceVisitor($registry, $nodeHelper, $dependencyGraph),
6467
new ApiTraitVisitor($registry, $nodeHelper, $dependencyGraph),

src/Visitor/NameResolver.php

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php
2+
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
8+
namespace Magento\SemanticVersionChecker\Visitor;
9+
10+
use PhpParser\Node;
11+
use PhpParser\Node\Name;
12+
use PhpParser\Node\Stmt\ClassMethod;
13+
use PhpParser\NodeAbstract;
14+
use PhpParser\NodeVisitor\NameResolver as ParserNameResolver;
15+
use PhpParser\BuilderHelpers;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
17+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
18+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
19+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
20+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
21+
use PHPStan\PhpDocParser\Lexer\Lexer;
22+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
23+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
24+
use PHPStan\PhpDocParser\Parser\TokenIterator;
25+
use PHPStan\PhpDocParser\Parser\TypeParser;
26+
27+
/**
28+
* Extended Name Resolver that parse and resolve also docblock params and return type hinting.
29+
*/
30+
class NameResolver extends ParserNameResolver
31+
{
32+
/**
33+
* Internal types that should not be resolved for docblock
34+
*
35+
* @var string[]
36+
*/
37+
private $internalTypes = [
38+
'string',
39+
'integer',
40+
'float',
41+
'double',
42+
'boolean',
43+
'bool',
44+
'array',
45+
'object',
46+
'null',
47+
'resource',
48+
'$this',
49+
];
50+
51+
/**
52+
* @inheritDoc
53+
*/
54+
public function enterNode(Node $node)
55+
{
56+
$return = parent::enterNode($node);
57+
58+
if ($node instanceof ClassMethod) {
59+
$this->resolveDocBlockParamTypes($node);
60+
}
61+
62+
return $return;
63+
}
64+
65+
/**
66+
* @param ClassMethod $node
67+
* @return void
68+
*/
69+
private function resolveDocBlockParamTypes(ClassMethod $node)
70+
{
71+
/** @var PhpDocNode $docNode */
72+
$docNode = $this->getParsedDocNode($node);
73+
if ($docNode) {
74+
$result = [];
75+
/** @var ParamTagValueNode[] $paramTags */
76+
$paramTags = $docNode->getParamTagValues();
77+
/** @var ParamTagValueNode $paramTag */
78+
foreach ($paramTags as $paramTag) {
79+
$paramNode = [
80+
'name' => $paramTag->parameterName ?? '',
81+
'type' => $this->parseType($paramTag->type),
82+
];
83+
$result['params'][] = $paramNode;
84+
}
85+
86+
/** @var ReturnTagValueNode[] $returnTags */
87+
$returnTags = $docNode->getReturnTagValues();
88+
/** @var ReturnTagValueNode $returnTag */
89+
$returnTag = array_shift($returnTags);
90+
if ($returnTag) {
91+
$result['return'] = $this->parseType($returnTag->type);
92+
}
93+
$node->setAttribute('docCommentParsed', $result);
94+
}
95+
}
96+
97+
/**
98+
* Parse param or return type into array of resolved types
99+
*
100+
* @param TypeNode $type
101+
* @return array
102+
*/
103+
private function parseType($type)
104+
{
105+
$result = [];
106+
if ($type instanceof UnionTypeNode) {
107+
foreach ($type->types as $typeNode) {
108+
$result[] = $this->normalizeAndResolve($typeNode);
109+
}
110+
} else {
111+
$result[] = $this->normalizeAndResolve($type);
112+
}
113+
114+
uasort(
115+
$result,
116+
function ($elementOne, $elementTwo) {
117+
return ((string)$elementOne < (string)$elementTwo) ? -1 : 1;
118+
}
119+
);
120+
121+
return $result;
122+
}
123+
124+
/**
125+
* @param TypeNode $type
126+
* @return NodeAbstract
127+
*/
128+
private function normalizeAndResolve($type)
129+
{
130+
$normalizedType = BuilderHelpers::normalizeType((string)$type);
131+
132+
if (in_array(strtolower((string)$type), $this->internalTypes)) {
133+
$resolvedType = $normalizedType;
134+
} else {
135+
$resolvedType = $this->resolveType($normalizedType);
136+
}
137+
138+
return $resolvedType;
139+
}
140+
141+
/**
142+
* Resolve type from Relative to FQCN
143+
*
144+
* @param $node
145+
* @return NodeAbstract
146+
*/
147+
private function resolveType($node)
148+
{
149+
if ($node instanceof Name) {
150+
return $this->resolveClassName($node);
151+
}
152+
if ($node instanceof Node\NullableType) {
153+
$node->type = $this->resolveType($node->type);
154+
return $node;
155+
}
156+
if ($node instanceof Node\UnionType) {
157+
foreach ($node->types as &$type) {
158+
$type = $this->resolveType($type);
159+
}
160+
return $node;
161+
}
162+
return $node;
163+
}
164+
165+
/**
166+
* Analyses the Method doc block and returns parsed node
167+
*
168+
* @param ClassMethod $method
169+
* @return PhpDocNode|null
170+
*/
171+
private function getParsedDocNode(ClassMethod $method)
172+
{
173+
$docComment = $method->getDocComment();
174+
if ($docComment !== null) {
175+
$lexer = new Lexer();
176+
$typeParser = new TypeParser();
177+
$constExprParser = new ConstExprParser();
178+
$phpDocParser = new PhpDocParser($typeParser, $constExprParser);
179+
$tokens = $lexer->tokenize((string)$docComment);
180+
$tokenIterator = new TokenIterator($tokens);
181+
$phpDocNode = $phpDocParser->parse($tokenIterator);
182+
183+
return $phpDocNode;
184+
}
185+
186+
return null;
187+
}
188+
}

0 commit comments

Comments
 (0)