You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(bridge-rector): decompose Assert::iterable() chains and allInstanceOf() into PHPUnit
The iterable head becomes assertIsIterable and shares the iterable
matchers with the array head: contains/notContains, allInstanceOf as
assertContainsOnlyInstancesOf, allOf over the exact native types as
assertContainsOnly*, and hasCount/sameSizeAs, which on the iterable head
convert only for countable sides since PHPUnit throws for a Generator.
Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: bridge/rector/FEATURE_PARITY.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,7 +15,7 @@ Conversion coverage across the three directions supported by `testo/bridge-recto
15
15
|---|:---:|:---:|:---:|
16
16
|**Basic assertions** (same/equals/true/false/null/count/contains/instanceOf/fail) | ✅ *`AssertCallToPhpUnitRector`; only inside a class — `$this->assert*` in instance scope, `self::assert*` where `$this` is unavailable (static helper/data provider); a call in a free function or at namespace level is left untouched (no valid `$this`/`self::` target)*| ✅ *`AssertCallToTestoRector`; restores arg order; `assertNotTrue`/`assertNotFalse`→`Assert::notSame($x, true|false)`; the `PHPUnit\Framework\assert*()` functions convert too; also inside a class only (a test method / static data provider), a call elsewhere is left untouched*| 🟡 *(`expect()->toX()`, 8 matchers, single only)*|
17
17
|**actual/expected argument swap**| ✅ | ✅ | ✅ |
18
-
| **Fluent / typed chains** (`Assert::string()->…`, Pest `->not->`, `toBeGreaterThan`) | 🟡 *`TypedAssertChainRector` decomposes into separate `assert*` lines (incl. 1→N `hasKeys`, `between`, `isList`→`assertIsList`, `sameElementsAs`→`assertEqualsCanonicalizing`); a non-variable subject is hoisted into a `$value` local so it is evaluated once; only inside a class (the emitted `$this->assert*` needs a method scope); JSON path/structure, `every`, custom matchers are left untouched + TODO* | 🟡 *`TypedAssertCallToTestoRector` converts the assertions whose faithful Testo form is a typed head + matcher: comparisons (`assertGreaterThan`→`Assert::numeric()->greaterThan()`, …), array keys (`assertArrayHasKey`→`Assert::array()->hasKeys()`), `assertEqualsCanonicalizing`→`sameElementsAs`, array-subject `assertEmpty`/`assertNotEmpty`→`blank`/`notBlank` (a non-object subject → `Assert::true(empty($x))`), `assertStringStartsWith`/`EndsWith`→`Assert::string()->startsWith()`/`endsWith()`, `assertStringContainsString`/`NotContainsString`→`Assert::string()->contains()`/`notContains()`, `assertNotContains`→`Assert::iterable()->notContains()`, `assertObjectHasProperty`→`Assert::object()->hasProperty()`, `assertIsList`→`Assert::array()->isList()`, `assertContainsOnlyInt`/`String`/…→`Assert::iterable()->allOf('int')` and `assertContainsOnlyInstancesOf`→`allInstanceOf()`, `assertSameSize`→`Assert::iterable()->sameSizeAs()` (`Countable` sides), `assertJson`→`Assert::json()` (JSON equality stays: PHPUnit compares canonical re-encoded JSON), `assertIs<Type>`→the type head, and the predicate checks (`assertIsBool`, `assertIsCallable`, `assertIsNot*`, `assertFileExists`, `assertDirectoryExists`, …)→`Assert::true|false(\is_bool($x))`, also the file/directory permission checks (`\file_exists($f) && !\is_readable($f)`, side-effect-free path), `assertFinite`/`Infinite`/`Nan` (`int|float` subject), the resource checks over `\gettype()` (a closed resource counts), `assertNotInstanceOf`→`Assert::false($x instanceof Foo)` and `assertObjectNotHasProperty`→`Assert::false(\property_exists($o, $p))` (object subject). Each is a 1:1 statement rewrite; `MergeAssertChainRector` then folds adjacent same-head chains. General coalescing of arbitrary independent `assert*` lines remains impractical* | 🧩 *function host now exists; `ExpectToAssertRector` still leaves negated `->not->` and chained `->toX()->toY()` expectations untouched — mappable next* |
18
+
| **Fluent / typed chains** (`Assert::string()->…`, Pest `->not->`, `toBeGreaterThan`) | 🟡 *`TypedAssertChainRector` decomposes into separate `assert*` lines (incl. 1→N `hasKeys`, `between`, `isList`→`assertIsList`, `sameElementsAs`→`assertEqualsCanonicalizing`, the `iterable` head→`assertIsIterable`, `allInstanceOf`→`assertContainsOnlyInstancesOf`, `allOf('int')`→`assertContainsOnlyInt` for the exact native types, `sameSizeAs`→`assertSameSize` on countable sides); a non-variable subject is hoisted into a `$value` local so it is evaluated once; only inside a class (the emitted `$this->assert*` needs a method scope); JSON path/structure, `every`, custom matchers are left untouched + TODO* | 🟡 *`TypedAssertCallToTestoRector` converts the assertions whose faithful Testo form is a typed head + matcher: comparisons (`assertGreaterThan`→`Assert::numeric()->greaterThan()`, …), array keys (`assertArrayHasKey`→`Assert::array()->hasKeys()`), `assertEqualsCanonicalizing`→`sameElementsAs`, array-subject `assertEmpty`/`assertNotEmpty`→`blank`/`notBlank` (a non-object subject → `Assert::true(empty($x))`), `assertStringStartsWith`/`EndsWith`→`Assert::string()->startsWith()`/`endsWith()`, `assertStringContainsString`/`NotContainsString`→`Assert::string()->contains()`/`notContains()`, `assertNotContains`→`Assert::iterable()->notContains()`, `assertObjectHasProperty`→`Assert::object()->hasProperty()`, `assertIsList`→`Assert::array()->isList()`, `assertContainsOnlyInt`/`String`/…→`Assert::iterable()->allOf('int')` and `assertContainsOnlyInstancesOf`→`allInstanceOf()`, `assertSameSize`→`Assert::iterable()->sameSizeAs()` (`Countable` sides), `assertJson`→`Assert::json()` (JSON equality stays: PHPUnit compares canonical re-encoded JSON), `assertIs<Type>`→the type head, and the predicate checks (`assertIsBool`, `assertIsCallable`, `assertIsNot*`, `assertFileExists`, `assertDirectoryExists`, …)→`Assert::true|false(\is_bool($x))`, also the file/directory permission checks (`\file_exists($f) && !\is_readable($f)`, side-effect-free path), `assertFinite`/`Infinite`/`Nan` (`int|float` subject), the resource checks over `\gettype()` (a closed resource counts), `assertNotInstanceOf`→`Assert::false($x instanceof Foo)` and `assertObjectNotHasProperty`→`Assert::false(\property_exists($o, $p))` (object subject). Each is a 1:1 statement rewrite; `MergeAssertChainRector` then folds adjacent same-head chains. General coalescing of arbitrary independent `assert*` lines remains impractical* | 🧩 *function host now exists; `ExpectToAssertRector` still leaves negated `->not->` and chained `->toX()->toY()` expectations untouched — mappable next* |
19
19
|**Exception expectation (bare)**| ✅ *bare `\Testo\Expect::exception($c)` → `$this->expectException($c)` (`ExpectExceptionToPhpUnitRector`); the attribute form `#[\Testo\Assert\ExpectException($c)]` → prepended `$this->expectException($c)` (`ExpectExceptionAttributeToPhpUnitRector`)*| 🟡 | ✅ *`TestCallToFunctionRector` folds `->throws(X::class)` into a prepended `\Testo\Expect::exception(X)` + `never` return type*|
20
20
|**Exception message/code (fluent)**`withMessage/withCode` ↔ `expectExceptionMessage/Code`| ✅ *`ExpectExceptionToPhpUnitRector` expands one chain into several statements (`withMessageContaining`→`expectExceptionMessage`, `withCode`→`expectExceptionCode`, regex `withMessagePattern`→`expectExceptionMessageMatches`); the exact `withMessage` also maps to `expectExceptionMessage` and loosens into a substring check; two modifiers on the same PHPUnit call abort the chain*| ✅ *`ExpectExceptionToTestoRector` folds the sibling `expectExceptionMessage/Code` after `expectException` into the `->withMessageContaining()/->withCode()` chain (StmtsAware), across assignments that cannot throw — PHPUnit matches the message as a substring; any other statement ends the run*| 🟡 *`->throws(X, 'msg')`'s second arg folds to `->withMessageContaining('msg')`; Pest has no exception-code modifier to map*|
21
21
|**Exception message by regex** (`expectExceptionMessageMatches`) | ✅ *`withMessagePattern`→`expectExceptionMessageMatches`*| ✅ *`ExpectExceptionToTestoRector` folds it into `->withMessagePattern()`*| ➖ |
0 commit comments