Skip to content

Commit 4bf5153

Browse files
committed
[dependencies] Add shadow dependency audit option
1 parent cdf59bc commit 4bf5153

8 files changed

Lines changed: 212 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Keep required PHPUnit matrix checks reporting after workflow-managed `.github/wiki` pointer commits by running the pull-request test workflow without top-level path filters and aligning the packaged consumer test wrapper (#230)
13+
- Ignore intentional Composer Dependency Analyser shadow dependency findings by default while adding `dependencies --show-shadow-dependencies` for audits (#233)
1314

1415
## [1.21.0] - 2026-04-24
1516

docs/commands/dependencies.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ Options
5555
Asks ``composer-dependency-analyser`` to dump usages for the given package
5656
or wildcard pattern and enables ``--show-all-usages`` automatically.
5757

58+
``--show-shadow-dependencies`` (optional)
59+
Reports shadow dependencies instead of applying the Fast Forward default
60+
ignore for intentional dependency groups.
61+
62+
By default, DevTools hides ``SHADOW_DEPENDENCY`` findings because Fast
63+
Forward packages may intentionally require ecosystem bundles, meta packages,
64+
or convenience packages that install related dependencies for consumers.
65+
Use this flag when auditing whether a package has accidental shadow
66+
dependencies that should be removed or documented more precisely.
67+
5868
``--json``
5969
Emit a structured machine-readable payload instead of the normal terminal
6070
output.
@@ -95,6 +105,12 @@ Dump all matched usages for one package:
95105
96106
composer dependencies --dump-usage=symfony/console
97107
108+
Audit shadow dependencies:
109+
110+
.. code-block:: bash
111+
112+
composer dependencies --show-shadow-dependencies
113+
98114
Apply the upgrade workflow and then analyze dependencies:
99115

100116
.. code-block:: bash
@@ -135,6 +151,8 @@ Behavior
135151
consumer repositories can extend the baseline instead of copying it whole
136152
- ``--dump-usages <package>`` and ``--show-all-usages`` when ``--dump-usage``
137153
is passed to the DevTools command
154+
- the ``FAST_FORWARD_DEV_TOOLS_SHOW_SHADOW_DEPENDENCIES`` process environment
155+
flag, which is enabled when ``--show-shadow-dependencies`` is passed
138156
- ``jack breakpoint`` maps ``--max-outdated`` to Jack's ``--limit`` option.
139157
- ``--max-outdated=-1`` keeps ``jack breakpoint`` in the workflow for reporting,
140158
but its failure is ignored so only missing or unused dependency findings fail

docs/configuration/overriding-defaults.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ consumers can extend the default configuration using the
141141
This approach keeps the Fast Forward baseline while letting consumer
142142
repositories add project-specific ignores or scan rules.
143143

144+
The baseline ignores ``SHADOW_DEPENDENCY`` findings by default because Fast
145+
Forward packages may intentionally require dependency groups, ecosystem bundles,
146+
or meta packages that install related dependencies for consumers. Run
147+
``composer dependencies --show-shadow-dependencies`` when you want to audit
148+
those findings and decide whether a package should keep, document, or remove a
149+
direct dependency.
150+
144151
What Is Not Overwritten Automatically
145152
--------------------------------------
146153

docs/running/specialized-commands.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Analyzes missing, unused, misplaced, and outdated Composer dependencies.
7777
composer dependencies --max-outdated=-1
7878
composer dependencies --dev
7979
composer dependencies --dump-usage=symfony/console
80+
composer dependencies --show-shadow-dependencies
8081
composer dependencies --upgrade --dev
8182
8283
Important details:
@@ -88,6 +89,10 @@ Important details:
8889
override locally;
8990
- ``--dump-usage=<package>`` forwards to
9091
``composer-dependency-analyser --dump-usages <package> --show-all-usages``;
92+
- ``--show-shadow-dependencies`` keeps shadow dependency findings visible for
93+
audits; without it, DevTools hides intentional Fast Forward dependency-group
94+
shadows so CI does not fail on ecosystem or meta packages that deliberately
95+
install related dependencies for consumers;
9196
- it uses ``jack breakpoint --limit=<max-outdated>`` to fail when too many
9297
outdated dependencies accumulate;
9398
- ``--max-outdated=-1`` keeps the Jack outdated report in the output but

src/Config/ComposerDependencyAnalyserConfig.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
*/
4242
final class ComposerDependencyAnalyserConfig
4343
{
44+
public const string ENV_SHOW_SHADOW_DEPENDENCIES = 'FAST_FORWARD_DEV_TOOLS_SHOW_SHADOW_DEPENDENCIES';
45+
4446
/**
4547
* Dependencies that are only required by the packaged DevTools distribution itself.
4648
*
@@ -90,6 +92,10 @@ public static function configure(?callable $customize = null): Configuration
9092
{
9193
$configuration = new Configuration();
9294

95+
if (! self::shouldShowShadowDependencies()) {
96+
self::applyIgnoresShadowDependencies($configuration);
97+
}
98+
9399
if (DevToolsPathResolver::isRepositoryCheckout()) {
94100
self::applyPackagedRepositoryIgnores($configuration);
95101
}
@@ -101,12 +107,39 @@ public static function configure(?callable $customize = null): Configuration
101107
return $configuration;
102108
}
103109

110+
/**
111+
* The default configuration ignores shadow dependencies because Fast
112+
* Forward packages MAY intentionally require dependency groups. For example,
113+
* ecosystem or meta packages can require related PSR or framework packages
114+
* so consumers do not need to install every package one by one.
115+
*
116+
* @param Configuration $configuration the analyser configuration to customize
117+
*
118+
* @return Configuration the modified configuration with shadow dependencies ignored
119+
*/
120+
public static function applyIgnoresShadowDependencies(Configuration $configuration): Configuration
121+
{
122+
$configuration->ignoreErrors([ErrorType::SHADOW_DEPENDENCY]);
123+
124+
return $configuration;
125+
}
126+
127+
/**
128+
* Determines whether shadow dependency reports SHOULD remain visible.
129+
*
130+
* @return bool
131+
*/
132+
public static function shouldShowShadowDependencies(): bool
133+
{
134+
return '1' === getenv(self::ENV_SHOW_SHADOW_DEPENDENCIES);
135+
}
136+
104137
/**
105138
* Applies the ignores required only by the packaged DevTools repository.
106139
*
107140
* @param Configuration $configuration the analyser configuration to customize
108141
*
109-
* @return void
142+
* @return Configuration the modified configuration with packaged repository ignores applied
110143
*/
111144
public static function applyPackagedRepositoryIgnores(Configuration $configuration): Configuration
112145
{

src/Console/Command/DependenciesCommand.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2323
use Composer\Command\BaseCommand;
2424
use FastForward\DevTools\Console\Input\HasJsonOption;
25+
use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig;
2526
use FastForward\DevTools\Process\ProcessBuilderInterface;
2627
use FastForward\DevTools\Process\ProcessQueueInterface;
2728
use InvalidArgumentException;
@@ -101,6 +102,11 @@ protected function configure(): void
101102
name: 'dump-usage',
102103
mode: InputOption::VALUE_REQUIRED,
103104
description: 'Dump usages for the given package pattern and show all matched usages.',
105+
)
106+
->addOption(
107+
name: 'show-shadow-dependencies',
108+
mode: InputOption::VALUE_NONE,
109+
description: 'Report shadow dependencies instead of applying Fast Forward intentional-shadow ignores.',
104110
);
105111
}
106112

@@ -176,7 +182,13 @@ private function getComposerDependencyAnalyserCommand(InputInterface $input): Pr
176182
->withArgument('--show-all-usages');
177183
}
178184

179-
return $processBuilder->build('vendor/bin/composer-dependency-analyser');
185+
$showShadowDependencies = (bool) $input->getOption('show-shadow-dependencies');
186+
$process = $processBuilder->build('vendor/bin/composer-dependency-analyser');
187+
$process->setEnv([
188+
ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES => $showShadowDependencies ? '1' : '0',
189+
]);
190+
191+
return $process;
180192
}
181193

182194
/**

tests/Config/ComposerDependencyAnalyserConfigTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
use ShipMonk\ComposerDependencyAnalyser\Config\Configuration;
2929
use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;
3030

31+
use function Safe\putenv;
32+
3133
#[CoversClass(ComposerDependencyAnalyserConfig::class)]
3234
#[UsesClass(DevToolsPathResolver::class)]
3335
final class ComposerDependencyAnalyserConfigTest extends TestCase
@@ -43,6 +45,48 @@ public function configureWillReturnConfiguration(): void
4345
self::assertInstanceOf(Configuration::class, $configuration);
4446
}
4547

48+
/**
49+
* @return void
50+
*/
51+
#[Test]
52+
public function configureWillIgnoreShadowDependenciesByDefault(): void
53+
{
54+
$originalValue = getenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES);
55+
56+
try {
57+
putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES);
58+
$configuration = ComposerDependencyAnalyserConfig::configure();
59+
60+
self::assertTrue(
61+
$configuration->getIgnoreList()
62+
->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, null, 'vendor/shadow-package')
63+
);
64+
} finally {
65+
self::restoreShadowDependenciesEnvironment($originalValue);
66+
}
67+
}
68+
69+
/**
70+
* @return void
71+
*/
72+
#[Test]
73+
public function configureWillKeepShadowDependenciesVisibleWhenRequested(): void
74+
{
75+
$originalValue = getenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES);
76+
77+
try {
78+
putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES . '=1');
79+
$configuration = ComposerDependencyAnalyserConfig::configure();
80+
81+
self::assertFalse(
82+
$configuration->getIgnoreList()
83+
->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, null, 'vendor/shadow-package')
84+
);
85+
} finally {
86+
self::restoreShadowDependenciesEnvironment($originalValue);
87+
}
88+
}
89+
4690
/**
4791
* @return void
4892
*/
@@ -102,4 +146,34 @@ public function applyPackagedRepositoryIgnoresWillReturnTheSameConfigurationInst
102146
ComposerDependencyAnalyserConfig::applyPackagedRepositoryIgnores($configuration)
103147
);
104148
}
149+
150+
/**
151+
* @return void
152+
*/
153+
#[Test]
154+
public function applyIgnoresShadowDependenciesWillReturnTheSameConfigurationInstance(): void
155+
{
156+
$configuration = new Configuration();
157+
158+
self::assertSame(
159+
$configuration,
160+
ComposerDependencyAnalyserConfig::applyIgnoresShadowDependencies($configuration)
161+
);
162+
}
163+
164+
/**
165+
* @param false|string $value
166+
*
167+
* @return void
168+
*/
169+
private static function restoreShadowDependenciesEnvironment(false|string $value): void
170+
{
171+
if (false === $value) {
172+
putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES);
173+
174+
return;
175+
}
176+
177+
putenv(ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES . '=' . $value);
178+
}
105179
}

tests/Console/Command/DependenciesCommandTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
use FastForward\DevTools\Console\Command\DependenciesCommand;
2323
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
24+
use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig;
2425
use FastForward\DevTools\Process\ProcessBuilder;
26+
use FastForward\DevTools\Process\ProcessBuilderInterface;
2527
use FastForward\DevTools\Process\ProcessQueueInterface;
2628
use PHPUnit\Framework\Attributes\CoversClass;
2729
use PHPUnit\Framework\Attributes\Test;
@@ -79,6 +81,8 @@ protected function setUp(): void
7981
->willReturn(false);
8082
$this->input->getOption('dump-usage')
8183
->willReturn(null);
84+
$this->input->getOption('show-shadow-dependencies')
85+
->willReturn(false);
8286
$this->input->getOption('json')
8387
->willReturn(false);
8488
$this->input->getOption('pretty-json')
@@ -167,6 +171,30 @@ public function executeWillIgnoreJackFailuresWhenMaxOutdatedIsDisabled(): void
167171
self::assertSame(DependenciesCommand::SUCCESS, $this->executeCommand());
168172
}
169173

174+
/**
175+
* @return void
176+
*/
177+
#[Test]
178+
public function composerDependencyAnalyserProcessWillHideShadowDependenciesByDefault(): void
179+
{
180+
$this->input->getOption('show-shadow-dependencies')
181+
->willReturn(false);
182+
183+
$this->assertComposerDependencyAnalyserEnvironment('0');
184+
}
185+
186+
/**
187+
* @return void
188+
*/
189+
#[Test]
190+
public function composerDependencyAnalyserProcessCanReportShadowDependencies(): void
191+
{
192+
$this->input->getOption('show-shadow-dependencies')
193+
->willReturn(true);
194+
195+
$this->assertComposerDependencyAnalyserEnvironment('1');
196+
}
197+
170198
/**
171199
* @return int
172200
*/
@@ -175,4 +203,36 @@ private function executeCommand(): int
175203
return (new ReflectionMethod($this->command, 'execute'))
176204
->invoke($this->command, $this->input->reveal(), $this->output->reveal());
177205
}
206+
207+
/**
208+
* @param string $expectedValue
209+
*
210+
* @return void
211+
*/
212+
private function assertComposerDependencyAnalyserEnvironment(string $expectedValue): void
213+
{
214+
$processBuilder = $this->prophesize(ProcessBuilderInterface::class);
215+
$configuredProcessBuilder = $this->prophesize(ProcessBuilderInterface::class);
216+
$process = $this->prophesize(Process::class);
217+
$command = new DependenciesCommand(
218+
$processBuilder->reveal(),
219+
$this->processQueue->reveal(),
220+
$this->fileLocator->reveal(),
221+
$this->logger->reveal(),
222+
);
223+
224+
$processBuilder->withArgument('--config', '/app/composer-dependency-analyser.php')
225+
->willReturn($configuredProcessBuilder->reveal())
226+
->shouldBeCalledOnce();
227+
$configuredProcessBuilder->build('vendor/bin/composer-dependency-analyser')
228+
->willReturn($process->reveal())
229+
->shouldBeCalledOnce();
230+
$process->setEnv([
231+
ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES => $expectedValue,
232+
])->willReturn($process->reveal())
233+
->shouldBeCalledOnce();
234+
235+
(new ReflectionMethod($command, 'getComposerDependencyAnalyserCommand'))
236+
->invoke($command, $this->input->reveal());
237+
}
178238
}

0 commit comments

Comments
 (0)