Skip to content

Commit

Permalink
Make @internal tag configurable
Browse files Browse the repository at this point in the history
Some projects, like MediaWiki, use the @internal tag with a different
semantics than the one imposed by deptrac. In the case of MediaWiki, the
intended semantics is "not for use by extensions", i.e. "internal to the
core repo".

To work around this issue, allow the config file to specify which tag
deptrac will interpret as "layer-internal". The @deptrac-internal tag
will continue to always work.

NOTE: This is a breaking change: the "@internal" tag will will now be
ignored unless explicitly configured to be used.
  • Loading branch information
brightbyte committed Dec 20, 2023
1 parent 3ab320b commit 0fa1193
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 38 deletions.
5 changes: 4 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,10 @@
->tag('kernel.event_subscriber');
$services
->set(DependsOnInternalToken::class)
->tag('kernel.event_subscriber');
->tag('kernel.event_subscriber')
->args([
'$config' => param('analyser'),
]);
$services
->set(UnmatchedSkippedViolations::class)
->tag('kernel.event_subscriber');
Expand Down
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ The following table shows the available config keys for Deptrac.
</thead>
<tbody>
<tr>
<td>analyser.internal_tag</td>
<td>
Specifies which doc block tag deptrac should use to identify layer-internal class-like tokens.
The default is <code>"@internal"</code>. May be set to <code>null</code> to disable.
Note that the tag <code>@deptrac-internal</code> will always be used to identify
layer-internal class-like tokens, even if this <code>internal_tag</code> is set to <code>null</code>.
</td>
<td>

```yaml
deptrac:
analyser:
internal_tag: ~
```
</td>
</tr>
<tr>
<td>analyser.types</td>
<td>
Expand Down
11 changes: 11 additions & 0 deletions src/Contract/Config/DeptracConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ final class DeptracConfig implements ConfigBuilderInterface
private array $formatters = [];
/** @var array<Ruleset> */
private array $rulesets = [];
/** @var ?string */
private ?string $internalTag = null;
/** @var array<string, EmitterType> */
private array $analyser = [];
/** @var array<string, array<string>> */
private array $skipViolations = [];
/** @var array<string> */
private array $excludeFiles = [];

public function internalTag(?string $tag): self
{
$this->internalTag = $tag;

return $this;
}

public function analysers(EmitterType ...$types): self
{
foreach ($types as $type) {
Expand Down Expand Up @@ -109,6 +118,8 @@ public function toArray(): array
$config['paths'] = $this->paths;
}

$config['analyser']['internal_tag'] = $this->internalTag;

if ([] !== $this->analyser) {
$config['analyser']['types'] = array_map(static fn (EmitterType $emitterType) => $emitterType->value, $this->analyser);
}
Expand Down
26 changes: 21 additions & 5 deletions src/Core/Analyser/EventHandler/DependsOnInternalToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
*/
class DependsOnInternalToken implements ViolationCreatingInterface
{
public function __construct(private readonly EventHelper $eventHelper) {}
private ?string $internalTag;

/**
* @param array{internal_tag:string|null, ...} $config
*/
public function __construct(
private readonly EventHelper $eventHelper,
array $config)
{
$this->internalTag = $config['internal_tag'];
}

public static function getSubscribedEvents()
{
Expand All @@ -29,11 +39,17 @@ public function invoke(ProcessEvent $event): void
foreach ($event->dependentLayers as $dependentLayer => $_) {
if ($event->dependerLayer !== $dependentLayer
&& $event->dependentReference instanceof ClassLikeReference
&& ($event->dependentReference->hasTag('@deptrac-internal')
|| $event->dependentReference->hasTag('@internal'))
) {
$this->eventHelper->addSkippableViolation($event, $ruleset, $dependentLayer, $this);
$event->stopPropagation();
$isInternal = $event->dependentReference->hasTag('@deptrac-internal');

if (!$isInternal && $this->internalTag) {
$isInternal = $event->dependentReference->hasTag($this->internalTag);
}

if ($isInternal) {
$this->eventHelper->addSkippableViolation($event, $ruleset, $dependentLayer, $this);

Check warning on line 50 in src/Core/Analyser/EventHandler/DependsOnInternalToken.php

View workflow job for this annotation

GitHub Actions / infection (ubuntu-20.04, 8.1)

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ $isInternal = $event->dependentReference->hasTag($this->internalTag); } if ($isInternal) { - $this->eventHelper->addSkippableViolation($event, $ruleset, $dependentLayer, $this); + $event->stopPropagation(); } }
$event->stopPropagation();
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/Supportive/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,10 @@ private function appendEmitterTypes(ArrayNodeDefinition $node): void
->arrayNode('analyser')
->addDefaultsIfNotSet()
->children()
->scalarNode('internal_tag')
->defaultNull()
->end()
->arrayNode('types')
->isRequired()
->defaultValue([
EmitterType::CLASS_TOKEN->value,
EmitterType::FUNCTION_TOKEN->value,
Expand Down
40 changes: 31 additions & 9 deletions tests/Core/Analyser/EventHandler/DependsOnInternalTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,41 +53,41 @@ private function makeEvent(
return $event;
}

public function testInvokeEnabled(): void
public function testInvoke(): void
{
$helper = new EventHelper([], new LayerProvider([]));
$handler = new DependsOnInternalToken($helper);
$handler = new DependsOnInternalToken($helper, ['internal_tag' => '@layer-internal']);

$event = $this->makeEvent([], []);
$handler->invoke($event);

$this->assertFalse(
$event->isPropagationStopped(),
'Propagation should continue if neither reference has the "internal" tag'
'Propagation should continue if neither reference has the "layer-internal" tag'
);

$event = $this->makeEvent(['@internal' => ['']], []);
$event = $this->makeEvent(['@layer-internal' => ['']], []);
$handler->invoke($event);

$this->assertFalse(
$event->isPropagationStopped(),
'Propagation should continue if only the depender is marked @internal'
'Propagation should continue if only the depender is marked @layer-internal'
);

$event = $this->makeEvent([], ['@internal' => ['']]);
$event = $this->makeEvent([], ['@layer-internal' => ['']]);
$handler->invoke($event);

$this->assertTrue(
$event->isPropagationStopped(),
'Propagation should be stopped if the dependent is marked @internal'
'Propagation should be stopped if the dependent is marked @layer-internal'
);

$event = $this->makeEvent([], ['@internal' => ['']], 'DependerLayer');
$event = $this->makeEvent([], ['@layer-internal' => ['']], 'DependerLayer');
$handler->invoke($event);

$this->assertFalse(
$event->isPropagationStopped(),
'Propagation should not be stopped if the dependent is marked @internal '.
'Propagation should not be stopped if the dependent is marked @layer-internal '.
'but dependent is in the same layer'
);

Expand All @@ -99,4 +99,26 @@ public function testInvokeEnabled(): void
'Propagation should be stopped if the dependent is marked @deptrac-internal'
);
}

public function testDefaultInternalTag(): void
{
$helper = new EventHelper([], new LayerProvider([]));
$handler = new DependsOnInternalToken($helper, ['internal_tag' => null]);

$event = $this->makeEvent([], ['@internal' => ['']]);
$handler->invoke($event);

$this->assertFalse(
$event->isPropagationStopped(),
'The @internal tag should not be used per default'
);

$event = $this->makeEvent([], ['@deptrac-internal' => ['']]);
$handler->invoke($event);

$this->assertTrue(
$event->isPropagationStopped(),
'The @deptrac-internal tag should be used per default'
);
}
}
58 changes: 36 additions & 22 deletions tests/Supportive/DependencyInjection/DeptracExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ final class DeptracExtensionTest extends TestCase
{
private ContainerBuilder $container;
private DeptracExtension $extension;
private array $formatterDefaults = [

private const FORMATTER_DEFAULTS = [
'graphviz' => [
'hidden_layers' => [],
'groups' => [],
Expand All @@ -32,6 +33,14 @@ final class DeptracExtensionTest extends TestCase
],
];

private const ANALYSER_DEFAULTS = [
'internal_tag' => null,
'types' => [
'class',
'function',
],
];

protected function setUp(): void
{
parent::setUp();
Expand All @@ -51,8 +60,8 @@ public function testDefaults(): void
self::assertSame([], $this->container->getParameter('layers'));
self::assertSame([], $this->container->getParameter('ruleset'));
self::assertSame([], $this->container->getParameter('skip_violations'));
self::assertSame($this->formatterDefaults, $this->container->getParameter('formatters'));
self::assertSame(['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::FUNCTION_TOKEN->value]], $this->container->getParameter('analyser'));
self::assertSame(self::FORMATTER_DEFAULTS, $this->container->getParameter('formatters'));
self::assertSame(self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser'));
self::assertSame(true, $this->container->getParameter('ignore_uncovered_internal_classes'));
}

Expand All @@ -69,8 +78,8 @@ public function testDefaultsWithEmptyRoot(): void
self::assertSame([], $this->container->getParameter('layers'));
self::assertSame([], $this->container->getParameter('ruleset'));
self::assertSame([], $this->container->getParameter('skip_violations'));
self::assertSame($this->formatterDefaults, $this->container->getParameter('formatters'));
self::assertSame(['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::FUNCTION_TOKEN->value]], $this->container->getParameter('analyser'));
self::assertSame(self::FORMATTER_DEFAULTS, $this->container->getParameter('formatters'));
self::assertSame(self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser'));
self::assertSame(true, $this->container->getParameter('ignore_uncovered_internal_classes'));
}

Expand Down Expand Up @@ -307,10 +316,12 @@ public function testNullAnalyser(): void
],
];

$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The child config "types" under "deptrac.analyser" must be configured.');

$this->extension->load($configs, $this->container);

self::assertSame(
self::ANALYSER_DEFAULTS,
$this->container->getParameter('analyser')
);
}

public function testEmptyAnalyser(): void
Expand All @@ -321,10 +332,12 @@ public function testEmptyAnalyser(): void
],
];

$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The child config "types" under "deptrac.analyser" must be configured.');

$this->extension->load($configs, $this->container);

self::assertSame(
self::ANALYSER_DEFAULTS,
$this->container->getParameter('analyser')
);
}

public function testInvalidAnalyserTypes(): void
Expand All @@ -333,7 +346,7 @@ public function testInvalidAnalyserTypes(): void
'deptrac' => [
'analyser' => [
'types' => ['invalid'],
],
] + self::ANALYSER_DEFAULTS,
],
];

Expand All @@ -349,14 +362,14 @@ public function testNullAnalyserTypes(): void
'deptrac' => [
'analyser' => [
'types' => null,
],
] + self::ANALYSER_DEFAULTS,
],
];

$this->extension->load($configs, $this->container);

self::assertSame(
['types' => []],
['types' => []] + self::ANALYSER_DEFAULTS,
$this->container->getParameter('analyser')
);
}
Expand All @@ -367,14 +380,14 @@ public function testEmptyAnalyserTypes(): void
'deptrac' => [
'analyser' => [
'types' => [],
],
] + self::ANALYSER_DEFAULTS,
],
];

$this->extension->load($configs, $this->container);

self::assertSame(
['types' => []],
['types' => []] + self::ANALYSER_DEFAULTS,
$this->container->getParameter('analyser')
);
}
Expand All @@ -385,14 +398,15 @@ public function testAnalyserWithDuplicateTypes(): void
'deptrac' => [
'analyser' => [
'types' => [EmitterType::CLASS_TOKEN->value, EmitterType::CLASS_TOKEN->value],
],
] + self::ANALYSER_DEFAULTS,
],
];

$this->extension->load($configs, $this->container);

self::assertSame(
['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::CLASS_TOKEN->value]],
['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::CLASS_TOKEN->value]]
+ self::ANALYSER_DEFAULTS,
$this->container->getParameter('analyser')
);
}
Expand Down Expand Up @@ -422,7 +436,7 @@ public function testGraphvizFormatterWithEmptyNodes(): void

$this->extension->load($configs, $this->container);

$expectedFormatterConfig = $this->formatterDefaults;
$expectedFormatterConfig = self::FORMATTER_DEFAULTS;
$actualFormatterConfig = $this->container->getParameter('formatters');

krsort($expectedFormatterConfig);
Expand All @@ -445,7 +459,7 @@ public function testGraphvizFormatterWithOldPointToGroupsConfig(): void

$this->extension->load($configs, $this->container);

$expectedFormatterConfig = $this->formatterDefaults;
$expectedFormatterConfig = self::FORMATTER_DEFAULTS;
$expectedFormatterConfig['graphviz'] = [
'point_to_groups' => true,
'hidden_layers' => [],
Expand Down Expand Up @@ -474,7 +488,7 @@ public function testGraphvizFormattersWithHiddenLayers(): void

$this->extension->load($configs, $this->container);

$expectedFormatterConfig = $this->formatterDefaults;
$expectedFormatterConfig = self::FORMATTER_DEFAULTS;
$expectedFormatterConfig['graphviz'] = [
'hidden_layers' => ['Utils'],
'groups' => [
Expand All @@ -499,7 +513,7 @@ public function testCodeclimateFormatterWithEmptyNodes(): void

$this->extension->load($configs, $this->container);

$expectedFormatterConfig = $this->formatterDefaults;
$expectedFormatterConfig = self::FORMATTER_DEFAULTS;
$actualFormatterConfig = $this->container->getParameter('formatters');

krsort($expectedFormatterConfig);
Expand Down

0 comments on commit 0fa1193

Please sign in to comment.