Report in-place sort calls that cannot change the array behind the sortWithoutEffect bleeding-edge toggle - #6296
Open
phpstan-bot wants to merge 1 commit into
Conversation
…ortWithoutEffect` bleeding-edge toggle
- New rule `PHPStan\Rules\Functions\SortWithoutEffectRule` (level 5), gated behind the
`sortWithoutEffect` feature toggle in `conf/bleedingEdge.neon`.
- `ksort()` on a `list` is reported as `ksort.list`: the keys are already `0..n-1` ascending.
Only reported when the flags are known to be `SORT_REGULAR` or `SORT_NUMERIC` (or absent);
`SORT_STRING`, `SORT_NATURAL` and non-constant flags bail out.
- Swept the whole in-place sort family for the same class of dead call:
- definitely empty array + any of `sort`, `rsort`, `usort`, `shuffle`, `asort`, `arsort`,
`ksort`, `krsort`, `uasort`, `uksort`, `natsort`, `natcasesort` → `<fn>.empty`.
- array with at most one element + a key-preserving sort (`asort`, `arsort`, `ksort`,
`krsort`, `uasort`, `uksort`, `natsort`, `natcasesort`) → `<fn>.singleElement`.
- array with at most one element that is also a list + a reindexing sort (`sort`, `rsort`,
`usort`, `shuffle`) → `<fn>.singleElement`. Reindexing sorts are deliberately not reported
on single-element non-lists, because they renumber the key.
- `krsort()` on a multi-element list is deliberately not reported - it reverses the array.
- Registers the toggle in `conf/config.neon`, `conf/parametersSchema.neon` and wires the rule
up in `conf/config.level5.neon`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ksort()on alistis always a no-op: a list's keys are0..n-1in ascending order already, so with the defaultSORT_REGULARflags there is nothing to reorder. This is the same class of dead callArrayValuesRulealready reports asarrayValues.list/arrayValues.empty, and it silently hides real bugs (the issue reports a third-party extension whose output was non-deterministic because aksort()was meant to be asort()).This PR adds
PHPStan\Rules\Functions\SortWithoutEffectRule, behind thesortWithoutEffectbleeding-edge toggle, and covers the whole family of in-place sort functions rather thanksort()alone.Changes
src/Rules/Functions/SortWithoutEffectRule.php— new rule listening onFuncCall. It normalizes the arguments (so named arguments work), takes the PHPDoc or native type of#1 $arraydepending ontreatPhpDocTypesAsCertain, and reports:ksort.list— the array is a list and the sort flags are known to beSORT_REGULARorSORT_NUMERIC(or omitted). The flags check is aisSuperTypeOf()against0|1, soSORT_STRING,SORT_NATURAL,SORT_REGULAR|SORT_FLAG_CASEand non-constantintflags all bail out.<fn>.empty— the array is definitely empty. Applies to every in-place sort:sort,rsort,usort,shuffle,asort,arsort,ksort,krsort,uasort,uksort,natsort,natcasesort.<fn>.singleElement— the array has at most one element (int<2, max>is not a supertype ofgetArraySize()). Reported for the key-preserving sorts unconditionally, and for the reindexing sorts (sort,rsort,usort,shuffle) only when the array is also a list.conf/bleedingEdge.neon,conf/config.neon,conf/parametersSchema.neon— newfeatureToggles.sortWithoutEffect.conf/config.level5.neon— registers the rule (mirroringArrayValuesRule's level) with aconditionalTagsentry on the toggle.tests/PHPStan/Rules/Functions/SortWithoutEffectRuleTest.php+ two data files.Analogous cases probed and deliberately not reported, because they are not no-ops:
krsort()on a list of two or more elements — it reverses the array.sort()/rsort()/usort()/shuffle()on a single-element array that is not a list (e.g.array{foo: int}) — they renumber the key to0.ksort()onnon-empty-array<string, int>, onarray{foo: int}|array{bar: int, baz: int}(size1|2), and onmixed.ksort($list, SORT_STRING)/SORT_NATURAL/ a non-constantint— string comparison orders"10"before"2", and unknown flags are skipped conservatively.Root cause
This is a missing check rather than a regression. The existing "call has no effect" rules only covered
array_values()andarray_filter(); the in-place sort family had no equivalent. The pattern behind all of them is the same: an argument type whose shape (empty, at most one element, already a list) makes the operation provably identity.Rather than special-casing
ksort(), the rule splits the sort functions along the one axis that decides the answer — whether the function preserves keys or reindexes the array — and derives the three no-effect conditions from the argument'sisIterableAtLeastOnce(),isList()andgetArraySize(). That way every sibling function is covered by construction and adding a new one is a single array entry.Test
tests/PHPStan/Rules/Functions/SortWithoutEffectRuleTest.php:testRule()analysesdata/sort-without-effect.php, which contains both reproducers from the issue (ksort()on alist<string>parameter, andksort()on an array built with$tips[] = ...), plus one isolated function per sort function for the empty, single-element, single-element-list, non-empty-map, union andmixedcases, and one per interestingksort()flags value. Each call lives in its own function so the by-reference type of the previous call cannot leak into the next assertion.testNamedArguments()(#[RequiresPhp('>= 8.0.0')]) analysesdata/sort-without-effect-named-args.phpand coversksort(array: $list)andksort(flags: SORT_STRING, array: $list), exercisingArgumentsNormalizer.Both the rule test and an end-to-end
bin/phpstan analyse -l 8run of the issue's snippet were checked to report nothing with the toggle off and the two expectedksort.listerrors withconf/bleedingEdge.neon.make tests,make phpstanandmake csare green. (make name-collisionfails ontests/PHPStan/Reflection/data/attribute-const-reflection.php, which reproduces on a clean checkout and is unrelated to this change.)Fixes phpstan/phpstan#15126