Skip to content

Add PHPStan dot-notation return-type extension and harden typing/fixtures#179

Open
voku wants to merge 3 commits into
masterfrom
codex/integriere-phpstan-annotations-957xnh
Open

Add PHPStan dot-notation return-type extension and harden typing/fixtures#179
voku wants to merge 3 commits into
masterfrom
codex/integriere-phpstan-annotations-957xnh

Conversation

@voku

@voku voku commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Improve static-analysis support by teaching PHPStan about Arrayy::get() dot-notation paths and meta()-derived keys, and to avoid unsound inferences when a subclass changes the path separator.
  • Make the runtime code and test-suite tolerable to stricter static checks while preserving the existing dynamic runtime API.
  • Add PhpStan-focused fixtures and tests to validate the new return-type extension and other typing behaviors.

Description

  • Introduces a new PHPStan dynamic method return-type extension Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension that resolves literal dot-notation paths against a typed subclass's TData array-shape, and a marker interface Arrayy\PHPStan\DefaultDotNotationTypeInterface to opt-in subclasses that promise to keep the default . separator.
  • Updates MetaDynamicStaticMethodReturnTypeExtension service registration and repository phpstan.neon to register both extensions and adds a tests/PHPStan/phpstan-fixtures.neon used by fixture-based PHPStan checks; documents the extensions in README.md and built docs.
  • Multiple runtime changes and type-safe relaxations: broadened signatures (e.g. extractValue), improved internalRemove dot-path handling (float->int cast and safer traversal), adjusted callAtPath to guard for non-array intermediates, and numerous @phpstan-ignore-line annotations across src/ to silence provably-intentional generic/typing mismatches while keeping runtime behavior unchanged.
  • JSON mapper and factory tweaks to better handle traversables and documented types, plus many phpdoc and test updates (new phpstan-oriented tests under tests/PHPStan/, new fixtures like AccessShapeUser, AccessShapeProfile, CallableGenericInferenceTest, and updates to existing tests to add narrower phpdoc generics and selective @phpstan-ignore-line comments).

Testing

  • Ran the PHPUnit test suite via phpunit against the modified codebase and the test changes, and the suite passed.
  • Executed PHPStan against the PHPStan fixtures using phpstan --configuration=tests/PHPStan/phpstan-fixtures.neon to validate the new dynamic extensions and their intended inferences, and the analysis completed successfully.
  • Added focused unit tests under 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 Reviewable

Summary by CodeRabbit

  • New Features
    • Improved PHPStan inference for get() dot-notation with brokered dynamic return-type resolution.
    • Added a marker interface for typed dot-notation implementations.
  • Bug Fixes
    • Hardened dot-notation traversal for nested reads and removals, including safer intermediate checks and early stopping when encountering scalar/non-array segments.
    • Replacement helpers now return empty results when key/value counts don’t match.
  • Documentation
    • Expanded PHPStan integration and extension registration guidance with updated examples.
  • Tests
    • Added/updated runtime and PHPStan fixtures covering access ways, JSON traversable mapping, and callable generic inference.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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.

Changes

PHPStan integration and typed access

Layer / File(s) Summary
Extension contract and inference
src/PHPStan/*, phpstan.neon, README.md, build/docs/base.md
Registers PHPStan extensions, documents typed access behavior, and resolves literal dot-notation get() return types.
Runtime access and mutation
src/Arrayy.php, tests/ArrayyTest.php, tests/InfrastructureCoverageTest.php
Adds guards for nested access, extraction, traversal, and removal; updates replacement behavior and related coverage.
Mapper and type contracts
src/Mapper/Json.php, src/Type/*, src/Create.php, src/Collection/*
Refines mapper and type-analysis metadata and adds targeted PHPStan suppressions.
Fixtures and validation
tests/PHPStan/*, tests/BasicArrayTest.php, tests/Collection/*, tests/JsonMapperCoverageTest.php, tests/TypeCheckCoreCoverageTest.php
Adds typed access and callable-inference fixtures, mapper coverage, and PHPStan validation.
Release documentation
CHANGELOG.md
Records dot-notation inference, callable generic inference, nested removal, and mapper traversal changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • voku/Arrayy#175: Covers closely related PHPStan integration and dot-notation runtime fixes.

Poem

I’m a rabbit with types in my ears,
Mapping dot-paths through PHPStan’s spheres.
Nested hops guard every stair,
Tests bloom softly in typed array air.
Hop, hop—clean inference everywhere!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a new PHPStan dot-notation return-type extension plus supporting typing and fixture updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/integriere-phpstan-annotations-957xnh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mergify

mergify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 91.369% (-1.0%) from 92.38% — codex/integriere-phpstan-annotations-957xnh into master

@coveralls

coveralls commented Jul 26, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 92.944% (+0.6%) from 92.38% — codex/integriere-phpstan-annotations-957xnh into master

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
src/Mapper/Json.php (1)

82-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the nested ternary for $jsonValues normalization.

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 new testMapPreservesTraversableEntries coverage.

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between 57c7fca and dfd1984.

📒 Files selected for processing (33)
  • README.md
  • build/docs/base.md
  • phpstan.neon
  • src/Arrayy.php
  • src/Collection/AbstractCollection.php
  • src/Create.php
  • src/Mapper/Json.php
  • src/PHPStan/DefaultDotNotationTypeInterface.php
  • src/PHPStan/GetDynamicMethodReturnTypeExtension.php
  • src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php
  • src/Type/DetectFirstValueTypeCollection.php
  • src/TypeCheck/TypeCheckCallback.php
  • src/TypeCheck/TypeCheckPhpDoc.php
  • tests/Account.php
  • tests/ArrayyTest.php
  • tests/BasicArrayTest.php
  • tests/Collection/BoolTypeTest.php
  • tests/Collection/CollectionTest.php
  • tests/Collection/StringTypeTest.php
  • tests/Collection/TypeTypeTest.php
  • tests/Collection/TypesTest.php
  • tests/JsonMapperCoverageTest.php
  • tests/JsonMapperTest.php
  • tests/MetaPhpStanIntegrationTest.php
  • tests/ModelA.php
  • tests/PHPStan/AccessShapeProfile.php
  • tests/PHPStan/AccessShapeUser.php
  • tests/PHPStan/AccessWaysTest.php
  • tests/PHPStan/AnalyseTest.php
  • tests/PHPStan/CallableGenericInferenceTest.php
  • tests/PHPStan/CustomSeparatorAccessUser.php
  • tests/PHPStan/phpstan-fixtures.neon
  • tests/TypeCheckCoreCoverageTest.php

Comment thread README.md
Comment on lines +125 to +133
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n -C4 '`@property`(-read)?|class (User|City)' README.md build/docs/base.md

Repository: 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:


🌐 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:


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.

Comment thread src/Arrayy.php
Comment on lines 7462 to 7472
if ($explodedPath !== []) {
if (!\is_array($currentOffset[$nextPath])) {
return;
}

$nestedOffset = &$currentOffset[$nextPath];
$this->callAtPath(
\implode($this->pathSeparator, $explodedPath),
$callable,
$currentOffset[$nextPath]
$nestedOffset
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 nested Arrayy/ArrayAccess values.
  • src/Arrayy.php#L761-L765: test final Arrayy offsets 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.

Comment on lines +54 to +70
$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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 src

Repository: 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()}")
PY

Repository: 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 -n

Repository: 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.

Comment on lines +82 to +86
if ($valueType->isArray()->yes()) {
$valueType = new GenericObjectType(
Arrayy::class,
[$valueType->getIterableKeyType(), $valueType->getIterableValueType(), $valueType]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n -C3 'GenericObjectType\(|`@phpstan-return` self<int' \
  src/PHPStan/GetDynamicMethodReturnTypeExtension.php src/Arrayy.php

Repository: 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 260

Repository: 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 260

Repository: 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.

Comment thread tests/ArrayyTest.php
Comment on lines +216 to 229
* @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']],
];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 -n

Repository: 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.

Comment thread tests/JsonMapperTest.php
$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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 . || true

Repository: 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' || true

Repository: 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:


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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 || true

Repository: 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 -200

Repository: 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between dfd1984 and 912a1fd.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • tests/InfrastructureCoverageTest.php
  • tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php

Comment thread tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/ArrayyTest.php (1)

4471-4478: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Align scalar-intermediate offsetUnset() behavior across dot-notation handling.

offsetUnset('user.name') replaces scalar user with null, but remove('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 like remove() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 912a1fd and d586dd7.

📒 Files selected for processing (2)
  • tests/ArrayyTest.php
  • tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants