diff --git a/bridge/rector/FEATURE_PARITY.md b/bridge/rector/FEATURE_PARITY.md index ee441d48..88c4389e 100644 --- a/bridge/rector/FEATURE_PARITY.md +++ b/bridge/rector/FEATURE_PARITY.md @@ -150,6 +150,9 @@ convert as a documented 🟡 (`RepeatRetryRector` / `RepeatRetryToTestoRector`). PHPUnit's `markTestIncomplete` moved off this list — it now converts to a Skipped throw with an `Incomplete: ` reason prefix (`MarkTestIncompleteRector`), a documented lossy 🟡 rather than a ⛔. +PHPUnit assertion methods require a resolved PHPUnit Assert/TestCase declaring class. Custom or +unresolved methods remain manual. + Also done: **declarative skip** — Testo's `#[Skip]` attribute (`testo/skip`) splits the skip rows in two. A skip that states something about the test rather than about a path through it now converts as an attribute in every direction: `SkipAttributeToPhpUnitRector` unrolls it into the leading diff --git a/bridge/rector/README.md b/bridge/rector/README.md index 2062365a..4ee9f4e4 100644 --- a/bridge/rector/README.md +++ b/bridge/rector/README.md @@ -66,6 +66,11 @@ memory-leak / retry / repeat, Pest higher-order & `arch()` tests, etc.) are **not silently dropped**: each is a documented stub rule plus an entry in the direction's `TODO.md`. +### PHPUnit assertion ownership + +Assertion methods must resolve to a declaration on PHPUnit's `Assert` or `TestCase`; keep PHPUnit +resolvable while migrating. An unrelated, overridden, magic or unresolved method is left untouched. + ### Argument order (important) Testo's comparison assertions are `(actual, expected)`; PHPUnit's are `(expected, actual)`. diff --git a/bridge/rector/src/Internal/PhpunitAssertionCall.php b/bridge/rector/src/Internal/PhpunitAssertionCall.php index 8e3eaf99..3bfc6d5b 100644 --- a/bridge/rector/src/Internal/PhpunitAssertionCall.php +++ b/bridge/rector/src/Internal/PhpunitAssertionCall.php @@ -18,6 +18,8 @@ * * Only a call inside a class counts, which is where a test method or a `static` data provider * lives; a call in a free function or at namespace level is left alone. + * Method calls must resolve to PHPUnit's Assert/TestCase declaration. An unresolved receiver, + * magic method, trait requirement, or a user declaration with the same name stays untouched. * * @internal */ @@ -50,7 +52,22 @@ public function name(Node $node): ?string return null; } - return $name; + if ($node instanceof FuncCall) { + return $name; + } + + $scope = $node->getAttribute(AttributeKey::SCOPE); + \assert($scope instanceof Scope); + $class = $scope->getClassReflection(); + if ($class === null || !$class->hasNativeMethod($name)) { + return null; + } + + $method = $class->getNativeMethod($name); + return \in_array($method->getDeclaringClass()->getName(), [ + 'PHPUnit\\Framework\\Assert', + 'PHPUnit\\Framework\\TestCase', + ], true) ? $method->getName() : null; } private function functionName(FuncCall $call): ?string diff --git a/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/assert_contains.php.inc b/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/assert_contains.php.inc index cfe05540..2ac54765 100644 --- a/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/assert_contains.php.inc +++ b/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/assert_contains.php.inc @@ -1,6 +1,6 @@ assertSame(1, 2); } +} + +class CustomFlatStaticAssertions +{ + public static function assertTrue($actual): string { return 'custom'; } + public function run(): string { return self::assertTrue(false); } +} + +class MagicFlatAssertions +{ + public function __call(string $name, array $arguments): string { return 'magic'; } + public function run(): string { return $this->assertSame(1, 2); } +} + +class UnresolvedFlatAssertions extends \MissingTesting\TestCase +{ + public function run(): void { $this->assertSame(1, 2); } +} diff --git a/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/fail.php.inc b/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/fail.php.inc index 58159e48..59d4db70 100644 --- a/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/fail.php.inc +++ b/bridge/rector/src/PhpunitToTesto/AssertCallToTestoRector/fail.php.inc @@ -1,6 +1,6 @@ assertIsString(123); + $this->assertIsString(123); + self::assertStringContainsString('x', 123); + return static::assertStringContainsString('x', 123); + } +} + +class InheritedUserAssertions extends UserAssertions +{ + public function run($receiver): string + { + return $this->assertIsString(123); + } +} + +class OverriddenUserAssertions extends UserAssertions +{ + public function assertIsString($actual): string { return 'overridden'; } + + public function run($receiver): string + { + return $this->assertIsString(123); + } +} + +class MagicAssertions +{ + public function __call(string $name, array $arguments): string { return 'magic'; } + public function run(): string { return $this->assertIsString(123); } +} + +class UnresolvedTypedAssertions extends \MissingTesting\TestCase +{ + public function run(): void { $this->assertIsString(123); } +} + +trait RequiredUserAssertions +{ + abstract public function assertIsString($actual): mixed; + public function run(): mixed { return $this->assertIsString(123); } +} diff --git a/bridge/rector/src/PhpunitToTesto/TypedAssertCallToTestoRector/empty_array_subject.php.inc b/bridge/rector/src/PhpunitToTesto/TypedAssertCallToTestoRector/empty_array_subject.php.inc index d601150e..7420df1e 100644 --- a/bridge/rector/src/PhpunitToTesto/TypedAssertCallToTestoRector/empty_array_subject.php.inc +++ b/bridge/rector/src/PhpunitToTesto/TypedAssertCallToTestoRector/empty_array_subject.php.inc @@ -1,6 +1,6 @@ phpstanConfig(__DIR__ . '/Stub/phpstan.neon'); + /** * Test suites for the Rector bridge. * diff --git a/skills/testo-migrate-from-phpunit/references/phpunit-to-testo-map.md b/skills/testo-migrate-from-phpunit/references/phpunit-to-testo-map.md index 25f43595..b1122ad5 100644 --- a/skills/testo-migrate-from-phpunit/references/phpunit-to-testo-map.md +++ b/skills/testo-migrate-from-phpunit/references/phpunit-to-testo-map.md @@ -7,6 +7,10 @@ row by hand. When in doubt about an attribute, read its class in the installed ` Testo is similar in spirit to PHPUnit but **not source-compatible**. Never run a blind regex pass — the assertion **argument order flips** (see the pitfalls), and discovery is attribute-based. +Rector converts assertion methods only when their declaring class resolves to PHPUnit's `Assert` +or `TestCase`. Keep PHPUnit available during migration; custom, overridden, magic and unresolved +methods remain untouched. + ## Which rows Rector handles | Handled automatically by the `phpunit-to-testo` Rector set | Needs AI/human work (no faithful rule) |