diff --git a/.github/workflows/bare_run.yaml b/.github/workflows/bare_run.yaml
deleted file mode 100644
index c6d1775d..00000000
--- a/.github/workflows/bare_run.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Bare Run on various PHP versions
-
-on:
- push:
- branches:
- - main
-
-jobs:
- bare_run:
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- php_version: ['7.4', '8.2']
-
- steps:
- -
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php_version }}
- coverage: none
-
- - run: composer require rector/argtyper
-
- - run: vendor/bin/argtyper list --ansi
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 00000000..42781bfb
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,25 @@
+name: Build
+
+on:
+ pull_request: null
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version: 'stable'
+
+ - run: gofmt -l . | tee /tmp/gofmt.out && test ! -s /tmp/gofmt.out
+
+ - run: go vet ./...
+
+ - run: go build ./...
+
+ - run: go test ./...
diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml
deleted file mode 100644
index c4f9ee8c..00000000
--- a/.github/workflows/code_analysis.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-name: Code Analysis
-
-on:
- pull_request: null
- push:
- branches:
- - main
-
-jobs:
- code_analysis:
- strategy:
- fail-fast: false
- matrix:
- actions:
- -
- name: 'PHPStan'
- run: composer phpstan --ansi
-
- -
- name: 'Composer Validate'
- run: composer validate --ansi
-
- -
- name: 'Rector'
- run: composer rector --ansi
-
- -
- name: 'Coding Standard'
- run: composer fix-cs --ansi
-
- -
- name: 'Tests'
- run: vendor/bin/phpunit
-
- -
- name: 'PHP Linter'
- run: vendor/bin/parallel-lint src tests
-
- -
- name: 'Dependency Analysis'
- run: vendor/bin/composer-dependency-analyser
-
- -
- name: 'Check Active Classes'
- run: vendor/bin/class-leak check src bin tests --ansi --skip-path=Fixture --skip-path=Source --skip-type="Rector\\ArgTyper\\PHPStan\\IgnoreAllErrorsExceptArgTyperErrorExtension"
-
- name: ${{ matrix.actions.name }}
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- # see https://github.com/shivammathur/setup-php
- - uses: shivammathur/setup-php@v2
- with:
- php-version: 8.3
- coverage: none
-
- # composer install cache - https://github.com/ramsey/composer-install
- - uses: "ramsey/composer-install@v2"
-
- - run: ${{ matrix.actions.run }}
-
diff --git a/.github/workflows/downgraded_release.yaml b/.github/workflows/downgraded_release.yaml
deleted file mode 100644
index 3176712b..00000000
--- a/.github/workflows/downgraded_release.yaml
+++ /dev/null
@@ -1,65 +0,0 @@
-name: Downgraded Release
-
-on:
- push:
- tags:
- - '*'
-
-jobs:
- downgrade_release:
- runs-on: ubuntu-latest
-
- steps:
- - uses: "actions/checkout@v3"
- with:
- token: ${{ secrets.ACCESS_TOKEN }}
-
- -
- uses: "shivammathur/setup-php@v2"
- with:
- php-version: 8.3
- coverage: none
-
- # invoke patches
- - run: composer install --ansi
-
- # but no dev packages
- - run: composer update --no-dev --ansi
-
- # get rector to "rector-local" directory, to avoid downgrading itself in the /vendor
- - run: mkdir rector-local
- - run: composer require rector/rector --working-dir rector-local --ansi
-
- # downgrade to PHP 7.2
- - run: rector-local/vendor/bin/rector process config src bin vendor --config build/rector-downgrade-php-72.php --ansi
-
- # clear the dev files
- - run: rm -rf tests ecs.php phpstan.neon phpunit.xml .gitignore .editorconfig
-
- # prefix and scope
- - run: sh prefix-code.sh
-
- # copy PHP 7.2 composer + workflows
- - run: cp -r build/target-repository/. .
-
- # clear the dev files
- - run: rm -rf build prefix-code.sh scoper.php rector.php rector-local php-scoper.phar
-
- # setup git user
- -
- run: |
- git config user.email "action@github.com"
- git config user.name "GitHub Action"
-
- # publish to the same repository with a new tag
- # see https://tomasvotruba.com/blog/how-to-release-php-81-and-72-package-in-the-same-repository/
- -
- name: "Tag Downgraded Code"
- run: |
- # separate a "git add" to add untracked (new) files too
- git add --all
- git commit -m "release PHP 7.2 downgraded"
-
- # force push tag, so there is only 1 version
- git tag "${GITHUB_REF#refs/tags/}" --force
- git push origin "${GITHUB_REF#refs/tags/}" --force
diff --git a/.github/workflows/run_on_project.yaml b/.github/workflows/run_on_project.yaml
deleted file mode 100644
index b9c25ed2..00000000
--- a/.github/workflows/run_on_project.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: Run on Project
-
-on:
- push:
- branches:
- - main
- pull_request: null
-
-jobs:
- run_on_project:
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- php_version: ['8.4']
-
- steps:
- -
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php_version }}
- coverage: none
-
- - run: composer require rector/argtyper --ansi
-
- # clone symfony/console project with dependencies
- - run: git clone https://github.com/symfony/console.git --depth=1
- - run: composer install --working-dir console --ansi
-
- # ensure phpstan is available
- - run: composer require --working-dir console phpstan/phpstan --ansi
-
- # on whole project, to be able to load vendor/autoload.php
- - run: vendor/bin/argtyper add-types console --ansi
diff --git a/.gitignore b/.gitignore
index 4d79a352..69a61fd3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,3 @@
-# composer
-/vendor
-composer.lock
-
-# phpunit
-.phpunit.result.cache
-
-# local metadata
-debug.json
\ No newline at end of file
+# go
+/argtyper
+*.out
diff --git a/README.md b/README.md
index ad3b75c9..d48c3a50 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,29 @@
# Fill Parameter Types based on Passed Values
-There are often more known types in your project than meets the eye.
-This tool detects the **real** types passed into method and function calls using PHPStan.
+There are often more known types in your project than meets the eye.
+This tool detects the types of **literal values** passed into method, constructor and function calls, then adds them as parameter type declarations.
```php
-$this->hotelOverview->makeRoomAvailable(324);
-```
-
-
-
-Later in the code...
-
-```php
-public function roomDetail(int $roomNumber)
+final class HotelOverview
{
- $this->hotelOverview->makeRoomAvailable($roomNumber);
-}
-```
-
-
-
-Later in tests...
+ public function makeRoomAvailable($roomNumber)
+ {
+ }
-```php
-public function test(int $roomNumber): void
-{
- $this->hotelOverview->makeRoomAvailable($roomNumber);
+ public function bookLobby()
+ {
+ $this->makeRoomAvailable(324);
+ }
}
```
-✅ Three times an `int` value is passed into `makeRoomAvailable()`.
+✅ An `int` value is passed into `makeRoomAvailable()`.
-Then [Rector](https://getrector.com) runs and fills in the missing type declarations:
+The tool fills in the missing type declaration:
```diff
final class HotelOverview
@@ -51,14 +39,14 @@ Then [Rector](https://getrector.com) runs and fills in the missing type declarat
-That’s it.
+That's it.
## Install
```bash
-composer require rector/argtyper --dev
+go install github.com/rectorphp/argtyper@latest
```
@@ -68,52 +56,46 @@ composer require rector/argtyper --dev
Run it in your project directory:
```bash
-vendor/bin/argtyper add-types .
+argtyper add-types .
```
-
-
Or on another project:
```bash
-vendor/bin/argtyper add-types project
+argtyper add-types /path/to/project
```
-To see more details during the process, add the `--debug` option.
+It scans the `src`, `lib`, `app`, `test` and `tests` directories.
## How It Works
-At first, a set of custom PHPStan rules scans your code and records the argument types passed to method calls, static calls, `new` expressions, and function calls. It stores this data in temporary `*.json` files in the following format:
+It is built on [php-parser-in-go](https://github.com/rectorphp/php-parser-in-go).
-```json
-[
- {
- "class": "HotelOverview",
- "method": "makeRoomAvailable",
- "position": 0,
- "type": "PHPStan\\Type\\IntegerType"
- }
-]
-```
-
-
-
-Then, custom Rector rules go through the codebase and fill in the known parameter types based on the collected data — but only where they’re missing.
+1. It walks every call site and records the type of each **literal** argument - `int`, `float`, `string`, `bool`, `array`, `null` and `new X()` (as `object`).
+2. It groups the recorded types per parameter position.
+3. It adds the type to each definition that is still missing one.
With a few exceptions:
-* If multiple types are found → it’s skipped.
-* If union or intersection types are found → it’s skipped as ambiguous.
-* If a `float` parameter type is declared but only `int` arguments are passed (e.g. `30.0`) → it’s skipped to avoid losing decimal precision.
+* If multiple different types are found for one parameter -> it is skipped as ambiguous.
+* If a single type plus `null` is found -> a nullable type is added.
+* Parameters that already have a type are left untouched.
+* Magic methods (except `__construct`) are skipped.
+* Methods that may override a parent or interface are skipped, unless they are private or a constructor.
-## Verify the Results
+## Scope
+
+The tool relies only on the parsed syntax tree, not on full type inference, so it works with values it can resolve statically:
+
+* **Literal arguments** - `f(324)`, `f("x")`, `f([1, 2])`. Variables and expressions are skipped.
+* **Statically resolvable call targets** - `new X()`, `X::method()`, `self::method()`, `$this->method()` and plain `function()` calls. Calls on other variables (`$service->method()`) are skipped, because the class cannot be known without type inference.
+* **Short class names** - classes are matched by their short name, not the fully qualified name.
-It’s not 100 % perfect, but in our tests it fills in about **95 %** of the data correctly and saves a huge amount of manual work.
-You can fix the remaining cases manually based on PHPStan or test feedback.
+This catches the easy, unambiguous cases and leaves the rest for you to fill manually based on PHPStan or test feedback.
diff --git a/bin/argtyper b/bin/argtyper
deleted file mode 100755
index 6103acbf..00000000
--- a/bin/argtyper
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env php
-create();
-
-/** @var \Symfony\Component\Console\Application $application */
-$application = $container->make(\Symfony\Component\Console\Application::class);
-
-$resultCode = $application->run();
-exit($resultCode);
diff --git a/bin/autoload.php b/bin/autoload.php
deleted file mode 100755
index 50a2f3bd..00000000
--- a/bin/autoload.php
+++ /dev/null
@@ -1,16 +0,0 @@
-withDowngradeSets(php72: true)
- ->withSkip([__DIR__ . '/../tests']);
diff --git a/build/target-repository/.github/FUNDING.yml b/build/target-repository/.github/FUNDING.yml
deleted file mode 100644
index f797866a..00000000
--- a/build/target-repository/.github/FUNDING.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-# These are supported funding model platforms
-github: tomasvotruba
-custom: https://www.paypal.me/rectorphp
diff --git a/build/target-repository/.github/workflows/auto_closer.yaml b/build/target-repository/.github/workflows/auto_closer.yaml
deleted file mode 100644
index de9fca80..00000000
--- a/build/target-repository/.github/workflows/auto_closer.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: Auto Closer PR
-
-on:
- pull_request_target:
- types: [opened]
-
-jobs:
- run:
- runs-on: ubuntu-latest
- steps:
- - uses: superbrothers/close-pull-request@v3
- with:
- # Optional. Post a issue comment just before closing a pull request.
- comment: |
- Hi, thank you for your contribution.
-
- Unfortunately, this repository is read-only. It's a split from our main monorepo repository.
-
- We'd like to kindly ask you to move the contribution there - https://github.com/symplify/symplify.
-
- We'll check it, review it and give you feed back right way.
-
- Thank you.
diff --git a/build/target-repository/composer.json b/build/target-repository/composer.json
deleted file mode 100644
index 201b18d6..00000000
--- a/build/target-repository/composer.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "rector/argtyper",
- "description": "Analyze real method argument types, and add them as type declarations",
- "license": "proprietary",
- "require": {
- "php": ">=7.4",
- "phpstan/phpstan": "^2.0",
- "rector/rector": "^2.0"
- },
- "bin": [
- "bin/argtyper",
- "bin/argtyper.php"
- ],
- "autoload": {
- "psr-4": {
- "Rector\\ArgTyper\\": "src"
- }
- }
-}
diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php
deleted file mode 100644
index 95296d48..00000000
--- a/composer-dependency-analyser.php
+++ /dev/null
@@ -1,10 +0,0 @@
-ignoreErrorsOnPackage('nikic/php-parser', [ErrorType::SHADOW_DEPENDENCY]);
diff --git a/composer.json b/composer.json
deleted file mode 100644
index 46127fb4..00000000
--- a/composer.json
+++ /dev/null
@@ -1,65 +0,0 @@
-{
- "name": "rector/argtyper",
- "description": "Analyze real method argument types, and add them as type declarations",
- "license": "proprietary",
- "bin": [
- "bin/argtyper"
- ],
- "require": {
- "php": "^8.3",
- "illuminate/container": "12.39.*",
- "nette/utils": "^4.1",
- "phpstan/phpstan": "^2.1",
- "rector/rector": "^2.3",
- "symfony/console": "^6.4",
- "symfony/finder": "^7.4",
- "symfony/process": "^7.4",
- "webmozart/assert": "^1.12|^2.0"
- },
- "require-dev": {
- "php-parallel-lint/php-parallel-lint": "^1.4",
- "phpecs/phpecs": "^2.3",
- "phpstan/extension-installer": "^1.4",
- "phpunit/phpunit": "^11.5",
- "rector/jack": "^0.5.1",
- "shipmonk/composer-dependency-analyser": "^1.8",
- "symplify/phpstan-extensions": "^12.0",
- "tomasvotruba/class-leak": "^2.1",
- "tomasvotruba/unused-public": "^2.1",
- "tracy/tracy": "^2.11"
- },
- "autoload": {
- "psr-4": {
- "Rector\\ArgTyper\\": "src"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "Rector\\ArgTyper\\Tests\\": "tests"
- },
- "classmap": [
- "tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture"
- ],
- "files": [
- "tests/PHPStan/CollectFuncCallArgTypesRule/Source/some_function.php"
- ]
- },
- "config": {
- "sort-packages": true,
- "platform-check": false,
- "allow-plugins": {
- "phpstan/extension-installer": true
- }
- },
- "replace": {
- "symfony/polyfill-ctype": "*",
- "symfony/polyfill-intl-normalizer": "*",
- "symfony/polyfill-mbstring": "*"
- },
- "scripts": {
- "check-cs": "vendor/bin/ecs check --ansi",
- "fix-cs": "vendor/bin/ecs check --fix --ansi",
- "phpstan": "vendor/bin/phpstan analyse --ansi",
- "rector": "vendor/bin/rector process --dry-run --ansi"
- }
-}
diff --git a/config/phpstan-collecting-data.neon b/config/phpstan-collecting-data.neon
deleted file mode 100644
index cf562eac..00000000
--- a/config/phpstan-collecting-data.neon
+++ /dev/null
@@ -1,22 +0,0 @@
-parameters:
- # avoid any other rules
- customRulesetUsed: true
-
- # avoid inline-ignores to be reported
- reportUnmatchedIgnoredErrors: false
-
- excludePaths:
- - "*/Fixture/*"
- - "*/Fixtures/*"
- - "*/fixtures/*"
- - "*/fixture/*"
-
-rules:
- - Rector\ArgTyper\PHPStan\Rule\CollectFuncCallArgTypesRule
- - Rector\ArgTyper\PHPStan\Rule\CollectCallLikeArgTypesRule
-
-services:
- -
- class: Rector\ArgTyper\PHPStan\IgnoreAllErrorsExceptArgTyperErrorExtension
- tags:
- - phpstan.ignoreErrorExtension
diff --git a/config/rector-argtyper.php b/config/rector-argtyper.php
deleted file mode 100644
index 448af636..00000000
--- a/config/rector-argtyper.php
+++ /dev/null
@@ -1,25 +0,0 @@
-withSkip([
- // run only on source code
- 'test',
- 'tests',
- 'Fixture',
- 'test',
- 'Tests',
- ])
- ->withRules([
- AddFunctionParamTypeRector::class,
- AddClassMethodParamTypeRector::class,
- // AddParamIterableDocblockTypeRector::class,
- ]);
diff --git a/ecs.php b/ecs.php
deleted file mode 100644
index 97ad402f..00000000
--- a/ecs.php
+++ /dev/null
@@ -1,14 +0,0 @@
-withPaths([
- __DIR__ . '/bin',
- __DIR__ . '/src',
- __DIR__ . '/tests',
- ])
- ->withPreparedSets(symplify: true, common: true, psr12: true)
- ->withSkip(['Fixture']);
diff --git a/func-call-collected-data.json b/func-call-collected-data.json
deleted file mode 100644
index 36388696..00000000
--- a/func-call-collected-data.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":0,"type":"PHPStan\\Type\\IntegerType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":1,"type":"PHPStan\\Type\\FloatType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":0,"type":"PHPStan\\Type\\IntegerType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":1,"type":"PHPStan\\Type\\FloatType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":0,"type":"PHPStan\\Type\\IntegerType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":1,"type":"PHPStan\\Type\\FloatType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":0,"type":"PHPStan\\Type\\IntegerType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":1,"type":"PHPStan\\Type\\FloatType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":0,"type":"PHPStan\\Type\\IntegerType"}
-{"function":"Rector\\ArgTyper\\Tests\\PHPStan\\CollectFuncCallArgTypesRule\\Source\\someFunction","position":1,"type":"PHPStan\\Type\\FloatType"}
diff --git a/go.mod b/go.mod
new file mode 100644
index 00000000..ba5a6d55
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/rectorphp/argtyper
+
+go 1.26
+
+require github.com/rectorphp/php-parser-in-go v0.1.1
diff --git a/go.sum b/go.sum
new file mode 100644
index 00000000..6593ebc7
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,8 @@
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rectorphp/php-parser-in-go v0.1.1 h1:9jMbxAtDBjubUlPFKB5lbbvpXn8Otxq9eG77Q7moqwk=
+github.com/rectorphp/php-parser-in-go v0.1.1/go.mod h1:6p9QnZnLGc9uruvKuSXlVt4uSIY5Rqe6h2aj9+hEBG8=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/internal/aggregate/aggregate.go b/internal/aggregate/aggregate.go
new file mode 100644
index 00000000..647bb973
--- /dev/null
+++ b/internal/aggregate/aggregate.go
@@ -0,0 +1,113 @@
+// Package aggregate turns collected argument records into a single resolved
+// type per parameter position, applying the same skip rules as the PHP tool.
+package aggregate
+
+import (
+ "sort"
+ "strconv"
+
+ "github.com/rectorphp/argtyper/internal/collect"
+)
+
+// Resolved is the final type decision for one parameter position.
+type Resolved struct {
+ Type string // source type keyword or "object:Short", never "null"
+ Nullable bool
+}
+
+// Types holds resolved parameter types keyed for fast lookup during apply.
+type Types struct {
+ methods map[string]Resolved // class \x00 method \x00 position
+ functions map[string]Resolved // function \x00 position
+}
+
+// Method returns the resolved type for a method/constructor parameter.
+func (t Types) Method(class, method string, position int) (Resolved, bool) {
+ resolved, ok := t.methods[methodKey(class, method, position)]
+ return resolved, ok
+}
+
+// Function returns the resolved type for a function parameter.
+func (t Types) Function(name string, position int) (Resolved, bool) {
+ resolved, ok := t.functions[functionKey(name, position)]
+ return resolved, ok
+}
+
+// Resolve groups records per parameter and keeps only unambiguous ones:
+// a single type, or a single type plus null (nullable).
+func Resolve(records []collect.Record) Types {
+ methodTypes := map[string]map[string]struct{}{}
+ functionTypes := map[string]map[string]struct{}{}
+
+ for _, record := range records {
+ if record.IsFunction {
+ key := functionKey(record.Name, record.Position)
+ addType(functionTypes, key, record.Type)
+ continue
+ }
+ key := methodKey(record.Class, record.Name, record.Position)
+ addType(methodTypes, key, record.Type)
+ }
+
+ return Types{
+ methods: resolveGroups(methodTypes),
+ functions: resolveGroups(functionTypes),
+ }
+}
+
+func resolveGroups(groups map[string]map[string]struct{}) map[string]Resolved {
+ resolved := map[string]Resolved{}
+
+ for key, typeSet := range groups {
+ types := make([]string, 0, len(typeSet))
+ for typeName := range typeSet {
+ types = append(types, typeName)
+ }
+ sort.Strings(types)
+
+ switch {
+ case len(types) == 1:
+ if types[0] == "null" {
+ continue
+ }
+ resolved[key] = Resolved{Type: types[0]}
+ case len(types) == 2 && contains(types, "null"):
+ resolved[key] = Resolved{Type: without(types, "null"), Nullable: true}
+ }
+ }
+
+ return resolved
+}
+
+func addType(groups map[string]map[string]struct{}, key, typeName string) {
+ if groups[key] == nil {
+ groups[key] = map[string]struct{}{}
+ }
+ groups[key][typeName] = struct{}{}
+}
+
+func methodKey(class, method string, position int) string {
+ return class + "\x00" + method + "\x00" + strconv.Itoa(position)
+}
+
+func functionKey(name string, position int) string {
+ return name + "\x00" + strconv.Itoa(position)
+}
+
+func contains(values []string, needle string) bool {
+ for _, value := range values {
+ if value == needle {
+ return true
+ }
+ }
+ return false
+}
+
+func without(values []string, needle string) string {
+ for _, value := range values {
+ if value != needle {
+ return value
+ }
+ }
+ return ""
+}
diff --git a/internal/aggregate/aggregate_test.go b/internal/aggregate/aggregate_test.go
new file mode 100644
index 00000000..912806cb
--- /dev/null
+++ b/internal/aggregate/aggregate_test.go
@@ -0,0 +1,77 @@
+package aggregate_test
+
+import (
+ "testing"
+
+ "github.com/rectorphp/argtyper/internal/aggregate"
+ "github.com/rectorphp/argtyper/internal/collect"
+)
+
+func TestResolveMethod(t *testing.T) {
+ tests := []struct {
+ name string
+ records []collect.Record
+ wantType string
+ wantNullable bool
+ wantFound bool
+ }{
+ {
+ name: "single type",
+ records: []collect.Record{{Class: "A", Name: "m", Type: "int"}},
+ wantType: "int",
+ wantFound: true,
+ },
+ {
+ name: "type plus null is nullable",
+ records: []collect.Record{
+ {Class: "A", Name: "m", Type: "string"},
+ {Class: "A", Name: "m", Type: "null"},
+ },
+ wantType: "string",
+ wantNullable: true,
+ wantFound: true,
+ },
+ {
+ name: "two real types is ambiguous",
+ records: []collect.Record{
+ {Class: "A", Name: "m", Type: "int"},
+ {Class: "A", Name: "m", Type: "string"},
+ },
+ wantFound: false,
+ },
+ {
+ name: "only null resolves to nothing",
+ records: []collect.Record{{Class: "A", Name: "m", Type: "null"}},
+ wantFound: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ resolved, found := aggregate.Resolve(test.records).Method("A", "m", 0)
+ if found != test.wantFound {
+ t.Fatalf("found=%v want %v", found, test.wantFound)
+ }
+ if found && (resolved.Type != test.wantType || resolved.Nullable != test.wantNullable) {
+ t.Errorf("got %+v want type=%s nullable=%v", resolved, test.wantType, test.wantNullable)
+ }
+ })
+ }
+}
+
+func TestResolveFunctionAndPositionIsolation(t *testing.T) {
+ records := []collect.Record{
+ {IsFunction: true, Name: "f", Position: 0, Type: "int"},
+ {IsFunction: true, Name: "f", Position: 1, Type: "string"},
+ }
+ types := aggregate.Resolve(records)
+
+ first, ok := types.Function("f", 0)
+ if !ok || first.Type != "int" {
+ t.Errorf("position 0: got %+v ok=%v", first, ok)
+ }
+ second, ok := types.Function("f", 1)
+ if !ok || second.Type != "string" {
+ t.Errorf("position 1: got %+v ok=%v", second, ok)
+ }
+}
diff --git a/internal/apply/apply.go b/internal/apply/apply.go
new file mode 100644
index 00000000..63ba93e6
--- /dev/null
+++ b/internal/apply/apply.go
@@ -0,0 +1,151 @@
+// Package apply fills missing parameter types in function, method and
+// constructor definitions from the resolved argument types.
+package apply
+
+import (
+ "strings"
+
+ "github.com/rectorphp/argtyper/internal/aggregate"
+ "github.com/rectorphp/argtyper/internal/phpast"
+ "github.com/rectorphp/php-parser-in-go/pkg/ast"
+)
+
+// Source adds parameter types to a single PHP source file. It returns the new
+// source, the number of types added, and whether the file changed. On a parse
+// error the original source is returned unchanged.
+func Source(src []byte, types aggregate.Types) (string, int, bool) {
+ root, err := phpast.Parse(src)
+ if err != nil || root == nil {
+ return string(src), 0, false
+ }
+
+ applier := &applier{types: types}
+ applier.walk(root, nil)
+
+ if applier.added == 0 {
+ return string(src), 0, false
+ }
+
+ return phpast.Print(root), applier.added, true
+}
+
+type applier struct {
+ types aggregate.Types
+ added int
+}
+
+func (a *applier) walk(node ast.Vertex, class *ast.StmtClass) {
+ if node == nil {
+ return
+ }
+
+ switch typed := node.(type) {
+ case *ast.StmtClass:
+ class = typed
+ case *ast.StmtFunction:
+ a.applyFunction(typed)
+ case *ast.StmtClassMethod:
+ a.applyMethod(typed, class)
+ }
+
+ for _, child := range phpast.Children(node) {
+ a.walk(child, class)
+ }
+}
+
+func (a *applier) applyFunction(function *ast.StmtFunction) {
+ name := phpast.ShortName(function.Name)
+ for position, paramNode := range function.Params {
+ param, ok := paramNode.(*ast.Parameter)
+ if !ok || !typeable(param) {
+ continue
+ }
+ if resolved, ok := a.types.Function(name, position); ok {
+ a.setType(param, resolved)
+ }
+ }
+}
+
+func (a *applier) applyMethod(method *ast.StmtClassMethod, class *ast.StmtClass) {
+ name := phpast.ShortName(method.Name)
+ if isMagicExceptConstructor(name) {
+ return
+ }
+ if !overridable(method, class) {
+ return
+ }
+
+ className := ""
+ if class != nil {
+ className = phpast.ShortName(class.Name)
+ }
+
+ for position, paramNode := range method.Params {
+ param, ok := paramNode.(*ast.Parameter)
+ if !ok || !typeable(param) {
+ continue
+ }
+ if resolved, ok := a.types.Method(className, name, position); ok {
+ a.setType(param, resolved)
+ }
+ }
+}
+
+func (a *applier) setType(param *ast.Parameter, resolved aggregate.Resolved) {
+ nullable := resolved.Nullable || hasNullDefault(param)
+ typeText := typeText(resolved.Type)
+
+ if nullable {
+ param.Type = &ast.Nullable{Expr: &ast.Identifier{Value: []byte(typeText + " ")}}
+ } else {
+ param.Type = &ast.Identifier{Value: []byte(typeText + " ")}
+ }
+ a.added++
+}
+
+// typeText turns a resolved type into the text written into source. The trailing
+// space that separates the type from the variable is added by the caller.
+func typeText(resolved string) string {
+ if strings.HasPrefix(resolved, "object:") {
+ return "\\" + strings.TrimPrefix(resolved, "object:")
+ }
+ return resolved
+}
+
+// typeable reports whether a parameter can receive a type: none yet and not
+// variadic, where positions would no longer line up.
+func typeable(param *ast.Parameter) bool {
+ return param.Type == nil && param.VariadicTkn == nil
+}
+
+// overridable reports whether typing this method is safe without a reflection
+// based parent lookup: constructors and private methods never override, and a
+// class with no parent or interface cannot override either.
+func overridable(method *ast.StmtClassMethod, class *ast.StmtClass) bool {
+ if phpast.ShortName(method.Name) == "__construct" {
+ return true
+ }
+ for _, modifierNode := range method.Modifiers {
+ if modifier, ok := modifierNode.(*ast.Identifier); ok {
+ if strings.ToLower(string(modifier.Value)) == "private" {
+ return true
+ }
+ }
+ }
+ if class == nil {
+ return true
+ }
+ return class.Extends == nil && len(class.Implements) == 0
+}
+
+func isMagicExceptConstructor(name string) bool {
+ return name != "__construct" && strings.HasPrefix(name, "__")
+}
+
+func hasNullDefault(param *ast.Parameter) bool {
+ constFetch, ok := param.DefaultValue.(*ast.ExprConstFetch)
+ if !ok {
+ return false
+ }
+ return strings.ToLower(phpast.ShortName(constFetch.Const)) == "null"
+}
diff --git a/internal/apply/apply_test.go b/internal/apply/apply_test.go
new file mode 100644
index 00000000..8d1f0fd9
--- /dev/null
+++ b/internal/apply/apply_test.go
@@ -0,0 +1,119 @@
+package apply_test
+
+import (
+ "testing"
+
+ "github.com/rectorphp/argtyper/internal/aggregate"
+ "github.com/rectorphp/argtyper/internal/apply"
+ "github.com/rectorphp/argtyper/internal/collect"
+)
+
+// run collects from every source, resolves types, then applies to target.
+func run(target string, sources ...string) (string, int) {
+ var records []collect.Record
+ for _, source := range sources {
+ records = append(records, collect.FromSource([]byte(source))...)
+ }
+ types := aggregate.Resolve(records)
+ output, count, _ := apply.Source([]byte(target), types)
+ return output, count
+}
+
+func TestApply(t *testing.T) {
+ tests := []struct {
+ name string
+ target string
+ callers []string
+ want string
+ }{
+ {
+ name: "adds int to method from this call",
+ target: "room(5); }\n}",
+ want: "room(5); }\n}",
+ },
+ {
+ name: "adds string to function",
+ target: "set(1); $this->set(null); }\n}",
+ want: "set(1); $this->set(null); }\n}",
+ },
+ {
+ name: "nullable from null default keeps question mark",
+ target: "set(\"x\"); }\n}",
+ want: "set(\"x\"); }\n}",
+ },
+ {
+ name: "keeps existing type untouched",
+ target: "set(1); }\n}",
+ want: "set(1); }\n}",
+ },
+ {
+ name: "skips ambiguous multiple types",
+ target: "set(1); $this->set(\"x\"); }\n}",
+ want: "set(1); $this->set(\"x\"); }\n}",
+ },
+ {
+ name: "skips magic method",
+ target: "__get(\"x\"); }\n}",
+ want: "__get(\"x\"); }\n}",
+ },
+ {
+ name: "skips override candidate in class with interface",
+ target: "set(1); }\n}",
+ want: "set(1); }\n}",
+ },
+ {
+ name: "types private method even with parent",
+ target: "set(1); }\n}",
+ want: "set(1); }\n}",
+ },
+ {
+ name: "adds array type",
+ target: "` and `self::` calls resolve.
+func (c *collector) walk(node ast.Vertex, class string) {
+ if node == nil {
+ return
+ }
+
+ switch typed := node.(type) {
+ case *ast.StmtClass:
+ class = phpast.ShortName(typed.Name)
+ case *ast.StmtTrait:
+ class = phpast.ShortName(typed.Name)
+ case *ast.StmtEnum:
+ class = phpast.ShortName(typed.Name)
+ }
+
+ c.visit(node, class)
+
+ for _, child := range phpast.Children(node) {
+ c.walk(child, class)
+ }
+}
+
+func (c *collector) visit(node ast.Vertex, class string) {
+ switch typed := node.(type) {
+ case *ast.ExprFunctionCall:
+ name := phpast.ShortName(typed.Function)
+ if name == "" {
+ return
+ }
+ c.record(typed.Args, Record{IsFunction: true, Name: name})
+
+ case *ast.ExprNew:
+ name := phpast.ShortName(typed.Class)
+ if name == "" {
+ return
+ }
+ c.record(typed.Args, Record{Class: name, Name: "__construct"})
+
+ case *ast.ExprStaticCall:
+ method, ok := typed.Call.(*ast.Identifier)
+ if !ok {
+ return
+ }
+ target := staticClassName(typed.Class, class)
+ if target == "" {
+ return
+ }
+ c.record(typed.Args, Record{Class: target, Name: string(method.Value)})
+
+ case *ast.ExprMethodCall:
+ if !phpast.IsThisVariable(typed.Var) || class == "" {
+ return
+ }
+ method, ok := typed.Method.(*ast.Identifier)
+ if !ok {
+ return
+ }
+ c.record(typed.Args, Record{Class: class, Name: string(method.Value)})
+
+ case *ast.ExprNullsafeMethodCall:
+ if !phpast.IsThisVariable(typed.Var) || class == "" {
+ return
+ }
+ method, ok := typed.Method.(*ast.Identifier)
+ if !ok {
+ return
+ }
+ c.record(typed.Args, Record{Class: class, Name: string(method.Value)})
+ }
+}
+
+// staticClassName resolves the class of a static call: self/static map to the
+// enclosing class, parent is unresolvable, anything else is a plain name.
+func staticClassName(classNode ast.Vertex, enclosing string) string {
+ name := phpast.ShortName(classNode)
+ switch strings.ToLower(name) {
+ case "self", "static":
+ return enclosing
+ case "parent":
+ return ""
+ default:
+ return name
+ }
+}
+
+func (c *collector) record(args []ast.Vertex, base Record) {
+ for position, argNode := range args {
+ arg, ok := argNode.(*ast.Argument)
+ if !ok {
+ continue
+ }
+ // skip named and variadic arguments, positions no longer line up
+ if arg.Name != nil || arg.VariadicTkn != nil {
+ continue
+ }
+
+ typeName := literalType(arg.Expr)
+ if typeName == "" {
+ continue
+ }
+
+ record := base
+ record.Position = position
+ record.Type = typeName
+ c.records = append(c.records, record)
+ }
+}
+
+// literalType returns the type of a literal argument expression, or "" when
+// the value is not a literal we can infer without a type engine.
+func literalType(expr ast.Vertex) string {
+ switch typed := expr.(type) {
+ case *ast.ScalarLnumber:
+ return "int"
+ case *ast.ScalarDnumber:
+ return "float"
+ case *ast.ScalarString, *ast.ScalarEncapsed, *ast.ScalarHeredoc:
+ return "string"
+ case *ast.ExprArray:
+ return "array"
+ case *ast.ExprUnaryMinus:
+ return numericType(typed.Expr)
+ case *ast.ExprUnaryPlus:
+ return numericType(typed.Expr)
+ case *ast.ExprConstFetch:
+ switch strings.ToLower(phpast.ShortName(typed.Const)) {
+ case "true", "false":
+ return "bool"
+ case "null":
+ return "null"
+ }
+ return ""
+ case *ast.ExprNew:
+ name := phpast.ShortName(typed.Class)
+ if name == "" {
+ return ""
+ }
+ return "object:" + name
+ default:
+ return ""
+ }
+}
+
+func numericType(expr ast.Vertex) string {
+ switch expr.(type) {
+ case *ast.ScalarLnumber:
+ return "int"
+ case *ast.ScalarDnumber:
+ return "float"
+ default:
+ return ""
+ }
+}
diff --git a/internal/collect/collect_test.go b/internal/collect/collect_test.go
new file mode 100644
index 00000000..f3831231
--- /dev/null
+++ b/internal/collect/collect_test.go
@@ -0,0 +1,97 @@
+package collect_test
+
+import (
+ "testing"
+
+ "github.com/rectorphp/argtyper/internal/collect"
+)
+
+func TestFromSource(t *testing.T) {
+ tests := []struct {
+ name string
+ src string
+ want []collect.Record
+ }{
+ {
+ name: "this method call int",
+ src: "set(5); }\n}",
+ want: []collect.Record{{Class: "A", Name: "set", Position: 0, Type: "int"}},
+ },
+ {
+ name: "function call string",
+ src: "set(1); }\n}",
+ want: nil,
+ },
+ {
+ name: "skips named argument",
+ src: " 1 {
+ projectPath = args[1]
+ }
+
+ files, err := finder.PHPFiles(projectPath)
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("Code dirs found in %q: %v\n\n", projectPath, finder.CodeDirectories(projectPath))
+
+ // 1. collect literal argument types across the whole project
+ fmt.Println("1. Collecting argument types...")
+ var records []collect.Record
+ for _, file := range files {
+ src, err := os.ReadFile(file)
+ if err != nil {
+ return err
+ }
+ records = append(records, collect.FromSource(src)...)
+ }
+ fmt.Printf(" Found %d arg types\n\n", len(records))
+
+ types := aggregate.Resolve(records)
+
+ // 2. add the resolved types to parameter declarations
+ fmt.Println("2. Adding types to parameters...")
+ added := 0
+ for _, file := range files {
+ src, err := os.ReadFile(file)
+ if err != nil {
+ return err
+ }
+
+ output, count, changed := apply.Source(src, types)
+ if !changed {
+ continue
+ }
+
+ if err := os.WriteFile(file, []byte(output), 0o644); err != nil {
+ return err
+ }
+ added += count
+ }
+
+ if added == 0 {
+ fmt.Println(" No new types added. Is your code that good?")
+ return nil
+ }
+
+ fmt.Printf(" Finished! Added %d new types\n", added)
+ return nil
+}
diff --git a/phpstan.neon b/phpstan.neon
deleted file mode 100644
index 46df4a7c..00000000
--- a/phpstan.neon
+++ /dev/null
@@ -1,33 +0,0 @@
-parameters:
- paths:
- - bin
- - src
- - tests
- - rector.php
- - ecs.php
-
- level: 8
- errorFormat: symplify
-
- reportUnmatchedIgnoredErrors: false
-
- # make sure iterable docblock are checked with real assertions
- treatPhpDocTypesAsCertain: false
-
- ignoreErrors:
- # irelevant
- - '#Calling PHPStan\\(.*?) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version#'
- - identifier: phpstanApi.constructor
-
- - identifier: phpstanApi.instanceofType
-
- # false positive
- - '#with generic interface PHPStan\\Collectors\\Collector#'
-
- -
- identifier: class.notFound
- path: tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/AddClassMethodParamTypeRectorTest.php
-
- excludePaths:
- - "*/Fixture/*"
- - "*/Source/*"
diff --git a/phpunit.xml b/phpunit.xml
deleted file mode 100644
index 7d4b4ab4..00000000
--- a/phpunit.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- tests
- tests/PHPStan/Fixture
-
-
-
diff --git a/prefix-code.sh b/prefix-code.sh
deleted file mode 100644
index 910bca81..00000000
--- a/prefix-code.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env bash
-
-# inspired from https://github.com/rectorphp/rector/blob/main/build/build-rector-scoped.sh
-
-# see https://stackoverflow.com/questions/66644233/how-to-propagate-colors-from-bash-script-to-github-action?noredirect=1#comment117811853_66644233
-export TERM=xterm-color
-
-# show errors
-set -e
-
-# script fails if trying to access to an undefined variable
-set -u
-
-
-# functions
-note()
-{
- MESSAGE=$1;
- printf "\n";
- echo "\033[0;33m[NOTE] $MESSAGE\033[0m";
-}
-
-# ---------------------------
-
-# 2. scope it
-note "Downloading php-scoper"
-wget https://github.com/humbug/php-scoper/releases/download/0.18.18/php-scoper.phar -N --no-verbose
-
-
-note "Running php-scoper"
-
-# Work around possible PHP memory limits
-php -d memory_limit=-1 php-scoper.phar add-prefix config src bin vendor composer.json --config scoper.php --force --ansi --output-dir scoped-code
-
-# the output code is in "/scoped-code", lets move it up
-# the local directories have to be empty to move easily
-rm -r config src bin vendor composer.json
-mv scoped-code/* .
-
-note "Dumping Composer Autoload"
-composer dump-autoload --ansi --classmap-authoritative --no-dev
-
-# make bin runnable without "php"
-chmod 777 "bin/argtyper"
-chmod 777 "bin/argtyper.php"
-
-note "Finished"
diff --git a/rector.php b/rector.php
deleted file mode 100644
index 91c32eed..00000000
--- a/rector.php
+++ /dev/null
@@ -1,29 +0,0 @@
-withPaths([
- __DIR__ . '/bin',
- __DIR__ . '/src',
- __DIR__ . '/tests',
- ])
- ->withRootFiles()
- ->withPhpSets()
- ->withPreparedSets(
- deadCode: true,
- codeQuality: true,
- codingStyle: true,
- typeDeclarations: true,
- typeDeclarationDocblocks: true,
- naming: true,
- instanceOf: true,
- earlyReturn: true,
- phpunitCodeQuality: true,
- )
- ->withSkip(['*/Source', '*/Fixture',
- \Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector::class => [
- __DIR__ . '/src/Command/ArgTyperCommand.php',
- ]]);
diff --git a/scoper.php b/scoper.php
deleted file mode 100644
index 7cc0582f..00000000
--- a/scoper.php
+++ /dev/null
@@ -1,20 +0,0 @@
-format('Ym');
-
-// see https://github.com/humbug/php-scoper
-return [
- 'prefix' => 'Argtyper' . $timestamp,
- 'expose-constants' => ['#^SYMFONY\_[\p{L}_]+$#'],
- 'exclude-namespaces' => ['#^Rector#', '#^PhpParser#', '#^PHPStan#', '#^Symfony\\\\Polyfill#'],
- 'exclude-files' => [
- // do not prefix "trigger_deprecation" from symfony - https://github.com/symfony/symfony/commit/0032b2a2893d3be592d4312b7b098fb9d71aca03
- // these paths are relative to this file location, so it should be in the root directory
- 'vendor/symfony/deprecation-contracts/function.php',
- ],
-];
diff --git a/src/Command/AddTypesCommand.php b/src/Command/AddTypesCommand.php
deleted file mode 100644
index 221caad3..00000000
--- a/src/Command/AddTypesCommand.php
+++ /dev/null
@@ -1,137 +0,0 @@
-setName('add-types');
- $this->setDescription(
- 'Find all passed values and their types to your local methods/functions calls, then add them as type declarations'
- );
-
- $this->addArgument('project-path', InputArgument::OPTIONAL, 'Path to the target project root', getcwd());
-
- $this->addOption('debug', null, null, 'Enable debug output');
- }
-
- /**
- * @return Command::*
- */
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $projectPath = (string) $input->getArgument('project-path');
-
- $project = new Project($projectPath);
- $isDebug = (bool) $input->getOption('debug');
-
- $this->symfonyStyle->writeln(sprintf('Code dirs found in the "%s" project>', $projectPath));
- $this->symfonyStyle->listing($project->getCodeDirectories());
- $this->symfonyStyle->newLine(2);
-
- // 1. Run PHPStan data collection
- $this->runPhpStan($project, $isDebug);
-
- $this->symfonyStyle->newLine();
-
- // 2. Run Rector to apply types, not on tests, just source
- // Discover source dirs
- $this->runRector($project, $isDebug);
-
- $this->removeTemporaryPHPStanJsonFiles();
-
- return Command::SUCCESS;
- }
-
- private function runPhpStan(Project $project, bool $isDebug): void
- {
- $this->symfonyStyle->title('1. Running PHPStan to collect data...');
-
- // Keep paths the same as in the original script
- $commands = [
- 'vendor/bin/phpstan',
- 'analyse',
- ...$project->getCodeDirectories(),
- '--configuration',
- (string) realpath(__DIR__ . '/../../config/phpstan-collecting-data.neon'),
- '--autoload-file',
- (string) realpath(__DIR__ . '/../../bin/autoload.php'),
- ];
-
- $this->processRunner->runProcess($commands, $project->getDirectory(), $isDebug);
-
- $collectedFileItems = FilesLoader::loadJsonl(ConfigFilePath::callLikes());
- $this->symfonyStyle->success(sprintf('Finished! Found %d arg types', count($collectedFileItems)));
- }
-
- private function runRector(Project $project, bool $isDebug): void
- {
- $this->symfonyStyle->title('2. Running Rector to add types...');
-
- $command = [
- 'vendor/bin/rector',
- 'process',
- ...$project->getAbsoluteCodeDirectories(),
- '--config',
- (string) realpath(__DIR__ . '/../../config/rector-argtyper.php'),
- '--clear-cache',
- ];
-
- // show output, so we know what exactly has changed
- // we have to use getcwd() as Rector is only available in project here
- $rectorOutput = $this->processRunner->runProcess($command, getcwd(), $isDebug);
-
- $addedTypesCount = $this->resolveAddedTypesCount($rectorOutput);
-
- if ($addedTypesCount === 0) {
- $this->symfonyStyle->writeln('No new types added. Is your code that good?>');
- $this->symfonyStyle->newLine();
- return;
- }
-
- $this->symfonyStyle->success(sprintf('Finished! We have added %d new types', $addedTypesCount));
- }
-
- private function resolveAddedTypesCount(string $rectorOutput): int
- {
- // regex: match lines that start with + but not +++ or @@
- $pattern = '/^(?:\+)(?!\+\+|@@).+/m';
-
- if (preg_match_all($pattern, $rectorOutput, $matches)) {
- return count($matches[0]);
- }
-
- return 0;
- }
-
- private function removeTemporaryPHPStanJsonFiles(): void
- {
- if (file_exists(ConfigFilePath::funcCalls())) {
- unlink(ConfigFilePath::funcCalls());
- }
-
- if (file_exists(ConfigFilePath::callLikes())) {
- unlink(ConfigFilePath::callLikes());
- }
- }
-}
diff --git a/src/Configuration/CallLikeTypesConfigurationProvider.php b/src/Configuration/CallLikeTypesConfigurationProvider.php
deleted file mode 100644
index 90de24e4..00000000
--- a/src/Configuration/CallLikeTypesConfigurationProvider.php
+++ /dev/null
@@ -1,151 +0,0 @@
-
- */
- private array $classMethodTypes = [];
-
- /**
- * @return array
- */
- public function matchByPosition(ClassMethod $classMethod): array
- {
- $scope = ScopeFetcher::fetch($classMethod);
-
- $classReflection = $scope->getClassReflection();
- if (! $classReflection instanceof ClassReflection) {
- return [];
- }
-
- if ($classReflection->isAnonymous()) {
- return [];
- }
-
- $classMethodTypes = $this->provide();
-
- $className = $classReflection->getName();
- $methodName = $classMethod->name->toString();
-
- $matchingClassMethodTypes = $this->matchByClassAndMethodNames($classMethodTypes, $className, $methodName);
- Assert::allIsInstanceOf($matchingClassMethodTypes, ClassMethodType::class);
-
- $classMethodTypesByPosition = [];
- foreach ($matchingClassMethodTypes as $matchingClassMethodType) {
- $classMethodTypesByPosition[$matchingClassMethodType->getPosition()][] = $matchingClassMethodType;
- }
-
- return $classMethodTypesByPosition;
- }
-
- /**
- * @api used only in tests
- * @param ClassMethodType[] $classMethodTypes
- */
- public function seedClassMethodTypes(array $classMethodTypes): void
- {
- Assert::allIsInstanceOf($classMethodTypes, ClassMethodType::class);
-
- $this->classMethodTypes = $classMethodTypes;
- }
-
- /**
- * @return array
- */
- private function provide(): array
- {
- if ($this->classMethodTypes !== []) {
- return $this->classMethodTypes;
- }
-
- $phpstanResultsData = FilesLoader::loadJsonl(ConfigFilePath::callLikes());
-
- $dataGroupedByPositionMethodAndClassNames = [];
-
- foreach ($phpstanResultsData as $phpstanResultData) {
- $dataGroupedByPositionMethodAndClassNames[$phpstanResultData['class']][$phpstanResultData['method']][$phpstanResultData['position']][] = $phpstanResultData['type'];
- }
-
- $classMethodTypes = [];
-
- foreach ($dataGroupedByPositionMethodAndClassNames as $className => $typesByPositionByMethodNames) {
- foreach ($typesByPositionByMethodNames as $methodName => $typesByPosition) {
- foreach ($typesByPosition as $position => $types) {
- $uniqueTypes = array_unique($types);
- $uniqueTypes = array_values($uniqueTypes);
- sort($uniqueTypes);
-
- if (count($uniqueTypes) === 1) {
- // easy path, pick sole type
- $classMethodTypes[] = new ClassMethodType(
- $className,
- $methodName,
- $position,
- $uniqueTypes[0]
- );
-
- continue;
- }
-
- if (in_array(NullType::class, $uniqueTypes) && count($uniqueTypes) === 2) {
- $typesWithoutNull = array_diff($uniqueTypes, [NullType::class]);
- $typesWithoutNull = array_values($typesWithoutNull);
-
- $typeWithoutNull = $typesWithoutNull[0];
-
- $classMethodTypes[] = new ClassMethodType(
- $className,
- $methodName,
- $position,
- $typeWithoutNull,
- true
- );
-
- continue;
- }
-
- // log invalid type to improve
- FilesLoader::writeJsonl('debug.json', [
- 'skipped_types' => $uniqueTypes,
- ]);
- }
- }
- }
-
- $this->classMethodTypes = $classMethodTypes;
-
- return $classMethodTypes;
- }
-
- /**
- * @param array $classMethodTypes
- * @return array
- */
- private function matchByClassAndMethodNames(array $classMethodTypes, string $className, string $methodName): array
- {
- return array_filter(
- $classMethodTypes,
- function (ClassMethodType $classMethodType) use ($className, $methodName): bool {
- if ($classMethodType->getClass() !== $className) {
- return false;
- }
-
- return $classMethodType->getMethod() === $methodName;
- }
- );
- }
-}
diff --git a/src/Configuration/FuncCallTypesConfigurationProvider.php b/src/Configuration/FuncCallTypesConfigurationProvider.php
deleted file mode 100644
index 89652eb7..00000000
--- a/src/Configuration/FuncCallTypesConfigurationProvider.php
+++ /dev/null
@@ -1,110 +0,0 @@
-
- */
- private array $funcCallTypes = [];
-
- /**
- * @return array
- */
- public function matchByPosition(Function_ $function): array
- {
- if (! $function->namespacedName instanceof Name) {
- return [];
- }
-
- $functionName = $function->namespacedName->toString();
-
- $functionTypes = $this->provide();
-
- $matchingFunctionTypes = array_filter(
- $functionTypes,
- fn (FuncCallType $funcCallType): bool => $funcCallType->getFunction() === $functionName
- );
-
- Assert::allIsInstanceOf($matchingFunctionTypes, FuncCallType::class);
-
- $typesByPosition = [];
-
- foreach ($matchingFunctionTypes as $matchingFunctionType) {
- $typesByPosition[$matchingFunctionType->getPosition()][] = $matchingFunctionType;
- }
-
- return $typesByPosition;
- }
-
- /**
- * @param array $funcCallTypes
- * @api used only in tests
- */
- public function seedTypes(array $funcCallTypes): void
- {
- Assert::allIsInstanceOf($funcCallTypes, FuncCallType::class);
- $this->funcCallTypes = $funcCallTypes;
- }
-
- /**
- * @return array
- */
- private function provide(): array
- {
- if ($this->funcCallTypes !== []) {
- return $this->funcCallTypes;
- }
-
- $phpstanResultsData = FilesLoader::loadJsonl(ConfigFilePath::funcCalls());
-
- $dataGroupedByPositionFunctionName = [];
-
- foreach ($phpstanResultsData as $phpstanResultData) {
- $dataGroupedByPositionFunctionName[$phpstanResultData['function']][$phpstanResultData['position']][] = $phpstanResultData['type'];
- }
-
- $funcCallTypes = [];
-
- foreach ($dataGroupedByPositionFunctionName as $functionName => $typesByPosition) {
- foreach ($typesByPosition as $position => $types) {
- $uniqueTypes = array_unique($types);
-
- if (count($uniqueTypes) === 1) {
- // easy path, pick sole type
- $funcCallTypes[] = new FuncCallType($functionName, $position, $uniqueTypes[0]);
- continue;
- }
-
- if (in_array(NullType::class, $uniqueTypes) && count($uniqueTypes) === 2) {
- $typesWithoutNull = array_diff($uniqueTypes, [NullType::class]);
-
- $typeWithoutNull = $typesWithoutNull[0];
-
- $funcCallTypes[] = new FuncCallType($functionName, $position, $typeWithoutNull, true);
- continue;
- }
-
- // log invalid type to improve
- FilesLoader::writeJsonl(getcwd() . '/debug.json', [
- 'skipped_types' => $uniqueTypes,
- ]);
- }
- }
-
- $this->funcCallTypes = $funcCallTypes;
-
- return $funcCallTypes;
- }
-}
diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php
deleted file mode 100644
index f75a7a06..00000000
--- a/src/DependencyInjection/ContainerFactory.php
+++ /dev/null
@@ -1,68 +0,0 @@
-singleton(Parser::class, static function (): Parser {
- $parserFactory = new ParserFactory();
- return $parserFactory->createForHostVersion();
- });
-
- $container->singleton(
- SymfonyStyle::class,
- static function (): SymfonyStyle {
- // use null output ofr tests to avoid printing
- $consoleOutput = defined('PHPUNIT_COMPOSER_INSTALL') ? new NullOutput() : new ConsoleOutput();
- return new SymfonyStyle(new ArrayInput([]), $consoleOutput);
- }
- );
-
- $container->singleton(Application::class, function (Container $container): Application {
- /** @var AddTypesCommand $addTypesCommand */
- $addTypesCommand = $container->make(AddTypesCommand::class);
-
- $application = new Application();
- $application->add($addTypesCommand);
-
- $this->hideDefaultCommands($application);
-
- return $application;
- });
-
- return $container;
- }
-
- /**
- * @see https://tomasvotruba.com/blog/how-make-your-tool-commands-list-easy-to-read
- */
- private function hideDefaultCommands(Application $application): void
- {
- $application->get('completion')
- ->setHidden();
- $application->get('help')
- ->setHidden();
- }
-}
diff --git a/src/Enum/ConfigFilePath.php b/src/Enum/ConfigFilePath.php
deleted file mode 100644
index 238e60c6..00000000
--- a/src/Enum/ConfigFilePath.php
+++ /dev/null
@@ -1,18 +0,0 @@
- $record
- */
- public static function writeJsonl(string $filePath, array $record): void
- {
- // ensure file exists
- if (! file_exists($filePath)) {
- touch($filePath);
- }
-
- // newline is important for JSONL format
- $line = Json::encode($record) . PHP_EOL;
-
- // Append the line and lock the file to prevent race conditions
- file_put_contents($filePath, $line, FILE_APPEND | LOCK_EX);
- }
-
- /**
- * @return array>
- */
- public static function loadJsonl(string $filePath): array
- {
- // ensure file exists
- if (! file_exists($filePath)) {
- touch($filePath);
- }
-
- $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
- Assert::isArray($lines);
-
- $records = [];
- foreach ($lines as $line) {
- $records[] = Json::decode($line, true);
- }
-
- return $records;
- }
-}
diff --git a/src/Helpers/ProjectDirectoryFinder.php b/src/Helpers/ProjectDirectoryFinder.php
deleted file mode 100644
index 5171737e..00000000
--- a/src/Helpers/ProjectDirectoryFinder.php
+++ /dev/null
@@ -1,66 +0,0 @@
-findCodeDirsAbsolute($projectPath) as $absoluteDir) {
- $relativeDirs[] = substr($absoluteDir, strlen((string) realpath($projectPath)) + 1);
- }
-
- return $relativeDirs;
- }
-
- /**
- * @return string[]
- */
- public function findCodeDirsAbsolute(string $projectPath): array
- {
- $fileInfos = $this->findDirectoriesInPaths($projectPath, self::POSSIBLE_CODE_DIRECTORIES);
-
- $absoluteDirs = [];
- foreach ($fileInfos as $fileInfo) {
- $absoluteDirs[] = $fileInfo->getRealPath();
- }
-
- return $absoluteDirs;
- }
-
- /**
- * @param string[] $desiredDirectoryNames
- * @return SplFileInfo[]
- */
- private function findDirectoriesInPaths(string $projectPath, array $desiredDirectoryNames): array
- {
- Assert::allString($desiredDirectoryNames);
-
- $finder = (new Finder())
- ->in($projectPath)
- ->directories()
- ->depth('== 0')
- ->name($desiredDirectoryNames)
- ->sortByName();
-
- /** @var SplFileInfo[] $fileInfos */
- $fileInfos = iterator_to_array($finder->getIterator());
- return $fileInfos;
- }
-}
diff --git a/src/Helpers/ReflectionChecker.php b/src/Helpers/ReflectionChecker.php
deleted file mode 100644
index 1470a5e2..00000000
--- a/src/Helpers/ReflectionChecker.php
+++ /dev/null
@@ -1,46 +0,0 @@
-isInternal()) {
- return true;
- }
-
- if (self::isVendor($classReflection)) {
- return true;
- }
-
- return ! $classReflection->hasMethod($methodName);
- }
-
- public static function shouldSkipFunctionReflection(FunctionReflection $functionReflection): bool
- {
- if ($functionReflection->isInternal()->yes()) {
- return true;
- }
-
- return self::isVendor($functionReflection);
- }
-
- private static function isVendor(ClassReflection|FunctionReflection $reflection): bool
- {
- $fileName = $reflection->getFileName();
-
- // most likely internal or magic
- if ($fileName === null) {
- return true;
- }
-
- // is part of vendor? we can't change that, so skip it
- return str_contains($fileName, '/vendor');
- }
-}
diff --git a/src/PHPStan/CallLikeClassReflectionResolver.php b/src/PHPStan/CallLikeClassReflectionResolver.php
deleted file mode 100644
index 2cdc6a45..00000000
--- a/src/PHPStan/CallLikeClassReflectionResolver.php
+++ /dev/null
@@ -1,63 +0,0 @@
-resolveNewAndStaticCall($callLike);
- }
-
- $methodCallerType = $scope->getType($callLike->var);
-
- // @todo check if this can be less strict, e.g. for nullable etc.
- if (! $methodCallerType->isObject()->yes()) {
- return null;
- }
-
- // unwrap "self::" and "$this" calls
- if ($methodCallerType instanceof StaticType) {
- $methodCallerType = $methodCallerType->getStaticObjectType();
- }
-
- if ($methodCallerType instanceof ObjectType) {
- return $methodCallerType->getClassReflection();
- }
-
- return null;
- }
-
- private function resolveNewAndStaticCall(New_|StaticCall $callLike): ?ClassReflection
- {
- if (! $callLike->class instanceof Name) {
- return null;
- }
-
- $className = $callLike->class->toString();
- if (! $this->reflectionProvider->hasClass($className)) {
- return null;
- }
-
- return $this->reflectionProvider->getClass($className);
- }
-}
diff --git a/src/PHPStan/IgnoreAllErrorsExceptArgTyperErrorExtension.php b/src/PHPStan/IgnoreAllErrorsExceptArgTyperErrorExtension.php
deleted file mode 100644
index dc12a032..00000000
--- a/src/PHPStan/IgnoreAllErrorsExceptArgTyperErrorExtension.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- *
- * @see \Rector\ArgTyper\Tests\PHPStan\CollectCallLikeArgTypesRule\CollectCallLikeArgTypesRuleTest
- */
-final readonly class CollectCallLikeArgTypesRule implements Rule
-{
- private TypeMapper $typeMapper;
-
- private CallLikeClassReflectionResolver $callLikeClassReflectionResolver;
-
- public function __construct(ReflectionProvider $reflectionProvider)
- {
- $this->typeMapper = new TypeMapper();
- $this->callLikeClassReflectionResolver = new CallLikeClassReflectionResolver($reflectionProvider);
- }
-
- /**
- * @return class-string
- */
- public function getNodeType(): string
- {
- return CallLike::class;
- }
-
- /**
- * @param MethodCall|FuncCall|StaticCall|NullsafeMethodCall $node
- */
- public function processNode(Node $node, Scope $scope): array
- {
- // nothing to find here
- if ($node->isFirstClassCallable() || $node->getArgs() === []) {
- return [];
- }
-
- if ($node instanceof FuncCall) {
- return [];
- }
-
- // 1.
- if ($node instanceof New_) {
- $methodName = '__construct';
- } elseif ($node->name instanceof Identifier) {
- $methodName = $node->name->toString();
- } else {
- return [];
- }
-
- $classReflection = $this->callLikeClassReflectionResolver->resolve($node, $scope);
-
- // nothing to find here
- if (! $classReflection instanceof ClassReflection) {
- return [];
- }
-
- if (ReflectionChecker::shouldSkipClassReflection($classReflection, $methodName)) {
- return [];
- }
-
- foreach ($node->getArgs() as $key => $arg) {
- $typeString = $this->typeMapper->mapToStringIfUseful($arg, $scope);
- if (! is_string($typeString)) {
- continue;
- }
-
- FilesLoader::writeJsonl(
- ConfigFilePath::callLikes(),
- [
- 'class' => $classReflection->getName(),
- 'method' => $methodName,
- 'position' => $key,
- 'type' => $typeString,
- ]
- );
- }
-
- // comply with contract, but never used
- return [];
- }
-}
diff --git a/src/PHPStan/Rule/CollectFuncCallArgTypesRule.php b/src/PHPStan/Rule/CollectFuncCallArgTypesRule.php
deleted file mode 100644
index 4d5f61e5..00000000
--- a/src/PHPStan/Rule/CollectFuncCallArgTypesRule.php
+++ /dev/null
@@ -1,80 +0,0 @@
-
- *
- * @see \Rector\ArgTyper\Tests\PHPStan\CollectFuncCallArgTypesRule\CollectFuncCallArgTypesRuleTest
- */
-final readonly class CollectFuncCallArgTypesRule implements Rule
-{
- private TypeMapper $typeMapper;
-
- public function __construct(
- private ReflectionProvider $reflectionProvider
- ) {
- $this->typeMapper = new TypeMapper();
- }
-
- public function getNodeType(): string
- {
- return FuncCall::class;
- }
-
- /**
- * @param FuncCall $node
- */
- public function processNode(Node $node, Scope $scope): array
- {
- // nothing to find here
- if ($node->isFirstClassCallable() || $node->getArgs() === []) {
- return [];
- }
-
- if (! $node->name instanceof Name) {
- return [];
- }
-
- if (! $this->reflectionProvider->hasFunction($node->name, $scope)) {
- return [];
- }
-
- $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
- if (ReflectionChecker::shouldSkipFunctionReflection($functionReflection)) {
- return [];
- }
-
- foreach ($node->getArgs() as $key => $arg) {
- $typeString = $this->typeMapper->mapToStringIfUseful($arg, $scope);
- if (! is_string($typeString)) {
- continue;
- }
-
- FilesLoader::writeJsonl(
- ConfigFilePath::funcCalls(),
- [
- 'function' => $functionReflection->getName(),
- 'position' => $key,
- 'type' => $typeString,
- ]
- );
- }
-
- // nothing to return, just comply with contract
- return [];
- }
-}
diff --git a/src/PHPStan/TypeMapper.php b/src/PHPStan/TypeMapper.php
deleted file mode 100644
index 909fb083..00000000
--- a/src/PHPStan/TypeMapper.php
+++ /dev/null
@@ -1,98 +0,0 @@
-name instanceof Identifier) {
- return null;
- }
-
- $type = $scope->getType($arg->value);
- if ($this->shouldSkipType($type)) {
- return null;
- }
-
- if ($type instanceof TypeWithClassName) {
- return 'object:' . $type->getClassName();
- }
-
- $genericType = $this->mapConstantToGenericTypes($type);
- return $genericType::class;
- }
-
- private function mapConstantToGenericTypes(Type $type): Type
- {
- // correct to generic types
- if ($type instanceof IntegerRangeType) {
- return new IntegerType();
- }
-
- if ($type instanceof ClassStringType) {
- return new StringType();
- }
-
- // allow adding "array" type in case of passing multiple array and constant array types
- if ($type instanceof ConstantArrayType) {
- return new ArrayType(new MixedType(), new MixedType());
- }
-
- if ($type instanceof ArrayType) {
- return $type;
- }
-
- if ($type instanceof ConstantStringType) {
- return new StringType();
- }
-
- if ($type instanceof ConstantIntegerType) {
- return new IntegerType();
- }
-
- if ($type instanceof ConstantFloatType) {
- return new FloatType();
- }
-
- if ($type instanceof ConstantBooleanType) {
- return new BooleanType();
- }
-
- return $type;
- }
-
- private function shouldSkipType(Type $type): bool
- {
- // unable to move to json for now, handle later
- if ($type instanceof MixedType) {
- return true;
- }
-
- return $type instanceof UnionType || $type instanceof IntersectionType;
- }
-}
diff --git a/src/Process/ProcessRunner.php b/src/Process/ProcessRunner.php
deleted file mode 100644
index 1505badd..00000000
--- a/src/Process/ProcessRunner.php
+++ /dev/null
@@ -1,36 +0,0 @@
-setTimeout(null);
-
- if ($isDebug) {
- $this->symfonyStyle->writeln(sprintf('$ %s', $process->getCommandLine()));
- $this->symfonyStyle->newLine();
- }
-
- $process->mustRun();
- return $process->getOutput();
- }
-}
diff --git a/src/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector.php b/src/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector.php
deleted file mode 100644
index 28a79e90..00000000
--- a/src/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector.php
+++ /dev/null
@@ -1,181 +0,0 @@
-getMethods() as $classMethod) {
- if ($this->shouldSkipClassMethod($classMethod)) {
- continue;
- }
-
- $classMethodTypesByPosition = $this->callLikeTypesConfigurationProvider->matchByPosition($classMethod);
- if ($classMethodTypesByPosition === []) {
- continue;
- }
-
- if ($this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($classMethod)) {
- continue;
- }
-
- foreach ($classMethod->getParams() as $position => $param) {
- // skip as already has complex type
- if ($param->type instanceof UnionType) {
- continue;
- }
-
- if ($param->type instanceof IntersectionType) {
- continue;
- }
-
- $paramClassMethodTypes = $classMethodTypesByPosition[$position] ?? null;
- if ($paramClassMethodTypes === null) {
- continue;
- }
-
- $classMethodType = $paramClassMethodTypes[0];
-
- // nothing useful in type declarations
- if (in_array(
- $classMethodType->getType(),
- [NullType::class, ResourceType::class, NeverType::class],
- true
- )) {
- continue;
- }
-
- // a null default value implies the type must stay nullable
- $isNullable = $classMethodType->isNullable()
- || $param->type instanceof NullableType
- || $this->hasNullDefault($param);
-
- $typeNode = TypeResolver::resolveTypeNode($classMethodType->getType());
-
- if ($this->shouldSkipOverride($param, $classMethodType)) {
- continue;
- }
-
- // already has the exact scalar type and nullability, nothing to change
- if ($typeNode instanceof Identifier && $this->hasSameScalarType($param, $typeNode, $isNullable)) {
- continue;
- }
-
- if ($classMethodType->isObjectType() && ($param->type instanceof Name || ($param->type instanceof NullableType && $param->type->type instanceof Name))) {
- // skip already set object type
- continue;
- }
-
- if ($isNullable) {
- $param->type = new NullableType($typeNode);
- $hasChanged = true;
- } else {
- $param->type = $typeNode;
- $hasChanged = true;
- }
- }
- }
-
- if (! $hasChanged) {
- return null;
- }
-
- return $node;
- }
-
- private function shouldSkipClassMethod(ClassMethod $classMethod): bool
- {
- // empty params
- if ($classMethod->getParams() === []) {
- return true;
- }
-
- if ($classMethod->name->toString() === MethodName::CONSTRUCT) {
- return false;
- }
-
- return $classMethod->isMagic();
- }
-
- private function hasSameScalarType(Param $param, Identifier $identifier, bool $isNullable): bool
- {
- if (($param->type instanceof NullableType) !== $isNullable) {
- return false;
- }
-
- $rawType = $param->type instanceof NullableType ? $param->type->type : $param->type;
- if (! $rawType instanceof Identifier) {
- return false;
- }
-
- return $rawType->toString() === $identifier->toString();
- }
-
- private function hasNullDefault(Param $param): bool
- {
- if (! $param->default instanceof ConstFetch) {
- return false;
- }
-
- return $param->default->name->toLowerString() === 'null';
- }
-
- private function shouldSkipOverride(Param $param, ClassMethodType $classMethodType): bool
- {
- $rawType = $param->type instanceof NullableType ? $param->type->type : $param->type;
-
- // just to be safe
- if ($rawType instanceof Identifier && in_array($rawType->toString(), ['iterable', 'float'], true)) {
- return true;
- }
-
- // skip already set object type
- return $classMethodType->isObjectType() && $rawType instanceof Name;
- }
-}
diff --git a/src/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector.php b/src/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector.php
deleted file mode 100644
index 2899c2ac..00000000
--- a/src/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector.php
+++ /dev/null
@@ -1,127 +0,0 @@
-getMethods() as $classMethod) {
- if ($classMethod->isMagic()) {
- continue;
- }
-
- if ($classMethod->getParams() === []) {
- continue;
- }
-
- $classMethodTypesByPosition = $this->callLikeTypesConfigurationProvider->matchByPosition($classMethod);
- if ($classMethodTypesByPosition === []) {
- continue;
- }
-
- foreach ($classMethod->getParams() as $position => $param) {
- // only look for array types
- if (! $this->isParamTypeArray($param)) {
- continue;
- }
-
- $paramClassMethodTypes = $classMethodTypesByPosition[$position] ?? null;
- if ($paramClassMethodTypes === null) {
- continue;
- }
-
- $classMethodType = $paramClassMethodTypes[0];
-
- $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
-
- /** @var string $paramName */
- $paramName = $this->getName($param->var);
-
- // already known
- if ($classMethodPhpDocInfo->getParamType($paramName) instanceof ParamTagValueNode) {
- continue;
- }
-
- if (! $this->isUsefulArrayType($classMethodType)) {
- continue;
- }
-
- $typeNode = $this->docStringTypeMapper->mapToTypeNode($classMethodType->getType());
- if (! $typeNode instanceof TypeNode) {
- continue;
- }
-
- $paramTagValueNode = new ParamTagValueNode($typeNode, false, '$' . $paramName, '', false);
- $classMethodPhpDocInfo->addTagValueNode($paramTagValueNode);
-
- $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod);
- $hasChanged = true;
-
- $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod);
- }
- }
-
- if (! $hasChanged) {
- return null;
- }
-
- return $node;
- }
-
- private function isParamTypeArray(Param $param): bool
- {
- if (! $param->type instanceof Node) {
- return false;
- }
-
- return $this->isName($param->type, 'array');
- }
-
- private function isUsefulArrayType(ClassMethodType $classMethodType): bool
- {
- if (! str_starts_with($classMethodType->getType(), 'array')) {
- return false;
- }
-
- // not detailed much
- return $classMethodType->getType() !== 'array';
- }
-}
diff --git a/src/Rector/Rector/Function_/AddFunctionParamTypeRector.php b/src/Rector/Rector/Function_/AddFunctionParamTypeRector.php
deleted file mode 100644
index b58277e5..00000000
--- a/src/Rector/Rector/Function_/AddFunctionParamTypeRector.php
+++ /dev/null
@@ -1,102 +0,0 @@
-getParams() === []) {
- return null;
- }
-
- $hasChanged = false;
-
- foreach ($node->getParams() as $position => $param) {
- $functionTypesByPosition = $this->funcCallTypesConfigurationProvider->matchByPosition($node);
- if ($functionTypesByPosition === []) {
- continue;
- }
-
- $paramFunctionTypes = $functionTypesByPosition[$position] ?? null;
- if ($paramFunctionTypes === null) {
- continue;
- }
-
- if (count($paramFunctionTypes) >= 2) {
- throw new NotImplementedException('Multiple types not implemented yet');
- }
-
- $paramFunctionType = $paramFunctionTypes[0];
-
- // nothing useful
- if (in_array($paramFunctionType->getType(), [NullType::class, ResourceType::class, NeverType::class])) {
- continue;
- }
-
- // a null default value implies the type must stay nullable
- $isNullable = $paramFunctionType->isNullable() || $this->hasDefaultNull($param);
- $typeNode = TypeResolver::resolveTypeNode($paramFunctionType->getType());
-
- if ($paramFunctionType->isObjectType() && ($param->type instanceof Name || ($param->type instanceof NullableType && $param->type->type instanceof Name))) {
- // skip already set object type
- continue;
- }
-
- if ($isNullable) {
- $param->type = new NullableType($typeNode);
- $hasChanged = true;
- } else {
- $param->type = $typeNode;
- $hasChanged = true;
- }
- }
-
- if (! $hasChanged) {
- return null;
- }
-
- return $node;
- }
-
- private function hasDefaultNull(Param $param): bool
- {
- if (! $param->default instanceof ConstFetch) {
- return false;
- }
-
- return $param->default->name->toLowerString() === 'null';
- }
-}
diff --git a/src/Rector/TypeMapper/DocStringTypeMapper.php b/src/Rector/TypeMapper/DocStringTypeMapper.php
deleted file mode 100644
index 1ee3c29e..00000000
--- a/src/Rector/TypeMapper/DocStringTypeMapper.php
+++ /dev/null
@@ -1,38 +0,0 @@
-tokenize('@param ' . $typeString . '$someParam');
-
- $constExprParser = new ConstExprParser($parserConfig);
- $typeParser = new TypeParser($parserConfig, $constExprParser);
-
- $phpDocParser = new PhpDocParser($parserConfig, $typeParser, $constExprParser);
-
- $phpDocTagNode = $phpDocParser->parseTag(new TokenIterator($tokens));
- if (! $phpDocTagNode->value instanceof ParamTagValueNode) {
- return null;
- }
-
- return $phpDocTagNode->value->type;
- }
-}
diff --git a/src/Rector/TypeResolver.php b/src/Rector/TypeResolver.php
deleted file mode 100644
index f68713b6..00000000
--- a/src/Rector/TypeResolver.php
+++ /dev/null
@@ -1,51 +0,0 @@
-position;
- }
-
- public function getClass(): string
- {
- return $this->class;
- }
-
- public function getMethod(): string
- {
- return $this->method;
- }
-
- public function getType(): string
- {
- return $this->type;
- }
-
- public function isObjectType(): bool
- {
- return str_starts_with($this->type, 'object:');
- }
-
- public function isNullable(): bool
- {
- return $this->isNullable;
- }
-}
diff --git a/src/Rector/ValueObject/FuncCallType.php b/src/Rector/ValueObject/FuncCallType.php
deleted file mode 100644
index fd78dd95..00000000
--- a/src/Rector/ValueObject/FuncCallType.php
+++ /dev/null
@@ -1,41 +0,0 @@
-function;
- }
-
- public function getPosition(): int
- {
- return $this->position;
- }
-
- public function getType(): string
- {
- return $this->type;
- }
-
- public function isObjectType(): bool
- {
- return str_starts_with($this->type, 'object:');
- }
-
- public function isNullable(): bool
- {
- return $this->isNullable;
- }
-}
diff --git a/src/ValueObject/Project.php b/src/ValueObject/Project.php
deleted file mode 100644
index 54400327..00000000
--- a/src/ValueObject/Project.php
+++ /dev/null
@@ -1,50 +0,0 @@
-findCodeDirsRelative($this->directory);
- }
-
- /**
- * @return string[]
- */
- public function getAbsoluteCodeDirectories(): array
- {
- $projectDirectoryFinder = new ProjectDirectoryFinder();
- return $projectDirectoryFinder->findCodeDirsAbsolute($this->directory);
- }
-
- public function getDirectory(): string
- {
- return $this->directory;
- }
-}
diff --git a/tests/PHPStan/CollectCallLikeArgTypesRule/CollectCallLikeArgTypesRuleTest.php b/tests/PHPStan/CollectCallLikeArgTypesRule/CollectCallLikeArgTypesRuleTest.php
deleted file mode 100644
index e134b47c..00000000
--- a/tests/PHPStan/CollectCallLikeArgTypesRule/CollectCallLikeArgTypesRuleTest.php
+++ /dev/null
@@ -1,128 +0,0 @@
-
- */
-final class CollectCallLikeArgTypesRuleTest extends RuleTestCase
-{
- protected function setUp(): void
- {
- parent::setUp();
-
- // cleanup collected data file
- @unlink(ConfigFilePath::callLikes());
- }
-
- public function testMissingParent(): void
- {
- $collectedTypes = $this->collectDataInFile(__DIR__ . '/Fixture/AllowMissingParentType.php');
-
- $this->assertCount(1, $collectedTypes);
- }
-
- public function testMethodCallAndStaticCall(): void
- {
- $collectedTypes = $this->collectDataInFile(__DIR__ . '/Fixture/MethodCalledArgs.php');
- $this->assertCount(2, $collectedTypes);
-
- $this->assertSame([
- 'class' => SomeObject::class,
- 'method' => 'setName',
- 'position' => 0,
- 'type' => StringType::class,
- ], $collectedTypes[0]);
-
- $this->assertSame([
- 'class' => SomeObject::class,
- 'method' => 'setAge',
- 'position' => 0,
- 'type' => IntegerType::class,
- ], $collectedTypes[1]);
- }
-
- public function testConstructor(): void
- {
- $collectedType = $this->collectDataInFile(__DIR__ . '/Fixture/ConstructorArgs.php');
-
- $this->assertSame([
- 'class' => ObjectWithConstructor::class,
- 'method' => '__construct',
- 'position' => 0,
- 'type' => IntegerType::class,
- ], $collectedType[0]);
-
- $this->assertSame([
- 'class' => ObjectWithConstructor::class,
- 'method' => '__construct',
- 'position' => 0,
- 'type' => IntegerType::class,
- ], $collectedType[1]);
-
- $this->assertSame([
- 'class' => ObjectWithConstructor::class,
- 'method' => '__construct',
- 'position' => 0,
- 'type' => StringType::class,
- ], $collectedType[2]);
- }
-
- public function testFloatAsInt(): void
- {
- $collectedType = $this->collectDataInFile(__DIR__ . '/Fixture/FloatAsInt.php');
-
- $this->assertSame([
- 'class' => ObjectWithConstructor::class,
- 'method' => '__construct',
- 'position' => 0,
- 'type' => FloatType::class,
- ], $collectedType[0]);
-
- $this->assertSame([
- 'class' => ObjectWithConstructor::class,
- 'method' => '__construct',
- 'position' => 0,
- 'type' => FloatType::class,
- ], $collectedType[1]);
- }
-
- /**
- * @return string[]
- */
- #[\Override]
- public static function getAdditionalConfigFiles(): array
- {
- return [__DIR__ . '/../../../config/phpstan-collecting-data.neon'];
- }
-
- protected function getRule(): Rule
- {
- return self::getContainer()->getByType(CollectCallLikeArgTypesRule::class);
- }
-
- /**
- * @return array>
- */
- private function collectDataInFile(string $fixtureFilePath): array
- {
- Assert::fileExists($fixtureFilePath);
- $this->analyse([$fixtureFilePath], []);
-
- return FilesLoader::loadJsonl(ConfigFilePath::callLikes());
- }
-}
diff --git a/tests/PHPStan/CollectCallLikeArgTypesRule/Fixture/AllowMissingParentType.php b/tests/PHPStan/CollectCallLikeArgTypesRule/Fixture/AllowMissingParentType.php
deleted file mode 100644
index 95217baf..00000000
--- a/tests/PHPStan/CollectCallLikeArgTypesRule/Fixture/AllowMissingParentType.php
+++ /dev/null
@@ -1,22 +0,0 @@
-run(100);
-
- $classWithMissingParentType->parentCall(100);
- }
-
- public function runAgain(MissingClass $missingClass)
- {
- $missingClass->run(200);
- }
-}
diff --git a/tests/PHPStan/CollectCallLikeArgTypesRule/Fixture/ConstructorArgs.php b/tests/PHPStan/CollectCallLikeArgTypesRule/Fixture/ConstructorArgs.php
deleted file mode 100644
index fec94eec..00000000
--- a/tests/PHPStan/CollectCallLikeArgTypesRule/Fixture/ConstructorArgs.php
+++ /dev/null
@@ -1,19 +0,0 @@
-setName('some name');
- }
-
- public function go(): void
- {
- SomeObject::setAge(100);
- }
-}
diff --git a/tests/PHPStan/CollectCallLikeArgTypesRule/Source/ClassWithMissingParentType.php b/tests/PHPStan/CollectCallLikeArgTypesRule/Source/ClassWithMissingParentType.php
deleted file mode 100644
index 29a9ebaf..00000000
--- a/tests/PHPStan/CollectCallLikeArgTypesRule/Source/ClassWithMissingParentType.php
+++ /dev/null
@@ -1,12 +0,0 @@
-name = $name;
- }
-
- public static function setAge($age): void
- {
- self::$age = $age;
- }
-}
diff --git a/tests/PHPStan/CollectFuncCallArgTypesRule/CollectFuncCallArgTypesRuleTest.php b/tests/PHPStan/CollectFuncCallArgTypesRule/CollectFuncCallArgTypesRuleTest.php
deleted file mode 100644
index 32f4f39f..00000000
--- a/tests/PHPStan/CollectFuncCallArgTypesRule/CollectFuncCallArgTypesRuleTest.php
+++ /dev/null
@@ -1,62 +0,0 @@
-
- */
-final class CollectFuncCallArgTypesRuleTest extends RuleTestCase
-{
- public function test(): void
- {
- $collectedTypes = $this->collectDataInFile(__DIR__ . '/Fixture/SimpleFunctionCall.php');
-
- $this->assertSame([
- 'function' => 'Rector\ArgTyper\Tests\PHPStan\CollectFuncCallArgTypesRule\Source\someFunction',
- 'position' => 0,
- 'type' => IntegerType::class,
- ], $collectedTypes[0]);
-
- $this->assertSame([
- 'function' => 'Rector\ArgTyper\Tests\PHPStan\CollectFuncCallArgTypesRule\Source\someFunction',
- 'position' => 1,
- 'type' => FloatType::class,
- ], $collectedTypes[1]);
- }
-
- /**
- * @return string[]
- */
- #[\Override]
- public static function getAdditionalConfigFiles(): array
- {
- return [__DIR__ . '/../../../config/phpstan-collecting-data.neon'];
- }
-
- protected function getRule(): Rule
- {
- return self::getContainer()->getByType(CollectFuncCallArgTypesRule::class);
- }
-
- /**
- * @return array>
- */
- private function collectDataInFile(string $fixtureFilePath): array
- {
- Assert::fileExists($fixtureFilePath);
- $this->analyse([$fixtureFilePath], []);
-
- return FilesLoader::loadJsonl(ConfigFilePath::funcCalls());
- }
-}
diff --git a/tests/PHPStan/CollectFuncCallArgTypesRule/Fixture/SimpleFunctionCall.php b/tests/PHPStan/CollectFuncCallArgTypesRule/Fixture/SimpleFunctionCall.php
deleted file mode 100644
index c223e31a..00000000
--- a/tests/PHPStan/CollectFuncCallArgTypesRule/Fixture/SimpleFunctionCall.php
+++ /dev/null
@@ -1,15 +0,0 @@
-getContainer()
- ->get(CallLikeTypesConfigurationProvider::class);
-
- $classMethodTypes = [
- new ClassMethodType(SkipParentContract::class, 'checkItem', 0, IntegerType::class),
- new ClassMethodType(KeepNullableDateTimeInterface::class, 'record', 0, 'object:' . \DateTime::class),
- new ClassMethodType(KeepDateTimeInterface::class, 'record', 0, 'object:' . \DateTime::class),
- new ClassMethodType(SkipIntToFloatOverride::class, 'passInteger', 0, IntegerType::class),
- new ClassMethodType(AddNullableForDefaultNull::class, 'run', 0, StringType::class),
- new ClassMethodType(KeepNullableScalarParam::class, 'translate', 0, StringType::class),
- new ClassMethodType(AddNullableScalarFromNullDefault::class, 'translate', 0, StringType::class),
- ];
- $callLikeTypesConfigurationProvider->seedClassMethodTypes($classMethodTypes);
-
- $this->doTestFile($filePath);
- }
-
- public static function provideData(): Iterator
- {
- return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
- }
-
- public function provideConfigFilePath(): string
- {
- return __DIR__ . '/config/configured_rule.php';
- }
-}
diff --git a/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/add_nullable_for_default_null.php.inc b/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/add_nullable_for_default_null.php.inc
deleted file mode 100644
index e6a45dec..00000000
--- a/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/add_nullable_for_default_null.php.inc
+++ /dev/null
@@ -1,25 +0,0 @@
-
------
-
diff --git a/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/add_nullable_scalar_from_null_default.php.inc b/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/add_nullable_scalar_from_null_default.php.inc
deleted file mode 100644
index 691c5351..00000000
--- a/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/add_nullable_scalar_from_null_default.php.inc
+++ /dev/null
@@ -1,25 +0,0 @@
-
------
-
diff --git a/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/keep_date_time_interface.php.inc b/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/keep_date_time_interface.php.inc
deleted file mode 100644
index 4e5e0e84..00000000
--- a/tests/Rector/Rector/ClassMethod/AddClassMethodParamTypeRector/Fixture/keep_date_time_interface.php.inc
+++ /dev/null
@@ -1,10 +0,0 @@
-withRules([AddClassMethodParamTypeRector::class]);
diff --git a/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/AddParamIterableDocblockTypeRectorTest.php b/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/AddParamIterableDocblockTypeRectorTest.php
deleted file mode 100644
index 9ed63de8..00000000
--- a/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/AddParamIterableDocblockTypeRectorTest.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getContainer()
- ->get(CallLikeTypesConfigurationProvider::class);
-
- $classMethodTypes = [
- new ClassMethodType(
- 'Rector\ArgTyper\Tests\Rector\Rector\ClassMethod\AddParamIterableDocblockTypeRector\Fixture\SomeClass',
- 'run',
- 0,
- 'array'
- ),
- ];
- $callLikeTypesConfigurationProvider->seedClassMethodTypes($classMethodTypes);
-
- $this->doTestFile($filePath);
- }
-
- public static function provideData(): \Iterator
- {
- return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
- }
-
- public function provideConfigFilePath(): string
- {
- return __DIR__ . '/config/configured_rule.php';
- }
-}
diff --git a/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/Fixture/some_fixture.php.inc b/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/Fixture/some_fixture.php.inc
deleted file mode 100644
index af4ec693..00000000
--- a/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/Fixture/some_fixture.php.inc
+++ /dev/null
@@ -1,28 +0,0 @@
-
------
- $items
- */
- public function run(array $items)
- {
- }
-}
-
-?>
diff --git a/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/config/configured_rule.php b/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/config/configured_rule.php
deleted file mode 100644
index 41230837..00000000
--- a/tests/Rector/Rector/ClassMethod/AddParamIterableDocblockTypeRector/config/configured_rule.php
+++ /dev/null
@@ -1,9 +0,0 @@
-withRules([AddParamIterableDocblockTypeRector::class]);
diff --git a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/AddFunctionParamTypeRectorTest.php b/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/AddFunctionParamTypeRectorTest.php
deleted file mode 100644
index 7d57af80..00000000
--- a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/AddFunctionParamTypeRectorTest.php
+++ /dev/null
@@ -1,50 +0,0 @@
-getContainer()
- ->get(FuncCallTypesConfigurationProvider::class);
-
- $funcCallTypesConfigurationProvider->seedTypes([
- new FuncCallType(
- 'Rector\ArgTyper\Tests\Rector\Rector\Function_\AddFunctionParamTypeRector\Fixture\simpleFunction',
- 0,
- StringType::class
- ),
- new FuncCallType(
- 'Rector\ArgTyper\Tests\Rector\Rector\Function_\AddFunctionParamTypeRector\Fixture\defaultNullFunction',
- 0,
- StringType::class
- ),
- ]);
-
- $this->doTestFile($filePath);
- }
-
- public static function provideData(): \Iterator
- {
- return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
- }
-
- public function provideConfigFilePath(): string
- {
- return __DIR__ . '/config/configured_rule.php';
- }
-}
diff --git a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/Fixture/default_null_function.php.inc b/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/Fixture/default_null_function.php.inc
deleted file mode 100644
index 05f711cb..00000000
--- a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/Fixture/default_null_function.php.inc
+++ /dev/null
@@ -1,19 +0,0 @@
-
------
-
diff --git a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/Fixture/simple_function.php.inc b/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/Fixture/simple_function.php.inc
deleted file mode 100644
index f045cac7..00000000
--- a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/Fixture/simple_function.php.inc
+++ /dev/null
@@ -1,19 +0,0 @@
-
------
-
diff --git a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/config/configured_rule.php b/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/config/configured_rule.php
deleted file mode 100644
index 324e6ef4..00000000
--- a/tests/Rector/Rector/Function_/AddFunctionParamTypeRector/config/configured_rule.php
+++ /dev/null
@@ -1,9 +0,0 @@
-withRules([AddFunctionParamTypeRector::class]);
diff --git a/tests/ValueObject/Fixture/src/some_file.php b/tests/ValueObject/Fixture/src/some_file.php
deleted file mode 100644
index b3d9bbc7..00000000
--- a/tests/ValueObject/Fixture/src/some_file.php
+++ /dev/null
@@ -1 +0,0 @@
-assertSame(__DIR__ . '/Fixture', $project->getDirectory());
-
- $this->assertSame(['src', 'tests'], $project->getCodeDirectories());
- $this->assertSame(
- [__DIR__ . '/Fixture/src', __DIR__ . '/Fixture/tests'],
- $project->getAbsoluteCodeDirectories()
- );
- }
-
- public function testMessage(): void
- {
- $this->expectExceptionMessage('The path "non-existing-path" is not a directory');
-
- new Project('non-existing-path');
- }
-
- public function testMissingAutoload(): void
- {
- $this->expectExceptionMessage(
- 'Could not find "vendor/autoload.php" in the project. Make sure its dependencies are installed'
- );
-
- new Project(__DIR__);
- }
-}