Skip to content

Commit 806e68c

Browse files
authored
Merge pull request #205 from coenjacobs/fix/202-remove-phpmd-suppression
Remove PHPMD suppression and move replacers into Replace/
2 parents 732a8b5 + b352a3b commit 806e68c

12 files changed

Lines changed: 527 additions & 406 deletions

docs/architecture.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ In code:
3434
$mover->deleteTargetDirs($packages);
3535
$mover->movePackages($packages);
3636
$replacer->replacePackages($packages);
37-
$replacer->replaceParentInTree($packages);
38-
$replacer->replaceParentClassesInDirectory($config->getClassmapDirectory());
37+
38+
$parentReplacer = new ParentReplacer($config, $replacer);
39+
$parentReplacer->setReplacedClasses($replacer->getReplacedClasses());
40+
$parentReplacer->replaceParentInTree($packages);
41+
$parentReplacer->replaceParentClassesInDirectory($config->getClassmapDirectory());
3942

4043
if ($config->getDeleteVendorDirectories()) {
4144
$mover->deletePackageVendorDirectories();
4245
}
4346
```
4447

45-
The `replaceParentInTree` step is important: after replacing namespaces/classes within each package, it also updates references in parent packages that depend on them. This ensures package A (which requires package B) gets updated with the new names from package B.
48+
The `ParentReplacer` handles cross-replacement: after `Replacer` rewrites namespaces/classes within each package, `ParentReplacer` propagates those renames into parent packages. This ensures package A (which requires package B) gets updated with the new names from package B.
4649

4750
## Configuration
4851

@@ -142,14 +145,15 @@ src/
142145
Config/ # Configuration models (Mozart, Package, Psr4, Classmap, Files, etc.)
143146
Composer/Autoload/ # Autoloader abstractions (NamespaceAutoloader, AbstractAutoloader)
144147
Replace/
145-
BaseReplacer.php # Abstract base (holds autoloader reference)
146-
Replacer.php # Interface: replace(string): string
148+
Replacer.php # Orchestrator (routes packages to correct replacer)
149+
ParentReplacer.php # Cross-replacement: propagates renames into parent packages
150+
AbstractAutoloadReplacer.php # Abstract base (holds autoloader reference)
151+
AutoloadReplacer.php # Interface: extends StringReplacer with setAutoloader()
147152
StringReplacer.php # Interface: same as Replacer but for class-map style
148153
Classmap/ # ClassmapReplacer, DeclarationVisitor, NameReplacer, NameVisitor
149154
Namespace/ # NamespaceReplacer, PrefixVisitor
150155
Support/ # AstUtils, ExistenceCheckTrait, NameNodeContextTrait
151156
Mover.php # Copies files from vendor/ to target directories
152-
Replacer.php # Orchestrator (routes packages to correct replacer)
153157
PackageFinder.php # Dependency tree resolution (BFS with deduplication)
154158
PackageFactory.php # Creates Package objects from composer.json
155159
FilesHandler.php # File I/O via Flysystem (read, write, copy, delete)

docs/replace-pipeline.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All code transformation in Mozart uses AST-based processing via `nikic/php-parse
44

55
## Overview
66

7-
The `Replacer` class (in `src/Replacer.php`) orchestrates all replacement. It routes each package to the right replacer based on its autoloader type:
7+
The `Replacer` class (in `src/Replace/Replacer.php`) orchestrates all replacement. It routes each package to the right replacer based on its autoloader type:
88

99
```
1010
Replacer (orchestrator)
@@ -60,9 +60,9 @@ Classmap replacement requires two passes, unlike namespace replacement which onl
6060
**Pass 2 — Reference updating** (`NameReplacer` + `NameVisitor`):
6161
- Runs after all declarations have been renamed
6262
- Uses the `replacedClasses` map to update references everywhere
63-
- Called via `Replacer::replaceParentClassesInDirectory()`
63+
- Called via `ParentReplacer::replaceParentClassesInDirectory()`
6464
- Only replaces simple (non-namespaced) names that appear in the map
65-
- `NameReplacer` implements `StringReplacer` (not `Replacer`), so it has no `setAutoloader()` — it operates purely on the class map, independent of any autoloader context. The `StringReplacer` interface exists because pass-2 reference replacement is a simple string-map operation: look up a name in the collected renames and substitute it. It doesn't need the autoloader context that `Replacer` provides, so using a separate interface keeps that dependency out
65+
- `NameReplacer` implements `StringReplacer` (not `AutoloadReplacer`), so it has no `setAutoloader()` — it operates purely on the class map, independent of any autoloader context. The `StringReplacer` interface exists because pass-2 reference replacement is a simple string-map operation: look up a name in the collected renames and substitute it. It doesn't need the autoloader context that `AutoloadReplacer` provides, so using a separate interface keeps that dependency out
6666

6767
This two-pass design exists because you can't know the full set of renamed classes until all declarations have been processed.
6868

@@ -128,8 +128,8 @@ Also handles concatenation patterns like `constant('Namespace\Class::' . $var)`
128128
Multiple points in the replacement flow need `is_dir()` checks because directories may not exist:
129129

130130
- `Replacer::replacePackageByAutoloader()` — classmap source path may not exist
131-
- `Replacer::replaceParentClassesInDirectory()` — directory may not exist yet
132131
- `Replacer::replaceInDirectory()` — namespace directory may not exist
132+
- `ParentReplacer::replaceParentClassesInDirectory()` — directory may not exist yet
133133
- `NamespaceAutoloader::getFiles()` — PSR-4 paths (especially when defined as arrays) may list non-existent directories
134134
- `Classmap::getFiles()` — classmap paths may not exist
135135
- `FilesHandler::getFilesFromPath()` — uses Symfony Finder with `->exclude('vendor')` to avoid processing nested vendor directories

src/Commands/Compose.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use CoenJacobs\Mozart\Mover;
77
use CoenJacobs\Mozart\PackageFactory;
88
use CoenJacobs\Mozart\PackageFinder;
9-
use CoenJacobs\Mozart\Replacer;
9+
use CoenJacobs\Mozart\Replace\ParentReplacer;
10+
use CoenJacobs\Mozart\Replace\Replacer;
1011

1112
class Compose
1213
{
@@ -65,8 +66,11 @@ public function execute(): void
6566
$mover->deleteTargetDirs($packages);
6667
$mover->movePackages($packages);
6768
$replacer->replacePackages($packages);
68-
$replacer->replaceParentInTree($packages);
69-
$replacer->replaceParentClassesInDirectory($config->getClassmapDirectory());
69+
70+
$parentReplacer = new ParentReplacer($config, $replacer);
71+
$parentReplacer->setReplacedClasses($replacer->getReplacedClasses());
72+
$parentReplacer->replaceParentInTree($packages);
73+
$parentReplacer->replaceParentClassesInDirectory($config->getClassmapDirectory());
7074

7175
if ($config->getDeleteVendorDirectories()) {
7276
$mover->deletePackageVendorDirectories();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use CoenJacobs\Mozart\Composer\Autoload\Autoloader;
66

7-
abstract class BaseReplacer implements Replacer
7+
abstract class AbstractAutoloadReplacer implements AutoloadReplacer
88
{
99
public Autoloader $autoloader;
1010

src/Replace/AutoloadReplacer.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace CoenJacobs\Mozart\Replace;
4+
5+
use CoenJacobs\Mozart\Composer\Autoload\Autoloader;
6+
7+
interface AutoloadReplacer extends StringReplacer
8+
{
9+
public function setAutoloader(Autoloader $autoloader): void;
10+
}

src/Replace/Classmap/ClassmapReplacer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
namespace CoenJacobs\Mozart\Replace\Classmap;
1010

1111
use CoenJacobs\Mozart\Exceptions\FileOperationException;
12-
use CoenJacobs\Mozart\Replace\BaseReplacer;
12+
use CoenJacobs\Mozart\Replace\AbstractAutoloadReplacer;
1313
use CoenJacobs\Mozart\Replace\Support\AstUtils;
1414

15-
class ClassmapReplacer extends BaseReplacer
15+
class ClassmapReplacer extends AbstractAutoloadReplacer
1616
{
1717
/** @var array<string,string> */
1818
protected array $replacedClasses = [];

src/Replace/Namespace/NamespaceReplacer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use CoenJacobs\Mozart\Composer\Autoload\NamespaceAutoloader;
66
use CoenJacobs\Mozart\Exceptions\FileOperationException;
7-
use CoenJacobs\Mozart\Replace\BaseReplacer;
7+
use CoenJacobs\Mozart\Replace\AbstractAutoloadReplacer;
88
use CoenJacobs\Mozart\Replace\Support\AstUtils;
99

1010
/**
@@ -14,7 +14,7 @@
1414
* references, avoiding the issues with regex-based replacement on constructs
1515
* like nullable type hints (?ClassName).
1616
*/
17-
class NamespaceReplacer extends BaseReplacer
17+
class NamespaceReplacer extends AbstractAutoloadReplacer
1818
{
1919
/**
2020
* The prefix to add to existing namespaces, for example: "My\Mozart\Prefix"

src/Replace/ParentReplacer.php

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
namespace CoenJacobs\Mozart\Replace;
4+
5+
use CoenJacobs\Mozart\Composer\Autoload\NamespaceAutoloader;
6+
use CoenJacobs\Mozart\Config\Mozart;
7+
use CoenJacobs\Mozart\Config\Package;
8+
use CoenJacobs\Mozart\FilesHandler;
9+
use CoenJacobs\Mozart\Replace\Classmap\NameReplacer;
10+
11+
class ParentReplacer
12+
{
13+
protected Mozart $config;
14+
15+
protected FilesHandler $files;
16+
17+
protected Replacer $replacer;
18+
19+
/** @var array<string,string> */
20+
protected array $replacedClasses = [];
21+
22+
public function __construct(Mozart $config, Replacer $replacer)
23+
{
24+
$this->config = $config;
25+
$this->files = new FilesHandler($config);
26+
$this->replacer = $replacer;
27+
}
28+
29+
/**
30+
* @param array<string,string> $replacedClasses
31+
*/
32+
public function setReplacedClasses(array $replacedClasses): void
33+
{
34+
$this->replacedClasses = $replacedClasses;
35+
}
36+
37+
/**
38+
* Replaces all occurrences of previously replaced classes, in the provided
39+
* directory. This to ensure that each package has its parents package
40+
* classes also replaced in its own files.
41+
*
42+
* Uses AST-based replacement to properly handle PHP syntax and avoid
43+
* incorrectly replacing class names in string literals or comments.
44+
*/
45+
public function replaceParentClassesInDirectory(string $directory): void
46+
{
47+
if (count($this->replacedClasses) === 0) {
48+
return;
49+
}
50+
51+
$directory = trim($directory, '//');
52+
53+
if (!is_dir($directory)) {
54+
return;
55+
}
56+
57+
$files = $this->files->getFilesFromPath($directory);
58+
$replacer = new NameReplacer($this->replacedClasses);
59+
60+
foreach ($files as $file) {
61+
$targetFile = $file->getPathName();
62+
63+
if ('.php' == substr($targetFile, -4, 4)) {
64+
try {
65+
$contents = $this->files->readFile($targetFile);
66+
} catch (\CoenJacobs\Mozart\Exceptions\FileOperationException) {
67+
// Skip files that cannot be read
68+
continue;
69+
}
70+
71+
$modifiedContents = $replacer->replace($contents);
72+
73+
if ($modifiedContents !== $contents) {
74+
$this->files->writeFile($targetFile, $modifiedContents);
75+
}
76+
}
77+
}
78+
}
79+
80+
/**
81+
* Replace everything in parent package, based on the dependency package.
82+
* This is done to ensure that package A (which requires package B), is also
83+
* updated with the replacements being made in package B.
84+
*/
85+
public function replaceParentPackage(Package $package, Package $parent): void
86+
{
87+
if ($this->config->isExcludedPackage($package)) {
88+
return;
89+
}
90+
91+
foreach ($parent->getAutoloaders() as $parentAutoloader) {
92+
foreach ($package->getAutoloaders() as $autoloader) {
93+
if ($parentAutoloader instanceof NamespaceAutoloader) {
94+
$namespace = str_replace('\\', DIRECTORY_SEPARATOR, $parentAutoloader->namespace);
95+
$directory = $this->config->getWorkingDir() . $this->config->getDepDirectory() . $namespace
96+
. DIRECTORY_SEPARATOR;
97+
98+
if ($autoloader instanceof NamespaceAutoloader) {
99+
$this->replacer->replaceInDirectory($autoloader, $directory);
100+
return;
101+
}
102+
103+
$directory = str_replace($this->config->getWorkingDir(), '', $directory);
104+
$this->replaceParentClassesInDirectory($directory);
105+
return;
106+
}
107+
108+
$directory = $this->config->getWorkingDir() .
109+
$this->config->getClassmapDirectory() . $parent->getDirectoryName();
110+
111+
if ($autoloader instanceof NamespaceAutoloader) {
112+
$this->replacer->replaceInDirectory($autoloader, $directory);
113+
return;
114+
}
115+
116+
$directory = str_replace($this->config->getWorkingDir(), '', $directory);
117+
$this->replaceParentClassesInDirectory($directory);
118+
}
119+
}
120+
}
121+
122+
/**
123+
* Get an array containing all the dependencies and dependencies.
124+
*
125+
* @param Package $package
126+
* @param Package[] $dependencies
127+
* @param array<string,bool> $visited
128+
* @return Package[]
129+
*/
130+
private function getAllDependenciesOfPackage(
131+
Package $package,
132+
array $dependencies = [],
133+
array &$visited = []
134+
): array {
135+
if (empty($package->getDependencies())) {
136+
return $dependencies;
137+
}
138+
139+
foreach ($package->getDependencies() as $dependency) {
140+
$name = $dependency->getName();
141+
if (isset($visited[$name])) {
142+
continue;
143+
}
144+
$visited[$name] = true;
145+
$dependencies[] = $dependency;
146+
$dependencies = $this->getAllDependenciesOfPackage($dependency, $dependencies, $visited);
147+
}
148+
149+
return $dependencies;
150+
}
151+
152+
/**
153+
* @param Package[] $packages
154+
*/
155+
public function replaceParentInTree(array $packages): void
156+
{
157+
foreach ($packages as $package) {
158+
if ($this->config->isExcludedPackage($package)) {
159+
continue;
160+
}
161+
162+
$dependencies = $this->getAllDependenciesOfPackage($package);
163+
164+
foreach ($dependencies as $dependency) {
165+
$this->replaceParentPackage($dependency, $package);
166+
}
167+
168+
$this->replaceParentInTree($package->getDependencies());
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)