├── 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 | [](https://dashboard.stryker-mutator.io/reports/github.com/roave/you-are-using-it-wrong/master)
25 | [](https://shepherd.dev/github/roave/you-are-using-it-wrong)
26 | [](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 |
--------------------------------------------------------------------------------