Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to prevent depending on tests and vendor directories #1220

Closed
TravisCarden opened this issue Jun 10, 2023 · 12 comments
Closed

How to prevent depending on tests and vendor directories #1220

TravisCarden opened this issue Jun 10, 2023 · 12 comments
Labels

Comments

@TravisCarden
Copy link

Hi! I'm having trouble getting my layer configuration to work. I'm trying to do two things: 1) keep my production code from accidentally using test classes, and 2) keep my domain layer from depending on vendor code.

And I'm having two problems: 1) Deptrac incorrectly counts it as a violation when a test class depends on a production class, and 2) it prints something like 600,000 violations in the vendor directory.

Here are the relevant portions of my directory structure:

.
├── src
│   ├── Domain
│   └── Infrastructure
├── tests
└── vendor

Here's my configuration file:

deptrac.yaml
parameters:

  paths:
    - ./src
    - ./tests
    - ./vendor

  layers:
    -
      name: Domain
      collectors:
        - type: directory
          regex: ./src/Domain/.*
    -
      name: Infrastructure
      collectors:
        - type: directory
          regex: ./src/Infrastructure/.*
    -
      name: Tests
      collectors:
        - type: directory
          regex: ./tests/.*
    -
      name: Vendor
      collectors:
        - type: directory
          regex: ./vendor/.*

  ruleset:

    Infrastructure:
      - Domain
      - Vendor

    Tests:
      - Domain
      - Infrastructure
      - Vendor

I've inserted three violations into src/Domain/Example.php: one dependency on each of the Infrastructure layer, the tests directory, and a vendor library. It correctly finds these violations, but it reports a ton of violations in tests and vendor that I don't want it to.

Here's a representative sample from the command output:

Output
----------- ---------------------------------------------------------------------------------------------
 Reason      Domain
----------- ---------------------------------------------------------------------------------------------
 Violation   Domain\Example must not depend on Tests\EndToEndTest (Tests)
             src/Domain/Example.php:37
 Violation   Domain\Example must not depend on Infrastructure\Filesystem (Infrastructure)
             src/Domain/Example.php:38
 Violation   Domain\Example must not depend on Prophecy\Argument (Vendor)
             src/Domain/Example.php:15
----------- ---------------------------------------------------------------------------------------------

----------- ---------------------------------------------------------------------------------------------
 Reason      Tests
----------- ---------------------------------------------------------------------------------------------
Violation   Tests\EndToEnd\EndToEndTest must not depend on Domain\Example (Domain)
            Tests\EndToEndTest::57 ->
            Domain\Example::5
            tests/EndToEndTest.php:5
Violation   Tests\EndToEnd\EndToEndTest must not depend on PHPUnit\Util\Test (Vendor)
            Tests\TestCase::13 ->
            PHPUnit\Framework\TestCase::26 ->
            PHPUnit\Util\Test::1044
----------- ---------------------------------------------------------------------------------------------

----------- ---------------------------------------------------------------------------------------------
 Reason      Vendor
----------- ---------------------------------------------------------------------------------------------
Violation   Tests\EndToEnd\EndToEndTest must not depend on Infrastructure\Example (Infrastructure)
            Tests\EndToEnd\EndToEndFunctionalTestCase::57 ->
            Infrastructure\Example::12
            tests/EndToEnd/EndToEndFunctionalTestCase.php:12
----------- ---------------------------------------------------------------------------------------------

I feel like I must misunderstand something fundamental. Thank you so much for any help!

@patrickkusebauch
Copy link
Collaborator

Let me see what I can glean from the data provided. I will start from the provided output and work backward.

----------- ---------------------------------------------------------------------------------------------
 Reason      Tests
----------- ---------------------------------------------------------------------------------------------
Violation   Tests\EndToEnd\EndToEndTest must not depend on Domain\Example (Domain)
            Tests\EndToEndTest::57 ->
            Domain\Example::5
            tests/EndToEndTest.php:5
Violation   Tests\EndToEnd\EndToEndTest must not depend on PHPUnit\Util\Test (Vendor)
            Tests\TestCase::13 ->
            PHPUnit\Framework\TestCase::26 ->
            PHPUnit\Util\Test::1044
----------- ---------------------------------------------------------------------------------------------

implies that you do not allow dependencies from Tests to Domain nor Vendor in your rulesets.

----------- ---------------------------------------------------------------------------------------------
 Reason      Vendor
----------- ---------------------------------------------------------------------------------------------
Violation   Tests\EndToEnd\EndToEndTest must not depend on Infrastructure\Example (Infrastructure)
            Tests\EndToEnd\EndToEndFunctionalTestCase::57 ->
            Infrastructure\Example::12
            tests/EndToEnd/EndToEndFunctionalTestCase.php:12
----------- ---------------------------------------------------------------------------------------------

implies that class-like (class, interface, trait, enum) Tests\EndToEnd\EndToEndTest is part of the Vendor layer.

Both of those things seem like you did not expect them to be the case.

Looking at the relevant parts of the provided configuration :

parameters:
  layers:
    -
      name: Vendor
      collectors:
        - type: directory
          regex: ./vendor/.*

  ruleset:
    Tests:
      - Domain

I would not expect this behavior either.

For the second problem (Tests\EndToEnd\EndToEndTest being part of the Vendor layer) the debug process is easy. You can use the deptrac debug:layer Vendor command to see all the tokens that are part of the Vendor layer. You can keep tweaking your collector configuration until the output is as you expect it.

For the first problem, I am stumped. It should not be possible given the information you've provided on this issue. Is it possible that you have mistyped something when transcribing your configuration to this issue post?

Also from the output, it seems that Tests\EndToEnd\EndToEndTest is part of both the Tests and Vendor layers:

----------- ---------------------------------------------------------------------------------------------
 Reason      Tests
----------- ---------------------------------------------------------------------------------------------
Violation   Tests\EndToEnd\EndToEndTest must not depend on Domain\Example (Domain)
            Tests\EndToEndTest::57 ->
            Domain\Example::5
            tests/EndToEndTest.php:5
----------- ---------------------------------------------------------------------------------------------

----------- ---------------------------------------------------------------------------------------------
 Reason      Vendor
----------- ---------------------------------------------------------------------------------------------
Violation   Tests\EndToEnd\EndToEndTest must not depend on Infrastructure\Example (Infrastructure)
            Tests\EndToEnd\EndToEndFunctionalTestCase::57 ->
            Infrastructure\Example::12
            tests/EndToEnd/EndToEndFunctionalTestCase.php:12
----------- ---------------------------------------------------------------------------------------------

Did you get a warning about that? deptrac emits warning messages when a token is part of more than one layer.

@TravisCarden
Copy link
Author

TravisCarden commented Jun 12, 2023

Thank you, @patrickkusebauch! I decided to try to reproduce the problem from scratch at https://github.com/TravisCarden/qossmic-deptrac-1220, but I failed to do so. You can see in that repo that exactly the expected results are produced, so something else is apparently going on with the implementation in my actual project (https://github.com/php-tuf/composer-stager). I'll leave this issue open while I do a little more investigation.

Update: I found one problem. It appears that my "Tests" layer regex was matching test directories in the vendor directory. I've fixed that by using %currentWorkingDirectory%. I still haven't figured out the other problems.

@gennadigennadigennadi
Copy link
Member

gennadigennadigennadi commented Jun 13, 2023

@patrickkusebauch how does Deptrac deal with @internal annotations? because that's why deptrac sees a dependency. @TravisCarden if you remove the @internal annotation from the Core\Stager-class deptrac stops complaining.

Related issue:
#538

Found also the documentation that explains the current behaviour: https://qossmic.github.io/deptrac/concepts/#internal-and-deptrac-internal-annotation

@gennadigennadigennadi
Copy link
Member

#867 mentions the current solution for @internal in a situation like yours.

@patrickkusebauch
Copy link
Collaborator

That's a good catch. That could be it. Can you try to run the dev-main branch of deptrac? It has better output for those sort of situations.

@gennadigennadigennadi
Copy link
Member

gennadigennadigennadi commented Jun 13, 2023

That's also what deptrac says on the main branch -> DependsOnInternalToken. @TravisCarden you should also update your deptrac.yml (deptrac.yaml will be loaded automatically ;-)), the key regex for the config of the directory layer is deprecated and will not work with the next release. The key is value now.

Your deptrac.yaml should look like this:

- name: Domain
  collectors:
    - type: directory
      value: ./src/Domain/*

@TravisCarden
Copy link
Author

Okay, great. I updated to qossmic/deptrac:dev-main, and (the new output is a nice improvement!) my @internal annotations are in fact causing failures, e.g.:

------------------------ -----------------------------------------------------------------------------------------------------------------
 Reason                   Tests
------------------------ -----------------------------------------------------------------------------------------------------------------
 DependsOnInternalToken   PhpTuf\ComposerStager\Tests\Core\BeginnerUnitTest must not depend on PhpTuf\ComposerStager\Domain\Core\Beginner
                          You are depending on a token that is internal to the layer and you are not part of that layer. (Domain)
                          tests/Core/BeginnerUnitTest.php:50

Is there a way to disable that behavior (DependsOnInternalToken)? My @internal annotations don't mean internal to a layer, they mean internal to my library--which is an API library that external clients are supposed to be able to depend directly on the Domain (API) layer but not the Infrastructure (internal) layer. It looks like that's the cause of most or all of thousands of violations from my vendor directory, too.

@patrickkusebauch
Copy link
Collaborator

There is, but it is IMHO stupid. We should do better. What you can do is to have a custom rule like this:

use Qossmic\Deptrac\Core\Ast\AstMap\ClassLike\ClassLikeReference;
use Qossmic\Deptrac\Contract\Analyser\ProcessEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class DependsOnInternalTokenSkipper implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            ProcessEvent::class => ['invoke', -1],
        ];
    }

    public function invoke(ProcessEvent $event): void
    {
        foreach ($event->dependentLayers as $dependentLayer => $_) {
            if ($event->dependerLayer !== $dependentLayer
                && $event->dependentReference instanceof ClassLikeReference
                && $event->dependentReference->isInternal
            ) {
                $event->stopPropagation();
            }
        }
    }

And then register it in your config file according to the documentation.

By the subscriber precedence, it will run right before the rule that causes the violations and will prevent it from running.

@TravisCarden
Copy link
Author

Thanks! That takes care of my @internal issue if I switch to deptrac:dev-main. However...

I don't think I could do that with deptrac-shim because I need to use its classes, right? (I don't want to pull in all of deptrac's dependencies, so I wouldn't want to switch away from the shim.)

Also, I still get some other errors I don't fully understand:

php ./vendor/qossmic/deptrac/deptrac.php --config-file=deptrac.yml
7279/7279 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

-------------------------- -------------------------------------------------------------------------------------------------------------------------------------------
 Reason                     API
-------------------------- -------------------------------------------------------------------------------------------------------------------------------------------
 DependsOnDisallowedLayer   Domain\Example must not depend on Stringable
                            You are depending on token that is a part of a layer that you are not allowed to depend on. (Vendor)
                            src/Domain/AnotherExample.php:2
 DependsOnDisallowedLayer   Domain\LogicException must not depend on JetBrains\PhpStorm\Internal\TentativeType
                            You are depending on token that is a part of a layer that you are not allowed to depend on. (Vendor)
                            LogicException::14 ->
                            Exception::13 ->
                            JetBrains\PhpStorm\Internal\TentativeType::393
                            vendor/jetbrains/phpstorm-stubs/Core/Core_c.php:393
-------------------------- -------------------------------------------------------------------------------------------------------------------------------------------

# Etc...

And of course, I don't want to actually scan the vendor directory for problems. I just want to forbid one of my layers from depending on it. (It takes a long time to scan and produces false positives as we've already seen.

@patrickkusebauch Re: "There is, but it is IMHO stupid. We should do better.", I've created a feature request to be "helpful". 😉 #1223

@gennadigennadigennadi
Copy link
Member

Also, I still get some other errors I don't fully understand:

php ./vendor/qossmic/deptrac/deptrac.php --config-file=deptrac.yml
7279/7279 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

-------------------------- -------------------------------------------------------------------------------------------------------------------------------------------
 Reason                     API
-------------------------- -------------------------------------------------------------------------------------------------------------------------------------------
 DependsOnDisallowedLayer   Domain\Example must not depend on Stringable
                            You are depending on token that is a part of a layer that you are not allowed to depend on. (Vendor)
                            src/Domain/AnotherExample.php:2
 DependsOnDisallowedLayer   Domain\LogicException must not depend on JetBrains\PhpStorm\Internal\TentativeType
                            You are depending on token that is a part of a layer that you are not allowed to depend on. (Vendor)
                            LogicException::14 ->
                            Exception::13 ->
                            JetBrains\PhpStorm\Internal\TentativeType::393
                            vendor/jetbrains/phpstorm-stubs/Core/Core_c.php:393
-------------------------- -------------------------------------------------------------------------------------------------------------------------------------------

# Etc...

@TravisCarden do you have this current config somewhere for me to checkout? Than I could have look.

@patrickkusebauch
Copy link
Collaborator

Looks to me like your vendor layer includes some stubs for core PHP classes. Common culprits are static analysis tools, IDE support tools and poly fills for future PHP versions.

@TravisCarden
Copy link
Author

I ended up solving this for myself with a custom PHPCS sniff that just looks for use statements that aren't in my namespace. It's crude, but my needs are simple. Thanks for your help! 🙂 I'll let you decide whether to close this issue.

@patrickkusebauch patrickkusebauch closed this as not planned Won't fix, can't repro, duplicate, stale Sep 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants