fix(go): a name written through an outside package is never a project symbol (#2233)

isVisibleAcrossFiles (before the same-file shortcut) rejects every Go
candidate for a reference its line writes as `pkg.Name` through an import
from outside the module (stdlib or third-party).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-01 02:09:41 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 27da256348
commit 32f681a398
3 changed files with 127 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In Go, a name written through an imported package from outside the module, like `context.Context`, `http.ResponseWriter` or testify's `require.Contains`, is no longer linked to a project symbol of that name. Before, fiber's `context.Context` parameters pointed at a method `Stream.Context`, its `require.Contains(…)` assertions at an extractor's `Contains`, and gin's `http.ResponseWriter` at its own `ResponseWriter` interface.
- Bare calls in Gradle Kotlin scripts (`build.gradle.kts`), like `plugins { }`, `dependencies { }` or `api(…)`, are no longer linked to a method or property of one of the project's own classes. Build logic's extension functions on Gradle's types still resolve. Before, nowinandroid's build scripts pointed every `plugins { }` and `dependencies { }` block at its build logic's `Graph` class, and every `api(…)` at a lint registry's `api` property.
- In Rust, `Self::Error` in a signature now resolves to the enclosing impl's or trait's own associated type. A path through a generic parameter, like `V::Value`, no longer resolves to an unrelated struct of that name. Before, serde's `Self::Error` signatures pointed at its one `struct Error`, and axum's `Self::Error` at `axum-core`'s error type.
- In Scala, a package object's member now resolves only where it is in scope: in its own package, the packages under it, or a file that imports through that package. Before, cats' laws resolved `Eq`, `Order` and `Monoid` to the `algebra` package object's aliases rather than the `cats` package object's, and sttp's akka tests reached a zio package object's `whenRequestMatches`.
@@ -0,0 +1,85 @@
/**
* A Go name written through an imported package from outside the module —
* `context.Context`, `http.ResponseWriter`, testify's `require.Contains` — is
* that package's, never a project symbol of the name: fiber's 85
* `context.Context` parameters went to a same-file method `Stream.Context`,
* and 403 `require.Contains(…)` to an extractor's `Contains`. A name through
* one of the module's own packages still resolves.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-ext-qual-'));
const files: Record<string, string> = {
'go.mod': 'module github.com/gofiber/fiber/v3\n\ngo 1.22\n',
'middleware/sse/sse.go': `package sse
import (
"context"
"github.com/gofiber/fiber/v3/extractors"
)
type Stream struct{}
func (s *Stream) Context() context.Context {
return nil
}
func run(ctx context.Context) bool {
return extractors.Contains("a", "b")
}
`,
'extractors/extractors.go': `package extractors
func Contains(haystack, needle string) bool {
return true
}
`,
'middleware/sse/sse_test.go': `package sse
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestStream(t *testing.T) {
require.Contains(t, "abc", "a")
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const targetsFrom = (file: string) => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind !== 'contains').map((e) => cg.getNode(e.target)!.qualifiedName);
};
describe('Go names through an outside package', () => {
it('are never a project symbol of that name', () => {
expect(targetsFrom('middleware/sse/sse.go')).not.toContain('Stream::Context');
expect(targetsFrom('middleware/sse/sse_test.go')).not.toContain('Contains');
});
it('while the module’s own packages still resolve', () => {
expect(targetsFrom('middleware/sse/sse.go')).toContain('Contains');
});
});
+41
View File
@@ -1300,6 +1300,42 @@ function rustModuleDir(filePath: string): string {
return path.posix.join(dir, base.replace(/\.rs$/, ''));
}
const GO_EXTERNAL_QUALIFIED = new WeakMap<ResolutionContext, Map<string, boolean>>();
/**
* Whether a Go reference is written through an imported package from outside
* the module — `context.Context`, `fmt.Errorf`, a third-party `gin.H` — read
* from its line, since the index keeps only the name.
*/
function isGoExternalQualified(ref: UnresolvedRef, context: ResolutionContext): boolean {
if (ref.referenceKind === 'imports') return false;
const name = ref.referenceName.split('.').pop()!;
if (!/^[A-Za-z_]\w*$/.test(name)) return false;
let memo = GO_EXTERNAL_QUALIFIED.get(context);
if (!memo) GO_EXTERNAL_QUALIFIED.set(context, (memo = new Map()));
const key = `${ref.filePath}\0${ref.line}\0${ref.column}\0${ref.referenceName}`;
const hit = memo.get(key);
if (hit !== undefined) return hit;
let external = false;
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
const at = Math.max(0, ref.column);
// The qualifier right before the name at the reference's column, or the
// line's only spelling of the name.
const before = line.startsWith(name, at) ? /(?:^|[^\w.])([A-Za-z_]\w*)\.$/.exec(line.slice(0, at))?.[1]
: !new RegExp(`(?<![\\w.])${name}\\b`).test(line) ? new RegExp(`(?:^|[^\\w.])([A-Za-z_]\\w*)\\.${name}\\b`).exec(line)?.[1] : undefined;
if (before) {
const imported = context.getImportMappings(ref.filePath, 'go').find((m) => m.localName === before);
if (imported) {
const mod = context.getGoModule?.();
const local = imported.source.startsWith('.') || imported.source.includes('/internal/') ||
(mod !== undefined && mod !== null && (imported.source === mod.modulePath || imported.source.startsWith(`${mod.modulePath}/`)));
external = !local;
}
}
memo.set(key, external);
return external;
}
const SCALA_OBJECT_PACKAGES = new WeakMap<ResolutionContext, Map<string, string | null>>();
/** The full package a Scala file's `package object X` opens (`algebra`, `cats.syntax`), or null for none. */
@@ -1413,6 +1449,10 @@ function isSfcPrivate(n: Node, context: ResolutionContext): boolean {
* checks its own survivor as well, since nothing runs after it.
*/
export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
// Go's `context.Context`, `http.Handler`: written through a package from
// outside the module, so nothing in it — not even the same file's method
// `Stream.Context` (fiber's 85 `context.Context` parameters went there).
if (ref.language === 'go' && candidate.language === 'go' && isGoExternalQualified(ref, context)) return false;
if (candidate.filePath === ref.filePath) return true;
// A vendored minified bundle's names are mangled: healthchecks' 369 `$(…)`
// (jQuery, a global) went to a one-letter helper inside bootstrap-native.min.js.
@@ -5907,6 +5947,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
PY_FIXTURE_TYPES.delete(context);
PY_PLUGGED_MODULES.delete(context);
SCALA_OBJECT_PACKAGES.delete(context);
GO_EXTERNAL_QUALIFIED.delete(context);
SCALA_SUPERS.delete(context);
SCALA_IMPORTS.delete(context);
ESM_EXPORT_LISTS.delete(context);