├── renovate.json ├── phpcs.xml.dist ├── src ├── Psalm │ ├── ProjectFilesToBeTypeChecked.php │ └── Configuration.php ├── Composer │ ├── Package.php │ ├── Project.php │ ├── PackagesRequiringStrictChecks.php │ └── PackageAutoload.php └── Hook.php ├── LICENSE ├── .github └── workflows │ ├── coding-standards.yml │ ├── psalm.yml │ ├── phpunit.yml │ ├── run-example.yml │ ├── mutation-tests.yml │ └── release-on-milestone-closed.yml ├── composer.json ├── psalm.xml └── README.md /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>Ocramius/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | src 15 | test 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Psalm/ProjectFilesToBeTypeChecked.php: -------------------------------------------------------------------------------- 1 | directories() as $directory) { 18 | $instance->addDirectory($directory); 19 | } 20 | 21 | foreach ($autoload->files() as $file) { 22 | $instance->addFile($file); 23 | } 24 | 25 | return $instance; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Roave, LLC. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Composer/Package.php: -------------------------------------------------------------------------------- 1 | dependencies = $dependencies; 21 | } 22 | 23 | /** 24 | * @param mixed[] $packageDefinition 25 | * @psalm-param array{ 26 | * name: string, 27 | * require?: array, 28 | * autoload?: array{ 29 | * psr-4?: array>, 30 | * psr-0?: array> 31 | * } 32 | * } $packageDefinition 33 | */ 34 | public static function fromPackageDefinition(array $packageDefinition, string $installationPath): self 35 | { 36 | return new self( 37 | $packageDefinition['name'], 38 | PackageAutoload::fromAutoloadDefinition($packageDefinition['autoload'] ?? [], $installationPath), 39 | ...array_keys($packageDefinition['require'] ?? []), 40 | ); 41 | } 42 | 43 | public function name(): string 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function autoload(): PackageAutoload 49 | { 50 | return $this->autoload; 51 | } 52 | 53 | public function requiresStrictChecks(): bool 54 | { 55 | return in_array(self::THIS_PACKAGE_NAME, $this->dependencies, true); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: "Check Coding Standards" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | coding-standards: 9 | name: "Check Coding Standards" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "locked" 17 | php-version: 18 | - "8.2" 19 | operating-system: 20 | - "ubuntu-latest" 21 | 22 | steps: 23 | - name: "Checkout" 24 | uses: "actions/checkout@v6" 25 | 26 | - name: "Install PHP" 27 | uses: "shivammathur/setup-php@2.30.0" 28 | with: 29 | coverage: "pcov" 30 | php-version: "${{ matrix.php-version }}" 31 | ini-values: memory_limit=-1 32 | tools: composer:v2, cs2pr 33 | 34 | - name: "Cache dependencies" 35 | uses: "actions/cache@v4" 36 | with: 37 | path: | 38 | ~/.composer/cache 39 | vendor 40 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 41 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 42 | 43 | - name: "Install lowest dependencies" 44 | if: ${{ matrix.dependencies == 'lowest' }} 45 | run: "composer update --prefer-lowest --no-interaction --no-progress" 46 | 47 | - name: "Install highest dependencies" 48 | if: ${{ matrix.dependencies == 'highest' }} 49 | run: "composer update --no-interaction --no-progress" 50 | 51 | - name: "Install locked dependencies" 52 | if: ${{ matrix.dependencies == 'locked' }} 53 | run: "composer install --no-interaction --no-progress" 54 | 55 | - name: "Coding Standard" 56 | run: "vendor/bin/phpcs -q --report=checkstyle | cs2pr" 57 | -------------------------------------------------------------------------------- /.github/workflows/psalm.yml: -------------------------------------------------------------------------------- 1 | name: "Static Analysis by Psalm" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | static-analysis-psalm: 9 | name: "Static Analysis by Psalm" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "locked" 17 | php-version: 18 | - "8.2" 19 | operating-system: 20 | - "ubuntu-latest" 21 | 22 | steps: 23 | - name: "Checkout" 24 | uses: "actions/checkout@v6" 25 | 26 | - name: "Install PHP" 27 | uses: "shivammathur/setup-php@2.30.0" 28 | with: 29 | coverage: "pcov" 30 | php-version: "${{ matrix.php-version }}" 31 | ini-values: memory_limit=-1 32 | tools: composer:v2, cs2pr 33 | 34 | - name: "Cache dependencies" 35 | uses: "actions/cache@v4" 36 | with: 37 | path: | 38 | ~/.composer/cache 39 | vendor 40 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 41 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 42 | 43 | - name: "Install lowest dependencies" 44 | if: ${{ matrix.dependencies == 'lowest' }} 45 | run: "composer update --prefer-lowest --no-interaction --no-progress" 46 | 47 | - name: "Install highest dependencies" 48 | if: ${{ matrix.dependencies == 'highest' }} 49 | run: "composer update --no-interaction --no-progress" 50 | 51 | - name: "Install locked dependencies" 52 | if: ${{ matrix.dependencies == 'locked' }} 53 | run: "composer install --no-interaction --no-progress" 54 | 55 | - name: "psalm" 56 | run: "vendor/bin/psalm --output-format=github --shepherd --stats" 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roave/you-are-using-it-wrong", 3 | "description": "Composer plugin enforcing strict type checks in downstream package dependants", 4 | "type": "composer-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Marco Pivetta", 9 | "email": "ocramius@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0", 14 | "ext-json": "*", 15 | "composer-plugin-api": "^2.1.0", 16 | "ocramius/package-versions": "^2.8.0", 17 | "vimeo/psalm": "^5.23.1", 18 | "psalm/plugin-phpunit": "^0.19.0" 19 | }, 20 | "require-dev": { 21 | "composer/composer": "^2.7.2", 22 | "doctrine/coding-standard": "^12.0.0", 23 | "infection/infection": "^0.27.11", 24 | "phpunit/phpunit": "^10.5.15", 25 | "symfony/process": "^7.0.4" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Roave\\YouAreUsingItWrong\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "RoaveTest\\YouAreUsingItWrong\\": "test/unit", 35 | "RoaveE2ETest\\YouAreUsingItWrong\\": "test/e2e" 36 | } 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "infection/extension-installer": false, 41 | "dealerdirect/phpcodesniffer-composer-installer": true 42 | }, 43 | "platform": { 44 | "php": "8.2.99" 45 | } 46 | }, 47 | "extra": { 48 | "class": "Roave\\YouAreUsingItWrong\\Hook" 49 | }, 50 | "scripts": { 51 | "post-update-cmd": "Roave\\YouAreUsingItWrong\\Hook::runTypeChecks", 52 | "post-install-cmd": "Roave\\YouAreUsingItWrong\\Hook::runTypeChecks" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: "PHPUnit tests" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | phpunit: 9 | name: "PHPUnit tests" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "lowest" 17 | - "highest" 18 | - "locked" 19 | php-version: 20 | - "8.2" 21 | - "8.3" 22 | operating-system: 23 | - "ubuntu-latest" 24 | 25 | steps: 26 | - name: "Checkout" 27 | uses: "actions/checkout@v6" 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: "Install PHP" 32 | uses: "shivammathur/setup-php@2.30.0" 33 | with: 34 | coverage: "pcov" 35 | php-version: "${{ matrix.php-version }}" 36 | ini-values: memory_limit=-1 37 | tools: composer:v2, cs2pr 38 | 39 | - name: "Cache dependencies" 40 | uses: "actions/cache@v4" 41 | with: 42 | path: | 43 | ~/.composer/cache 44 | vendor 45 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 46 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 47 | 48 | - name: "Install lowest dependencies" 49 | if: ${{ matrix.dependencies == 'lowest' }} 50 | run: "composer update --prefer-lowest --no-interaction --no-progress" 51 | 52 | - name: "Install highest dependencies" 53 | if: ${{ matrix.dependencies == 'highest' }} 54 | run: "composer update --no-interaction --no-progress" 55 | 56 | - name: "Install locked dependencies" 57 | if: ${{ matrix.dependencies == 'locked' }} 58 | run: "composer install --no-interaction --no-progress" 59 | 60 | - name: "Checkout current HEAD as a named ref" 61 | run: "git checkout -b ci-run-branch" 62 | 63 | - name: "Tests" 64 | run: "vendor/bin/phpunit" 65 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/run-example.yml: -------------------------------------------------------------------------------- 1 | name: "Run example usage" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | phpunit: 9 | name: "Run example usage" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "lowest" 17 | - "highest" 18 | - "locked" 19 | php-version: 20 | - "8.2" 21 | - "8.3" 22 | operating-system: 23 | - "ubuntu-latest" 24 | 25 | steps: 26 | - name: "Checkout" 27 | uses: "actions/checkout@v6" 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: "Install PHP" 32 | uses: "shivammathur/setup-php@2.30.0" 33 | with: 34 | coverage: "pcov" 35 | php-version: "${{ matrix.php-version }}" 36 | ini-values: memory_limit=-1 37 | tools: composer:v2, cs2pr 38 | 39 | - name: "Cache dependencies" 40 | uses: "actions/cache@v4" 41 | with: 42 | path: | 43 | ~/.composer/cache 44 | vendor 45 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 46 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 47 | 48 | - name: "Install lowest dependencies" 49 | if: ${{ matrix.dependencies == 'lowest' }} 50 | run: "composer update --prefer-lowest --no-interaction --no-progress" 51 | 52 | - name: "Install highest dependencies" 53 | if: ${{ matrix.dependencies == 'highest' }} 54 | run: "composer update --no-interaction --no-progress" 55 | 56 | - name: "Install locked dependencies" 57 | if: ${{ matrix.dependencies == 'locked' }} 58 | run: "composer install --no-interaction --no-progress" 59 | 60 | - name: "Checkout current HEAD as a named ref" 61 | run: "git checkout -b ci-run-branch" 62 | 63 | - name: "Run example usage" 64 | run: "cd example && ./run-example.sh" 65 | -------------------------------------------------------------------------------- /.github/workflows/mutation-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Mutation tests" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | mutation-tests: 9 | name: "Mutation tests" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "locked" 17 | php-version: 18 | - "8.2" 19 | operating-system: 20 | - "ubuntu-latest" 21 | 22 | steps: 23 | - name: "Checkout" 24 | uses: "actions/checkout@v6" 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: "Install PHP" 29 | uses: "shivammathur/setup-php@2.30.0" 30 | with: 31 | coverage: "pcov" 32 | php-version: "${{ matrix.php-version }}" 33 | ini-values: memory_limit=-1 34 | tools: composer:v2, cs2pr 35 | 36 | - name: "Cache dependencies" 37 | uses: "actions/cache@v4" 38 | with: 39 | path: | 40 | ~/.composer/cache 41 | vendor 42 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 43 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 44 | 45 | - name: "Install lowest dependencies" 46 | if: ${{ matrix.dependencies == 'lowest' }} 47 | run: "composer update --prefer-lowest --no-interaction --no-progress" 48 | 49 | - name: "Install highest dependencies" 50 | if: ${{ matrix.dependencies == 'highest' }} 51 | run: "composer update --no-interaction --no-progress" 52 | 53 | - name: "Install locked dependencies" 54 | if: ${{ matrix.dependencies == 'locked' }} 55 | run: "composer install --no-interaction --no-progress" 56 | 57 | - name: "Checkout current HEAD as a named ref" 58 | run: "git checkout -b ci-run-branch" 59 | 60 | - name: "Infection" 61 | run: "vendor/bin/infection" 62 | env: 63 | INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} 64 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 65 | 66 | -------------------------------------------------------------------------------- /src/Psalm/Configuration.php: -------------------------------------------------------------------------------- 1 | project_files = $files; 30 | $this->use_docblock_types = true; 31 | $this->checkedNamespaces = $checkedNamespaces; 32 | 33 | $this->setIncludeCollector(new IncludeCollector()); 34 | } 35 | 36 | public static function forStrictlyCheckedNamespacesAndProjectFiles( 37 | ProjectFileFilter $projectFileFilter, 38 | string ...$namespaces, 39 | ): self { 40 | return new self($projectFileFilter, ...$namespaces); 41 | } 42 | 43 | /** {@inheritDoc} */ 44 | public function getReportingLevelForIssue(CodeIssue $e): string 45 | { 46 | if ( 47 | ($e instanceof ClassIssue && $this->identifierMatchesNamespace($e->fq_classlike_name)) 48 | || ($e instanceof PropertyIssue && $this->identifierMatchesNamespace($e->property_id)) 49 | || ($e instanceof MethodIssue && $this->identifierMatchesNamespace($e->method_id)) 50 | || ( 51 | ($e instanceof FunctionIssue || $e instanceof ArgumentIssue) 52 | && $this->identifierMatchesNamespace((string) $e->function_id) 53 | ) 54 | ) { 55 | return parent::getReportingLevelForIssue($e); 56 | } 57 | 58 | return self::REPORT_SUPPRESS; 59 | } 60 | 61 | private function identifierMatchesNamespace(string $identifier): bool 62 | { 63 | foreach ($this->checkedNamespaces as $namespace) { 64 | if (stripos($identifier, $namespace) === 0) { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Composer/Project.php: -------------------------------------------------------------------------------- 1 | packagesRequiringStrictTypeChecks = $packagesRequiringStrictChecks; 28 | } 29 | 30 | public static function fromComposerInstallationContext( 31 | RootPackageInterface $rootPackage, 32 | Locker $locker, 33 | string $currentWorkingDirectory, 34 | ): self { 35 | /** @psalm-var array{packages: array, packages-dev?: array} $lockData */ 36 | $lockData = $locker->getLockData(); 37 | 38 | return new self( 39 | PackageAutoload::fromComposerRootPackage($rootPackage, $currentWorkingDirectory), 40 | PackagesRequiringStrictChecks::fromComposerLocker($locker, $currentWorkingDirectory), 41 | array_filter( 42 | array_merge($lockData['packages'], $lockData['packages-dev'] ?? []), 43 | static function (array $package): bool { 44 | return $package['name'] === self::THIS_PACKAGE_NAME; 45 | }, 46 | ) !== [], 47 | $currentWorkingDirectory, 48 | ); 49 | } 50 | 51 | public function rootPackageAutoload(): PackageAutoload 52 | { 53 | return $this->rootPackageAutoload; 54 | } 55 | 56 | public function packagesRequiringStrictTypeChecks(): PackagesRequiringStrictChecks 57 | { 58 | return $this->packagesRequiringStrictTypeChecks; 59 | } 60 | 61 | public function strictTypeChecksAreEnforcedByLocalInstallation(): bool 62 | { 63 | return $this->strictTypeChecksAreEnforcedByLocalInstallation; 64 | } 65 | 66 | public function alreadyHasOwnPsalmConfiguration(): bool 67 | { 68 | return file_exists($this->projectDirectory . '/psalm.xml') || file_exists($this->projectDirectory . '/psalm.xml.dist'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/release-on-milestone-closed.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions 2 | 3 | name: "Automatic Releases" 4 | 5 | on: 6 | milestone: 7 | types: 8 | - "closed" 9 | 10 | jobs: 11 | release: 12 | name: "GIT tag, release & create merge-up PR" 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: "Checkout" 17 | uses: "actions/checkout@v6" 18 | 19 | - name: "Release" 20 | uses: "laminas/automatic-releases@v1" 21 | with: 22 | command-name: "laminas:automatic-releases:release" 23 | env: 24 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} 25 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} 26 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} 27 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} 28 | 29 | - name: "Create Merge-Up Pull Request" 30 | uses: "laminas/automatic-releases@v1" 31 | with: 32 | command-name: "laminas:automatic-releases:create-merge-up-pull-request" 33 | env: 34 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} 35 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} 36 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} 37 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} 38 | 39 | - name: "Create and/or Switch to new Release Branch" 40 | uses: "laminas/automatic-releases@v1" 41 | with: 42 | command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor" 43 | env: 44 | "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }} 45 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} 46 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} 47 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} 48 | 49 | - name: "Bump Changelog Version On Originating Release Branch" 50 | uses: "laminas/automatic-releases@1.24.0" 51 | with: 52 | command-name: "laminas:automatic-releases:bump-changelog" 53 | env: 54 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} 55 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} 56 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} 57 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} 58 | 59 | - name: "Create new milestones" 60 | uses: "laminas/automatic-releases@v1" 61 | with: 62 | command-name: "laminas:automatic-releases:create-milestones" 63 | env: 64 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} 65 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }} 66 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }} 67 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }} 68 | -------------------------------------------------------------------------------- /src/Composer/PackagesRequiringStrictChecks.php: -------------------------------------------------------------------------------- 1 | */ 22 | private array $packages; 23 | 24 | /** @param array $packages */ 25 | private function __construct(array $packages) 26 | { 27 | $this->packages = array_values($packages); 28 | } 29 | 30 | public static function fromComposerLocker(Locker $locker, string $projectInstallationPath): self 31 | { 32 | /** 33 | * @var array{ 34 | * packages: array, 37 | * autoload?: array{ 38 | * psr-4?: array>, 39 | * psr-0?: array> 40 | * } 41 | * }>, 42 | * packages-dev?: array, 45 | * autoload?: array{ 46 | * psr-4?: array>, 47 | * psr-0?: array> 48 | * } 49 | * }> 50 | * } $lockData 51 | */ 52 | $lockData = $locker->getLockData(); 53 | 54 | return new self(array_filter( 55 | array_map( 56 | static function (array $packageDefinition) use ($projectInstallationPath): Package { 57 | return Package::fromPackageDefinition( 58 | $packageDefinition, 59 | $projectInstallationPath . '/vendor/' . $packageDefinition['name'], 60 | ); 61 | }, 62 | array_merge($lockData['packages'], $lockData['packages-dev'] ?? []), 63 | ), 64 | static function (Package $package): bool { 65 | return $package->requiresStrictChecks(); 66 | }, 67 | )); 68 | } 69 | 70 | /** @return list */ 71 | public function packagesForWhichUsagesAreToBeTypeChecked(): array 72 | { 73 | return array_map(static function (Package $package): string { 74 | return $package->name(); 75 | }, $this->packages); 76 | } 77 | 78 | /** @return array */ 79 | public function namespacesForWhichUsagesAreToBeTypeChecked(): array 80 | { 81 | return array_merge([], ...array_map(static function (Package $package): array { 82 | return $package 83 | ->autoload() 84 | ->namespaces(); 85 | }, $this->packages)); 86 | } 87 | 88 | public function printPackagesToBeCheckedToComposerIo(IOInterface $io): void 89 | { 90 | $io->write('' . self::THIS_PACKAGE_NAME . ': following package usages will be checked:'); 91 | 92 | foreach ($this->packages as $package) { 93 | self::printPackage($package, $io); 94 | } 95 | } 96 | 97 | private static function printPackage(Package $package, IOInterface $io): void 98 | { 99 | $io->write(' - ' . $package->name()); 100 | 101 | $namespaces = $package->autoload()->namespaces(); 102 | 103 | array_walk( 104 | $namespaces, 105 | static function (string $namespace) use ($io): void { 106 | self::printPackageNamespace($namespace, $io); 107 | }, 108 | ); 109 | } 110 | 111 | private static function printPackageNamespace(string $namespace, IOInterface $io): void 112 | { 113 | $io->write(' - - ' . $namespace); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Composer/PackageAutoload.php: -------------------------------------------------------------------------------- 1 | > $psr4 20 | * @param array> $psr0 21 | * @param array $classMap 22 | * @param array $files 23 | */ 24 | private function __construct( 25 | private array $psr4, 26 | private array $psr0, 27 | private array $classMap, 28 | private array $files, 29 | ) { 30 | } 31 | 32 | /** 33 | * @param mixed[] $autoloadDefinition 34 | * @psalm-param array{ 35 | * psr-4?: array>, 36 | * psr-0?: array>, 37 | * files?: array, 38 | * classmap?: array 39 | * } $autoloadDefinition 40 | */ 41 | public static function fromAutoloadDefinition(array $autoloadDefinition, string $packageDirectory): self 42 | { 43 | $prefixWithCurrentDir = static function (string $path) use ($packageDirectory): string { 44 | return $packageDirectory . '/' . $path; 45 | }; 46 | 47 | return new self( 48 | array_map( 49 | /** 50 | * @param string|array $paths 51 | * 52 | * @return array 53 | */ 54 | static function ($paths) use ($prefixWithCurrentDir): array { 55 | return array_map($prefixWithCurrentDir, (array) $paths); 56 | }, 57 | $autoloadDefinition['psr-4'] ?? [], 58 | ), 59 | array_map( 60 | /** 61 | * @param string|array $paths 62 | * 63 | * @return array 64 | */ 65 | static function ($paths) use ($prefixWithCurrentDir): array { 66 | return array_map($prefixWithCurrentDir, (array) $paths); 67 | }, 68 | $autoloadDefinition['psr-0'] ?? [], 69 | ), 70 | array_map($prefixWithCurrentDir, $autoloadDefinition['classmap'] ?? []), 71 | array_map($prefixWithCurrentDir, $autoloadDefinition['files'] ?? []), 72 | ); 73 | } 74 | 75 | public static function fromComposerRootPackage(RootPackageInterface $package, string $projectDirectory): self 76 | { 77 | /** 78 | * @psalm-var array{ 79 | * psr-4?: array>, 80 | * psr-0?: array>, 81 | * classmap?: array, 82 | * files?: array 83 | * } $autoload 84 | */ 85 | $autoload = $package->getAutoload(); 86 | 87 | return self::fromAutoloadDefinition($autoload, $projectDirectory); 88 | } 89 | 90 | /** @return array */ 91 | public function directories(): array 92 | { 93 | return array_filter(array_map('realpath', array_merge( 94 | [], 95 | array_filter($this->classMap, 'is_dir'), 96 | ...array_values($this->psr0), 97 | ...array_values($this->psr4), 98 | ))); 99 | } 100 | 101 | /** @return array */ 102 | public function files(): array 103 | { 104 | return array_filter(array_map('realpath', array_merge( 105 | [], 106 | array_filter($this->classMap, 'is_file'), 107 | $this->files, 108 | ))); 109 | } 110 | 111 | /** @return array */ 112 | public function namespaces(): array 113 | { 114 | return array_merge(array_keys($this->psr4), array_keys($this->psr0)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Hook.php: -------------------------------------------------------------------------------- 1 | 44 | */ 45 | public static function getSubscribedEvents(): array 46 | { 47 | return [ 48 | ScriptEvents::POST_INSTALL_CMD => 'runTypeChecks', 49 | ScriptEvents::POST_UPDATE_CMD => 'runTypeChecks', 50 | ]; 51 | } 52 | 53 | /** 54 | * @throws RuntimeException 55 | * 56 | * @psalm-suppress PossiblyUnusedMethod this method is only ever used in the context of hooks 57 | * declared in `composer.json` 58 | */ 59 | public static function runTypeChecks(Event $composerEvent): void 60 | { 61 | $io = $composerEvent->getIO(); 62 | $composer = $composerEvent->getComposer(); 63 | $project = Project::fromComposerInstallationContext($composer->getPackage(), $composer->getLocker(), getcwd()); 64 | 65 | if (! $project->strictTypeChecksAreEnforcedByLocalInstallation()) { 66 | $io->write('' . self::THIS_PACKAGE_NAME . ': plugin not installed locally - skipping type checks...'); 67 | 68 | return; 69 | } 70 | 71 | if ($project->alreadyHasOwnPsalmConfiguration()) { 72 | $io->write('' . self::THIS_PACKAGE_NAME . ': psalm configuration detected - assuming static analysis will run later; not running psalm now'); 73 | 74 | return; 75 | } 76 | 77 | $io->write('' . self::THIS_PACKAGE_NAME . ': checking strictly type-checked packages...'); 78 | 79 | $project 80 | ->packagesRequiringStrictTypeChecks() 81 | ->printPackagesToBeCheckedToComposerIo($io); 82 | 83 | self::analyseProject($project); 84 | 85 | $io->write('' . self::THIS_PACKAGE_NAME . ': ... done checking strictly type-checked packages'); 86 | } 87 | 88 | private static function analyseProject(Project $project): void 89 | { 90 | if (! defined('PSALM_VERSION')) { 91 | define('PSALM_VERSION', Versions::getVersion('vimeo/psalm')); 92 | } 93 | 94 | if (! defined('PHP_PARSER_VERSION')) { 95 | define('PHP_PARSER_VERSION', Versions::getVersion('nikic/php-parser')); 96 | } 97 | 98 | // At this stage of the installation, project dependencies are not yet autoloadable 99 | require_once getcwd() . '/vendor/autoload.php'; 100 | 101 | $startTime = microtime(true); 102 | $files = ProjectFilesToBeTypeChecked::fromAutoloadDefinitions($project->rootPackageAutoload()); 103 | $config = Configuration::forStrictlyCheckedNamespacesAndProjectFiles( 104 | $files, 105 | ...$project 106 | ->packagesRequiringStrictTypeChecks() 107 | ->namespacesForWhichUsagesAreToBeTypeChecked(), 108 | ); 109 | $projectAnalyzer = new ProjectAnalyzer($config, new Providers(new FileProvider()), new ReportOptions()); 110 | 111 | $config->visitComposerAutoloadFiles($projectAnalyzer); 112 | $projectAnalyzer->check(__DIR__); 113 | 114 | // NOTE: this calls exit(1) on failed checks - currently not a problem, but it may become one 115 | IssueBuffer::finish($projectAnalyzer, true, $startTime); 116 | } 117 | 118 | public function deactivate(Composer $composer, IOInterface $io): void 119 | { 120 | // Nothing to do here, as all features are provided through event listeners 121 | } 122 | 123 | public function uninstall(Composer $composer, IOInterface $io): void 124 | { 125 | // Nothing to do here, as all features are provided through event listeners 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roave/you-are-using-it-wrong 2 | 3 | ## Archived: no longer necessary 4 | 5 | This project has been initially introduced to educate the PHP community about 6 | type checkers, their importance, and also to bother those that don't rely on them. 7 | 8 | This project won't be updated to support `vimeo/psalm:^6`, since it is both 9 | technically challenging, and no longer necessary, as type-checkers are now 10 | unequivocally part of the minimum toolkit of every professional PHP developer. 11 | 12 | Since the [first release of this package](https://github.com/Roave/you-are-using-it-wrong/releases/tag/1.0.0) 13 | in 2019, the community has grown, and we are proud of the work we've done to 14 | advocate for type-checkers. 15 | 16 | We've been noisy, annoying, cheeky about it: it has actually helped a lot! 17 | 18 | To everyone that now pushes for better coding practices through 19 | [`vimeo/psalm`](https://github.com/vimeo/psalm) and 20 | [`phpstan/phpstan`](https://github.com/phpstan/phpstan-src/): thank you ♥️. 21 | 22 | ## Overview 23 | 24 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Froave%2Fyou-are-using-it-wrong)](https://dashboard.stryker-mutator.io/reports/github.com/roave/you-are-using-it-wrong/master) 25 | [![Type Coverage](https://shepherd.dev/github/roave/you-are-using-it-wrong/coverage.svg)](https://shepherd.dev/github/roave/you-are-using-it-wrong) 26 | [![Packagist](https://img.shields.io/packagist/v/roave/you-are-using-it-wrong.svg)](https://packagist.org/packages/roave/you-are-using-it-wrong) 27 | 28 | This package enforces type checks during composer installation in downstream 29 | consumers of your package. This only applies to usages of classes, properties, 30 | methods and functions declared within packages that directly depend on 31 | *roave/you-are-using-it-wrong*. 32 | 33 | Issues that the static analyser finds that do not relate to these namespaces 34 | will not be reported. 35 | 36 | `roave/you-are-using-it-wrong` comes with a zero-configuration out-of-the-box 37 | setup. 38 | 39 | By default, it hooks into `composer install` and `composer update`, preventing 40 | a successful command execution if there are type errors in usages of protected 41 | namespaces. 42 | 43 | The usage of this plugin is highly endorsed for authors of new PHP libraries 44 | who appreciate the advantages of static types. 45 | 46 | This project is built with the hope that libraries with larger user-bases will 47 | raise awareness of type safety (or current lack thereof) in the PHP ecosystem. 48 | 49 | As annoying as it might sound, it is not uncommon for library maintainers to 50 | respond to support questions caused by lack of type checks in downstream 51 | projects. In addition to that, relying more on static types over runtime checks, 52 | it is possible to reduce code size and maintenance burden by strengthening the 53 | API boundaries of a library. 54 | 55 | ### Installation 56 | 57 | This package is designed to be installed as a dependency of PHP **libraries**. 58 | 59 | In your library, add it to your 60 | `composer.json`: 61 | 62 | ```sh 63 | composer require roave/you-are-using-it-wrong 64 | ``` 65 | 66 | No further changes are needed for this tool to start operating as per its 67 | design, if your declared types are already reflecting your library requirements. 68 | 69 | Please also note that this should **not** be used in `"require-dev"`, but 70 | specifically in `"require"` in order for the type checks to be applied to 71 | downstream consumers of your code. 72 | 73 | ### Examples 74 | 75 | You can experiment with the following example by running `cd examples && ./run-example.sh`. 76 | 77 | Given you are the author of `my/awesome-library`, which has following `composer.json`: 78 | 79 | ```json 80 | { 81 | "name": "my/awesome-library", 82 | "type": "library", 83 | "autoload": { 84 | "psr-4": { 85 | "My\\AwesomeLibrary\\": "src" 86 | } 87 | }, 88 | "require": { 89 | "roave/you-are-using-it-wrong": "^1.0.0" 90 | } 91 | } 92 | ``` 93 | 94 | Given following `my/awesome-library/src/MyHelloWorld.php`: 95 | 96 | ```php 97 | $people */ 104 | public static function sayHello(array $people) : string 105 | { 106 | return 'Hello ' . implode(', ', $people) . '!'; 107 | } 108 | } 109 | ``` 110 | 111 | Given following downstream `a/project/composer.json` project that 112 | depends on your `my/awesome-library`: 113 | 114 | ```json 115 | { 116 | "name": "a/project", 117 | "type": "project", 118 | "autoload": { 119 | "psr-4": { 120 | "The\\Project\\": "src" 121 | } 122 | }, 123 | "require": { 124 | "my/awesome-library": "^1.0.0" 125 | } 126 | } 127 | ``` 128 | 129 | And following `a/project/src/MyExample.php`: 130 | 131 | ```php 132 | 147 | - Installing roave/you-are-using-it-wrong (1.0.0): ... 148 | - Installing my/awesome-library (1.0.0): ... 149 | ... 150 | 151 | roave/you-are-using-it-wrong: checking strictly type-checked packages... 152 | Scanning files... 153 | Analyzing files... 154 | 155 | ERROR: InvalidScalarArgument - a-project/src/MyExample.php:4:48 156 | - Argument 1 of My\AwesomeLibrary\MyHelloWorld::sayhello expects array, 157 | array{0:int(123), 1:int(456)} provided 158 | echo \My\AwesomeLibrary\MyHelloWorld::sayHello([123, 456]); 159 | 160 | $ echo $? 161 | 1 162 | ``` 163 | 164 | ## Workarounds 165 | 166 | This package is designed to be quite invasive from a type-check perspective, 167 | but it will bail out of any checks if a [`psalm configuration`](https://psalm.dev/docs/configuration/) 168 | is detected in the root of the installation/project. 169 | If that is the case, the tool assumes that the author of the project is already 170 | responsible for ensuring type-safety within their own domain, and therefore 171 | bails out without performing further checks. 172 | 173 | As mentioned above, the design of the tool circles around raising awareness of 174 | static type usage in the PHP ecosystem, and therefore it will only give up if 175 | it is sure that library consumers are already taking care of the matter on their 176 | own. 177 | 178 | ## Professional Support 179 | 180 | If you need help with setting up this library in your project, you can contact 181 | us at team@roave.com for consulting/support. 182 | --------------------------------------------------------------------------------