Only treat implode() separator as guaranteeing a non-empty result when the array has at least two elements#5801
Conversation
…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
left a comment
There was a problem hiding this comment.
Add non regression test for phpstan/phpstan#5521
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Done. I addressed VincentLanglet's review request to add a regression test for issue #5521. What I didThe reviewer asked for a non-regression test covering #5521 ( I added a 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 Verification
Committed as A note for the record: because |
|
phpstan-symfony failure is expected since it's implode on oversize array. |
VincentLanglet
left a comment
There was a problem hiding this comment.
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.
Summary
implode(string, non-empty-array)was incorrectly inferred asnon-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'sisNonFalsyString()/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 fornon-empty-array/non-empty-listofstringandnon-empty-string, andarray{string, string}/array{string, string, string}(size >= 2 keepsnon-falsy-string).tests/PHPStan/Analyser/nsrt/non-empty-string.phpandnon-falsy-string.php: corrected assertions that encoded the old, unsound behavior (e.g.implode(' ', non-empty-array<string>)isstring, notnon-falsy-string;implode($glue, non-empty-array)isstring, notnon-empty-string).build/stubs/PhpParserName.stub: declaredPhpParser\Node\Name::getParts(): non-empty-list<non-empty-string>(dev-only stub used bymake phpstan) so thatimplode('\\', $nameParts)inFunctionDefinitionCheckkeeps inferringnon-empty-string.str_repeat()andstr_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
implode(',', non-empty-array<string>)now infersstringinstead ofnon-falsy-string(added toimplode.php).non-empty-string/non-empty-listofstringandnon-empty-stringcases, andarray{string, string}cases proving the separator still refines the result when the array is known to have >= 2 elements.non-empty-string.php/non-falsy-string.phpto the corrected results.make tests), self-analysis (make phpstan), andmake cs-fixall pass.Fixes phpstan/phpstan#14763
Fixes phpstan/phpstan#5521