mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 14:49:36 +08:00
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:
co-authored by
Claude Opus 5.5
parent
27da256348
commit
32f681a398
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user