Skip to content

PHP: static call on a use-imported out-of-corpus class binds a same-short-named in-corpus class (#16 on the static path) (PE-80049) #73

Description

@filipechagas

Found while validating the fork on lawnstarter/signup-api for PE-80049 (https://lawnstarter.atlassian.net/browse/PE-80049).

Summary

A static call on a class whose use import points outside the corpus binds to an unrelated same-short-named in-corpus class, minting a wrong INFERRED 0.8 calls edge. The file's use statement explicitly disambiguates the receiver and is ignored.

This is the failure mode of closed issue #16, but on the static-call path rather than the property-typed-receiver path. The use-import-map work (#18#23, in particular #21 "PhpNameResolver decisive refusal") appears not to cover static receivers.

Real-world instance

In signup-api, app/Providers/AppServiceProvider.php:

use Illuminate\Support\Facades\Event;   // L22
...
Event::listen(ProspectCreated::class, ProspectCreatedListener::class);   // L147

Illuminate\Support\Facades\Event is a vendor class, outside the corpus. The repo also contains an unrelated App\Segment\Event (a Braze payload model, app/Segment/Event.php). The extractor emits:

{
  "relation": "calls",
  "confidence": "INFERRED",
  "confidence_score": 0.8,
  "source_file": "app/Providers/AppServiceProvider.php",
  "source_location": "L147",
  "source": "app_providers_appserviceprovider_appserviceprovider_boot",
  "target": "app_segment_event_event"
}

So graphify affected "App\Segment\Event" reports AppServiceProvider::boot() as a dependent of a Braze model it has nothing to do with — a wrong-node answer for the "who calls X" and "what breaks if X changes" shapes.

Minimal reproduction

Two files:

app/Segment/Event.php

<?php
namespace App\Segment;
class Event
{
    public function unrelated(): void {}
}

app/Providers/AppServiceProvider.php

<?php
namespace App\Providers;

use Illuminate\Support\Facades\Event;

class AppServiceProvider
{
    public function boot(): void
    {
        Event::listen('some.event', 'SomeListener');
    }
}

Then:

graphify update .
python3 -c "import json;g=json.load(open('graphify-out/graph.json'));[print(l) for l in g['links'] if l['relation']=='calls']"

Actual

One calls edge, app_providers_appserviceprovider_appserviceprovider_boot -> app_segment_event_event, INFERRED 0.8.

Expected

No edge. The use Illuminate\Support\Facades\Event; in this file binds the short name Event to an FQN outside the corpus, so the receiver should be refused decisively — the same refusal #21 applies elsewhere. Binding to App\Segment\Event requires ignoring an explicit, unambiguous import in the same file.

Why it matters beyond the false edge

Laravel repos import facades by short name constantly (Event, Cache, Log, Http, Auth, Queue, Config). Any repo that also has a domain class sharing one of those names gets silent cross-domain contamination of its blast-radius answers. In signup-api today only one edge is affected, but the trigger is a naming coincidence rather than anything the author did wrong.

Environment: graphify 0.9.40, fork origin/v8 @ 5e9d95c11837aeabbbb5d90cb45544f49dc58ca4, cold cache.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions