Skip to content

Fix CI pipeline: phpunit.xml validation warning, php-parser v4 test skip, and comprehensive type-analysis regression coverage#83

Merged
voku merged 28 commits intomasterfrom
copilot/fix-failing-phpdoc-test
Apr 11, 2026
Merged

Fix CI pipeline: phpunit.xml validation warning, php-parser v4 test skip, and comprehensive type-analysis regression coverage#83
voku merged 28 commits intomasterfrom
copilot/fix-failing-phpdoc-test

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 11, 2026

  • Identify Bug 1: splitTypeAndVariable() uses str_replace($variableName, ...) which corrupts array shape types when a key name matches the parameter variable name (e.g., array{userId: int} $userIdarray{: int} $)
  • Identify Bug 2: parseModernPhpdocTokens() uses trim(str, ')(') which aggressively strips parentheses from DNF types (e.g., (Foo&Bar)|nullFoo & Bar)|null)
  • Fix Bug 1 in Utils::splitTypeAndVariable() — replace $variableName with '$' . $variableName in str_replace
  • Fix Bug 2 in Utils::parseModernPhpdocTokens() — strip exactly one pair of balanced outer parens instead of using trim
  • Add test for Bug 1: testArrayShapeKeyMatchingVariableNameIsNotCorrupted
  • Add test for Bug 2: testDnfPhpDocTypeIsNotCorrupted
  • Run existing tests — all 57 pass (55 original + 2 new)

Copilot AI changed the title [WIP] Fix failing test methods['foo_broken']->parameters['lall'] Add regression coverage for malformed @param phpDocRaw preservation Apr 11, 2026
Copilot AI requested a review from voku April 11, 2026 06:57
Copilot finished work on behalf of voku April 11, 2026 06:57
@voku voku marked this pull request as ready for review April 11, 2026 07:16
@voku
Copy link
Copy Markdown
Owner

voku commented Apr 11, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a new test case, testBrokenParamPhpDocRawIsPreserved, to ensure that malformed PhpDoc parameters are correctly handled by preserving the raw string and returning a null extended type. Feedback indicates that this new test is redundant because the same assertions are already covered in testSimpleOneClassWithTrait, suggesting that the duplicate logic should be consolidated.

Comment thread tests/ParserTest.php
Comment on lines +235 to +250
public function testBrokenParamPhpDocRawIsPreserved(): void
{
if (PHP_VERSION_ID < 80000) {
static::markTestSkipped('only for PHP >= 8.0');
}

$phpCode = PhpCodeParser::getPhpFiles(__DIR__ . '/Dummy8.php');
$phpClasses = $phpCode->getClasses();

static::assertSame(
'array{stdClass: \stdClass, numbers: int|float $lall <foo/>',
$phpClasses[Dummy8::class]->methods['foo_broken']->parameters['lall']->phpDocRaw
);

static::assertNull($phpClasses[Dummy8::class]->methods['foo_broken']->parameters['lall']->typeFromPhpDocExtended);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The test method testBrokenParamPhpDocRawIsPreserved is redundant. The exact same assertions for Dummy8::foo_broken (checking both phpDocRaw and typeFromPhpDocExtended) are already implemented in testSimpleOneClassWithTrait on lines 205-208 and 217.

Duplicate test logic increases maintenance overhead without providing additional coverage. If the goal is to have a focused test for this regression, consider moving the assertions from testSimpleOneClassWithTrait into this new method instead of duplicating them.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a focused regression test to ensure malformed @param phpDoc text is preserved verbatim in phpDocRaw while remaining unparseable as an extended type.

Changes:

  • Added testBrokenParamPhpDocRawIsPreserved() targeting Dummy8::foo_broken()’s malformed @param handling.
  • Asserted the raw malformed @param string is preserved exactly in parameters['lall']->phpDocRaw.
  • Asserted the same malformed value still does not produce typeFromPhpDocExtended.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/ParserTest.php Outdated
Comment on lines +235 to +249
public function testBrokenParamPhpDocRawIsPreserved(): void
{
if (PHP_VERSION_ID < 80000) {
static::markTestSkipped('only for PHP >= 8.0');
}

$phpCode = PhpCodeParser::getPhpFiles(__DIR__ . '/Dummy8.php');
$phpClasses = $phpCode->getClasses();

static::assertSame(
'array{stdClass: \stdClass, numbers: int|float $lall <foo/>',
$phpClasses[Dummy8::class]->methods['foo_broken']->parameters['lall']->phpDocRaw
);

static::assertNull($phpClasses[Dummy8::class]->methods['foo_broken']->parameters['lall']->typeFromPhpDocExtended);
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This test duplicates assertions already present in testSimpleOneClassWithTrait() for Dummy8::foo_broken() (see tests/ParserTest.php around the existing phpDocRaw / typeFromPhpDocExtended checks). To reduce duplication/maintenance, consider either (a) moving those assertions out of testSimpleOneClassWithTrait() into this focused test and removing the originals, or (b) extracting a small helper that returns the parsed Dummy8 class so both tests don’t need to re-parse Dummy8.php.

Suggested change
public function testBrokenParamPhpDocRawIsPreserved(): void
{
if (PHP_VERSION_ID < 80000) {
static::markTestSkipped('only for PHP >= 8.0');
}
$phpCode = PhpCodeParser::getPhpFiles(__DIR__ . '/Dummy8.php');
$phpClasses = $phpCode->getClasses();
static::assertSame(
'array{stdClass: \stdClass, numbers: int|float $lall <foo/>',
$phpClasses[Dummy8::class]->methods['foo_broken']->parameters['lall']->phpDocRaw
);
static::assertNull($phpClasses[Dummy8::class]->methods['foo_broken']->parameters['lall']->typeFromPhpDocExtended);
private function getParsedDummy8Class()
{
$phpCode = PhpCodeParser::getPhpFiles(__DIR__ . '/Dummy8.php');
$phpClasses = $phpCode->getClasses();
return $phpClasses[Dummy8::class];
}
public function testBrokenParamPhpDocRawIsPreserved(): void
{
if (PHP_VERSION_ID < 80000) {
static::markTestSkipped('only for PHP >= 8.0');
}
$phpClass = $this->getParsedDummy8Class();
static::assertSame(
'array{stdClass: \stdClass, numbers: int|float $lall <foo/>',
$phpClass->methods['foo_broken']->parameters['lall']->phpDocRaw
);
static::assertNull($phpClass->methods['foo_broken']->parameters['lall']->typeFromPhpDocExtended);

Copilot uses AI. Check for mistakes.
@voku
Copy link
Copy Markdown
Owner

voku commented Apr 11, 2026

@copilot expand the new test by getting all the information that we can from our libary, focus on the types and run a blind spot analysis for yourself based on the provided results afterwards

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 11, 2026

@copilot expand the new test by getting all the information that we can from our libary, focus on the types and run a blind spot analysis for yourself based on the provided results afterwards

Expanded in f4f4072. The focused test now covers the broken param/return type projections (type, all phpDoc-derived variants, raw phpDoc, and normalized getMethodsInfo() output including parser diagnostics). Blind-spot check: the remaining Dummy8 assertions in testSimpleOneClassWithTrait() still cover the unrelated non-broken cases, so I kept the focused test centered on malformed-type behavior.

Copilot AI changed the title Add regression coverage for malformed @param phpDocRaw preservation Expand regression coverage for malformed @param type handling and phpDocRaw preservation Apr 11, 2026
Copilot finished work on behalf of voku April 11, 2026 07:27
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for PHP 8.4 features, specifically property hooks and asymmetric visibility, across the parser models and reflection logic. It also implements a recovery mechanism for broken PHPDoc types by attempting to parse partial strings when a full parse fails. Key changes include updates to PHPClass, PHPProperty, and PHPParameter to handle the new syntax, as well as the addition of several test fixtures. Review feedback identifies a potential runtime error in PHPParameter due to a missing method call and suggests refactoring redundant autoloading logic in PHPClass to use a shared base class helper.

$this->typeFromPhpDocExtended = $recoveredType;
}

$this->addParseError($e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The method addParseError() does not appear to be defined in PHPParameter or its base class BasePHPElement. This will likely result in a fatal error at runtime. Based on the implementation in PHPFunction.php, you should manually add the error to the parseError array.

                $tmpErrorMessage = $parameterName . ':' . ($this->line ?? '?') . ' | ' . $e->getMessage();
                $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;

Comment on lines +66 to +68
$canAutoload = (\PHP_VERSION_ID >= 80200 || !self::nodeUsesPHP82PlusSyntax($node))
&& (\PHP_VERSION_ID >= 80300 || !self::nodeUsesPHP83PlusSyntax($node));
&& (\PHP_VERSION_ID >= 80300 || !self::nodeUsesPHP83PlusSyntax($node))
&& (\PHP_VERSION_ID >= 80400 || !self::containsPHP84PlusSyntax($node));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This logic is redundant as it is now encapsulated in the canAutoloadFromPhpNode() method in the base class BasePHPClass. Reusing the shared helper improves maintainability and aligns with the PR's objective of removing redundant class-specific logic.

        $canAutoload = self::canAutoloadFromPhpNode($node);

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +508 to +530
public static function recoverBrokenPhpdocType(string $input): ?string
{
$parts = [];
foreach (self::modernPhpdocTokens($input)->getTokens() as $token) {
if (!empty($token[0])) {
$parts[] = $token[0];
}
}

for ($i = \count($parts); $i > 0; --$i) {
$candidate = \trim(\implode('', \array_slice($parts, 0, $i)));
if ($candidate === '') {
return null;
}

try {
return self::modernPhpdoc($candidate);
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
continue;
}
}

return null;
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

recoverBrokenPhpdocType() retries parsing by building progressively shorter candidates and calling modernPhpdoc() each time (which re-tokenizes and re-parses). In the worst case this is O(n²) and can become expensive when many invalid phpdocs are encountered in a large codebase. Consider reusing the initial tokenization (avoid re-lexing), and/or adding a small upper bound on retry attempts (e.g., stop after N failures or when the candidate length drops below a threshold).

Copilot uses AI. Check for mistakes.
@voku
Copy link
Copy Markdown
Owner

voku commented Apr 11, 2026

@copilot apply changes based on the comments in this thread

Copilot AI and others added 2 commits April 11, 2026 15:24
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 11, 2026

@copilot apply changes based on the comments in this thread

Applied in 9433ca1. recoverBrokenPhpdocType() now reuses the initial phpdoc tokenization, parses shortened candidates from token slices instead of re-lexing on every retry, and bounds the recovery loop. Verified with the malformed-phpdoc targeted tests, full PHPUnit, and PHPStan.

Copilot finished work on behalf of voku April 11, 2026 15:26
@voku
Copy link
Copy Markdown
Owner

voku commented Apr 11, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for PHP 8.4 features, including property hooks and asymmetric visibility, while also improving the handling of broken PHPDoc types through a new recovery mechanism. The changes include updates to the AST model to capture these new property attributes and a refactoring of how promoted properties are merged. Feedback is provided regarding the efficiency of locating the constructor in PHPClass.php and a potential issue where promoted property visibility might not correctly override default values from PHPDoc tags.

Comment on lines +466 to +499
foreach ($node->getMethods() as $method) {
if ($method->name->name !== '__construct') {
continue;
}

foreach ($node->stmts as $stmt) {
if ($stmt instanceof \PhpParser\Node\Stmt\ClassMethod) {
if (self::containsPHP82PlusType($stmt->returnType)) {
return true;
foreach ($method->params as $parameter) {
if (!self::isPromotedParameter($parameter)) {
continue;
}
foreach ($stmt->params as $param) {
if (self::containsPHP82PlusType($param->type)) {
return true;
}

$parameterVar = $parameter->var;
if (
!($parameterVar instanceof \PhpParser\Node\Expr\Variable)
|| !\is_string($parameterVar->name)
) {
continue;
}
} elseif ($stmt instanceof \PhpParser\Node\Stmt\Property) {
if (self::containsPHP82PlusType($stmt->type)) {
return true;

$promotedProperty = (new PHPProperty($this->parserContainer))
->readObjectFromPromotedParam($parameter, $this->name);

$propertyName = $parameterVar->name;
$existingProperty = $this->properties[$propertyName] ?? null;
if ($existingProperty !== null) {
$this->mergePromotedPropertyData($existingProperty, $promotedProperty, $parameter);

continue;
}

$this->properties[$propertyName] = $promotedProperty;
}
}

return false;
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Instead of iterating through all methods to find the constructor, you can use the getMethod() helper provided by the Class_ node. This is more efficient and makes the code cleaner.

        $method = $node->getMethod('__construct');
        if ($method === null) {
            return;
        }

        foreach ($method->params as $parameter) {
            if (!self::isPromotedParameter($parameter)) {
                continue;
            }

            $parameterVar = $parameter->var;
            if (
                !($parameterVar instanceof \PhpParser\Node\Expr\Variable)
                || !\is_string($parameterVar->name)
            ) {
                continue;
            }

            $promotedProperty = (new PHPProperty($this->parserContainer))
                ->readObjectFromPromotedParam($parameter, $this->name);

            $propertyName = $parameterVar->name;
            $existingProperty = $this->properties[$propertyName] ?? null;
            if ($existingProperty !== null) {
                $this->mergePromotedPropertyData($existingProperty, $promotedProperty, $parameter);

                continue;
            }

            $this->properties[$propertyName] = $promotedProperty;
        }

Comment on lines 507 to 509
if ($existingProperty->access === '' && $promotedProperty->access !== '') {
$existingProperty->access = $promotedProperty->access;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The condition existingProperty->access === '' is too restrictive. When a property is extracted from a PHPDoc @property tag (in readPhpDocProperties), its access is defaulted to 'public'. If that same property is later found as a promoted constructor parameter with a different visibility (e.g., private), the promoted visibility should take precedence. Removing the check for an empty string ensures the actual visibility modifier from the code is used.

        if ($promotedProperty->access !== '') {
            $existingProperty->access = $promotedProperty->access;
        }

…rruption and modernPhpdoc DNF-type paren mangling

Agent-Logs-Url: https://github.com/voku/Simple-PHP-Code-Parser/sessions/0b99f73d-a186-4d7c-86b0-69b4b663c5a4

Co-authored-by: voku <264695+voku@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
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

Copilot finished work on behalf of voku April 11, 2026 18:22
@voku voku merged commit 5248e16 into master Apr 11, 2026
6 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failing test >methods['foo_broken']->parameters['lall']->phpDocRaw

3 participants