Skip to content

Keep accepts() reasons and messages of big types readable on bleeding edge - #6850

Draft
ondrejmirtes wants to merge 1 commit into
2.3.xfrom
accepts-reasons-offset-paths
Draft

ondrejmirtes wants to merge 1 commit into
2.3.xfrom
accepts-reasons-offset-paths

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

A return.type error in a real-world data provider (rows built by stored closures, typed per call by closureSignaturesFromUsages) came with an 8 KB message and a 31 KB tip of 68 lines. Each line embedded a full literal shape, and the root cause was one missing bool in a @return array shape. This PR keeps such errors readable. Everything is bleeding edge only; the legacy output is unchanged.

Before / after on that case

Message: 8,148 → 401 chars, and it now shows the difference directly:

… should return array<string, array{list<array{int, int, int, list<int>, bool}>, int, list<ChildData>, bool, IntInterval, list<int>}>
but returns non-empty-array<literal-string&non-falsy-string, array{non-empty-list<array{int, int, int, list<int>, bool, bool}>, int, list<ChildData>, bool, IntInterval, list<int>}>.

Tip: 31.5 KB / 68 lines → 395 chars / 2 lines:

• Offsets 'case 0' › 0 › 0, 'case 2' › 0 › 1, 'case 3' › 0 › 0 and 154 more (array{int, int, int, list<int>, bool}): Sealed array shape does not accept array with extra key 5 => true.
• Offsets 'case 1' › 0 › 0, 'case 2' › 0 › 0, 'case 4' › 0 › 1 and 53 more (array{int, int, int, list<int>, bool}): Sealed array shape does not accept array with extra key 5 => false.

Changes

Offset paths (AcceptsReasonHelper):

  • ArrayType::accepts() now says which entry a reason belongs to.
  • ConstantArrayType::checkOurKeys() builds paths (Offset 'a' › 0 › 1 (T): reason) instead of repeating the whole accepted type at every nesting level. Only the innermost offset names its types.
  • Unsealed shapes report their extra offsets the same way.

Grouping in tips: RuleErrorBuilder::acceptsReasonsTip() merges reasons that differ only in their offset path, listing 3 offsets and a count. A tip shows at most 5 reasons, then N more reasons not shown.

Changes to individual reasons:

  • Sealed array shape does not accept array with extra key 5 => true. now names the value of the extra key.
  • ObjectShapeType merges missing properties into one reason: Foo does not have properties $a, $b, $c and 17 more.
  • IntersectionType: … is not a list / … might be empty no longer repeat an accepted array described in more than 120 chars; it becomes Array.
  • Sealed array shapes … cannot be intersected describes both shapes without their literal values. Union members that share keys (e.g. ArrayIterator of literal rows) then produce one reason instead of one per member.

Long messages (AcceptedTypeDescriptionHelper, used for return.type and argument.type): an accepted type whose description exceeds 1000 chars is described with:

  • literal values widened (keeping lowercase-string etc.),
  • shapes with more than 8 keys, and lists of a single value type, generalized.

How I checked

I surveyed every reason-producing site in accepts() / isSuperTypeOf() and reproduced the candidates with generated snippets. The table lists the ones that blew up:

Case Tip before Tip after
object shape, 20 missing properties 8.6 KB, 20 lines 1 line
unsealed shape, 40 wrong extra entries 6.6 KB, 40 lines 1 line
non-empty-list<lowercase-string> given a big possibly-empty literal list 1.5 KB message + 1.5 KB tip 136 + 21 chars
Iterator<int, array{id: int}> given an ArrayIterator of 30 literal rows 6.8 KB, 30 lines 1 line

Sites that stayed small and are unchanged:

  • union member wrappers (Type #n from the union), which grouping now merges
  • object-shape property values
  • template argument reasons
  • callable parameter reasons
  • mixed subtraction reasons

Turbo: ArrayType, ConstantArrayType, IntersectionType and ObjectShapeType are mirrored. PHP and native output are byte-identical on all scenarios, with bleeding edge on and off. The full suite passes with and without the extension. Smoke, side-by-side, signature parity and make lint-turbo all pass.

Expectation changes in existing tests

  • the extra-key value
  • offset paths (Offset 0 › 'count')
  • merged missing properties in testObjectShapes
  • bug-8146b: its 7 KB literal message is now the widened one-liner

New tests:

  • testNestedArrayShapeReturnReasons
  • testReadableAcceptsReasons — 6 cases, all failing without the change
  • AcceptsReasonHelperTest

🤖 Generated with Claude Code

https://claude.ai/code/session_01RFQVeuWzHD5AobZdsRHdT3

… edge

A return type mismatch in a data provider built by closures (with
closureSignaturesFromUsages inferring literal shapes per call) produced an
8 KB message and a 31 KB tip of 68 near-identical lines. On bleeding edge:

- Nested array reasons are offset paths (`Offset 'case 0' › 0 › 0 (T): reason`)
  instead of repeating the whole accepted type at every level;
  ArrayType::accepts() now names the entry each reason comes from.
- Reasons differing only in their offset path are reported once, listing
  the offsets; at most 5 reasons are shown in a tip.
- A sealed shape rejecting an extra key names its value (`extra key 5 => true`).
- Unsealed shapes report their extra offsets as offset paths too.
- Object shapes report missing properties in one reason.
- "is not a list" / "might be empty" do not repeat a long accepted array.
- "Sealed array shapes cannot be intersected" describes the shapes without
  literal values, so union members with the same keys share the reason.
- return.type and argument.type describe an accepted type longer than
  1000 characters with literal values widened and big shapes generalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFQVeuWzHD5AobZdsRHdT3
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.

1 participant