Avatar for the carthage-software user
carthage-software
mago
BlogDocsChangelog

Performance History

Latest Results

fix(analyzer): track hook-property writes against the read type (#2112)
main
6 hours ago
fix(analyzer): support split @property-read/@property-write types A magic property can document asymmetric access with separate `@property-read` and `@property-write` tags — normally a `__set()` that converts, so the write type is wider than the read type (e.g. `@property-read int|false` with `@property-write int|string`). A split declaration exists precisely to express that difference; a symmetric one would just be `@property`. Making it work takes three steps, none of which is correct without the others: Each `@property*` tag creates a fresh magic-property entry keyed by the property name, so a second tag for the same name replaces the first: the property ends up read-only or write-only depending on tag order. Reading a write-only property is then reported as an invalid read, and writing a read-only one as an invalid write — half the documented interface disappears, and which half is arbitrary. Combine the tags into one readable-and-writable entry instead. That entry has a single type slot, so combining alone is not enough: the read type wins the slot and every write the `@property-write` tag allows becomes an invalid-property-assignment-value false positive. Store the write tag's type in a dedicated write slot on PropertyMetadata (`None` means writes accept the read type) and resolve assignments against it, including the require-extends, mixin, and intersection fallback paths. Property accesses are also memoized: an assignment caches the written value against the access id, and a later read of the same expression returns it instead of re-resolving. A read of a magic property goes through `__get`, which yields the `@property-read` type regardless of what was written, so the cached write-side value makes the read report a type the property can never produce. Clamp the memoized type of a magic property to what a read can observe: intersect the written value with the `@property-read` type, falling back to the full read type when the two are disjoint (the write converts). A write already assignable to the read type is tracked precisely — an `int` write above narrows a later read to `int`, while a `string` write leaves it `int|false`. Real and dynamic-property (`stdClass`) accesses have no distinct read type and round-trip, so they keep memoizing the written value unchanged; the new `read_type` slot on ResolvedProperty carries the `@property-read` type only for magic properties resolved for assignment. Fixes #2079
dotdash:fix_analyzer_support_split_property_read_property_write_types
14 hours ago
fix(analyzer): resolve @property tags by call-site visibility A `@property`/`@property-read`/`@property-write` tag documents the external interface a class exposes through `__get`/`__set`. A real property of the same name may coexist with it; the standard example is a private property that the class mutates internally but exposes read-only via `__get()`. Which of the two governs an access follows PHP's runtime rule: the real property wherever it is visible, the magic methods (and thus the tag) everywhere else. That decision depends on the call site, so it cannot be baked into the stored metadata. The scanner nevertheless stores tags as regular entries in the class's property map (one entry per name) and resolves collisions while scanning: a real property declared next to a tag inherits the tag's public visibility and, for `@property-read`, its readonly flag. As a result, internal writes to such a private property are reported as writes to a readonly property, internal accesses use the tag's type instead of the real one, and a real property inherited from a parent or trait either masks the tag or is masked by it, depending on populator order. Store tags separately in `ClassLikeMetadata::magic_properties`, keep `properties` for real declarations only, and decide per usage in the new `resolve_declared_property` which one governs. A tag whose type narrows a visible real property's type refines it (a wide inherited `public $db` narrowed by `@property` on a subclass); a conflicting tag type is ignored wherever the real property is visible. The unused and write-only property analyses skip real properties shadowed by a tag: they are reachable through `__get`/`__set` from outside the class, which those analyses cannot see. Type-level consumers follow suit: structural `object{...}` containment consults tags alongside real declarations, while `properties-of<T>` now excludes them (they are not real properties), matching Psalm.
dotdash:fix_analyzer_resolve_property_tags_by_call_site_visibility
1 day ago

Latest Branches

CodSpeed Performance Gauge
0%
fix(analyzer): track hook-property writes against the read type#2112
13 hours ago
47dea2a
dotdash:fix_analyzer_track_hook_property_writes_against_the_read_type
CodSpeed Performance Gauge
-18%
1 day ago
8b5f69a
dotdash:fix_analyzer_support_split_property_read_property_write_types
CodSpeed Performance Gauge
0%
1 month ago
aea5ae3
dotdash:fix_analyzer_resolve_property_tags_by_call_site_visibility
© 2026 CodSpeed Technology
Home Terms Privacy Docs