From 4c9f2e857f85bd28e0e3f3d416494797c63bd8fd Mon Sep 17 00:00:00 2001 From: DeGraciaMathieu Date: Mon, 5 Jan 2026 19:01:01 +0100 Subject: [PATCH 1/5] 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. --- AGENTS.md | 189 ++++++++++++++++++ .../Adapters/PhpParser/AstClassAnalysis.php | 35 ++++ .../PhpParser/DependencyCollectorVisitor.php | 52 +++++ .../PhpAstClassDependenciesParser.php | 29 +++ 4 files changed, 305 insertions(+) create mode 100644 AGENTS.md create mode 100644 app/Infrastructure/Analyze/Adapters/PhpParser/AstClassAnalysis.php create mode 100644 app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php create mode 100644 app/Infrastructure/Analyze/Adapters/PhpParser/PhpAstClassDependenciesParser.php diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d8a34c8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,189 @@ +# AGENTS.md + +This file provides guidance for agentic coding assistants operating in this repository. +Follow these instructions when reading, modifying, or adding code. + +--- + +## Project Overview + +- PHP 8.2+ CLI application built with **Laravel Zero** +- Purpose: analyze PHP class dependencies, coupling, instability, and cycles +- Architecture: layered (Application / Domain / Infrastructure) +- Testing: **Pest** (on top of PHPUnit) +- Formatting: **Laravel Pint** + +--- + +## Environment & Prerequisites + +- PHP >= 8.2 +- Composer +- Xdebug (optional, for coverage) + +Install dependencies: + +- `composer install` + +--- + +## Build, Lint, and Test Commands + +### Running the Application + +- Main binary: `class-dependencies-analyzer` +- Example: + - `php class-dependencies-analyzer analyze:class app` + +### Tests (Pest) + +- Run full test suite: + - `composer test` + - `vendor/bin/pest -p` + +- Run a single test file: + - `vendor/bin/pest tests/Unit/FooTest.php` + +- Run a single test by name: + - `vendor/bin/pest --filter="it does something"` + +- Run a specific testsuite: + - `vendor/bin/pest --testsuite=Unit` + +- Parallel execution is enabled by default via `-p` + +### Coverage + +- Run tests with coverage: + - `composer coverage` + +### Linting / Formatting + +- Format code using Pint: + - `vendor/bin/pint` + +- Check formatting without writing: + - `vendor/bin/pint --test` + +### Healthcheck Scripts + +Defined in `composer.json`: + +- `composer healthcheck` +- Includes multiple analyzer self-checks and a test run + +--- + +## Code Style Guidelines + +### General + +- Follow **PSR-12** and Laravel conventions +- Prefer clarity over cleverness +- Keep classes small and single-purpose + +### Imports + +- Use fully-qualified imports (`use ...`) at top of file +- One import per line +- Remove unused imports +- Group imports logically (PHP, App, Vendor) + +### Formatting + +- Enforced by **Laravel Pint** +- 4 spaces indentation +- One class per file +- Trailing commas in multiline argument lists + +### Naming Conventions + +- Classes: `StudlyCase` +- Methods: `camelCase` +- Variables: `camelCase` +- Constants: `SCREAMING_SNAKE_CASE` +- Interfaces: descriptive nouns (no `Interface` suffix preferred) + +### Types & Signatures + +- Always use scalar and object type hints +- Always declare return types +- Prefer `readonly` and promoted constructor properties where applicable +- Avoid mixed types unless strictly necessary + +### Error Handling + +- Use exceptions for exceptional states +- Catch `Throwable` only at application boundaries +- Domain logic should not swallow exceptions +- Present errors via presenters or CLI output, not `echo` + +### Null & Defensive Code + +- Prefer explicit null checks +- Avoid deeply nested conditionals +- Fail fast when input is invalid + +--- + +## Architecture Rules + +### Application Layer + +- Orchestrates use cases +- Depends on Domain abstractions (ports) +- No infrastructure details + +### Domain Layer + +- Contains core business logic +- Framework-agnostic +- No IO, no framework dependencies + +### Infrastructure Layer + +- Implements ports (filesystem, CLI, adapters) +- Can depend on frameworks and vendor libraries + +### Dependency Direction + +- Infrastructure → Application → Domain +- Never the reverse + +--- + +## Testing Guidelines + +- Prefer **Unit tests** for domain logic +- Use **Feature tests** for CLI commands and integration +- Tests should be deterministic and isolated +- Use Mockery for mocking ports + +--- + +## Filesystem & Safety Rules + +- Do not modify files in `vendor/` +- Do not commit generated reports or artifacts +- Avoid touching unrelated files + +--- + +## Git & Commits + +- Do not commit unless explicitly requested +- Follow existing commit message style +- Never rewrite history without permission + +--- + +## Agent Behavior Expectations + +- Respect this file for all edits in this repository +- Keep changes minimal and focused +- Ask before making large refactors +- Do not introduce new tools or dependencies without approval + +--- + +End of AGENTS.md diff --git a/app/Infrastructure/Analyze/Adapters/PhpParser/AstClassAnalysis.php b/app/Infrastructure/Analyze/Adapters/PhpParser/AstClassAnalysis.php new file mode 100644 index 0000000..e57a1b6 --- /dev/null +++ b/app/Infrastructure/Analyze/Adapters/PhpParser/AstClassAnalysis.php @@ -0,0 +1,35 @@ +fqcn; + } + + public function dependencies(): array + { + return $this->dependencies; + } + + public function isInterface(): bool + { + return $this->isInterface; + } + + public function isAbstract(): bool + { + return $this->isAbstract; + } +} diff --git a/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php b/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php new file mode 100644 index 0000000..d4d33e9 --- /dev/null +++ b/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php @@ -0,0 +1,52 @@ +fqcn = $node->namespacedName?->toString(); + $this->isAbstract = $node->isAbstract(); + } + + if ($node instanceof Interface_) { + $this->fqcn = $node->namespacedName?->toString(); + $this->isInterface = true; + } + + if ($node instanceof Enum_) { + $this->fqcn = $node->namespacedName?->toString(); + } + + if ($node instanceof Node\Name) { + $this->dependencies[] = $node->toString(); + } + + if ($node instanceof Node\Attribute) { + $this->dependencies[] = $node->name->toString(); + } + } + + public function analysis(): AstClassAnalysis + { + return new AstClassAnalysis( + fqcn: $this->fqcn ?? '', + dependencies: array_values(array_unique($this->dependencies)), + isInterface: $this->isInterface, + isAbstract: $this->isAbstract, + ); + } +} diff --git a/app/Infrastructure/Analyze/Adapters/PhpParser/PhpAstClassDependenciesParser.php b/app/Infrastructure/Analyze/Adapters/PhpParser/PhpAstClassDependenciesParser.php new file mode 100644 index 0000000..dcb4fad --- /dev/null +++ b/app/Infrastructure/Analyze/Adapters/PhpParser/PhpAstClassDependenciesParser.php @@ -0,0 +1,29 @@ +createForNewestSupportedVersion(); + $ast = $parser->parse($code); + + $collector = new DependencyCollectorVisitor(); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver()); + $traverser->addVisitor($collector); + $traverser->traverse($ast); + + return $collector->analysis(); + } +} From d1d03a5ee957f040ddda1de21e48231072cc04b1 Mon Sep 17 00:00:00 2001 From: DeGraciaMathieu Date: Mon, 5 Jan 2026 19:05:49 +0100 Subject: [PATCH 2/5] Adds support for modern PHP syntax analysis Improves the analysis of PHP classes by adding support for modern syntax features (PHP 8.1+), including enums, readonly properties, attributes and interface analysis within classes. The update ensures accurate identification of class dependencies and correct flagging of abstract/interface status when modern syntax is used. --- .../PhpParser/DependencyCollectorVisitor.php | 3 +- tests/Fixtures/Php85/ModernClass.php | 36 +++++++++++++++++++ .../PhpAstClassDependenciesParserTest.php | 31 ++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/Php85/ModernClass.php create mode 100644 tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php diff --git a/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php b/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php index d4d33e9..51b3e32 100644 --- a/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php +++ b/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php @@ -20,9 +20,10 @@ public function enterNode(Node $node): void if ($node instanceof Class_) { $this->fqcn = $node->namespacedName?->toString(); $this->isAbstract = $node->isAbstract(); + $this->isInterface = false; } - if ($node instanceof Interface_) { + if ($node instanceof Interface_ && $this->fqcn === null) { $this->fqcn = $node->namespacedName?->toString(); $this->isInterface = true; } diff --git a/tests/Fixtures/Php85/ModernClass.php b/tests/Fixtures/Php85/ModernClass.php new file mode 100644 index 0000000..8ba10a8 --- /dev/null +++ b/tests/Fixtures/Php85/ModernClass.php @@ -0,0 +1,36 @@ +clock; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} diff --git a/tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php b/tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php new file mode 100644 index 0000000..7d125bf --- /dev/null +++ b/tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php @@ -0,0 +1,31 @@ +parse(__DIR__ . '/../../../Fixtures/Php85/ModernClass.php'); + + expect($analysis->fqcn())->toBe('Tests\\Fixtures\\Php85\\ModernClass'); + + expect($analysis->dependencies())->toContain( + 'Tests\\Fixtures\\Php85\\AbstractBase', + 'Tests\\Fixtures\\Php85\\Contract', + 'IteratorAggregate', + 'DateTimeInterface', + 'Tests\\Fixtures\\Php85\\Status', + 'Tests\\Fixtures\\Php85\\CustomAttribute', + 'ArrayIterator', + 'Traversable', + ); +}); + +it('marks interface and abstract correctly', function () { + $parser = app(PhpAstClassDependenciesParser::class); + + $analysis = $parser->parse(__DIR__ . '/../../../Fixtures/Php85/ModernClass.php'); + + expect($analysis->isAbstract())->toBeFalse(); + expect($analysis->isInterface())->toBeFalse(); +}); From 4aee07ba0e9eb53c6f55fdb4ecb2151bc4f71145 Mon Sep 17 00:00:00 2001 From: DeGraciaMathieu Date: Mon, 5 Jan 2026 19:10:39 +0100 Subject: [PATCH 3/5] Adds PHP 8.5 to the testing matrix Extends the testing matrix to include PHP 8.5. This ensures compatibility and reduces the risk of regressions with the latest PHP version. --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d1a58f2..528b679 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - php: [8.2, 8.3, 8.4] + php: [8.2, 8.3, 8.4, 8.5] steps: - uses: actions/checkout@v1 From ca6315cdd704be9c280b96eb981a8cd6232bc6b5 Mon Sep 17 00:00:00 2001 From: DeGraciaMathieu Date: Mon, 5 Jan 2026 19:16:07 +0100 Subject: [PATCH 4/5] Excludes built-in types from dependencies Prevents built-in PHP types from being incorrectly identified as dependencies. This change improves the accuracy of dependency analysis by filtering out types like `string`, `int`, `array`, etc., ensuring that only actual project dependencies are included in the results. --- .../PhpParser/DependencyCollectorVisitor.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php b/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php index 51b3e32..c4873d1 100644 --- a/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php +++ b/app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php @@ -33,7 +33,10 @@ public function enterNode(Node $node): void } if ($node instanceof Node\Name) { - $this->dependencies[] = $node->toString(); + $name = $node->toString(); + if (! $this->isBuiltinType($name)) { + $this->dependencies[] = $name; + } } if ($node instanceof Node\Attribute) { @@ -50,4 +53,13 @@ public function analysis(): AstClassAnalysis isAbstract: $this->isAbstract, ); } + + private function isBuiltinType(string $name): bool + { + return in_array(strtolower($name), [ + 'string', 'int', 'float', 'bool', 'array', 'callable', + 'iterable', 'object', 'mixed', 'null', 'false', 'true', + 'never', 'void', 'self', 'parent', 'static', + ], true); + } } From e8157e020ec866c2d00b3e7bef806bb71ec82b5a Mon Sep 17 00:00:00 2001 From: DeGraciaMathieu Date: Mon, 5 Jan 2026 19:20:39 +0100 Subject: [PATCH 5/5] Removes PHP 8.5 from testing matrix PHP 8.5 is not yet released, so remove it from the testing matrix. This prevents errors in the build process due to the non-existent PHP version. --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 528b679..d1a58f2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - php: [8.2, 8.3, 8.4, 8.5] + php: [8.2, 8.3, 8.4] steps: - uses: actions/checkout@v1