Skip to content

analysis: Coverage.Issues is a dead contract field — declared, normalized, serialized, never populated #137

Description

@JordanCoin

analysis.Coverage publishes three things: status, sources, and issues. The third is永 always empty, on every repository, in every mode, regardless of what happened during the scan.

Evidence

The only producer sets it to an empty slice and nothing ever adds to it — scanner/outcome.go:97:

coverage := analysis.Coverage{
    Sources: append([]analysis.Source(nil), sources...),
    Issues:  []analysis.Issue{},          // never appended to, anywhere
}

rg -n "Issues" --type go finds only the declaration (analysis/contracts.go:47), the normalizer that clones and sorts it (contracts.go:52-74), and that one empty literal. There is no write path.

Confirmed behaviourally on every repo I tried, including ones with genuinely unresolvable references:

$ codemap --json --deps <repo with a missing local import> | jq .coverage
{"status": "complete", "sources": [{"name":"ast-grep","status":"authoritative"}], "issues": []}

Why it matters

The scanner drops references constantly and by design — unparseable import forms, targets absent from disk, ambiguous matches, macro-generated and dynamically-constructed paths. Every one of those is discarded with a bare continue / return nil. The contract then reports issues: [], which a consumer reads as "nothing was dropped."

This is the same defect class as #132 and #136, but at the contract layer rather than in one resolver: it is the field whose entire job is to disclose gaps, and it discloses nothing. It is also why #132 and #136 stayed invisible — had issues been real, a TypeScript repo losing 515 edges and a Python package losing all 31 intra-package imports would both have announced themselves.

NormalizeCoverage sorting and cloning a permanently-empty slice is a good indicator: the plumbing was built and the producer never was.

Scope note

The drop sites are in at least three places, in more than one resolver — the generic path in scanner/filegraph.go, the Rust path in scanner/rustgraph.go, and extraction-time discards in scanner/astgrep.go where if path != "" / if mod != "" guards throw matches away before resolution is even attempted. That last one is easy to miss because it happens upstream of anything that looks like resolution.

The distinction that decides whether this is useful or noise: a reference the scanner FAILED to resolve is not the same as one it INTENTIONALLY does not resolve. Go stdlib, third-party modules, and external crates are out of scope by design; reporting them produces hundreds of false entries on any real repo and makes the array worse than empty. I measured one implementation that got this wrong: 887 entries on this repository, all standard library.

Trap for whoever implements this

ImportReference.Line is populated from m.Range.Start.Line (scanner/astgrep.go:373), and ast-grep's range is 0-based. Today this is invisible because FileAnalysis.References is json:"-" and no surface exposes a line number. The moment issues carry Line, every reported line is off by one unless converted. Two separate implementations hit this.

Related: #132, #136 (both would have been disclosed by a working issues), #111 (per-query granularity), #133 (the consumer that makes it actionable).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions