Skip to content

Only treat implode() separator as guaranteeing a non-empty result when the array has at least two elements#5801

Open
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-i85h8y2
Open

Only treat implode() separator as guaranteeing a non-empty result when the array has at least two elements#5801
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-i85h8y2

Conversation

@phpstan-bot

@phpstan-bot phpstan-bot commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

implode(string, non-empty-array) was incorrectly inferred as non-falsy-string. The glue/separator only appears between elements, so a non-empty array with a single element drops the separator entirely — implode(',', ['']) is ''. The fix only lets the separator contribute non-emptiness/non-falsiness when the array is provably of size >= 2.

Changes

  • src/Type/Php/ImplodeFunctionReturnTypeExtension.php: compute $separatorAppears = IntegerRangeType::createAllGreaterThanOrEqualTo(2)->isSuperTypeOf($arrayType->getArraySize())->yes() and only consider the separator's isNonFalsyString()/isNonEmptyString() when it holds. The value-type contribution is unchanged (it is valid for any array with at least one element).
  • tests/PHPStan/Analyser/nsrt/implode.php: added regression tests for non-empty-array/non-empty-list of string and non-empty-string, and array{string, string}/array{string, string, string} (size >= 2 keeps non-falsy-string).
  • tests/PHPStan/Analyser/nsrt/non-empty-string.php and non-falsy-string.php: corrected assertions that encoded the old, unsound behavior (e.g. implode(' ', non-empty-array<string>) is string, not non-falsy-string; implode($glue, non-empty-array) is string, not non-empty-string).
  • build/stubs/PhpParserName.stub: declared PhpParser\Node\Name::getParts(): non-empty-list<non-empty-string> (dev-only stub used by make phpstan) so that implode('\\', $nameParts) in FunctionDefinitionCheck keeps inferring non-empty-string.
  • Probed analogous string-building extensions str_repeat() and str_pad() — both already correctly gate their accessory types on the multiplier/length, so they needed no change.

Root cause

The extension assumed that a non-empty array plus a non-empty/non-falsy separator yields a non-empty/non-falsy string. That is only true when the separator is actually emitted, which requires at least two elements. For a single-element array the separator never appears, so the result is just the (possibly empty) single value. The fix introduces a size-based guard (getArraySize() >= 2) before letting the separator refine the result type.

Test

  • Reproducer from the issue: implode(',', non-empty-array<string>) now infers string instead of non-falsy-string (added to implode.php).
  • Added non-empty-string / non-empty-list of string and non-empty-string cases, and array{string, string} cases proving the separator still refines the result when the array is known to have >= 2 elements.
  • Updated pre-existing assertions in non-empty-string.php / non-falsy-string.php to the corrected results.
  • Full suite (make tests), self-analysis (make phpstan), and make cs-fix all pass.

Fixes phpstan/phpstan#14763
Fixes phpstan/phpstan#5521

…hen the array has at least two elements

- The separator passed to `implode()`/`join()` only appears *between* elements, so a single-element array (e.g. `implode(',', [''])`) drops it entirely and can still produce an empty/falsy string.
- `ImplodeFunctionReturnTypeExtension` now gates the separator's `non-empty-string`/`non-falsy-string` contribution on the array being provably of size >= 2 (via `getArraySize()` and `IntegerRangeType::createAllGreaterThanOrEqualTo(2)`). The value-type contribution is unchanged since it holds for any non-empty array.
- Updated existing `non-empty-string`/`non-falsy-string` assertions that encoded the previous, unsound behavior to the now-correct `string`/`non-empty-string` results.
- Added regression tests in `implode.php` covering `non-empty-array`/`non-empty-list` of `string` and `non-empty-string`, plus `array{string, string}` (provably >= 2 elements stays `non-falsy-string`).
- Taught PHPStan's own analysis that `PhpParser\Node\Name::getParts()` returns `non-empty-list<non-empty-string>` (dev-only stub) so building a class name with `implode('\\', ...)` stays `non-empty-string`.
- Probed sibling string builders `str_repeat()` and `str_pad()`: both already gate the relevant accessory on the multiplier/length, so no change needed.

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add non regression test for phpstan/phpstan#5521

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. I addressed VincentLanglet's review request to add a regression test for issue #5521.

What I did

The reviewer asked for a non-regression test covering #5521 (implode(',', [null]) / implode(',', ['']) was previously inferred as a non-empty string, triggering a false === always-false error).

I added a bug5521() method to tests/PHPStan/Analyser/nsrt/implode.php with the faithful reproducer from the issue's playground:

public function bug5521() {
    // https://github.com/phpstan/phpstan/issues/5521
    $foo = [null];
    assertType("''", implode(',', $foo));

    $bar = [''];
    assertType("''", implode(',', $bar));
}

Both single-element arrays drop the separator entirely and correctly infer '' (an empty string), which is what makes the original === '' comparison valid rather than a false positive.

Verification

  • Confirmed the exact rule-level reproducer from the issue (if (implode(',', [null]) === '')) no longer produces the bogus "between non-empty-string and ''" error.
  • NodeScopeResolverTest for implode.php passes.
  • make phpstan self-analysis is clean.

Committed as Add regression test for #5521 and pushed to the PR branch.

A note for the record: because [null]/[''] are literal constant arrays, they constant-fold to '' and pass regardless of this PR's specific size-≥2 guard — but they are the verbatim reproducer from the issue, so they're the right test to lock in #5521's resolved behavior. The PR's actual guard logic (single-element non-empty-array<string>string) is already covered by the existing nonEmptyArrayOfStrings() test.

@VincentLanglet

Copy link
Copy Markdown
Contributor

phpstan-symfony failure is expected since it's implode on oversize array.

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This fix is correct to me, PHPStan shouldn't make false assumption.

It's better to infer string even if it's non falsy, than inferring non falsy and promoting bugs by asking developer to remove safe guards.

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.

Incorrect implode() return type implode non-empty string false error reported

2 participants