Skip to content

Commit 5781433

Browse files
committed
Add ClassExtends and InterfaceExtends node visitors - Close #6
1 parent 7900a3d commit 5781433

File tree

5 files changed

+571
-0
lines changed

5 files changed

+571
-0
lines changed

src/Exception/LogicException.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/php-code-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/php-code-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/php-code-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModeling\CodeAst\Exception;
12+
13+
use LogicException as BaseException;
14+
15+
class LogicException extends BaseException implements CodeAstException
16+
{
17+
}

src/NodeVisitor/ClassExtends.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/php-code-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/php-code-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/php-code-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModeling\CodeAst\NodeVisitor;
12+
13+
use OpenCodeModeling\CodeAst\Exception\LogicException;
14+
use PhpParser\Node\Name;
15+
use PhpParser\Node\Stmt;
16+
use PhpParser\Node\Stmt\Namespace_;
17+
use PhpParser\NodeVisitorAbstract;
18+
19+
final class ClassExtends extends NodeVisitorAbstract
20+
{
21+
/**
22+
* @var string
23+
*/
24+
private $extends;
25+
26+
public function __construct(string $extends)
27+
{
28+
$this->extends = $extends;
29+
}
30+
31+
public function afterTraverse(array $nodes): ?array
32+
{
33+
$newNodes = [];
34+
35+
foreach ($nodes as $node) {
36+
$newNodes[] = $node;
37+
38+
if ($node instanceof Namespace_) {
39+
foreach ($node->stmts as $stmt) {
40+
if ($stmt instanceof Stmt\Class_) {
41+
if ($this->checkExtendsExists($stmt)) {
42+
return null;
43+
}
44+
$stmt->extends = new Name($this->extends);
45+
}
46+
}
47+
} elseif ($node instanceof Stmt\Class_) {
48+
if ($this->checkExtendsExists($node)) {
49+
return null;
50+
}
51+
$node->extends = new Name($this->extends);
52+
}
53+
}
54+
55+
return $newNodes;
56+
}
57+
58+
private function checkExtendsExists(Stmt\Class_ $node): bool
59+
{
60+
$exists = $this->extends === (string) $node->extends;
61+
62+
if (false === $exists && null !== $node->extends) {
63+
throw new LogicException(\sprintf(
64+
'Class "%s" extends already from class "%s". Could not add extends from class "%s"',
65+
$node->name->name,
66+
(string) $node->extends,
67+
$this->extends
68+
));
69+
}
70+
71+
return $exists;
72+
}
73+
}

src/NodeVisitor/InterfaceExtends.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/php-code-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/php-code-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/php-code-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModeling\CodeAst\NodeVisitor;
12+
13+
use PhpParser\Node\Name;
14+
use PhpParser\Node\Stmt;
15+
use PhpParser\Node\Stmt\Namespace_;
16+
use PhpParser\NodeVisitorAbstract;
17+
18+
final class InterfaceExtends extends NodeVisitorAbstract
19+
{
20+
/**
21+
* @var string[]
22+
*/
23+
private $extends;
24+
25+
public function __construct(string ...$extends)
26+
{
27+
$this->extends = $extends;
28+
}
29+
30+
public function afterTraverse(array $nodes): ?array
31+
{
32+
$extends = $this->filterExtends($nodes);
33+
34+
if (\count($extends) === 0) {
35+
return null;
36+
}
37+
38+
$newNodes = [];
39+
40+
foreach ($nodes as $node) {
41+
$newNodes[] = $node;
42+
43+
if ($node instanceof Stmt\Interface_) {
44+
$classExtends = $node->extends;
45+
foreach ($extends as $import) {
46+
$classExtends[] = new Name($import);
47+
}
48+
$node->extends = $classExtends;
49+
}
50+
51+
if ($node instanceof Namespace_) {
52+
foreach ($node->stmts as $stmt) {
53+
if ($stmt instanceof Stmt\Interface_) {
54+
$classExtends = $stmt->extends;
55+
foreach ($extends as $import) {
56+
$classExtends[] = new Name($import);
57+
}
58+
$stmt->extends = $classExtends;
59+
}
60+
}
61+
}
62+
}
63+
64+
return $newNodes;
65+
}
66+
67+
private function filterExtends(array $nodes): array
68+
{
69+
$extends = $this->extends;
70+
71+
foreach ($nodes as $node) {
72+
if ($node instanceof Namespace_) {
73+
foreach ($node->stmts as $stmt) {
74+
if ($stmt instanceof Stmt\Interface_) {
75+
foreach ($stmt->extends as $extendName) {
76+
$extends = \array_filter($extends, static function (string $extend) use ($extendName) {
77+
return $extend !== (string) $extendName;
78+
});
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
return $extends;
86+
}
87+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenCodeModelingTest\CodeAst\NodeVisitor;
6+
7+
use OpenCodeModeling\CodeAst\Code\ClassGenerator;
8+
use OpenCodeModeling\CodeAst\Exception\LogicException;
9+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassExtends;
10+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassNamespace;
11+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassFile;
12+
use PhpParser\NodeTraverser;
13+
use PhpParser\Parser;
14+
use PhpParser\ParserFactory;
15+
use PhpParser\PrettyPrinter\Standard;
16+
use PHPUnit\Framework\TestCase;
17+
18+
final class ClassExtendsTest extends TestCase
19+
{
20+
/**
21+
* @var Parser
22+
*/
23+
private $parser;
24+
25+
/**
26+
* @var Standard
27+
*/
28+
private $printer;
29+
30+
public function setUp(): void
31+
{
32+
$this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
33+
$this->printer = new Standard(['shortArraySyntax' => true]);
34+
}
35+
36+
/**
37+
* @test
38+
*/
39+
public function it_generates_class_extends_for_empty_file(): void
40+
{
41+
$ast = $this->parser->parse('');
42+
43+
$nodeTraverser = new NodeTraverser();
44+
$nodeTraverser->addVisitor(new ClassFile(new ClassGenerator('TestClass')));
45+
$nodeTraverser->addVisitor(new ClassExtends('MyBaseClass'));
46+
47+
$expected = <<<EOF
48+
<?php
49+
50+
class TestClass extends MyBaseClass
51+
{
52+
}
53+
EOF;
54+
55+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
56+
}
57+
58+
/**
59+
* @test
60+
*/
61+
public function it_checks_class_extends_for_existing_file(): void
62+
{
63+
$ast = $this->parser->parse('<?php class TestClass {}');
64+
65+
$nodeTraverser = new NodeTraverser();
66+
$nodeTraverser->addVisitor(new ClassFile(new ClassGenerator('TestClass')));
67+
$nodeTraverser->addVisitor(new ClassExtends('MyBaseClass'));
68+
69+
$expected = <<<EOF
70+
<?php
71+
72+
class TestClass extends MyBaseClass
73+
{
74+
}
75+
EOF;
76+
77+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
78+
}
79+
80+
/**
81+
* @test
82+
*/
83+
public function it_generates_class_extends_with_namespace_for_empty_file(): void
84+
{
85+
$ast = $this->parser->parse('');
86+
87+
$nodeTraverser = new NodeTraverser();
88+
$nodeTraverser->addVisitor(new ClassNamespace('My\\Awesome\\Service'));
89+
$nodeTraverser->addVisitor(new ClassFile(new ClassGenerator('TestClass')));
90+
$nodeTraverser->addVisitor(new ClassExtends('MyBaseClass'));
91+
92+
$expected = <<<EOF
93+
<?php
94+
95+
namespace My\Awesome\Service;
96+
97+
class TestClass extends MyBaseClass
98+
{
99+
}
100+
EOF;
101+
102+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
103+
}
104+
105+
/**
106+
* @test
107+
*/
108+
public function it_generates_class_extends_for_namespace_file(): void
109+
{
110+
$code = <<<EOF
111+
<?php
112+
113+
namespace My\Awesome\Service;
114+
EOF;
115+
116+
$ast = $this->parser->parse($code);
117+
118+
$nodeTraverser = new NodeTraverser();
119+
$nodeTraverser->addVisitor(new ClassFile(new ClassGenerator('TestClass')));
120+
$nodeTraverser->addVisitor(new ClassExtends('MyBaseClass'));
121+
122+
$expected = <<<EOF
123+
<?php
124+
125+
namespace My\Awesome\Service;
126+
127+
class TestClass extends MyBaseClass
128+
{
129+
}
130+
EOF;
131+
132+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
133+
}
134+
135+
/**
136+
* @test
137+
*/
138+
public function it_checks_class_extends_with_namespace_for_existing_file(): void
139+
{
140+
$code = <<<EOF
141+
<?php
142+
143+
namespace My\Awesome\Service;
144+
145+
class TestClass {}
146+
EOF;
147+
148+
$ast = $this->parser->parse($code);
149+
150+
$nodeTraverser = new NodeTraverser();
151+
$nodeTraverser->addVisitor(new ClassFile(new ClassGenerator('TestClass')));
152+
$nodeTraverser->addVisitor(new ClassExtends('MyBaseClass'));
153+
154+
$expected = <<<EOF
155+
<?php
156+
157+
namespace My\Awesome\Service;
158+
159+
class TestClass extends MyBaseClass
160+
{
161+
}
162+
EOF;
163+
164+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
165+
}
166+
167+
/**
168+
* @test
169+
*/
170+
public function it_throws_exception_if_multi_inheritance(): void
171+
{
172+
$code = <<<EOF
173+
<?php
174+
175+
namespace My\Awesome\Service;
176+
177+
class TestClass extends OtherBaseClass {}
178+
EOF;
179+
180+
$ast = $this->parser->parse($code);
181+
182+
$nodeTraverser = new NodeTraverser();
183+
$nodeTraverser->addVisitor(new ClassFile(new ClassGenerator('TestClass')));
184+
$nodeTraverser->addVisitor(new ClassExtends('MyBaseClass'));
185+
186+
$this->expectException(LogicException::class);
187+
188+
$nodeTraverser->traverse($ast);
189+
}
190+
191+
}

0 commit comments

Comments
 (0)