Skip to content

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
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-9p12jr7
Open

Report in-place sort calls that cannot change the array behind the sortWithoutEffect bleeding-edge toggle#6296
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-9p12jr7

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

ksort() on a list is always a no-op: a list's keys are 0..n-1 in ascending order already, so with the default SORT_REGULAR flags there is nothing to reorder. This is the same class of dead call ArrayValuesRule already reports as arrayValues.list / arrayValues.empty, and it silently hides real bugs (the issue reports a third-party extension whose output was non-deterministic because a ksort() was meant to be a sort()).

This PR adds PHPStan\Rules\Functions\SortWithoutEffectRule, behind the sortWithoutEffect bleeding-edge toggle, and covers the whole family of in-place sort functions rather than ksort() alone.

Changes

  • src/Rules/Functions/SortWithoutEffectRule.php — new rule listening on FuncCall. It normalizes the arguments (so named arguments work), takes the PHPDoc or native type of #1 $array depending on treatPhpDocTypesAsCertain, and reports:
    • ksort.list — the array is a list and the sort flags are known to be SORT_REGULAR or SORT_NUMERIC (or omitted). The flags check is a isSuperTypeOf() against 0|1, so SORT_STRING, SORT_NATURAL, SORT_REGULAR|SORT_FLAG_CASE and non-constant int flags 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 of getArraySize()). 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 — new featureToggles.sortWithoutEffect.
  • conf/config.level5.neon — registers the rule (mirroring ArrayValuesRule's level) with a conditionalTags entry 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 to 0.
  • ksort() on non-empty-array<string, int>, on array{foo: int}|array{bar: int, baz: int} (size 1|2), and on mixed.
  • ksort($list, SORT_STRING) / SORT_NATURAL / a non-constant int — 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() and array_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's isIterableAtLeastOnce(), isList() and getArraySize(). 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() analyses data/sort-without-effect.php, which contains both reproducers from the issue (ksort() on a list<string> parameter, and ksort() 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 and mixed cases, and one per interesting ksort() 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')]) analyses data/sort-without-effect-named-args.php and covers ksort(array: $list) and ksort(flags: SORT_STRING, array: $list), exercising ArgumentsNormalizer.

Both the rule test and an end-to-end bin/phpstan analyse -l 8 run of the issue's snippet were checked to report nothing with the toggle off and the two expected ksort.list errors with conf/bleedingEdge.neon. make tests, make phpstan and make cs are green. (make name-collision fails on tests/PHPStan/Reflection/data/attribute-const-reflection.php, which reproduces on a clean checkout and is unrelated to this change.)

Fixes phpstan/phpstan#15126

…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`.
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.

Report ksort() on a list as a call with no effect

2 participants