A qualifier that names a CLASS anchors on its package: Rack::MockRequest.new would bind to Rack::Protection::new — so Ruby constant-receiver calls stay unresolved #296
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#296
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while fixing #294, measured on the ruby-sinatra corpus. The honest status today is unresolved and disclosed, not wrong — this issue is about why the obvious fix is wrong.
The gap
Auth.resolve_user(t)andOuter::Auth.resolve_user(t)— a call on a constant receiver, i.e. a class/module method — are emitted as an unqualifiedmethod_callwith no receiver captured. They do not resolve, andname_fallback_shape_excludedsays so, which is the disclosure working. The reporter measured 12 such calls to one method, all unresolved, andreview_diffthen raised "untested change" on methods whose tests call them exactly this way.The obvious fix, and what it did
Emit them as Rust's
Foo::bar()is emitted:CALL,qualified: true, qualifier = the receiver as written. Measured on ruby-sinatra, joined bind-for-bind against the clean build:constant-receiver calls: 801; resolved 11 -> 112.
4 of the old 11 were phantoms that the qualifier correctly removed (
Regexp.escape->Rack::Protection::EscapedParams::escape,File.unlinkx2 ->Test::unlink,ChildProcess.build->RackTest::build). Good.But roughly 60 of the new binds were phantoms, through
TIER1Q_PASS1(40):precision_gatestayed 7/7 throughout: none of these sites is a declared decoy.Why
Tier 1Q reads a qualifier as a module path — right for Rust
crate::util::foo(), where the target may sit anywhere under the path. It anchorsRack::MockRequeston the packageRackand then takes the uniquenewbeneath it. In Ruby a constant receiver names a class, and the target must be defined on that class (or its ancestors).newis the pathological name: Ruby classes almost never define it, so the raredef self.newabsorbs everyX.newin its namespace.And one correct bind was lost:
Server = IntegrationHelper::BaseServer; Server.all_async— a constant alias, thealias_qualified_refsvacuityCountBasisalready names.What would close it
A qualifier that names a container binds only a member whose parent is that container —
parent.qualified_nameequals the qualifier, or ends with it at a::boundary. That is one clause on a structural fact, and it is not Ruby-specific: PHPFoo::bar(), C#Foo.Bar()and RustType::method()have the same shape. It must not replace path anchoring for module qualifiers (crate::util::f), so the producer has to say which it is — probably a role bit on the ref, the wayTYPE_POSITIONis carried.Acceptance, per the usual bar:
Auth.resolve_userresolves toAuth::resolve_user.ruby_package_parityholds: the guest must be able to express the same bit.Sub.parent_methodwhereparent_methodis on a superclass stays unresolved rather than binding by name.Related: #294 (the fix that found this), #194 (Ruby's receiver-locality exclusion).