Skip to content

Commit 4c9f2e8

Browse files
Adds PHP-Parser based dependency analyzer
Implements a class dependency analyzer using the PHP-Parser library. This allows for static analysis of PHP code to determine class dependencies, interfaces, and abstract classes. The implementation provides an adapter that conforms to the ClassDependenciesParser interface, enabling a flexible and extensible architecture. It also adds an AGENTS.md file that explains how agents should operate within the codebase.
1 parent aad21ea commit 4c9f2e8

File tree

4 files changed

+305
-0
lines changed

4 files changed

+305
-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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
}
24+
25+
if ($node instanceof Interface_) {
26+
$this->fqcn = $node->namespacedName?->toString();
27+
$this->isInterface = true;
28+
}
29+
30+
if ($node instanceof Enum_) {
31+
$this->fqcn = $node->namespacedName?->toString();
32+
}
33+
34+
if ($node instanceof Node\Name) {
35+
$this->dependencies[] = $node->toString();
36+
}
37+
38+
if ($node instanceof Node\Attribute) {
39+
$this->dependencies[] = $node->name->toString();
40+
}
41+
}
42+
43+
public function analysis(): AstClassAnalysis
44+
{
45+
return new AstClassAnalysis(
46+
fqcn: $this->fqcn ?? '',
47+
dependencies: array_values(array_unique($this->dependencies)),
48+
isInterface: $this->isInterface,
49+
isAbstract: $this->isAbstract,
50+
);
51+
}
52+
}
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+
}

0 commit comments

Comments
 (0)