Skip to content

Commit 25f2dd4

Browse files
committed
meh
1 parent 8bc6210 commit 25f2dd4

10 files changed

Lines changed: 666 additions & 78 deletions

File tree

tools/chorale/bin/chorale

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,37 +34,35 @@ use Chorale\Util\DiffUtil;
3434
use Chorale\Plan\PlanBuilder;
3535
use Chorale\Console\PlanCommand;
3636

37-
$paths = new PathUtils();
38-
$renderer = new TemplateRenderer();
39-
$sorting = new Sorting();
40-
$identity = new PackageIdentity();
41-
$defaults = new ConfigDefaults();
42-
$schema = new SchemaValidator();
43-
$backup = new BackupManager();
44-
$json = new JsonReporter();
45-
$summary = new RunSummary();
46-
$loader = new ConfigLoader();
47-
$composerMeta = new ComposerMetadata();
37+
$paths = new PathUtils();
38+
$renderer = new TemplateRenderer();
39+
$sorting = new Sorting();
40+
$identity = new PackageIdentity();
41+
$defaults = new ConfigDefaults();
42+
$schema = new SchemaValidator();
43+
$backup = new BackupManager();
44+
$json = new JsonReporter();
45+
$summary = new RunSummary();
46+
$loader = new ConfigLoader();
47+
$composerMeta = new ComposerMetadata();
48+
$composerReader = new ComposerJsonReader();
49+
$stateStore = new FilesystemStateStore();
50+
$hasher = new ContentHasher();
51+
$diffs = new DiffUtil();
4852

53+
$ruleEngine = new RuleEngine($renderer);
4954
$writer = new ConfigWriter($backup);
5055
$normalizer = new ConfigNormalizer($sorting, $defaults);
5156
$scanner = new PackageScanner($paths);
5257
$matcher = new PatternMatcher($paths);
5358
$resolver = new RepoResolver($renderer, $paths);
5459
$required = new RequiredFilesChecker();
5560
$conflicts = new ConflictDetector($matcher);
56-
57-
$composerReader = new ComposerJsonReader();
58-
$depMerger = new DependencyMerger($composerReader);
59-
$ruleEngine = new RuleEngine();
60-
$stateStore = new FilesystemStateStore();
61-
$hasher = new ContentHasher();
62-
$splitDecider = new SplitDecider($stateStore, $hasher);
63-
$diffs = new DiffUtil();
61+
$depMerger = new DependencyMerger($composerReader);
62+
$splitDecider = new SplitDecider($stateStore, $hasher);
6463

6564
$planner = new PlanBuilder(
6665
defaults: $defaults,
67-
//configLoader: $loader,
6866
scanner: $scanner,
6967
matcher: $matcher,
7068
resolver: $resolver,

tools/chorale/src/Composer/ComposerJsonReader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function read(string $absolutePath): array
1818
}
1919

2020
$json = json_decode($raw, true);
21+
2122
return is_array($json) ? $json : [];
2223
}
2324
}

tools/chorale/src/Composer/ComposerJsonReaderInterface.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
interface ComposerJsonReaderInterface
88
{
9-
/** @return array<string,mixed> {} if missing/invalid */
9+
/**
10+
* @return array<string, mixed>
11+
* if missing/invalid, it will return an empty array
12+
*/
1013
public function read(string $absolutePath): array;
1114
}

tools/chorale/src/Composer/DependencyMerger.php

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,215 @@
44

55
namespace Chorale\Composer;
66

7-
/**
8-
* Skeleton merger: returns empty requires with no conflicts.
9-
* Implement strategies: union-caret (default), union-loose, intersect, max.
10-
*/
117
final readonly class DependencyMerger implements DependencyMergerInterface
128
{
9+
public function __construct(
10+
private readonly ComposerJsonReaderInterface $reader
11+
) {}
12+
1313
public function computeRootMerge(string $projectRoot, array $packagePaths, array $options = []): array
1414
{
15-
// TODO: Walk each package composer.json, gather require/require-dev,
16-
// filter monorepo packages, merge by strategy, compute conflicts.
17-
return [
18-
'require' => [],
15+
16+
$opts = [
17+
'strategy_require' => (string) ($options['strategy_require'] ?? 'union-caret'),
18+
'strategy_require_dev' => (string) ($options['strategy_require-dev'] ?? 'union-caret'),
19+
'exclude_monorepo_packages' => (bool) ($options['exclude_monorepo_packages'] ?? true),
20+
'monorepo_names' => (array) ($options['monorepo_names'] ?? []),
21+
];
22+
23+
$monorepo = array_map('strtolower', array_values($opts['monorepo_names']));
24+
25+
$reqs = [];
26+
$devs = [];
27+
$byDepConstraints = [
28+
'require' => [],
1929
'require-dev' => [],
20-
'conflicts' => [],
2130
];
31+
32+
foreach ($packagePaths as $relPath) {
33+
$pc = $this->reader->read(rtrim($projectRoot, '/') . '/' . $relPath . '/composer.json');
34+
if ($pc === []) {
35+
continue;
36+
}
37+
38+
$name = strtolower((string) ($pc['name'] ?? $relPath));
39+
foreach ((array) ($pc['require'] ?? []) as $dep => $ver) {
40+
if (!is_string($dep)) {
41+
continue;
42+
}
43+
44+
if (!is_string($ver)) {
45+
continue;
46+
}
47+
48+
if ($opts['exclude_monorepo_packages'] && in_array(strtolower($dep), $monorepo, true)) {
49+
continue;
50+
}
51+
52+
$byDepConstraints['require'][$dep][$name] = $ver;
53+
}
54+
55+
foreach ((array) ($pc['require-dev'] ?? []) as $dep => $ver) {
56+
if (!is_string($dep)) {
57+
continue;
58+
}
59+
60+
if (!is_string($ver)) {
61+
continue;
62+
}
63+
64+
if ($opts['exclude_monorepo_packages'] && in_array(strtolower($dep), $monorepo, true)) {
65+
continue;
66+
}
67+
68+
$byDepConstraints['require-dev'][$dep][$name] = $ver;
69+
}
70+
}
71+
72+
$conflicts = [];
73+
$reqs = $this->mergeMap($byDepConstraints['require'], $opts['strategy_require'], $conflicts);
74+
$devs = $this->mergeMap($byDepConstraints['require-dev'], $opts['strategy_require_dev'], $conflicts);
75+
76+
ksort($reqs);
77+
ksort($devs);
78+
79+
return [
80+
'require' => $reqs,
81+
'require-dev' => $devs,
82+
'conflicts' => array_values($conflicts),
83+
];
84+
}
85+
86+
/**
87+
* @param array<string,array<string,string>> $constraintsPerDep
88+
* @param array<string,array<string,mixed>> $conflictsOut
89+
* @return array<string,string>
90+
*/
91+
private function mergeMap(array $constraintsPerDep, string $strategy, array &$conflictsOut): array
92+
{
93+
$out = [];
94+
foreach ($constraintsPerDep as $dep => $byPkg) {
95+
$constraint = $this->chooseConstraint(array_values($byPkg), $strategy, $dep, $byPkg, $conflictsOut);
96+
if ($constraint !== null) {
97+
$out[$dep] = $constraint;
98+
}
99+
}
100+
101+
return $out;
102+
}
103+
104+
/**
105+
* @param list<string> $constraints
106+
* @param array<string,string> $byPkg
107+
*/
108+
private function chooseConstraint(array $constraints, string $strategy, string $dep, array $byPkg, array &$conflictsOut): ?string
109+
{
110+
$strategy = strtolower($strategy);
111+
$norm = array_map([$this,'normalizeConstraint'], array_filter($constraints, 'is_string'));
112+
if ($norm === []) {
113+
return null;
114+
}
115+
116+
if ($strategy === 'union-caret') {
117+
return $this->chooseUnionCaret($norm, $dep, $byPkg, $conflictsOut);
118+
}
119+
120+
if ($strategy === 'union-loose') {
121+
return '*';
122+
}
123+
124+
if ($strategy === 'max') {
125+
return $this->maxLowerBound($norm);
126+
}
127+
128+
if ($strategy === 'intersect') {
129+
// naive: if all share same major series, pick max lower bound; else conflict
130+
$majors = array_unique(array_map(static fn($c): int => $c['major'], $norm));
131+
if (count($majors) > 1) {
132+
$this->recordConflict($dep, $byPkg, $conflictsOut, 'intersect-empty');
133+
return null;
134+
}
135+
136+
return $this->maxLowerBound($norm);
137+
}
138+
139+
// default fallback
140+
return $this->chooseUnionCaret($norm, $dep, $byPkg, $conflictsOut);
141+
}
142+
143+
/** @param list<array{raw:string,major:int,minor:int,patch:int,type:string}> $norm */
144+
private function chooseUnionCaret(array $norm, string $dep, array $byPkg, array &$conflictsOut): string
145+
{
146+
// Prefer highest ^MAJOR.MINOR; if any non-caret constraints exist, record a conflict and still pick a sane default.
147+
$caret = array_values(array_filter($norm, static fn($c): bool => $c['type'] === 'caret'));
148+
if ($caret !== []) {
149+
usort($caret, [$this,'cmpSemver']);
150+
$best = end($caret);
151+
return '^' . $best['major'] . '.' . $best['minor'];
152+
}
153+
154+
// If exact pins or ranges exist, pick the "max lower bound" and record conflict
155+
$this->recordConflict($dep, $byPkg, $conflictsOut, 'non-caret-mixed');
156+
return $this->maxLowerBound($norm);
157+
}
158+
159+
/** @param list<array{raw:string,major:int,minor:int,patch:int,type:string}> $norm */
160+
private function maxLowerBound(array $norm): string
161+
{
162+
usort($norm, [$this,'cmpSemver']);
163+
$best = end($norm);
164+
if ($best['type'] === 'caret') {
165+
return '^' . $best['major'] . '.' . $best['minor'];
166+
}
167+
168+
// fallback to exact lower bound
169+
return $best['raw'];
170+
}
171+
172+
/** @param array<string,string> $byPkg */
173+
private function recordConflict(string $dep, array $byPkg, array &$conflictsOut, string $reason): void
174+
{
175+
$conflictsOut[$dep] = [
176+
'package' => $dep,
177+
'versions' => array_values(array_unique(array_values($byPkg))),
178+
'packages' => array_keys($byPkg),
179+
'reason' => $reason,
180+
];
181+
}
182+
183+
/** @return array{raw:string,major:int,minor:int,patch:int,type:string} */
184+
private function normalizeConstraint(string $raw): array
185+
{
186+
$raw = trim($raw);
187+
if ($raw === '' || $raw === '*') {
188+
return ['raw' => '*', 'major' => 0, 'minor' => 0, 'patch' => 0, 'type' => 'wild'];
189+
}
190+
191+
if ($raw[0] === '^') {
192+
$v = substr($raw, 1);
193+
[$M,$m,$p] = $this->parseSemver($v);
194+
return ['raw' => '^' . $M . '.' . $m, 'major' => $M, 'minor' => $m, 'patch' => $p, 'type' => 'caret'];
195+
}
196+
197+
// naive parse: try to get leading semver numbers
198+
[$M,$m,$p] = $this->parseSemver($raw);
199+
return ['raw' => $M . '.' . $m . '.' . $p, 'major' => $M, 'minor' => $m, 'patch' => $p, 'type' => 'pin'];
200+
}
201+
202+
/** @return array{0:int,1:int,2:int} */
203+
private function parseSemver(string $raw): array
204+
{
205+
$raw = ltrim($raw, 'vV');
206+
$parts = preg_split('/[^\d]+/', $raw);
207+
$M = (int) ($parts[0] ?? 0);
208+
$m = (int) ($parts[1] ?? 0);
209+
$p = (int) ($parts[2] ?? 0);
210+
return [$M,$m,$p];
211+
}
212+
213+
/** @param array{major:int,minor:int,patch:int} $a @param array{major:int,minor:int,patch:int} $b */
214+
private function cmpSemver(array $a, array $b): int
215+
{
216+
return [$a['major'],$a['minor'],$a['patch']] <=> [$b['major'],$b['minor'],$b['patch']];
22217
}
23218
}

0 commit comments

Comments
 (0)