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
47 changes: 37 additions & 10 deletions src/Analyser/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public function analyse(
$classNodes = $this->withRecursiveParents($classNodes);

if ($hasExtendedClassAwareRule) {
$this->markExtendedClasses($classNodes);
$this->markExtendedClasses($classNodes, $extractionResult);
}

$fileAnalysisProvider = new FileAnalysisProvider($extractionResult->fileAnalyses);
Expand Down Expand Up @@ -663,7 +663,7 @@ private function dependenciesForInheritanceDependency(
*
* @param list<ClassNode> $classNodes
*/
private function markExtendedClasses(array $classNodes): void
private function markExtendedClasses(array $classNodes, ExtractionResult $extractionResult): void
{
$extended = [];

Expand All @@ -673,6 +673,14 @@ private function markExtendedClasses(array $classNodes): void
}
}

// Anonymous classes (`new class extends Foo {}`) have no ClassNode of
// their own, so the classes they extend are tracked separately.
foreach ($extractionResult->anonymousClassNodes as $anonymousClassNode) {
if ($anonymousClassNode->extends !== null) {
$extended[$anonymousClassNode->extends] = true;
}
}

foreach ($classNodes as $classNode) {
// Only classes appear in parentClasses; traits, interfaces, and enums
// never do, so they are left with the default (not extended).
Expand Down Expand Up @@ -836,9 +844,10 @@ private function collectClassNodes(
?AnalyserOptions $analyserOptions = null,
bool $withFileAnalysis = true,
): ExtractionResult {
$classNodes = [];
$fileAnalyses = [];
$filesToParse = [];
$classNodes = [];
$fileAnalyses = [];
$anonymousClassNodes = [];
$filesToParse = [];

foreach ($files as $file) {
if ($withFileAnalysis) {
Expand All @@ -856,32 +865,40 @@ private function collectClassNodes(
$classNodes[] = $cachedClassNode;
}

foreach ($cachedResult['anonymousClassNodes'] as $cachedAnonymousClassNode) {
$anonymousClassNodes[] = $cachedAnonymousClassNode;
}

$fileAnalyses[$file] = $cachedResult['fileAnalysis'];

continue;
}

$cachedClassNodes = $this->analysisResultCache?->loadClassNodes(
$cachedResult = $this->analysisResultCache?->loadClassNodes(
$file,
$this->classNodeCacheNamespace,
);

if ($cachedClassNodes === null) {
if ($cachedResult === null) {
$filesToParse[] = $file;
continue;
}

foreach ($cachedClassNodes as $cachedClassNode) {
foreach ($cachedResult['classNodes'] as $cachedClassNode) {
$classNodes[] = $cachedClassNode;
}

foreach ($cachedResult['anonymousClassNodes'] as $cachedAnonymousClassNode) {
$anonymousClassNodes[] = $cachedAnonymousClassNode;
}
}

$progressHandler?->start(count($filesToParse));

if ($filesToParse === []) {
$progressHandler?->finish();

return new ExtractionResult($classNodes, $fileAnalyses);
return new ExtractionResult($classNodes, $fileAnalyses, $anonymousClassNodes);
}

$options = $analyserOptions ?? AnalyserOptions::parallel();
Expand Down Expand Up @@ -911,6 +928,15 @@ private function collectClassNodes(
}
}

$anonymousClassNodesByFile = array_fill_keys($filesToParse, []);
foreach ($parsedResult->anonymousClassNodes as $parsedAnonymousClassNode) {
$anonymousClassNodes[] = $parsedAnonymousClassNode;

if (isset($anonymousClassNodesByFile[$parsedAnonymousClassNode->file])) {
$anonymousClassNodesByFile[$parsedAnonymousClassNode->file][] = $parsedAnonymousClassNode;
}
}

foreach ($parsedResult->fileAnalyses as $file => $fileAnalysis) {
$fileAnalyses[$file] = $fileAnalysis;
}
Expand All @@ -921,12 +947,13 @@ private function collectClassNodes(
$this->classNodeCacheNamespace,
$fileClassNodes,
$fileAnalyses[$fileToParse] ?? null,
$anonymousClassNodesByFile[$fileToParse] ?? [],
);
}

$progressHandler?->finish();

return new ExtractionResult($classNodes, $fileAnalyses);
return new ExtractionResult($classNodes, $fileAnalyses, $anonymousClassNodes);
}

/**
Expand Down
26 changes: 26 additions & 0 deletions src/Analyser/AnonymousClassNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Boundwize\StructArmed\Analyser;

/**
* An anonymous class declaration (`new class ... {}`). Anonymous classes never
* become ClassNodes — they cannot be referenced by name and no rule targets
* them directly — but the class they extend is still extended within the
* scanned paths, which extended-class-aware rules must take into account.
*
* The usage example is on MustBeFinalRule, which must skip if target class is extended by an anonymous class.
*
* Note: Other properties like anonymous class's traits, implements, etc may come
* later if needed for future needed rules.
*/
final readonly class AnonymousClassNode
{
public function __construct(
public string $file,
public int $line,
public ?string $extends,
) {
}
}
19 changes: 19 additions & 0 deletions src/Analyser/ClassCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ final class ClassCollector extends NodeVisitorAbstract
/** @var list<ClassNode> */
private array $nodes = [];

/** @var list<AnonymousClassNode> */
private array $anonymousClassNodes = [];

private string $currentFile = '';

/** @var list<string> */
Expand Down Expand Up @@ -126,6 +129,12 @@ public function getNodes(): array
return $this->nodes;
}

/** @return list<AnonymousClassNode> */
public function getAnonymousClassNodes(): array
{
return $this->anonymousClassNodes;
}

public function enterNode(Node $node): null
{
if ($node instanceof Namespace_) {
Expand Down Expand Up @@ -174,6 +183,16 @@ public function leaveNode(Node $node): null
}

if (! $node->name instanceof Identifier) {
// Anonymous classes never become ClassNodes, but the class they
// extend is still extended within the scanned paths.
if ($node instanceof Class_) {
$this->anonymousClassNodes[] = new AnonymousClassNode(
file: $this->currentFile,
line: $node->getStartLine(),
extends: $node->extends instanceof Name ? $node->extends->toString() : null,
);
}

return null;
}

Expand Down
6 changes: 5 additions & 1 deletion src/Analyser/ClassNodeExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public function extract(
}
}

return new ExtractionResult($classCollector->getNodes(), $fileAnalyses);
return new ExtractionResult(
$classCollector->getNodes(),
$fileAnalyses,
$classCollector->getAnonymousClassNodes(),
);
}
}
2 changes: 2 additions & 0 deletions src/Analyser/ExtractionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
/**
* @param list<ClassNode> $classNodes
* @param array<string, FileAnalysis> $fileAnalyses
* @param list<AnonymousClassNode> $anonymousClassNodes
*/
public function __construct(
public array $classNodes,
public array $fileAnalyses,
public array $anonymousClassNodes = [],
) {
}
}
14 changes: 8 additions & 6 deletions src/Analyser/Parallel/ClassNodeWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,19 @@ public static function run(string $inputFile, string $outputFile, mixed $outputS
);

file_put_contents($outputFile, serialize([
'nodes' => $result->classNodes,
'fileAnalyses' => $result->fileAnalyses,
'error' => null,
'nodes' => $result->classNodes,
'fileAnalyses' => $result->fileAnalyses,
'anonymousClassNodes' => $result->anonymousClassNodes,
'error' => null,
]));

return 0;
} catch (Throwable $throwable) {
file_put_contents($outputFile, serialize([
'nodes' => [],
'fileAnalyses' => [],
'error' => sprintf('%s: %s', $throwable::class, $throwable->getMessage()),
'nodes' => [],
'fileAnalyses' => [],
'anonymousClassNodes' => [],
'error' => sprintf('%s: %s', $throwable::class, $throwable->getMessage()),
]));

return 1;
Expand Down
30 changes: 25 additions & 5 deletions src/Analyser/Parallel/ParallelClassNodeExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Boundwize\StructArmed\Analyser\Parallel;

use Boundwize\StructArmed\Analyser\AnonymousClassNode;
use Boundwize\StructArmed\Analyser\ClassNode;
use Boundwize\StructArmed\Analyser\ExtractionResult;
use Boundwize\StructArmed\Analyser\FileAnalysis;
Expand Down Expand Up @@ -128,10 +129,11 @@ public function extract(
];
}

$nodes = [];
$fileAnalyses = [];
$failure = null;
$pending = $processes;
$nodes = [];
$fileAnalyses = [];
$anonymousClassNodes = [];
$failure = null;
$pending = $processes;

while ($pending !== []) {
$anyActivity = false;
Expand Down Expand Up @@ -220,6 +222,24 @@ public function extract(

$fileAnalyses[$file] = $fileAnalysis;
}

$workerAnonClassNodes = $result['anonymousClassNodes'] ?? [];

if (! is_array($workerAnonClassNodes)) {
throw new RuntimeException(
'Parallel analysis worker returned invalid anonymous class nodes.'
);
}

foreach ($workerAnonClassNodes as $workerAnonClassNode) {
if (! $workerAnonClassNode instanceof AnonymousClassNode) {
throw new RuntimeException(
'Parallel analysis worker returned invalid anonymous class nodes.'
);
}

$anonymousClassNodes[] = $workerAnonClassNode;
}
} catch (RuntimeException $runtimeException) {
$failure ??= $runtimeException->getMessage();
} finally {
Expand All @@ -243,7 +263,7 @@ public function extract(
throw new RuntimeException($failure);
}

return new ExtractionResult($nodes, $fileAnalyses);
return new ExtractionResult($nodes, $fileAnalyses, $anonymousClassNodes);
}

/**
Expand Down
Loading
Loading