Skip to content

Commit 0f35f4a

Browse files
Merge pull request #7 from DeGraciaMathieu/php-parser-adapter
Php parser adapter
2 parents aad21ea + e8157e0 commit 0f35f4a

File tree

6 files changed

+385
-0
lines changed

6 files changed

+385
-0
lines changed

AGENTS.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# AGENTS.md
2+
3+
This file provides guidance for agentic coding assistants operating in this repository.
4+
Follow these instructions when reading, modifying, or adding code.
5+
6+
---
7+
8+
## Project Overview
9+
10+
- PHP 8.2+ CLI application built with **Laravel Zero**
11+
- Purpose: analyze PHP class dependencies, coupling, instability, and cycles
12+
- Architecture: layered (Application / Domain / Infrastructure)
13+
- Testing: **Pest** (on top of PHPUnit)
14+
- Formatting: **Laravel Pint**
15+
16+
---
17+
18+
## Environment & Prerequisites
19+
20+
- PHP >= 8.2
21+
- Composer
22+
- Xdebug (optional, for coverage)
23+
24+
Install dependencies:
25+
26+
- `composer install`
27+
28+
---
29+
30+
## Build, Lint, and Test Commands
31+
32+
### Running the Application
33+
34+
- Main binary: `class-dependencies-analyzer`
35+
- Example:
36+
- `php class-dependencies-analyzer analyze:class app`
37+
38+
### Tests (Pest)
39+
40+
- Run full test suite:
41+
- `composer test`
42+
- `vendor/bin/pest -p`
43+
44+
- Run a single test file:
45+
- `vendor/bin/pest tests/Unit/FooTest.php`
46+
47+
- Run a single test by name:
48+
- `vendor/bin/pest --filter="it does something"`
49+
50+
- Run a specific testsuite:
51+
- `vendor/bin/pest --testsuite=Unit`
52+
53+
- Parallel execution is enabled by default via `-p`
54+
55+
### Coverage
56+
57+
- Run tests with coverage:
58+
- `composer coverage`
59+
60+
### Linting / Formatting
61+
62+
- Format code using Pint:
63+
- `vendor/bin/pint`
64+
65+
- Check formatting without writing:
66+
- `vendor/bin/pint --test`
67+
68+
### Healthcheck Scripts
69+
70+
Defined in `composer.json`:
71+
72+
- `composer healthcheck`
73+
- Includes multiple analyzer self-checks and a test run
74+
75+
---
76+
77+
## Code Style Guidelines
78+
79+
### General
80+
81+
- Follow **PSR-12** and Laravel conventions
82+
- Prefer clarity over cleverness
83+
- Keep classes small and single-purpose
84+
85+
### Imports
86+
87+
- Use fully-qualified imports (`use ...`) at top of file
88+
- One import per line
89+
- Remove unused imports
90+
- Group imports logically (PHP, App, Vendor)
91+
92+
### Formatting
93+
94+
- Enforced by **Laravel Pint**
95+
- 4 spaces indentation
96+
- One class per file
97+
- Trailing commas in multiline argument lists
98+
99+
### Naming Conventions
100+
101+
- Classes: `StudlyCase`
102+
- Methods: `camelCase`
103+
- Variables: `camelCase`
104+
- Constants: `SCREAMING_SNAKE_CASE`
105+
- Interfaces: descriptive nouns (no `Interface` suffix preferred)
106+
107+
### Types & Signatures
108+
109+
- Always use scalar and object type hints
110+
- Always declare return types
111+
- Prefer `readonly` and promoted constructor properties where applicable
112+
- Avoid mixed types unless strictly necessary
113+
114+
### Error Handling
115+
116+
- Use exceptions for exceptional states
117+
- Catch `Throwable` only at application boundaries
118+
- Domain logic should not swallow exceptions
119+
- Present errors via presenters or CLI output, not `echo`
120+
121+
### Null & Defensive Code
122+
123+
- Prefer explicit null checks
124+
- Avoid deeply nested conditionals
125+
- Fail fast when input is invalid
126+
127+
---
128+
129+
## Architecture Rules
130+
131+
### Application Layer
132+
133+
- Orchestrates use cases
134+
- Depends on Domain abstractions (ports)
135+
- No infrastructure details
136+
137+
### Domain Layer
138+
139+
- Contains core business logic
140+
- Framework-agnostic
141+
- No IO, no framework dependencies
142+
143+
### Infrastructure Layer
144+
145+
- Implements ports (filesystem, CLI, adapters)
146+
- Can depend on frameworks and vendor libraries
147+
148+
### Dependency Direction
149+
150+
- Infrastructure → Application → Domain
151+
- Never the reverse
152+
153+
---
154+
155+
## Testing Guidelines
156+
157+
- Prefer **Unit tests** for domain logic
158+
- Use **Feature tests** for CLI commands and integration
159+
- Tests should be deterministic and isolated
160+
- Use Mockery for mocking ports
161+
162+
---
163+
164+
## Filesystem & Safety Rules
165+
166+
- Do not modify files in `vendor/`
167+
- Do not commit generated reports or artifacts
168+
- Avoid touching unrelated files
169+
170+
---
171+
172+
## Git & Commits
173+
174+
- Do not commit unless explicitly requested
175+
- Follow existing commit message style
176+
- Never rewrite history without permission
177+
178+
---
179+
180+
## Agent Behavior Expectations
181+
182+
- Respect this file for all edits in this repository
183+
- Keep changes minimal and focused
184+
- Ask before making large refactors
185+
- Do not introduce new tools or dependencies without approval
186+
187+
---
188+
189+
End of AGENTS.md
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Infrastructure\Analyze\Adapters\PhpParser;
4+
5+
use App\Infrastructure\Analyze\Ports\ClassAnalysis;
6+
7+
final class AstClassAnalysis implements ClassAnalysis
8+
{
9+
public function __construct(
10+
private readonly string $fqcn,
11+
private readonly array $dependencies,
12+
private readonly bool $isInterface = false,
13+
private readonly bool $isAbstract = false,
14+
) {}
15+
16+
public function fqcn(): string
17+
{
18+
return $this->fqcn;
19+
}
20+
21+
public function dependencies(): array
22+
{
23+
return $this->dependencies;
24+
}
25+
26+
public function isInterface(): bool
27+
{
28+
return $this->isInterface;
29+
}
30+
31+
public function isAbstract(): bool
32+
{
33+
return $this->isAbstract;
34+
}
35+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace App\Infrastructure\Analyze\Adapters\PhpParser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use PhpParser\Node\Stmt\Class_;
8+
use PhpParser\Node\Stmt\Interface_;
9+
use PhpParser\Node\Stmt\Enum_;
10+
11+
final class DependencyCollectorVisitor extends NodeVisitorAbstract
12+
{
13+
private array $dependencies = [];
14+
private ?string $fqcn = null;
15+
private bool $isInterface = false;
16+
private bool $isAbstract = false;
17+
18+
public function enterNode(Node $node): void
19+
{
20+
if ($node instanceof Class_) {
21+
$this->fqcn = $node->namespacedName?->toString();
22+
$this->isAbstract = $node->isAbstract();
23+
$this->isInterface = false;
24+
}
25+
26+
if ($node instanceof Interface_ && $this->fqcn === null) {
27+
$this->fqcn = $node->namespacedName?->toString();
28+
$this->isInterface = true;
29+
}
30+
31+
if ($node instanceof Enum_) {
32+
$this->fqcn = $node->namespacedName?->toString();
33+
}
34+
35+
if ($node instanceof Node\Name) {
36+
$name = $node->toString();
37+
if (! $this->isBuiltinType($name)) {
38+
$this->dependencies[] = $name;
39+
}
40+
}
41+
42+
if ($node instanceof Node\Attribute) {
43+
$this->dependencies[] = $node->name->toString();
44+
}
45+
}
46+
47+
public function analysis(): AstClassAnalysis
48+
{
49+
return new AstClassAnalysis(
50+
fqcn: $this->fqcn ?? '',
51+
dependencies: array_values(array_unique($this->dependencies)),
52+
isInterface: $this->isInterface,
53+
isAbstract: $this->isAbstract,
54+
);
55+
}
56+
57+
private function isBuiltinType(string $name): bool
58+
{
59+
return in_array(strtolower($name), [
60+
'string', 'int', 'float', 'bool', 'array', 'callable',
61+
'iterable', 'object', 'mixed', 'null', 'false', 'true',
62+
'never', 'void', 'self', 'parent', 'static',
63+
], true);
64+
}
65+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Infrastructure\Analyze\Adapters\PhpParser;
4+
5+
use PhpParser\ParserFactory;
6+
use PhpParser\NodeTraverser;
7+
use PhpParser\NodeVisitor\NameResolver;
8+
use App\Infrastructure\Analyze\Ports\ClassDependenciesParser;
9+
use App\Infrastructure\Analyze\Ports\ClassAnalysis;
10+
11+
final class PhpAstClassDependenciesParser implements ClassDependenciesParser
12+
{
13+
public function parse(string $file): ClassAnalysis
14+
{
15+
$code = file_get_contents($file);
16+
17+
$parser = (new ParserFactory())->createForNewestSupportedVersion();
18+
$ast = $parser->parse($code);
19+
20+
$collector = new DependencyCollectorVisitor();
21+
22+
$traverser = new NodeTraverser();
23+
$traverser->addVisitor(new NameResolver());
24+
$traverser->addVisitor($collector);
25+
$traverser->traverse($ast);
26+
27+
return $collector->analysis();
28+
}
29+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Tests\Fixtures\Php85;
4+
5+
use Attribute;
6+
use DateTimeInterface;
7+
use IteratorAggregate;
8+
9+
#[Attribute]
10+
class CustomAttribute {}
11+
12+
interface Contract {}
13+
14+
abstract class AbstractBase {}
15+
16+
enum Status: string {
17+
case Active = 'active';
18+
}
19+
20+
final class ModernClass extends AbstractBase implements Contract, IteratorAggregate
21+
{
22+
public function __construct(
23+
private readonly DateTimeInterface $clock,
24+
) {}
25+
26+
#[CustomAttribute]
27+
public function handle(Status|Contract|null $value): ?DateTimeInterface
28+
{
29+
return $this->clock;
30+
}
31+
32+
public function getIterator(): \Traversable
33+
{
34+
return new \ArrayIterator([]);
35+
}
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use App\Infrastructure\Analyze\Adapters\PhpParser\PhpAstClassDependenciesParser;
4+
5+
it('detects dependencies from modern PHP syntax (8.1+) ', function () {
6+
$parser = app(PhpAstClassDependenciesParser::class);
7+
8+
$analysis = $parser->parse(__DIR__ . '/../../../Fixtures/Php85/ModernClass.php');
9+
10+
expect($analysis->fqcn())->toBe('Tests\\Fixtures\\Php85\\ModernClass');
11+
12+
expect($analysis->dependencies())->toContain(
13+
'Tests\\Fixtures\\Php85\\AbstractBase',
14+
'Tests\\Fixtures\\Php85\\Contract',
15+
'IteratorAggregate',
16+
'DateTimeInterface',
17+
'Tests\\Fixtures\\Php85\\Status',
18+
'Tests\\Fixtures\\Php85\\CustomAttribute',
19+
'ArrayIterator',
20+
'Traversable',
21+
);
22+
});
23+
24+
it('marks interface and abstract correctly', function () {
25+
$parser = app(PhpAstClassDependenciesParser::class);
26+
27+
$analysis = $parser->parse(__DIR__ . '/../../../Fixtures/Php85/ModernClass.php');
28+
29+
expect($analysis->isAbstract())->toBeFalse();
30+
expect($analysis->isInterface())->toBeFalse();
31+
});

0 commit comments

Comments
 (0)