Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions app/Infrastructure/Analyze/Adapters/PhpParser/AstClassAnalysis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Infrastructure\Analyze\Adapters\PhpParser;

use App\Infrastructure\Analyze\Ports\ClassAnalysis;

final class AstClassAnalysis implements ClassAnalysis
{
public function __construct(
private readonly string $fqcn,
private readonly array $dependencies,
private readonly bool $isInterface = false,
private readonly bool $isAbstract = false,
) {}

public function fqcn(): string
{
return $this->fqcn;
}

public function dependencies(): array
{
return $this->dependencies;
}

public function isInterface(): bool
{
return $this->isInterface;
}

public function isAbstract(): bool
{
return $this->isAbstract;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Infrastructure\Analyze\Adapters\PhpParser;

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Enum_;

final class DependencyCollectorVisitor extends NodeVisitorAbstract
{
private array $dependencies = [];
private ?string $fqcn = null;
private bool $isInterface = false;
private bool $isAbstract = false;

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_ && $this->fqcn === null) {
$this->fqcn = $node->namespacedName?->toString();
$this->isInterface = true;
}

if ($node instanceof Enum_) {
$this->fqcn = $node->namespacedName?->toString();
}

if ($node instanceof Node\Name) {
$name = $node->toString();
if (! $this->isBuiltinType($name)) {
$this->dependencies[] = $name;
}
}
Comment on lines +35 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and the actual implementation
cat -n app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php | head -60

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1993


🏁 Script executed:

# Search for other implementations of dependency analysis or visitor patterns
fd DependencyCollector --type f | head -20

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 167


🏁 Script executed:

# Look for how the visitor is used and initialized
rg -n "DependencyCollectorVisitor" --type=php | head -20

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 375


🏁 Script executed:

# Check if there are parent node tracking or context filtering mechanisms
rg -n "getAttribute.*parent|parent.*getAttribute" --type=php | head -15

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Search for other visitor implementations to understand the pattern
ast-grep --pattern 'class $_ extends NodeVisitor {
  $$$
}'

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Search for tests related to DependencyCollectorVisitor
fd -e "test" -o -name "*Test*" --type f | xargs rg -l "DependencyCollectorVisitor" 2>/dev/null || echo "No direct test references found"

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 495


🏁 Script executed:

# Look for test files in common locations
fd ".*Test\.php" --type f | head -20

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 953


🏁 Script executed:

# Check the PhpAstClassDependenciesParser to understand context
cat -n app/Infrastructure/Analyze/Adapters/PhpParser/PhpAstClassDependenciesParser.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1147


🏁 Script executed:

# Look for examples or documentation about what dependencies should be collected
rg -n "dependencies|class.*dependency" --type=php -i | head -30

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 3949


🏁 Script executed:

# Check if there are functional tests or examples
fd -e "php" --type f | xargs rg -l "DependencyCollector|dependencies" | head -10

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 583


🏁 Script executed:

# Look at the entire visitor pattern in use
rg -n -B5 -A10 "new DependencyCollectorVisitor" --type=php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1771


🏁 Script executed:

cat -n tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1352


🏁 Script executed:

# Look for test fixtures that show example code being parsed
fd "Fixtures\|fixtures" -i --type f | head -10

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Check what the actual dependencies should be
rg -A20 "detects dependencies from modern PHP syntax" tests/

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 2365


🏁 Script executed:

cat -n tests/Fixtures/Php85/ModernClass.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1013


🏁 Script executed:

# Look for function names, constants, or other names that might be false positives
rg "function|const " tests/Fixtures/Php85/ModernClass.php -n

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 256


🏁 Script executed:

# Search for more test fixtures to see edge cases
fd "\.php" tests/Fixtures/ --type f | head -20

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 127


🏁 Script executed:

# Search for other test fixtures with more complex code patterns
find tests/Fixtures -name "*.php" -type f | xargs wc -l | sort -rn | head -10

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 130


🏁 Script executed:

# Look for any tests that might show what should NOT be captured
rg "should.*not|must.*not|must.*exclude" tests/ -i

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 332


🏁 Script executed:

# Check if there are function calls or constants in other fixtures that might be false positives
find tests/Fixtures -name "*.php" -type f -exec grep -l "function\|const\|::" {} \;

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 127


🏁 Script executed:

# Examine the NameResolver behavior in PHP Parser documentation or usage
rg "NameResolver" --type=php -B2 -A2

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1280


🏁 Script executed:

# Check if there are any fixtures with function calls or constants that might be captured as false positives
find tests/Fixtures -name "*.php" -exec grep -H "function \|const \|::\|new " {} \; 2>/dev/null | head -20

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 434


🏁 Script executed:

# Create a test case to see what Node\Name captures with function calls
cat > /tmp/test_parser.php << 'EOF'
<?php
namespace Tests;
use SomeClass;

class MyClass extends SomeClass {
    public function myFunc(OtherClass $param): ResultClass {
        callFunction();
        SOME_CONSTANT;
        new AnotherClass();
        return $param;
    }
}
EOF

cd /tmp && php -r "
require '/repo/vendor/autoload.php';
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Node;

class DebugVisitor extends \PhpParser\NodeVisitorAbstract {
    public function enterNode(Node \$node) {
        if (\$node instanceof Node\Name) {
            echo 'Node\\Name: ' . \$node->toString() . ' (type: ' . \$node->getType() . ')' . PHP_EOL;
        }
    }
}

\$code = file_get_contents('/tmp/test_parser.php');
\$parser = (new ParserFactory())->createForNewestSupportedVersion();
\$ast = \$parser->parse(\$code);

\$traverser = new NodeTraverser();
\$traverser->addVisitor(new NameResolver());
\$traverser->addVisitor(new DebugVisitor());
\$traverser->traverse(\$ast);
" 2>&1 || echo "Test execution had issues"

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 452


🏁 Script executed:

# Check the actual PHP Parser documentation for Node\Name behavior
# Look at how the parser represents different name contexts
rg "Node\\\\Name" --type=php -B2 -A2 | head -40

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1121


🏁 Script executed:

# Check what types of values are in the dependencies in actual test run
# Look for constant names, builtin type names that might be captured
rg "null|true|false|int|string|bool" tests/Fixtures/Php85/ModernClass.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 210


🏁 Script executed:

# Search for any documentation or comments about Node\Name in the codebase
rg "Node.*Name|dependency.*context" --type=php -B1 -A1 | head -30

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 3314


🏁 Script executed:

# Check if there are integration tests that show actual output
find tests -name "*.php" -exec grep -l "dependencies()" {} \; | xargs grep -A5 "dependencies()"

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1603


🏁 Script executed:

# Let me check what the actual test expects - does toContain mean other values are allowed?
# Run the test to see if it passes and what the actual output is
grep -A30 "toContain" tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 752


🏁 Script executed:

# Check if there's a test case with actual function calls
fd "\.php" tests/Fixtures -exec grep -l "function\|::" {} \;

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 297


🏁 Script executed:

# Look for any additional test fixtures or edge cases
ls -la tests/Fixtures/

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 253


🏁 Script executed:

# Check if 'null' keyword appears in actual dependencies (which would be a false positive from union types)
rg "null|true|false" tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Check the full analyzer flow to see if filtering happens elsewhere
rg "dependencies.*filter|filter.*dependencies" --type=php -i

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Let me understand PHP parser structure better - check what type of node represents function calls
rg "Node\\\\" --type=php | grep -i "call\|const\|function" | head -20

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Check the Jerowork adapter which seems to have more sophisticated filtering
cat -n app/Infrastructure/Analyze/Adapters/Jerowork/NativeDecliner.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1709


🏁 Script executed:

# Look at what other adapters exist and how they handle dependencies
fd ".*Decliner\.php|.*Processor\.php" --type f app/Infrastructure/

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 154


🏁 Script executed:

# Let me check if there's a way to see what dependencies() actually returns
# Look for any test that checks the exact list or checks for unwanted items
grep -r "dependencies()" tests/Unit/Infrastructure/Analyze/ -A10 -B2

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1455


🏁 Script executed:

# Check if the tool filters out primitives somewhere
rg "string|int|bool|null|void|mixed" app/Infrastructure/Analyze/Adapters/PhpParser/ --type=php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1494


🏁 Script executed:

# Search for where dependencies are used after being collected
rg "->dependencies()" app/ --type=php -B2 -A2 | head -40

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 495


🏁 Script executed:

# Check if there's any documentation or comments explaining the expected behavior
rg "dependency|collect" app/Infrastructure/Analyze/ --type=php -i -B1 -A1 | head -50

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 4743


🏁 Script executed:

# Let's check if php parser represents null, string, etc as Node\Name
# by looking at PHP Parser source or documentation in vendor
find vendor -path "*php-parser*" -name "*.md" -o -name "README*" 2>/dev/null | head -5

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 73


🏁 Script executed:

# Check what actually gets collected - run a deeper analysis
# Look for any place where dependencies are checked for validity
rg "array_unique|dependencies\[\]" app/Infrastructure/Analyze/ --type=php -B2 -A2

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1641


🏁 Script executed:

# Check if there are any filtering mechanisms AFTER collection
rg "in_array|array_filter|array_diff" app/Infrastructure/Analyze/ --type=php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 364


🏁 Script executed:

# Most importantly - let me see what primitives or keywords might be captured
cat app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php | grep -A20 "enterNode"

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 747


🏁 Script executed:

# Check if the test actually verifies all dependencies, not just a subset
grep -B5 -A15 "toContain" tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 811


🏁 Script executed:

# Let me verify if Node\Name captures function names
# Look for examples with function calls in test fixtures or codebase
find . -name "*.php" -type f -exec grep -l "function.*(" {} \; 2>/dev/null | grep -i test | head -5

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 303


🏁 Script executed:

# Check if there are any integration tests with more complex code
find tests -name "*.php" -type f | xargs wc -l | sort -rn | head -10

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 565


🏁 Script executed:

# Let me look at what PHP Parser documentation says about Node\Name
# by checking if there's any inline documentation
grep -r "Node\\\\Name" . --include="*.php" -B2 -A2 | head -40

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1141


🏁 Script executed:

# The key question: does the test actually verify NO other dependencies exist?
# Or just that these 8 are present?
grep -B2 -A20 "it('detects dependencies" tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 935


🏁 Script executed:

# Create a simple test to understand PHP Parser's behavior with function calls
python3 << 'PYTHON'
# Check PHP Parser source or documentation structure
import subprocess
import os

# Look for PHP Parser files in vendor
result = subprocess.run(['find', 'vendor/nikic/php-parser', '-name', '*.php', '-path', '*/Node*'], 
                       capture_output=True, text=True)
files = result.stdout.strip().split('\n')
php_parser_nodes = [f for f in files if 'Name' in f][:10]
for f in php_parser_nodes:
    print(f)
PYTHON

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 177


🏁 Script executed:

# Final verification - check the complete test to see if it verifies exact dependencies
cat tests/Unit/Infrastructure/Analyze/PhpAstClassDependenciesParserTest.php

Repository: DeGraciaMathieu/php-class-dependencies-analyzer

Length of output: 1135


Collecter sélectivement selon le contexte du nœud parent

La capture de tous les Node\Name sans filtrage entraîne la collecte de noms non pertinents. Le codebase contient une alternative (adapter Jerowork) qui filtre explicitement les types primitifs (null, string, int, bool, etc.) et les classes/fonctions natives PHP, confirmant que ce risque est réel.

Actuellement, array_unique() ne déduplique que ; il n'élimine pas les faux positifs sémantiques. Le test utilise toContain() plutôt que toBe(), ce qui permet des dépendances supplémentaires non détectées.

Considérez un filtrage par contexte parent pour capturer uniquement les dépendances de classe valides : extends, implements, type hints, new, instanceof, catch, attributes.

🤖 Prompt for AI Agents
In @app/Infrastructure/Analyze/Adapters/PhpParser/DependencyCollectorVisitor.php
around lines 35-37, DependencyCollectorVisitor currently collects every
Node\Name which yields many false positives; update the logic that adds
$node->toString() so it only records names when the parent node context
represents a real dependency: check $node->getAttribute('parent') and allow
adding only when parent is one of Class_::extends, Class_::implements,
Interface_::extends, FunctionLike/Param::type, FunctionLike::returnType,
Expr\New_, Expr\Instanceof_, Stmt\Catch_, or Node\Attribute (and similar
type/attribute positions); additionally ignore built-in/primitive type names and
special names (self, static, parent) and PHP internal classes, then keep
array_unique() for deduplication. Locate and change the conditional around the
Node\Name handling in DependencyCollectorVisitor to perform these context and
name checks before pushing to $this->dependencies.


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,
);
}

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Infrastructure\Analyze\Adapters\PhpParser;

use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use App\Infrastructure\Analyze\Ports\ClassDependenciesParser;
use App\Infrastructure\Analyze\Ports\ClassAnalysis;

final class PhpAstClassDependenciesParser implements ClassDependenciesParser
{
public function parse(string $file): ClassAnalysis
{
$code = file_get_contents($file);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Gestion d'erreur manquante pour la lecture de fichier

file_get_contents() retourne false en cas d'échec (fichier introuvable, permissions insuffisantes, etc.), ce qui causera une erreur de type à la ligne 18 lorsque false sera passé à parse().

🔎 Solution proposée avec gestion d'erreur
+$code = @file_get_contents($file);
+
+if ($code === false) {
+    throw new \RuntimeException("Impossible de lire le fichier : {$file}");
+}
-$code = file_get_contents($file);

Alternative sans opérateur @ :

+if (!file_exists($file)) {
+    throw new \RuntimeException("Fichier introuvable : {$file}");
+}
+
+if (!is_readable($file)) {
+    throw new \RuntimeException("Fichier non lisible : {$file}");
+}
+
 $code = file_get_contents($file);
+
+if ($code === false) {
+    throw new \RuntimeException("Erreur lors de la lecture du fichier : {$file}");
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$code = file_get_contents($file);
$code = @file_get_contents($file);
if ($code === false) {
throw new \RuntimeException("Impossible de lire le fichier : {$file}");
}
Suggested change
$code = file_get_contents($file);
if (!file_exists($file)) {
throw new \RuntimeException("Fichier introuvable : {$file}");
}
if (!is_readable($file)) {
throw new \RuntimeException("Fichier non lisible : {$file}");
}
$code = file_get_contents($file);
if ($code === false) {
throw new \RuntimeException("Erreur lors de la lecture du fichier : {$file}");
}
🤖 Prompt for AI Agents
In
@app/Infrastructure/Analyze/Adapters/PhpParser/PhpAstClassDependenciesParser.php
around line 15, file_get_contents($file) can return false and the code later
passes $code into parse(), causing a type error; update the
PhpAstClassDependenciesParser code to check the result of
file_get_contents($file) (the $code variable) and handle failures explicitly: if
it === false, throw a clear exception or return a controlled error/log message
including the $file path and errno/reason, otherwise continue to call parse()
with the valid string; locate the file_get_contents call and the surrounding
parse() usage in the parse method of PhpAstClassDependenciesParser to implement
this check.


$parser = (new ParserFactory())->createForNewestSupportedVersion();
$ast = $parser->parse($code);

$collector = new DependencyCollectorVisitor();

$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$traverser->addVisitor($collector);
$traverser->traverse($ast);

return $collector->analysis();
}
}
36 changes: 36 additions & 0 deletions tests/Fixtures/Php85/ModernClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Tests\Fixtures\Php85;

use Attribute;
use DateTimeInterface;
use IteratorAggregate;

#[Attribute]
class CustomAttribute {}

interface Contract {}

abstract class AbstractBase {}

enum Status: string {
case Active = 'active';
}

final class ModernClass extends AbstractBase implements Contract, IteratorAggregate
{
public function __construct(
private readonly DateTimeInterface $clock,
) {}

#[CustomAttribute]
public function handle(Status|Contract|null $value): ?DateTimeInterface
{
return $this->clock;
}
Comment on lines +27 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Paramètre inutilisé dans la méthode handle().

Le paramètre $value est déclaré mais jamais utilisé dans le corps de la méthode. Même dans une fixture de test, cela peut prêter à confusion.

🔎 Correction proposée

Si le paramètre est nécessaire uniquement pour tester la détection des types union, considère soit :

  1. L'utiliser dans le corps de la méthode
  2. Le supprimer si non nécessaire
     #[CustomAttribute]
-    public function handle(Status|Contract|null $value): ?DateTimeInterface
+    public function handle(): ?DateTimeInterface
     {
         return $this->clock;
     }

Ou si tu veux démontrer la gestion des types union :

     #[CustomAttribute]
     public function handle(Status|Contract|null $value): ?DateTimeInterface
     {
+        // Utilisation du paramètre pour démonstration
+        if ($value instanceof Status) {
+            return $this->clock;
+        }
         return $this->clock;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function handle(Status|Contract|null $value): ?DateTimeInterface
{
return $this->clock;
}
#[CustomAttribute]
public function handle(): ?DateTimeInterface
{
return $this->clock;
}
🧰 Tools
🪛 PHPMD (2.15.0)

27-27: Avoid unused parameters such as '$value'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In @tests/Fixtures/Php85/ModernClass.php around lines 27-30, The method
handle(Status|Contract|null $value): ?DateTimeInterface declares a parameter
$value that is never used; either remove $value from the handle signature if it
isn’t needed for the fixture, or use $value inside handle (e.g., read a property
or branch on its type) to make its presence meaningful; update any related
tests/fixtures to match the changed signature and ensure ModernClass::handle
remains consistent with the intended union-type demonstration.


public function getIterator(): \Traversable
{
return new \ArrayIterator([]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use App\Infrastructure\Analyze\Adapters\PhpParser\PhpAstClassDependenciesParser;

it('detects dependencies from modern PHP syntax (8.1+) ', function () {
$parser = app(PhpAstClassDependenciesParser::class);

$analysis = $parser->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();
});