Skip to content

Fix/cgo line directives - #282

Open
JBacchelli wants to merge 3 commits into
scip-code:mainfrom
JBacchelli:fix/cgo-line-directives
Open

Fix/cgo line directives#282
JBacchelli wants to merge 3 commits into
scip-code:mainfrom
JBacchelli:fix/cgo-line-directives

Conversation

@JBacchelli

@JBacchelli JBacchelli commented Aug 3, 2026

Copy link
Copy Markdown

Anchor cgo occurrences to real .go source via //line directives

Problem

scip-go attributes cgo occurrences to files that do not exist in the repository.

For a package that uses cgo, the go command compiles rewritten sources out of the
build cache ($GOCACHE/.../foo.cgo1.go, _cgo_gotypes.go). Fset.File(pos).Name()
returns those cache paths, so documents are emitted keyed by e.g.
../../../home/me/.cache/go-build/6c/6c45706…-d, and the real .go file gets no
document at all.

On gravitational/teleport (2767 Go files, 12 cgo packages) this is total: 44 of
44
_Cfunc_/_C2func_ references — every Go→C call site in the repo — land in
build-cache documents. Anything built on the index (cross-references, call graphs,
go-to-definition) points at paths the checkout does not contain.

Occurrence ranges were already //line-adjusted; only document attribution was
physical.

Fix

Key each document by its //line-adjusted origin and route occurrences
per-occurrence rather than per-file:

  • visitors.OriginFile resolves a position through Fset.Position, so cgo's
    //line directives map it back to the real source; CleanResolve resolves
    symlinks so origins stay comparable to pkg.GoFiles.
  • RealGoFiles gates which origins get a document. Occurrences whose origin is not
    real source — compiler glue, a yacc .y, a bare build-cache path — are dropped
    instead of mis-attributed.
  • A generated file can legitimately attribute occurrences to several real sources,
    so fileVisitor carries the whole document map and targetDoc routes each
    occurrence individually.
  • Document.InBounds rejects ranges that escape the origin's source (negative
    columns from a column-less //line, positions past EOF, reversed ranges) rather
    than emitting a location downstream SCIP consumers reject.
  • Document.RepairRange handles the case where the range is only too wide: cgo
    rewrites C.puts to _Cfunc_puts before type checking, and range width comes
    from the identifier the checker reports, so a correctly-positioned occurrence is
    emitted 5 characters too wide and can spill past end-of-line. An out-of-bounds
    range gets one repair attempt that re-measures the identifier actually present at
    that column, accepted only if the result is in bounds — so it never invents a
    location.

Supporting changes: loader requests packages.NeedFiles so pkg.GoFiles is
populated, and RangeFromName / scipRange return a scip.Range instead of a bare
[]int32 so ranges can be bounds-checked and rewritten as a value.

Result

teleport, same commit and toolchain, counting _C[2]func_ references and the Go→C
call edges they resolve to:

refs in build cache resolved edges
before 44 44 8, all unusable
after 27 0 7, all on real source

The raw reference count drops because occurrences that only ever existed in
generated glue are no longer emitted against fabricated paths. What remains is
anchored in real source: lib/srv/uacc/uacc_linux.go, lib/shell/shell_unix.go,
lib/inventory/metadata/metadata_linux.go, lib/system/signal.go.

Known gaps

  • C.free accounts for 16 of the references not emitted on teleport. They sit
    inside defer C.free(...), whose rewritten wrapper reports end-of-line positions
    with no identifier to measure. All libc, so they resolve to no definition either
    way.
  • One call site — teleport's 5-argument, multi-line C.mygetpwnam_r(...) with
    pointer arguments — is still absent. It is not rejected by the bounds check, so
    the cause is elsewhere and is not addressed here.
  • A widened range that happens to stay in bounds is kept unrepaired and is therefore
    too wide (C.add emitted covering C.add(C.in). Re-measuring unconditionally
    would fix it, but package references are deliberately sized from the import path
    rather than the source token (emitImportReference), so a blanket re-measure would
    truncate github.com/foo/bar to github. Fixing it properly needs the range
    builder to know whether the name it was handed is the source spelling.

Testing

go test ./... passes; no snapshot goldens changed. New unit tests cover InBounds
(well-formed, past-EOF, negative, reversed, unreadable-source) and RepairRange
(recovery, seven rejection cases, unreadable source, non-ASCII byte widths).
Validated end to end on teleport and navidrome by indexing each repo and checking
that every emitted cgo call site resolves inside the checkout.

Jacopo Bacchelli and others added 3 commits July 30, 2026 07:55
scip-go emitted one SCIP Document per *compiled* Go file, keyed by
pkg.Fset.File(f.Package).Name(). For cgo packages that physical file is
cgo's generated output under the build cache (foo.cgo1.go,
_cgo_gotypes.go), so every occurrence in a cgo file -- including each
`C.<fn>` call site -- was attributed to an ephemeral GOCACHE path
instead of the user's real .go source.

Occurrence *ranges* were already computed with Fset.Position(), which
honors the //line directives cgo emits, so line/column were correct --
only the document they were attached to was wrong.

Fix: key documents (and route each occurrence) by the //line-adjusted
origin file, and drop occurrences that don't map to real source:

- loader: request NeedFiles so pkg.GoFiles is populated.
- visitors.OriginFile(pkg, pos): the cleaned, //line-adjusted path for a
  position. visitors.RealGoFiles(pkg): the package's on-disk source set.
- VisitPackageSyntax / index.Index / ListMissing: key documents by
  OriginFile instead of the physical compiled path, and skip files whose
  origin is not real source (e.g. cgo's _cgo_gotypes.go glue).
- fileVisitor: drop occurrences whose //line-adjusted position resolves
  to another file, or to a line/column outside the origin. cgo rewrites
  such as `defer C.f(x)` and inserted thunks like _cgoCheckPointer
  otherwise yield out-of-bounds ranges that downstream SCIP consumers
  reject.

Non-cgo packages are unaffected (origin == physical file), so existing
snapshots are unchanged. For cgo, `C.<fn>` references now resolve to
the real <file>.go at the call site.
The previous commit attributed each document to its file's //line origin and
dropped any occurrence that didn't fit that one file. That is safe for cgo and
for all //line-free code, but a generated file whose //line points at a
*different* real source file would lose those occurrences.

Generalize it: accumulate occurrences on the document of each occurrence's own
//line-resolved origin (via a shared path->document map), emitting one document
per source file at the end. An occurrence is dropped only when its origin is not
a real source document (cgo's _cgo_gotypes.go glue, a yacc .y, a build-cache
path) or its range does not fit that source.

- Paths are compared symlink-resolved (CleanResolve), so a module reached
  through a symlinked directory no longer drops real files as "not a GoFile".
- InBounds now also rejects negative line/column (a "//line file:N" directive
  with no column collapses to column 0, i.e. scip -1) and reversed ranges, so
  routing can never emit a malformed occurrence.
- Document owns occurrence/symbol accumulation, bounds checking, and ToScip
  rendering; the file visitor routes via targetDoc and attaches symbols in Finish.

Verified: existing snapshots are byte-identical (no regression); protobuf/normal
Go output is byte-identical to upstream; cgo Go->C refs still anchor to the real
.go; a //line-to-another-real-.go file now routes there; malformed (negative
column) occurrences are dropped rather than emitted. InBounds unit-tested.
cgo rewrites `C.puts` to `_Cfunc_puts` before the type checker sees it, and an
occurrence's range is sized from the identifier the checker reports. Once the
`//line` directives map the position back to the original source the start
column is right, but the range is five characters too wide and can run past the
end of the real line. Those occurrences were rejected as out of bounds even
though they were perfectly well located.

Give an out-of-bounds range one repair attempt before dropping it: measure the
identifier actually present at that column in the origin source and re-emit with
that width. The repair is only accepted if the result is in bounds, so it never
invents a location -- a synthesized position such as cgo's `defer C.f(x)`
wrapper, which lands at end-of-line where there is no identifier, finds nothing
to measure and is still dropped.

Measured on gravitational/teleport (2767 Go files, 12 cgo packages), same commit
and same toolchain, counting `_C[2]func_` references and the Go->C call edges
they resolve to:

    unpatched scip-go     44 refs / 8 edges  -- but every one anchored in
                                                ~/.cache/go-build, so unusable
    before this commit    25 refs / 6 edges  -- all on real source
    after  this commit    27 refs / 7 edges  -- all on real source

The two recovered references are `C.gnu_get_libc_version` and
`C.resetInterruptSignalHandler`; the second adds a bridged edge in a package that
previously contributed none. Multi-line calls are covered too: `C.fill(cs,\n
&out,\n C.int(1))` is recovered with a range covering exactly `C.fill`.

Two known gaps remain, both distinct from what this fixes:

  - `C.free` accounts for 16 of the still-missing teleport references. They sit
    inside `defer C.free(...)`, whose rewritten wrapper reports end-of-line
    positions with no identifier to measure. They are libc calls with no C
    definition in the indexed surface, so they never produced edges regardless.

  - One call site, teleport's 5-argument `C.mygetpwnam_r(...)` spanning several
    lines with pointer arguments, is still absent. It is not rejected by the
    bounds check -- no drop is recorded for it -- so it never reaches the
    occurrence-emitting path at all, and the cause is elsewhere. Reproducible
    with a `getpwnam_r`-style wrapper called with `&pwd` and an
    `unsafe.Pointer`-derived cast.

A related wart this deliberately does not touch: a mangled range that happens to
stay in bounds is kept unrepaired and is therefore too wide -- `C.add` is emitted
covering `C.add(C.in`. Re-measuring every range would fix that, but package
references are sized from the import path rather than the source token
(emitImportReference), so a blanket re-measure would truncate `github.com/foo/bar`
to `github`. Fixing it properly needs the range builder to know whether the name
it was handed is the source spelling.
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