Skip to content

Commit 2ea794e

Browse files
committed
optimize code, tests and AGENTS.md with fable 5
1 parent d665318 commit 2ea794e

14 files changed

Lines changed: 439 additions & 182 deletions

AGENTS.md

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,99 @@
11
# Project Context — chubbyphp-typescript
22

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.

doc/Arr.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ $a->concat(new Arr(3, 4), 5); // [1, 2, 3, 4, 5]
124124

125125
---
126126

127-
### `copyWithin(int $target, int $start, ?int $end = null): self`
127+
### `copyWithin(int $target, int $start = 0, ?int $end = null): self`
128128

129129
Shallow copies a portion of the array to another location within the same array, modifying it in place and returning it.
130130

@@ -188,7 +188,7 @@ $arr->filter(static fn ($v) => $v > 25); // [30, 40]
188188

189189
### `find(callable $callback, ?object $thisArg = null): mixed`
190190

191-
Returns the first element that passes the callback, or `null`.
191+
Returns the first element that passes the callback, or `null`. Unlike most iteration methods, `find()` does not skip holes in sparse arrays: the callback receives `null` for them.
192192

193193
```php
194194
$arr = new Arr(5, 12, 8, 130, 44);
@@ -199,7 +199,7 @@ $arr->find(static fn ($v) => $v > 10); // 12
199199

200200
### `findIndex(callable $callback, ?object $thisArg = null): int`
201201

202-
Returns the index of the first element that passes the callback, or `-1`.
202+
Returns the index of the first element that passes the callback, or `-1`. Like `find()`, it does not skip holes in sparse arrays.
203203

204204
```php
205205
$arr = new Arr(5, 12, 8, 130, 44);
@@ -210,7 +210,7 @@ $arr->findIndex(static fn ($v) => $v > 10); // 1
210210

211211
### `findLast(callable $callback, ?object $thisArg = null): mixed`
212212

213-
Returns the last element that passes the callback, or `null`.
213+
Returns the last element that passes the callback, or `null`. Like `find()`, it does not skip holes in sparse arrays.
214214

215215
```php
216216
$arr = new Arr(5, 12, 8, 130, 44);
@@ -221,7 +221,7 @@ $arr->findLast(static fn ($v) => $v > 10); // 44
221221

222222
### `findLastIndex(callable $callback, ?object $thisArg = null): int`
223223

224-
Returns the index of the last element that passes the callback, or `-1`.
224+
Returns the index of the last element that passes the callback, or `-1`. Like `find()`, it does not skip holes in sparse arrays.
225225

226226
```php
227227
$arr = new Arr(5, 12, 8, 130, 44);
@@ -294,13 +294,15 @@ $arr->indexOf('z'); // -1
294294

295295
### `join(?string $separator = null): string`
296296

297-
Joins all elements into a string separated by `$separator` (default `','`).
297+
Joins all elements into a string separated by `$separator` (default `','`). Floats are rendered like JavaScript's `String(number)` (shortest round-trip representation, `NaN`, `Infinity`, `1e+21`, ...).
298298

299299
```php
300300
$arr = new Arr('Wind', 'Rain', 'Fire');
301301
$arr->join(); // 'Wind,Rain,Fire'
302302
$arr->join(' - '); // 'Wind - Rain - Fire'
303303
$arr->join(', '); // 'Wind, Rain, Fire'
304+
305+
(new Arr(0.1, 1.0, INF))->join(); // '0.1,1,Infinity'
304306
```
305307

306308
---
@@ -478,7 +480,7 @@ $arr->toLocaleString('en-US', ['style' => 'currency', 'currency' => 'EUR']); //
478480

479481
### `toReversed(): self`
480482

481-
Returns a new array with the elements reversed (does not modify the original).
483+
Returns a new array with the elements reversed (does not modify the original). The result is never sparse: holes become explicit `null` values.
482484

483485
```php
484486
$arr = new Arr(1, 2, 3);
@@ -490,7 +492,7 @@ $arr->toReversed(); // [3, 2, 1]
490492

491493
### `toSorted(?callable $callback = null): self`
492494

493-
Returns a new array with the elements sorted (does not modify the original).
495+
Returns a new array with the elements sorted (does not modify the original). The result is never sparse: holes become explicit `null` values sorted to the end.
494496

495497
```php
496498
$arr = new Arr(3, 1, 2);
@@ -502,7 +504,7 @@ $sorted = $arr->toSorted();
502504

503505
### `toSpliced(int $start, ?int $deleteCount = null, mixed ...$items): self`
504506

505-
Returns a new array with elements removed/replaced (does not modify the original).
507+
Returns a new array with elements removed/replaced (does not modify the original). The result is never sparse: holes become explicit `null` values.
506508

507509
```php
508510
$arr = new Arr(1, 2, 3, 4);

0 commit comments

Comments
 (0)