|
1 | 1 | # Project Context — chubbyphp-typescript |
2 | 2 |
|
3 | | -PHP port of JavaScript's `Array` API with TypeScript-style generics. Keep behavior close to JavaScript `Array`; the test suite is the spec. |
4 | | - |
5 | | -## Where To Work |
6 | | -- `src/Arr.php` is the main implementation. |
7 | | -- `tests/Unit/ArrTest.php` is the main spec and is organized by JS API section order. |
8 | | -- `tests/Integration/DocumentationExamplesTest.php` verifies the PHP examples in `README.md` and `doc/Arr.md`. |
9 | | -- Update `doc/Arr.md` and `README.md` when public behavior changes. |
10 | | - |
11 | | -## Important Conventions |
12 | | -- Keep `declare(strict_types=1);`, typed properties, `final` classes, and the detailed PHPDoc generics/callable signatures in `Arr`. |
13 | | -- Method ordering rule for `Arr.php`: (1) magic methods `__*`; (2) interface methods, in the order listed in `implements`; (3) the rest sorted by MDN JS `Array` order (statics first, then instance methods); (4) non-JS methods at the end. Mirror that order in `tests/Unit/ArrTest.php`, `doc/Arr.md`, and `tests/Integration/DocumentationExamplesTest.php`. |
14 | | -- `thisArg` support is intentional: bind only non-static `Closure` callables; other callables ignore it. |
15 | | - |
16 | | -## Important Behavior |
17 | | -- Preserve sparse-array semantics. `length` may be greater than the number of populated indexes. |
18 | | -- Holes read back as `null` through `at()`, `values()`, `toArray()`, and `jsonSerialize()`, but `offsetExists()`/`isset()` must still distinguish between missing indexes and explicit `null` values. |
19 | | -- Favor JavaScript `Array` semantics over idiomatic PHP shortcuts. |
20 | | - |
21 | | -## Verification |
22 | | -- Use Composer scripts as the default workflow: `composer test:unit`, `composer test:integration`, `composer test:static-analysis`, `composer test:cs`, or `composer test`. |
23 | | -- PHPUnit runs in random order, so avoid hidden test coupling. |
24 | | -- Keep every PHP example in `README.md` and `doc/Arr.md` executable and in sync with `tests/Integration/DocumentationExamplesTest.php`; add one integration test per documented example block. |
| 3 | +PHP (^8.3) port of JavaScript's `Array` API with TypeScript-style generics. |
| 4 | + |
| 5 | +**Golden rule:** behave exactly like JavaScript `Array`, not like idiomatic PHP. |
| 6 | +When unsure what JS does, do not guess — run it: `node` is available. |
| 7 | + |
| 8 | +```bash |
| 9 | +node -e 'console.log([1, , 3].findIndex(x => x === undefined))' # prints 1 |
| 10 | +``` |
| 11 | + |
| 12 | +## File Map |
| 13 | + |
| 14 | +| Path | Purpose | |
| 15 | +|---|---| |
| 16 | +| `src/Arr.php` | The whole implementation (one class) | |
| 17 | +| `src/RangeError.php`, `src/NumberFormatError.php` | Project exception classes | |
| 18 | +| `tests/Unit/ArrTest.php` | Main spec, organized in JS API order (see ordering rule) | |
| 19 | +| `tests/Unit/Test262*Test.php` | Ports of the official test262 suite, one file per `Array` method | |
| 20 | +| `tests/Integration/DocumentationExamplesTest.php` | Mirrors every PHP example block in `README.md` / `doc/Arr.md` | |
| 21 | +| `tests/Stub/*.php` | Shared test fixtures | |
| 22 | +| `doc/Arr.md` | API documentation with runnable examples | |
| 23 | + |
| 24 | +## Commands (run from repo root) |
| 25 | + |
| 26 | +| Command | What it checks | |
| 27 | +|---|---| |
| 28 | +| `composer test` | Everything below, in order — **must exit 0 before you are done** | |
| 29 | +| `composer test:lint` | `php -l` on all files | |
| 30 | +| `composer test:static-analysis` | PHPStan level 9 (analyzes `src/` only, not tests) | |
| 31 | +| `composer test:cs` | php-cs-fixer dry-run (covers `src/` AND `tests/`) | |
| 32 | +| `composer test:unit` | PHPUnit Unit suite with coverage | |
| 33 | +| `composer test:integration` | PHPUnit Integration suite | |
| 34 | +| `composer test:infection` | Mutation testing, fails below 90% MSI (needs `test:unit` coverage first) | |
| 35 | +| `composer fix:cs` | Auto-fix code style | |
| 36 | + |
| 37 | +Quality gates to preserve: |
| 38 | +- 100% line and method coverage of `src/Arr.php` (check the coverage summary `composer test:unit` prints). |
| 39 | +- Mutation score ≥ 90%. Kill mutants with **exact** assertions: assert exact strings/values, |
| 40 | + test boundary values (`0`, `-1`, `length`, `length - 1`, `21`, `-6`), and test negative numbers, |
| 41 | + empty arrays, and sparse arrays for every new branch. |
| 42 | + |
| 43 | +## Hard Conventions |
| 44 | + |
| 45 | +- Keep `declare(strict_types=1);`, `final` classes, typed properties, and the detailed |
| 46 | + PHPDoc generics/callable signatures (`@template T`, `callable(null|T, int, self<T>): bool`, ...). |
| 47 | +- Method ordering in `Arr.php`: (1) magic methods `__*`; (2) interface methods in the order |
| 48 | + listed in `implements`; (3) JS API methods in MDN order (statics first, then instance methods); |
| 49 | + (4) non-JS helpers (`toArray()`, private helpers) at the end. |
| 50 | + Mirror the same order in `tests/Unit/ArrTest.php`, `doc/Arr.md`, and |
| 51 | + `tests/Integration/DocumentationExamplesTest.php`. |
| 52 | +- `thisArg` support: bind only non-static `Closure` callables via `Closure::bindTo`; |
| 53 | + every other callable type silently ignores `$thisArg`. |
| 54 | + |
| 55 | +## JavaScript Semantics Cheat Sheet (read before touching `src/Arr.php`) |
| 56 | + |
| 57 | +- PHP `null` plays the role of JS `undefined` everywhere. |
| 58 | +- **Sparse arrays:** `length` can exceed the number of stored indexes ("holes"). |
| 59 | + Reading a hole returns `null`, but `offsetExists()`/`isset()` returns `false` for holes |
| 60 | + and `true` for explicit `null` values. Never collapse that distinction. |
| 61 | +- **Methods that SKIP holes** (callback not invoked / index not matched): |
| 62 | + `every`, `some`, `filter`, `forEach`, `map` (keeps holes in result), `reduce`, `reduceRight`, |
| 63 | + `flat`, `indexOf`, `lastIndexOf`. |
| 64 | +- **Methods that DO NOT skip holes** (hole is treated as `null`): |
| 65 | + `find`, `findIndex`, `findLast`, `findLastIndex`, `includes`, `join`, `keys`, `entries`, `values`, `fill`, `at`. |
| 66 | +- **Always-dense results:** `toReversed`, `toSorted`, `toSpliced`, `with` never return sparse |
| 67 | + arrays — holes materialize as explicit `null`s. |
| 68 | +- Iteration methods capture `$len = $this->internalLength` **before** the loop |
| 69 | + (JS uses the initial length even if the callback mutates the array). |
| 70 | +- `$this->internalArray` keys are in **insertion order, not index order** |
| 71 | + (`$a[2] = ...; $a[0] = ...;`). When order matters, never `foreach` over it directly; |
| 72 | + loop `for ($i = 0; $i < $len; ++$i)` and check `\array_key_exists($i, ...)`. |
| 73 | +- Floats stringify via `numberToString()` which implements ECMAScript `Number::toString` |
| 74 | + (shortest round-trip): `0.1` → `"0.1"`, `1.0` → `"1"`, `-0.0` → `"0"`, `INF` → `"Infinity"`, |
| 75 | + `NAN` → `"NaN"`, `1e21` → `"1e+21"`, `1e-7` → `"1e-7"`. Do not use `sprintf('%g')` or `(string)` casts. |
| 76 | +- `sort()` moves explicit `null`s (JS `undefined`) to the end without calling the comparator; |
| 77 | + holes end up after those (length stays, trailing indexes unset). |
| 78 | +- Out-of-range / non-numeric `ArrayAccess` offsets become string "properties" stored separately |
| 79 | + (they never affect `length`), mirroring JS property access; offsets that cannot be stringified throw. |
| 80 | + |
| 81 | +## Testing Rules |
| 82 | + |
| 83 | +- The test suite is the spec. PHPUnit runs tests in **random order** — no hidden coupling between tests. |
| 84 | +- `tests/Unit/Test262*Test.php`: each test method names the original test262 file in its PHPDoc. |
| 85 | + Non-portable test262 cases are recorded as two comment lines: |
| 86 | + `// SKIPPED: test/built-ins/Array/...js` followed by `// Reason: ...`. |
| 87 | + Adapted cases explain the adaptation in a comment. Keep that style when adding or changing tests. |
| 88 | +- If you change public behavior: update `tests/Unit/ArrTest.php`, the matching `Test262*Test.php`, |
| 89 | + `doc/Arr.md`, and `README.md` together. |
| 90 | +- Every PHP example block in `README.md` and `doc/Arr.md` must stay executable and have exactly |
| 91 | + one matching test in `tests/Integration/DocumentationExamplesTest.php` (`testDoc<Method>Example`). |
| 92 | +- PHPStan does not analyze `tests/`, but php-cs-fixer does — run `composer fix:cs` after writing tests. |
| 93 | + |
| 94 | +## Done Checklist |
| 95 | + |
| 96 | +1. JS behavior verified against `node` (not assumed). |
| 97 | +2. `composer test` exits 0 (includes 90% MSI and code style). |
| 98 | +3. Coverage still 100% for `src/Arr.php`. |
| 99 | +4. Docs + integration examples updated if public behavior changed. |
0 commit comments