Add PHPStan dot-notation return-type extension and harden typing/fixtures#179
Add PHPStan dot-notation return-type extension and harden typing/fixtures#179voku wants to merge 3 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
WalkthroughThe pull request adds PHPStan inference for literal dot-notation access, introduces a default-separator interface, strengthens runtime guards and type metadata, updates JSON mapper handling, and adds analysis fixtures and behavioral tests. ChangesPHPStan integration and typed access
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Tick the box to add this pull request to the merge queue (same as
|
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
src/Mapper/Json.php (1)
82-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the nested ternary for
$jsonValuesnormalization.SonarCloud flags this as a nested ternary; the logic is otherwise correct and consistent with the widened
object|iterable<array-key,mixed>param type and the newtestMapPreservesTraversableEntriescoverage.♻️ Suggested refactor
- $jsonValues = $json instanceof \Traversable - ? $json - : (\is_object($json) ? \get_object_vars($json) : $json); + if ($json instanceof \Traversable) { + $jsonValues = $json; + } elseif (\is_object($json)) { + $jsonValues = \get_object_vars($json); + } else { + $jsonValues = $json; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Mapper/Json.php` around lines 82 - 85, Refactor the $jsonValues normalization in the surrounding mapping method to remove the nested ternary while preserving its existing behavior: retain Traversable values, convert other objects with get_object_vars(), and leave non-objects unchanged before the foreach loop.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 125-133: Add matching `@property` or `@property-read` declarations to
the demonstrated User and City model classes used in README.md lines 125-133 and
build/docs/base.md lines 124-132, covering profile and nested name access shown
by $user->profile->name. Keep the existing TData examples unchanged and ensure
both documentation sites consistently declare the magic properties rather than
implying unsupported static access.
In `@src/Arrayy.php`:
- Around line 7462-7472: Update the dotted traversal guard in Arrayy::callAtPath
at src/Arrayy.php:7462-7472 to accept nested Arrayy/ArrayAccess values, allowing
read-only traversal beyond native arrays. Also update the final-offset check at
src/Arrayy.php:761-765 to use the nested Arrayy/ArrayAccess offset-existence API
instead of requiring a native array.
In `@src/PHPStan/GetDynamicMethodReturnTypeExtension.php`:
- Around line 82-86: Preserve the receiver’s concrete subclass in nested array
type inference: update the GenericObjectType construction in
GetDynamicMethodReturnTypeExtension to use the receiver class instead of
Arrayy::class, and revise the related `@phpstan-return` annotation in
Arrayy::chunk to use generic static types rather than self so get() and chunk()
retain static::create()’s concrete class.
- Around line 54-70: The traversal in the dynamic return-type logic should not
use the fallback when an offset cannot be resolved from the receiver’s static
type, because get() may read from its third array argument. Update the offset
handling in the method containing getArrayyDataType and getFallbackType so
unresolved or unavailable offsets return null, while preserving getFallbackType
only for offsets statically known to be absent.
In `@tests/ArrayyTest.php`:
- Around line 216-229: The cleanProvider fixture containing the negative key
must be conditional on PHP 8.3 or newer. Guard the `[-8 => -9, 1, 2 => false]`
case and its expected result using the project’s existing PHP-version detection
approach, while preserving the fixture on PHP 8.3+ and excluding it on PHP
8.0–8.2.
In `@tests/JsonMapperTest.php`:
- Line 17: Update the test setup that passes the json_encode result to
GetAccountsResponse::createFromJsonMapper so the string|false value is narrowed
to a string before the call. Prefer JSON_THROW_ON_ERROR or assert/validate
successful encoding, and remove the unnecessary PHPStan suppression once the
argument type is guaranteed.
In `@tests/PHPStan/AnalyseTest.php`:
- Line 111: Update the assertion in AnalyseTest around the set->find($closure)
call to verify the exact expected return value '6' using strict equality,
replacing the is_scalar check while preserving the existing fixture and closure.
---
Nitpick comments:
In `@src/Mapper/Json.php`:
- Around line 82-85: Refactor the $jsonValues normalization in the surrounding
mapping method to remove the nested ternary while preserving its existing
behavior: retain Traversable values, convert other objects with
get_object_vars(), and leave non-objects unchanged before the foreach loop.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 795562b9-710d-45c9-b633-ca9dd0bdfdcd
📒 Files selected for processing (33)
README.mdbuild/docs/base.mdphpstan.neonsrc/Arrayy.phpsrc/Collection/AbstractCollection.phpsrc/Create.phpsrc/Mapper/Json.phpsrc/PHPStan/DefaultDotNotationTypeInterface.phpsrc/PHPStan/GetDynamicMethodReturnTypeExtension.phpsrc/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.phpsrc/Type/DetectFirstValueTypeCollection.phpsrc/TypeCheck/TypeCheckCallback.phpsrc/TypeCheck/TypeCheckPhpDoc.phptests/Account.phptests/ArrayyTest.phptests/BasicArrayTest.phptests/Collection/BoolTypeTest.phptests/Collection/CollectionTest.phptests/Collection/StringTypeTest.phptests/Collection/TypeTypeTest.phptests/Collection/TypesTest.phptests/JsonMapperCoverageTest.phptests/JsonMapperTest.phptests/MetaPhpStanIntegrationTest.phptests/ModelA.phptests/PHPStan/AccessShapeProfile.phptests/PHPStan/AccessShapeUser.phptests/PHPStan/AccessWaysTest.phptests/PHPStan/AnalyseTest.phptests/PHPStan/CallableGenericInferenceTest.phptests/PHPStan/CustomSeparatorAccessUser.phptests/PHPStan/phpstan-fixtures.neontests/TypeCheckCoreCoverageTest.php
| All three access styles can therefore participate in static analysis: | ||
|
|
||
| ```php | ||
| $name = $user->get('profile.name', 'Guest'); // dot path resolved from TData | ||
| $name = $user['profile']['name']; // ArrayAccess / array-shape inference | ||
| $name = $user->profile->name; // @property-read declarations | ||
| ``` | ||
|
|
||
| Dot-notation inference intentionally applies to literal dotted paths on typed `Arrayy` subclasses with a stable array-shape `TData` that implement `Arrayy\PHPStan\DefaultDotNotationTypeInterface`. Implementing this marker is a promise that the subclass keeps Arrayy's default `.` separator and does not switch it with `changeSeparator()`; without that promise, splitting the path statically would be unsound. Dynamic strings, wildcard paths, custom separators, and plain `Arrayy` instances retain the method's safe general return type. Object-property access should be declared with `@property` or `@property-read`; `meta()` keeps generated key access precise. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C4 '`@property`(-read)?|class (User|City)' README.md build/docs/base.mdRepository: voku/Arrayy
Length of output: 14366
🌐 Web query:
PHPStan undefined property @property-read example
💡 Result:
The @property-read PHPDoc tag is used in PHPStan to declare "magic" properties that are available for reading but not writing, typically when a class implements the get magic method [1][2][3]. If you are encountering an "Access to an undefined property" error despite using @property-read, it is usually due to one of the following reasons: 1. Improper Placement: The @property-read tag must be placed in the PHPDoc block immediately preceding the class or trait definition [2][3]. It cannot be used to declare "abstract properties" on interfaces or classes in a way that enforces implementation by subclasses [4][5]. 2. PHP 8.2+ Dynamic Property Restrictions: Since PHP 8.2, native support for dynamic properties has been restricted. When you type-hint an interface that uses @property-read, PHPStan may report an "Access to an undefined property" error because PHP interfaces do not natively support property declarations [6][4]. To resolve this, you can use the @phpstan-require-extends tag on the interface to reference a base class that supports dynamic properties [6][4]. 3. Missing Type Information: PHPStan requires a type definition for @property-read tags (e.g., / @property-read string $name */) [7]. Omitting the type will cause a parsing error [7]. Recommended Solutions: - Verify Placement: Ensure the tag is above the class/trait [1][2]. - Use @phpstan-require-extends: If using interfaces in PHP 8.2+, add @phpstan-require-extends [6][4]. - Use Proper Syntax: Ensure the tag follows the format @property-read [Type] [name] [1][2]. - Alternatives: For complex scenarios, consider using a custom class reflection extension or configuring universalObjectCratesClasses in your phpstan.neon file [6][8][9]. If the code is static, refactoring to use a standard getter method (e.g., getFieldName) is the most robust approach [5].
Citations:
- 1: https://phpstan.org/writing-php-code/phpdocs-basics
- 2: https://docs.phpdoc.org/guide/references/phpdoc/tags/property.html
- 3: https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc-tags.md
- 4: [PHP^8.2] Access to an undefined property when type hinting interface with
@propertyphpstan/phpstan#10302 - 5: Access to an undefined property with @property in PHPDoc isn't working phpstan/phpstan#10200
- 6: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property
- 7: property-read annotation without type can't be parsed phpstan/phpstan#2187
- 8: https://phpstan.org/error-identifiers/property.notFound
- 9: Access to an undefined property if __set / __get exists phpstan/phpstan#3593
🌐 Web query:
"Access to an undefined property" PHPStan array-shape without @property-read``
💡 Result:
The "Access to an undefined property" error when working with PHPStan object-shapes (often referred to as array-shapes in PHPStan notation) typically occurs when you attempt to access a property that is not explicitly defined in the type definition [1][2]. If you are encountering this error even for optional properties (e.g., object{prop?: string}), ensure you are using the latest version of PHPStan [3][4]. Recent updates (particularly in the 2.1.x development branch) have significantly improved how PHPStan handles optional properties in object shapes to reduce false positives [3][5][4]. If the error persists or is intentional, you have several ways to address it: 1. Use Null Coalescing Operator: If the property is optional, accessing it directly may trigger an error. Using the null coalescing operator (e.g., $object->prop ?? 'default') is the idiomatic way to handle potentially missing properties safely [6][4]. 2. Suppress the Error: If you are certain the property will exist at runtime, you can ignore the specific error using its identifier, property.notFound [2]. This can be done inline: // @phpstan-ignore property.notFound $value = $object->prop; Or globally in your phpstan.neon configuration: parameters: ignoreErrors: - identifier: property.notFound 3. Narrow the Type: Ensure that the variable is correctly typed as the object-shape before access [1]. If the variable is typed as a broader object or mixed, PHPStan cannot guarantee the property exists [1]. 4. Check Configuration: Ensure that strict dynamic property checking rules are not set too aggressively if your codebase relies on dynamic access, though this is less common for fixed object-shapes [6]. For most scenarios involving optional object-shape properties, upgrading your PHPStan version is the most effective solution, as the maintainers have addressed several issues where valid optional property accesses were incorrectly flagged as undefined [3][5][4].
Citations:
- 1: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property
- 2: https://phpstan.org/error-identifiers/property.notFound
- 3: Optional array shape property doesn't change to required after is_array phpstan/phpstan#11610
- 4: Optional keys in nested object shapes ($A->B->C ?? []) phpstan/phpstan#10379
- 5: Not null Array shape property compared to null should report an error phpstan/phpstan#11609
- 6: Nullsafe with optional object shape report undefined property phpstan/phpstan#11424
Declare the magic properties used by the PHPStan examples.
The examples in README.md and build/docs/base.md read $user->profile and $user->profile->name, but the shown User and City classes declare only @template T array shapes and no @property/@property-read tags. Add matching declarations to the demonstrated models, or state that property-style access is runtime-only in this context.
📍 Affects 2 files
README.md#L125-L133(this comment)build/docs/base.md#L124-L132
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 125 - 133, Add matching `@property` or `@property-read`
declarations to the demonstrated User and City model classes used in README.md
lines 125-133 and build/docs/base.md lines 124-132, covering profile and nested
name access shown by $user->profile->name. Keep the existing TData examples
unchanged and ensure both documentation sites consistently declare the magic
properties rather than implying unsupported static access.
| if ($explodedPath !== []) { | ||
| if (!\is_array($currentOffset[$nextPath])) { | ||
| return; | ||
| } | ||
|
|
||
| $nestedOffset = &$currentOffset[$nextPath]; | ||
| $this->callAtPath( | ||
| \implode($this->pathSeparator, $explodedPath), | ||
| $callable, | ||
| $currentOffset[$nextPath] | ||
| $nestedOffset | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restore nested Arrayy support for dotted ArrayAccess.
get() supports ArrayAccess intermediates, but these guards only accept arrays. Consequently, $user['city.name'] returns null when city is an Arrayy subclass because offsetGet() first calls offsetExists().
src/Arrayy.php#L7462-L7472: allow the read-only dotted traversal to continue through nestedArrayy/ArrayAccessvalues.src/Arrayy.php#L761-L765: test finalArrayyoffsets with its offset-existence API instead of requiring a native array.
📍 Affects 1 file
src/Arrayy.php#L7462-L7472(this comment)src/Arrayy.php#L761-L765
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Arrayy.php` around lines 7462 - 7472, Update the dotted traversal guard
in Arrayy::callAtPath at src/Arrayy.php:7462-7472 to accept nested
Arrayy/ArrayAccess values, allowing read-only traversal beyond native arrays.
Also update the final-offset check at src/Arrayy.php:761-765 to use the nested
Arrayy/ArrayAccess offset-existence API instead of requiring a native array.
| $dataType = $this->getArrayyDataType($scope->getType($methodCall->var)); | ||
| if ($dataType === null || !$dataType->isConstantArray()->yes()) { | ||
| return null; | ||
| } | ||
|
|
||
| $valueType = $dataType; | ||
| foreach (\explode('.', $paths[0]->getValue()) as $segment) { | ||
| $valueType = TypeCombinator::removeNull($valueType); | ||
| $nestedDataType = $this->getArrayyDataType($valueType); | ||
| if ($nestedDataType !== null) { | ||
| $valueType = $nestedDataType; | ||
| } | ||
|
|
||
| $offsetType = new ConstantStringType($segment); | ||
| if (!$valueType->isOffsetAccessible()->yes() || $valueType->hasOffsetValueType($offsetType)->no()) { | ||
| return $this->getFallbackType($methodCall, $scope); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C3 'function get\(' src/Arrayy.php
rg -n -P -C3 --glob '*.php' '\->get\([^,\n]+,\s*[^,\n]+,\s*' tests srcRepository: voku/Arrayy
Length of output: 1507
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate relevant files =="
fd -a 'GetDynamicMethodReturnTypeExtension\.php|Arrayy\.php|phpstan|tests' . | sed 's#^\./##' | head -200
echo
echo "== Static return extension file =="
file="$(fd 'GetDynamicMethodReturnTypeExtension\.php' . | head -1 || true)"
if [ -n "$file" ]; then
wc -l "$file"
cat -n "$file"
fi
echo
echo "== Arrayy get signature and body =="
wc -l src/Arrayy.php
sed -n '3000,3110p' src/Arrayy.php | cat -n
for off in $(seq -20 20); do
line=$((3028 + off))
if [ $line -ge 1 ]; then
sed -n "${line},${line}p" src/Arrayy.php | cat -n
fi
done
echo
echo "== get calls with 3+ arguments in PHP files =="
python3 - <<'PY'
import subprocess, re, pathlib
files = subprocess.check_output(['git','ls-files','*.php'], text=True).splitlines()
for p in files:
text = pathlib.Path(p).read_text()
for i,line in enumerate(text.splitlines(),1):
if re.search(r'->get\([^,\n]+,\s*[^,\n]+,\s*', line):
print(f"{p}:{i}:{line.strip()}")
PYRepository: voku/Arrayy
Length of output: 11255
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Arrayy get remainder =="
sed -n '3100,3175p' src/Arrayy.php | cat -n
echo
echo "== PHPStan config and fixtures =="
cat -n phpstan.neon
cat -n tests/PHPStan/phpstan-fixtures.neon
echo
echo "== Static array implementations =="
sed -n '1,320p' src/StaticArrayy.php | cat -nRepository: voku/Arrayy
Length of output: 8652
Do not treat unavailable static offsets as a missing path.
This extension always infers from the receiver’s TData, even though get() can read a third $array argument. If that third argument carries a different shape, unknown offsets resolve incorrectly to the fallback. Return null for unresolved traversal so PHPStan uses the normal get() return type, and reserve the fallback for offsets statically known to be absent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/PHPStan/GetDynamicMethodReturnTypeExtension.php` around lines 54 - 70,
The traversal in the dynamic return-type logic should not use the fallback when
an offset cannot be resolved from the receiver’s static type, because get() may
read from its third array argument. Update the offset handling in the method
containing getArrayyDataType and getFallbackType so unresolved or unavailable
offsets return null, while preserving getFallbackType only for offsets
statically known to be absent.
| if ($valueType->isArray()->yes()) { | ||
| $valueType = new GenericObjectType( | ||
| Arrayy::class, | ||
| [$valueType->getIterableKeyType(), $valueType->getIterableValueType(), $valueType] | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C3 'GenericObjectType\(|`@phpstan-return` self<int' \
src/PHPStan/GetDynamicMethodReturnTypeExtension.php src/Arrayy.phpRepository: voku/Arrayy
Length of output: 1267
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant PHPStan extension and Arrayy chunk declarations/usages without executing repo code.
printf '--- Extension outline ---\n'
ast-grep outline src/PHPStan/GetDynamicMethodReturnTypeExtension.php --view expanded || true
printf '\n--- Extension relevant lines ---\n'
cat -n src/PHPStan/GetDynamicMethodReturnTypeExtension.php | sed -n '1,130p'
printf '\n--- Arrayy chunk declaration ---\n'
cat -n src/Arrayy.php | sed -n '1320,1365p'
printf '\n--- Chunk implementations/usages references ---\n'
rg -n -C2 'function chunk|chunk\(|static::create\(|new static|extends Arrayy|subclasses|Arrayy' src -g '*.php' | head -n 260Repository: voku/Arrayy
Length of output: 240
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant PHPStan extension and Arrayy chunk declarations/usages without executing repo code.
printf '%s\n' '--- Extension outline ---'
ast-grep outline src/PHPStan/GetDynamicMethodReturnTypeExtension.php --view expanded || true
printf '%s\n' ''
printf '%s\n' '--- Extension relevant lines ---'
cat -n src/PHPStan/GetDynamicMethodReturnTypeExtension.php | sed -n '1,130p'
printf '%s\n' ''
printf '%s\n' '--- Arrayy chunk declaration ---'
cat -n src/Arrayy.php | sed -n '1320,1365p'
printf '%s\n' ''
printf '%s\n' '--- Chunk implementations/usages references ---'
rg -n -C2 'function chunk|chunk\(|static::create\(|new static|extends Arrayy|subclasses|Arrayy' src -g '*.php' | head -n 260Repository: voku/Arrayy
Length of output: 23846
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Locate get method and create helper ---'
rg -n -C3 "function get\b|function create\(|function createFromObjectVars|static::create|new static\(" src/Arrayy.php src -g '*.php'
printf '%s\n' ''
printf '%s\n' '--- Arrayy class declaration section ---'
grep -n '^/\*\*$\|class Arrayy|template\|`@template`\|function get(' src/Arrayy.php | sed -n '1,80p'
printf '%s\n' ''
printf '%s\n' '--- chunk body ---'
cat -n src/Arrayy.php | sed -n '1351,1395p'Repository: voku/Arrayy
Length of output: 50367
Preserve static::create() as the PHPStan class for nested arrays.
get() and chunk() both rebuild array values with static::create(), so PHPStan should keep the concrete subclass for those nested wrappers instead of widening them to Arrayy::* / self. Use the receiver’s concrete type for the inferred new GenericObjectType(...) and replace @phpstan-return self<int,... with generic @phpstan-return static<int,static<...,...>,...>.
📍 Affects 2 files
src/PHPStan/GetDynamicMethodReturnTypeExtension.php#L82-L86(this comment)src/Arrayy.php#L1345-L1349
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/PHPStan/GetDynamicMethodReturnTypeExtension.php` around lines 82 - 86,
Preserve the receiver’s concrete subclass in nested array type inference: update
the GenericObjectType construction in GetDynamicMethodReturnTypeExtension to use
the receiver class instead of Arrayy::class, and revise the related
`@phpstan-return` annotation in Arrayy::chunk to use generic static types rather
than self so get() and chunk() retain static::create()’s concrete class.
| * @return array<mixed> | ||
| */ | ||
| public function cleanProvider(): array | ||
| { | ||
| // breaking-change from PHP8 | ||
| // -> Implement the negative_array_index RFC: https://github.com/php/php-src/commit/6732028273b109cb342387ab5580c367f629d0ac | ||
| if (\PHP_VERSION_ID >= 80000) { | ||
| return [ | ||
| [[], []], | ||
| [[null, false], []], | ||
| [[0 => true], [0 => true]], | ||
| [[0 => -9, 0], [0 => -9]], | ||
| [[-8 => -9, 1, 2 => false], [-8 => -9, -7 => 1]], | ||
| [[0 => 1.18, 1 => false], [0 => 1.18]], | ||
| [['foo' => false, 'foo', 'lall'], ['foo', 'lall']], | ||
| ]; | ||
| } | ||
|
|
||
| return [ | ||
| [[], []], | ||
| [[null, false], []], | ||
| [[0 => true], [0 => true]], | ||
| [[0 => -9, 0], [0 => -9]], | ||
| [[-8 => -9, 1, 2 => false], [-8 => -9, 0 => 1]], | ||
| [[-8 => -9, 1, 2 => false], [-8 => -9, -7 => 1]], | ||
| [[0 => 1.18, 1 => false], [0 => 1.18]], | ||
| [['foo' => false, 'foo', 'lall'], ['foo', 'lall']], | ||
| ]; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check the project's declared minimum PHP version and CI test matrix.
fd -HI composer.json --max-depth 1 --exec cat {}
fd -HI '*.yml' .github/workflows --exec grep -n -i 'php-version\|php:' {}Repository: voku/Arrayy
Length of output: 1467
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "composer requirement:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path('composer.json')
if p.exists():
data=json.loads(p.read_text())
print(data.get('require',{}).get('php'))
PY
echo
echo "workflow files:"
find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print
echo
echo "PHP version configuration in workflows:"
if [ -d .github/workflows ]; then
grep -RIn -E 'php-version|php:' .github/workflows || true
fi
echo
echo "tests/ArrayyTest.php relevant section:"
nl -ba tests/ArrayyTest.php | sed -n '200,235p'Repository: voku/Arrayy
Length of output: 547
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "composer requirement:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path('composer.json')
if p.exists():
data=json.loads(p.read_text())
print(data.get('require',{}).get('php'))
PY
echo
echo "PHP version configuration in workflows:"
grep -RIn -E 'php-version|php:' .github/workflows || true
echo
echo "tests/ArrayyTest.php relevant section:"
sed -n '200,235p' tests/ArrayyTest.php | cat -nRepository: voku/Arrayy
Length of output: 1685
Add a PHP 8.3 guard around the negative-key autopoint fixture.
The minimum supported runtime is PHP >=8.0.0, and CI still includes 8.0, 8.1, and 8.2; before PHP 8.3, [-8 => -9, 1, 2 => false] still auto-indexes the next numeric key to 0, so this expects the PHP 8.3 negative-array-index behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/ArrayyTest.php` around lines 216 - 229, The cleanProvider fixture
containing the negative key must be conditional on PHP 8.3 or newer. Guard the
`[-8 => -9, 1, 2 => false]` case and its expected result using the project’s
existing PHP-version detection approach, while preserving the fixture on PHP
8.3+ and excluding it on PHP 8.0–8.2.
| $found = false; | ||
|
|
||
| GetAccountsResponse::createFromJsonMapper($json) | ||
| GetAccountsResponse::createFromJsonMapper($json) // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | rg '(^|/)JsonMapperTest\.php$|JsonMapper|JsonMapper.php|GetAccountsResponse' || true
echo "== tests/JsonMapperTest.php relevant lines =="
if [ -f tests/JsonMapperTest.php ]; then
nl -ba tests/JsonMapperTest.php | sed -n '1,80p'
fi
echo "== search createFromJsonMapper definitions/usages =="
rg -n "function createFromJsonMapper|createFromJsonMapper\(" -S . || trueRepository: voku/Arrayy
Length of output: 341
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== local codebase references to JsonMapper =="
rg -n "JsonMapper|json_encode|JSON_THROW_ON_ERROR|PHPStan|phpstan-ignore-line" -S . || true
echo "== git diff stat/name status =="
git diff --stat || true
git diff -- tests/JsonMapperTest.php | sed -n '1,160p' || trueRepository: voku/Arrayy
Length of output: 50367
🌐 Web query:
PHP json_encode return type string|false documentation
💡 Result:
The official PHP documentation defines the return type of the json_encode function as string|false [1][2]. Specifically, the function returns a JSON-encoded string on success, or false on failure [1][3]. This return type signature is accurately reflected in modern PHP documentation, which also notes that the function accepts a mixed value, along with optional int flags and depth parameters [1][4]. If the JSON_THROW_ON_ERROR flag is used, the function will throw a JsonException on failure rather than returning false [3].
Citations:
- 1: https://www.php.net/function.json-encode.php
- 2: https://www.php.net/manual/en/function.json-encode.php
- 3: https://mirror.math.princeton.edu/pub/php/manual/en/function.json-encode.php
- 4: https://php.watch/codex/json_encode
Narrow the json_encode() result before calling the mapper.
json_encode() returns string|false, while createFromJsonMapper() accepts only string. Narrow the value before the call, e.g. with a null-check/assert, or use JSON_THROW_ON_ERROR so failure cannot pass a boolean into the mapper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/JsonMapperTest.php` at line 17, Update the test setup that passes the
json_encode result to GetAccountsResponse::createFromJsonMapper so the
string|false value is narrowed to a string before the call. Prefer
JSON_THROW_ON_ERROR or assert/validate successful encoding, and remove the
unnecessary PHPStan suppression once the argument type is guaranteed.
| }; | ||
| \PHPStan\Testing\assertType('bool|float|int|string', $set->find($closure)); | ||
| static::assertTrue(is_scalar($set->find($closure))); | ||
| static::assertTrue(is_scalar($set->find($closure))); // @phpstan-ignore-line function.alreadyNarrowedType, staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
echo "Tracked test file:"
git ls-files tests/PHPStan/AnalyseTest.php || true
echo
echo "Relevant section:"
if [ -f tests/PHPStan/AnalyseTest.php ]; then
nl -ba tests/PHPStan/AnalyseTest.php | sed -n '80,130p'
fi
echo
echo "Search for find method and related fixture names:"
rg -n "function find|function createSet|class .*Set|find\\(|is_scalar|phpstan-ignore-line" tests/PHPStan tests src . --glob '!vendor/**' --glob '!node_modules/**' | head -200 || trueRepository: voku/Arrayy
Length of output: 43474
🏁 Script executed:
#!/bin/bash
set -eu
echo "Locate PHPStan test setup files:"
fd -a 'AnalyseTest|Setup|Rule|Provider|fixtures' tests . | sed 's#^\./##' | head -200
echo
echo "Repository files under tests/PHPStan:"
git ls-files tests/PHPStan | head -200Repository: voku/Arrayy
Length of output: 943
🏁 Script executed:
#!/bin/bash
set -eu
echo "AnalyseTest.php relevant lines:"
sed -n '90,120p' tests/PHPStan/AnalyseTest.php
echo
echo "PHPStan fixture references around static->find usage:"
rg -n "static::|new Arrayy|create\\(|find\\(|phpstan-ignore-line" tests/PHPStan/*.php tests/PHPStan/*.neon src/Arrayy.php | sed -n '1,220p' || true
echo
echo "find() implementation and docs:"
sed -n '2746,2795p' src/Arrayy.php
echo
echo "PHP is_scalar behavior probe:"
php -r 'foreach ([false, true, 1, "6", null, [], new stdClass()] as $v) { echo var_export($v, true) . " => " . (is_scalar($v) ? "scalar" : "not scalar") . "\n"; }'Repository: voku/Arrayy
Length of output: 28627
Assert the value returned by find(), not merely that it is scalar.
find() can return false when no match exists, and false is scalar, so this assertion can pass even if the method fails to return '6'. Assert static::assertSame('6', $set->find($closure)) instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/PHPStan/AnalyseTest.php` at line 111, Update the assertion in
AnalyseTest around the set->find($closure) call to verify the exact expected
return value '6' using strict equality, replacing the is_scalar check while
preserving the existing fixture and closure.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php`:
- Around line 99-103: Update the reflection setup for invoking
GetDynamicMethodReturnTypeExtension::getFallbackType() by enabling access on the
ReflectionMethod with setAccessible(true) before calling invoke, while
preserving the existing assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 150ea41d-c82d-4961-837d-293fc5a9120e
📒 Files selected for processing (3)
CHANGELOG.mdtests/InfrastructureCoverageTest.phptests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/ArrayyTest.php (1)
4471-4478: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAlign scalar-intermediate
offsetUnset()behavior across dot-notation handling.
offsetUnset('user.name')replaces scalaruserwithnull, butremove('user.name')stops at the scalar and leaves the root value unchanged. Unset semantics are clearly destructive (unset()for top-level keys too), so this is likely unintended for the scalar-intermediate path; either preserve the scalar here likeremove()does, or keep the test/API consistent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/ArrayyTest.php` around lines 4471 - 4478, Align offsetUnset() with remove() for dot-notation paths containing a scalar intermediate: update the scalar-intermediate handling so offsetUnset('user.name') preserves the existing scalar instead of replacing it with null, and adjust the test around offsetUnset/offsetExists to assert the consistent result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/ArrayyTest.php`:
- Around line 4471-4478: Align offsetUnset() with remove() for dot-notation
paths containing a scalar intermediate: update the scalar-intermediate handling
so offsetUnset('user.name') preserves the existing scalar instead of replacing
it with null, and adjust the test around offsetUnset/offsetExists to assert the
consistent result.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 202639de-1605-4ea0-b793-456d9fe7fa5b
📒 Files selected for processing (2)
tests/ArrayyTest.phptests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php
|




Motivation
Arrayy::get()dot-notation paths andmeta()-derived keys, and to avoid unsound inferences when a subclass changes the path separator.Description
Arrayy\PHPStan\GetDynamicMethodReturnTypeExtensionthat resolves literal dot-notation paths against a typed subclass'sTDataarray-shape, and a marker interfaceArrayy\PHPStan\DefaultDotNotationTypeInterfaceto opt-in subclasses that promise to keep the default.separator.MetaDynamicStaticMethodReturnTypeExtensionservice registration and repositoryphpstan.neonto register both extensions and adds atests/PHPStan/phpstan-fixtures.neonused by fixture-based PHPStan checks; documents the extensions inREADME.mdand built docs.extractValue), improvedinternalRemovedot-path handling (float->int cast and safer traversal), adjustedcallAtPathto guard for non-array intermediates, and numerous@phpstan-ignore-lineannotations acrosssrc/to silence provably-intentional generic/typing mismatches while keeping runtime behavior unchanged.tests/PHPStan/, new fixtures likeAccessShapeUser,AccessShapeProfile,CallableGenericInferenceTest, and updates to existing tests to add narrower phpdoc generics and selective@phpstan-ignore-linecomments).Testing
phpunitagainst the modified codebase and the test changes, and the suite passed.phpstan --configuration=tests/PHPStan/phpstan-fixtures.neonto validate the new dynamic extensions and their intended inferences, and the analysis completed successfully.tests/PHPStan/that exercise dot-notation resolution, callable generic inference, and JSON-mapper behaviors, and all new tests passed under the CI test run.Codex Task
This change is
Summary by CodeRabbit
get()dot-notation with brokered dynamic return-type resolution.