Skip to content

Commit 3e89db7

Browse files
authored
Merge pull request #1 from hirokinoue/feature/analyze-function-like
Refactoring the FunctionLike search method
2 parents d6b7ce1 + 224da0b commit 3e89db7

File tree

6 files changed

+148
-95
lines changed

6 files changed

+148
-95
lines changed

src/Parse/VariableParser.php

Lines changed: 35 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,125 +5,65 @@
55
namespace Smeghead\PhpVariableHardUsage\Parse;
66

77
use PhpParser\Node\Expr\Variable;
8-
use PhpParser\Node\Stmt;
9-
use PhpParser\Node\Stmt\ClassMethod;
10-
use PhpParser\Node\Stmt\Class_;
11-
use PhpParser\Node\Stmt\Function_;
12-
use PhpParser\NodeDumper;
13-
use PhpParser\NodeVisitor\FindingVisitor;
8+
use PhpParser\Node\FunctionLike;
9+
use PhpParser\NodeFinder;
10+
use PhpParser\NodeTraverser;
1411
use PhpParser\Parser;
1512
use Smeghead\PhpVariableHardUsage\Parse\Exception\ParseFailedException;
13+
use Smeghead\PhpVariableHardUsage\Parse\Visitor\FunctionLikeFindingVisitor;
1614

1715
final class VariableParser
1816
{
1917
private Parser $parser;
2018

19+
private NodeFinder $nodeFinder;
20+
2121
public function __construct()
2222
{
2323
$this->parser = (new \PhpParser\ParserFactory())->createForNewestSupportedVersion();
24+
$this->nodeFinder = new NodeFinder();
2425
}
2526

26-
/**
27-
* @param list<Stmt> $stmt
28-
* @return list<Function_>
29-
*/
30-
private function getFunctions(array $stmt): array
27+
public function parse(string $content): ParseResult
3128
{
32-
$functionVisitor = new FindingVisitor(function ($node) {
33-
return $node instanceof Function_;
34-
});
35-
$traverser = new \PhpParser\NodeTraverser();
36-
$traverser->addVisitor($functionVisitor);
37-
$traverser->traverse($stmt);
29+
$stmts = $this->parser->parse($content);
30+
if ($stmts === null) {
31+
throw new ParseFailedException();
32+
}
3833

39-
return $functionVisitor->getFoundNodes(); // @phpstan-ignore-line
40-
}
34+
$traverser = new NodeTraverser();
35+
$visitor = new FunctionLikeFindingVisitor(fn($node) => $node instanceof FunctionLike);
36+
$traverser->addVisitor($visitor);
37+
$traverser->traverse($stmts);
38+
$functionLikes = $visitor->getFoundNodes();
4139

42-
/**
43-
* @param Function_|ClassMethod $function
44-
* @return list<Variable>
45-
*/
46-
private function getVariables(Function_|ClassMethod $function): array
47-
{
48-
$variableVisitor = new FindingVisitor(function ($node) {
49-
return $node instanceof Variable;
50-
});
51-
$traverser = new \PhpParser\NodeTraverser();
52-
$traverser->addVisitor($variableVisitor);
53-
$traverser->traverse([$function]);
40+
$functions = $this->collectParseResultPerFunctionLike($functionLikes);
5441

55-
return $variableVisitor->getFoundNodes(); // @phpstan-ignore-line
42+
return new ParseResult($functions);
5643
}
5744

5845
/**
59-
* @param list<Stmt> $stmts
46+
* @param list<FunctionLike> $functionLikes
6047
* @return list<Func>
6148
*/
62-
private function parseFunctions(array $stmts): array
49+
private function collectParseResultPerFunctionLike(array $functionLikes): array
6350
{
64-
$foundFunctions = $this->getFunctions($stmts);
65-
66-
$functions = [];
67-
foreach ($foundFunctions as $foundFunction) {
68-
$variables = $this->getVariables($foundFunction);
69-
$func = new Func($foundFunction->name->name);
51+
return array_map(function (FunctionLike $function) {
52+
$className = $function->getAttribute('className'); // Get the class name set in FunctionLikeFindingVisitor
53+
$functionName = $function->name->name ?? $function->getType() . '@' . $function->getStartLine();
54+
$functionIdentifier = sprintf(
55+
'%s%s',
56+
isset($className) ? $className . '::' : '',
57+
$functionName
58+
);
59+
60+
$func = new Func($functionIdentifier);
61+
62+
$variables = $this->nodeFinder->findInstanceOf($function, Variable::class);
7063
foreach ($variables as $variable) {
7164
$func->addVariable(new VarReference($variable->name, $variable->getLine())); // @phpstan-ignore-line
7265
}
73-
$functions[] = $func;
74-
}
75-
return $functions;
76-
}
77-
78-
/**
79-
* @param list<Stmt> $stmt
80-
* @return list<Class_>
81-
*/
82-
private function getClasses(array $stmt): array
83-
{
84-
$classVisitor = new FindingVisitor(function ($node) {
85-
return $node instanceof \PhpParser\Node\Stmt\Class_;
86-
});
87-
$traverser = new \PhpParser\NodeTraverser();
88-
$traverser->addVisitor($classVisitor);
89-
$traverser->traverse($stmt);
90-
91-
return $classVisitor->getFoundNodes(); // @phpstan-ignore-line
92-
}
93-
94-
/**
95-
* @param list<Stmt> $stmts
96-
* @return list<Func>
97-
*/
98-
private function parseClasses(array $stmts): array
99-
{
100-
$foundClasses = $this->getClasses($stmts);
101-
102-
$methods = [];
103-
foreach ($foundClasses as $foundClass) {
104-
foreach ($foundClass->getMethods() as $method) {
105-
$variables = $this->getVariables($method);
106-
$func = new Func(sprintf('%s::%s', $foundClass->name, $method->name->name));
107-
foreach ($variables as $variable) {
108-
$func->addVariable(new VarReference($variable->name, $variable->getLine())); // @phpstan-ignore-line
109-
}
110-
$methods[] = $func;
111-
}
112-
}
113-
return $methods;
114-
}
115-
116-
public function parse(string $content): ParseResult
117-
{
118-
$stmts = $this->parser->parse($content);
119-
if ($stmts === null) {
120-
throw new ParseFailedException();
121-
}
122-
$dumper = new NodeDumper();
123-
// echo $dumper->dump($stmts) . PHP_EOL;
124-
125-
$functions = $this->parseFunctions($stmts) + $this->parseClasses($stmts);
126-
127-
return new ParseResult($functions);
66+
return $func;
67+
}, $functionLikes);
12868
}
12969
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Smeghead\PhpVariableHardUsage\Parse\Visitor;
6+
7+
use PhpParser\Node;
8+
use PhpParser\NodeVisitor\FindingVisitor;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
12+
/**
13+
* FindingVisitor that searches for elements of FunctionLike
14+
*
15+
* For ClassMethod, set the class name to ClassName as Attribute.
16+
*/
17+
class FunctionLikeFindingVisitor extends FindingVisitor {
18+
protected array $foundNodes = [];
19+
private ?string $currentClass = null;
20+
21+
public function enterNode(Node $node) {
22+
if ($node instanceof Class_) {
23+
// Record class name
24+
$this->currentClass = $node->name ? $node->name->name : null;
25+
}
26+
27+
if ($node instanceof ClassMethod) {
28+
$node->setAttribute('className', $this->currentClass);
29+
}
30+
return parent::enterNode($node);
31+
}
32+
33+
public function leaveNode(Node $node) {
34+
if ($node instanceof Class_) {
35+
// Reset because it leaves the class scope
36+
$this->currentClass = null;
37+
}
38+
}
39+
}

test/VariableParserTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,27 @@ public function testParseClass(): void
5151
$this->assertSame('num', $vars[3]->name);
5252
$this->assertSame(15, $vars[3]->lineNumber, 'second $num (15)');
5353
}
54+
55+
public function testParseAnonymousFunction(): void
56+
{
57+
$parser = new VariableParser();
58+
$content = file_get_contents($this->fixtureDir . '/AnonymousFunction.php');
59+
$result = $parser->parse($content);
60+
61+
$this->assertInstanceOf(ParseResult::class, $result);
62+
$functions = $result->functions;
63+
$this->assertCount(1, $functions);
64+
$this->assertEquals('Expr_Closure@4', $functions[0]->name);
65+
$this->assertCount(4, $functions[0]->getVariables());
66+
67+
$vars = $functions[0]->getVariables();
68+
$this->assertSame('a', $vars[0]->name);
69+
$this->assertSame(4, $vars[0]->lineNumber, 'first $a (4)');
70+
$this->assertSame('b', $vars[1]->name);
71+
$this->assertSame(4, $vars[1]->lineNumber, 'second $b (4)');
72+
$this->assertSame('a', $vars[2]->name);
73+
$this->assertSame(5, $vars[2]->lineNumber, 'second $a (5)');
74+
$this->assertSame('b', $vars[3]->name);
75+
$this->assertSame(5, $vars[3]->lineNumber, 'second $b (5)');
76+
}
5477
}

test/fixtures/AnonymousFunction.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
$items = ['ca', 'aa', 'bc', 'ab', 'ac'];
4+
usort($items, function (string $a, string $b): int {
5+
return $a <=> $b;
6+
});

test/fixtures/improved_my_average.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
/**
4+
* 最小値と最大値を除いて平均を返却します。
5+
*/
6+
function improved_my_avearge(array $items): float {
7+
sort($items);
8+
$newItems = array_slice($items, 1, count($items) - 2);
9+
$sum = array_sum($newItems);
10+
return $sum / count($newItems);
11+
}
12+
13+
$items = [3, 99, 40, 45, 50, 52];
14+
print_r(improved_my_avearge($items) . PHP_EOL);

test/fixtures/ugly_my_average.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/**
4+
* 最小値と最大値を除いて平均を返却します。
5+
*/
6+
function ugly_my_average(array $items): float {
7+
$minValue = PHP_INT_MAX;
8+
$maxValue = PHP_INT_MIN;
9+
10+
for ($i = 0; $i < count($items); $i++) {
11+
$v = $items[$i];
12+
if ($v < $minValue) {
13+
$minValue = $v;
14+
}
15+
if ($v > $maxValue) {
16+
$maxValue = $v;
17+
}
18+
}
19+
$sum = 0;
20+
for ($i = 0; $i < count($items); $i++) {
21+
$v = $items[$i];
22+
if (in_array($v, [$minValue, $maxValue])) {
23+
continue;
24+
}
25+
$sum += $v;
26+
}
27+
return $sum / (count($items) - 2);
28+
}
29+
30+
$items = [3, 99, 40, 45, 50, 52];
31+
print_r(ugly_my_average($items) . PHP_EOL);

0 commit comments

Comments
 (0)