mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-29 01:41:42 +00:00
fix(go): resolve registry primary edge cases
This commit is contained in:
parent
ec65a3cfbd
commit
bf42cf5e3e
24 changed files with 320 additions and 117 deletions
|
|
@ -1,5 +1,10 @@
|
|||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
|
||||
import {
|
||||
findNodeAtRange,
|
||||
nodeToCapture,
|
||||
syntheticCapture,
|
||||
type SyntaxNode,
|
||||
} from '../../utils/ast-helpers.js';
|
||||
import { getGoParser, getGoScopeQuery } from './query.js';
|
||||
import { recordGoCacheHit, recordGoCacheMiss } from './cache-stats.js';
|
||||
import { computeGoCallArity, computeGoDeclarationArity } from './arity-metadata.js';
|
||||
|
|
@ -8,37 +13,6 @@ import { synthesizeGoReceiverBinding } from './receiver-binding.js';
|
|||
import { synthesizeGoTypeBindings } from './type-binding.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
|
||||
/** Go builtin types that must not be qualified with a package prefix. */
|
||||
const GO_BUILTIN_TYPES = new Set([
|
||||
'bool',
|
||||
'byte',
|
||||
'comparable',
|
||||
'complex128',
|
||||
'complex64',
|
||||
'error',
|
||||
'float32',
|
||||
'float64',
|
||||
'int',
|
||||
'int16',
|
||||
'int32',
|
||||
'int64',
|
||||
'int8',
|
||||
'rune',
|
||||
'string',
|
||||
'uint',
|
||||
'uint16',
|
||||
'uint32',
|
||||
'uint64',
|
||||
'uint8',
|
||||
'uintptr',
|
||||
'any',
|
||||
]);
|
||||
|
||||
function inferPackageName(sourceText: string): string | null {
|
||||
const match = sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
export function emitGoScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
|
|
@ -56,7 +30,6 @@ export function emitGoScopeCaptures(
|
|||
|
||||
const rawMatches = getGoScopeQuery().matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
const pkgName = inferPackageName(sourceText);
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
|
|
@ -89,6 +62,8 @@ export function emitGoScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
if (isRawMultiAssignTypeBinding(tree.rootNode, grouped)) continue;
|
||||
|
||||
const declAnchor = grouped['@declaration.function'] ?? grouped['@declaration.method'];
|
||||
if (declAnchor !== undefined) {
|
||||
const fnNode =
|
||||
|
|
@ -167,38 +142,27 @@ export function emitGoScopeCaptures(
|
|||
});
|
||||
}
|
||||
|
||||
// Qualify same-package return-type captures: strip wrapper types
|
||||
// (`*`, `[]`, etc.) first, then prepend `pkg.` so the rawName
|
||||
// matches the qualified name we stamped on declarations.
|
||||
// Cross-package types (`*models.User`) already carry a dot and are
|
||||
// left as-is.
|
||||
if (pkgName !== null) {
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
const match = out[i]!;
|
||||
if (match['@type-binding.return'] === undefined) continue;
|
||||
const typeCap = match['@type-binding.type'];
|
||||
if (typeCap === undefined || typeCap.text.includes('.')) continue;
|
||||
let raw = typeCap.text.trim();
|
||||
while (raw.startsWith('*')) raw = raw.slice(1).trim();
|
||||
if (raw.startsWith('[]')) raw = raw.slice(2).trim();
|
||||
// Strip chan prefix so element type gets qualified:
|
||||
// chan Event → pkg.Event (correct for cross-receiver dispatch)
|
||||
// chan int → excluded by GO_BUILTIN_TYPES below (no phantom pkg.int)
|
||||
if (raw.startsWith('chan ')) raw = raw.slice(5).trim();
|
||||
if (raw.includes('.') || raw.startsWith('func(') || raw.startsWith('map[')) continue;
|
||||
if (GO_BUILTIN_TYPES.has(raw)) continue;
|
||||
const idx = raw.indexOf('[');
|
||||
if (idx !== -1) raw = raw.slice(0, idx);
|
||||
out[i] = {
|
||||
...match,
|
||||
'@type-binding.type': {
|
||||
name: '@type-binding.type',
|
||||
text: pkgName + '.' + raw,
|
||||
range: { ...typeCap.range },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function isRawMultiAssignTypeBinding(
|
||||
rootNode: SyntaxNode,
|
||||
grouped: Record<string, Capture>,
|
||||
): boolean {
|
||||
const anchor =
|
||||
grouped['@type-binding.constructor'] ??
|
||||
grouped['@type-binding.call-return'] ??
|
||||
grouped['@type-binding.assertion'];
|
||||
if (anchor === undefined) return false;
|
||||
|
||||
const node = findNodeAtRange(rootNode, anchor.range, 'short_var_declaration');
|
||||
if (node === null) return false;
|
||||
const lhs = node.childForFieldName('left');
|
||||
const rhs = node.childForFieldName('right');
|
||||
if (lhs === null) return false;
|
||||
if (rhs === null) return false;
|
||||
return (
|
||||
lhs.namedChildren.filter((c) => c.type === 'identifier').length >= 2 &&
|
||||
rhs.namedChildren.length >= 2
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export function interpretGoImport(captures: CaptureMatch): ParsedImport | null {
|
|||
if (kind === 'dot') return { kind: 'wildcard', targetRaw: source };
|
||||
if (kind === 'alias') {
|
||||
if (alias === undefined || name === undefined) return null;
|
||||
return { kind: 'alias', localName: alias, importedName: name, alias, targetRaw: source };
|
||||
return { kind: 'namespace', localName: alias, importedName: name, targetRaw: source };
|
||||
}
|
||||
if (kind === 'namespace') {
|
||||
if (name === undefined) return null;
|
||||
|
|
|
|||
|
|
@ -19,16 +19,38 @@ export function populateGoOwners(parsed: ParsedFile): void {
|
|||
// struct/interface declarations, this handles struct field ownership.
|
||||
populateClassOwnedMembers(parsed);
|
||||
|
||||
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
||||
if (moduleScope === undefined) return;
|
||||
populateGoOwnersInPackage([parsed]);
|
||||
}
|
||||
|
||||
export function populateGoWorkspaceOwners(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
ctx: { readonly fileContents: ReadonlyMap<string, string> },
|
||||
): void {
|
||||
const filesByPackage = new Map<string, ParsedFile[]>();
|
||||
for (const parsed of parsedFiles) {
|
||||
const pkgName = inferPackageName(ctx.fileContents.get(parsed.filePath) ?? '');
|
||||
if (pkgName === null) continue;
|
||||
const key = `${packageDir(parsed.filePath)}\0${pkgName}`;
|
||||
const bucket = filesByPackage.get(key) ?? [];
|
||||
bucket.push(parsed);
|
||||
filesByPackage.set(key, bucket);
|
||||
}
|
||||
|
||||
for (const bucket of filesByPackage.values()) {
|
||||
populateGoOwnersInPackage(bucket);
|
||||
}
|
||||
}
|
||||
|
||||
function populateGoOwnersInPackage(parsedFiles: readonly ParsedFile[]): void {
|
||||
// Build struct name → def map from ALL scopes' ownedDefs (struct defs
|
||||
// live in Class scopes now, not Module scope).
|
||||
const structByQualifiedName = new Map<string, string>(); // qname → nodeId
|
||||
for (const scope of parsed.scopes) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (isClassLike(def.type) && def.qualifiedName) {
|
||||
structByQualifiedName.set(def.qualifiedName, def.nodeId);
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const scope of parsed.scopes) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (isClassLike(def.type) && def.qualifiedName) {
|
||||
structByQualifiedName.set(def.qualifiedName, def.nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,37 +59,50 @@ export function populateGoOwners(parsed: ParsedFile): void {
|
|||
// scope whose typeBindings carry the self entry (kept there by
|
||||
// goBindingScopeFor). Match the self rawName against struct defs.
|
||||
if (structByQualifiedName.size > 0) {
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Function') continue;
|
||||
const methodDefs = scope.ownedDefs.filter(
|
||||
(d) => d.type === 'Method' && d.ownerId === undefined,
|
||||
);
|
||||
if (methodDefs.length === 0) continue;
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Function') continue;
|
||||
const methodDefs = scope.ownedDefs.filter(
|
||||
(d) => d.type === 'Method' && d.ownerId === undefined,
|
||||
);
|
||||
if (methodDefs.length === 0) continue;
|
||||
|
||||
// Find the self typeBinding in this Function scope.
|
||||
let receiverType: string | undefined;
|
||||
for (const [, tb] of scope.typeBindings) {
|
||||
if (tb.source === 'self') {
|
||||
receiverType = tb.rawName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (receiverType === undefined) continue;
|
||||
|
||||
let ownerId = structByQualifiedName.get(receiverType);
|
||||
if (ownerId === undefined) {
|
||||
for (const [qname, nodeId] of structByQualifiedName) {
|
||||
if (qname.endsWith('.' + receiverType)) {
|
||||
ownerId = nodeId;
|
||||
// Find the self typeBinding in this Function scope.
|
||||
let receiverType: string | undefined;
|
||||
for (const [, tb] of scope.typeBindings) {
|
||||
if (tb.source === 'self') {
|
||||
receiverType = tb.rawName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ownerId !== undefined) {
|
||||
for (const def of methodDefs) {
|
||||
(def as { ownerId?: string }).ownerId = ownerId;
|
||||
if (receiverType === undefined) continue;
|
||||
|
||||
let ownerId = structByQualifiedName.get(receiverType);
|
||||
if (ownerId === undefined) {
|
||||
for (const [qname, nodeId] of structByQualifiedName) {
|
||||
if (qname.endsWith('.' + receiverType)) {
|
||||
ownerId = nodeId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ownerId !== undefined) {
|
||||
for (const def of methodDefs) {
|
||||
(def as { ownerId?: string }).ownerId = ownerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function inferPackageName(sourceText: string): string | null {
|
||||
const match = sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function packageDir(filePath: string): string {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const idx = normalized.lastIndexOf('/');
|
||||
return idx === -1 ? '' : normalized.slice(0, idx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ const GO_SCOPE_QUERY = `
|
|||
left: (expression_list (identifier) @declaration.name)) @declaration.variable
|
||||
|
||||
;; Imports
|
||||
(import_declaration) @import.statement
|
||||
(import_spec) @import.statement
|
||||
|
||||
;; Type bindings — parameter annotations
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateGoOwners } from './method-owners.js';
|
||||
import { populateGoOwners, populateGoWorkspaceOwners } from './method-owners.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { loadGoModulePath } from '../../language-config.js';
|
||||
import { goProvider } from '../go.js';
|
||||
|
|
@ -36,6 +36,7 @@ export const goScopeResolver: ScopeResolver = {
|
|||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateGoOwners(parsed),
|
||||
populateWorkspaceOwners: (parsedFiles, ctx) => populateGoWorkspaceOwners(parsedFiles, ctx),
|
||||
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
|
|
|
|||
|
|
@ -11,18 +11,18 @@ export function synthesizeGoTypeBindings(rootNode: SyntaxNode): CaptureMatch[] {
|
|||
if (lhs === null) continue;
|
||||
|
||||
// Multi-assignment: pair LHS identifiers positionally with RHS
|
||||
// composite_literals or call_expressions. The tree-sitter query
|
||||
// produces all combinations; these correct pairings overwrite them.
|
||||
// expressions. The tree-sitter query produces all LHS/RHS
|
||||
// combinations; emit only the positions whose RHS carries an
|
||||
// inferable type.
|
||||
const lhsIds = lhs.namedChildren.filter((c) => c.type === 'identifier');
|
||||
const rhsExprs = right.namedChildren.filter(
|
||||
(c) => c.type === 'composite_literal' || c.type === 'call_expression',
|
||||
);
|
||||
const rhsExprs = right.namedChildren;
|
||||
if (lhsIds.length >= 2 && rhsExprs.length >= 2) {
|
||||
for (let i = 0; i < Math.min(lhsIds.length, rhsExprs.length); i++) {
|
||||
const lhsId = lhsIds[i]!;
|
||||
const rhsExpr = rhsExprs[i]!;
|
||||
const typeNode = extractTypeNode(rhsExpr);
|
||||
const typeName = typeNode ? extractSimpleTypeNameText(typeNode) : rhsExpr.text;
|
||||
if (typeNode === null) continue;
|
||||
const typeName = extractSimpleTypeNameText(typeNode);
|
||||
out.push({
|
||||
'@type-binding.multi-assign': syntheticCapture(
|
||||
'@type-binding.multi-assign',
|
||||
|
|
@ -239,12 +239,46 @@ function extractTypeNode(expr: SyntaxNode): SyntaxNode | null {
|
|||
null
|
||||
);
|
||||
}
|
||||
if (expr.type === 'unary_expression') {
|
||||
const operand = expr.childForFieldName('operand');
|
||||
return operand === null ? null : extractTypeNode(operand);
|
||||
}
|
||||
if (expr.type === 'call_expression') {
|
||||
const fn = expr.childForFieldName('function');
|
||||
if (fn?.type === 'identifier' && fn.text === 'new') {
|
||||
const args = expr.childForFieldName('arguments');
|
||||
return (
|
||||
args?.namedChildren.find((c) =>
|
||||
['type_identifier', 'qualified_type', 'pointer_type'].includes(c.type),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
if (fn?.type === 'identifier' && fn.text === 'make') {
|
||||
const args = expr.childForFieldName('arguments');
|
||||
const container = args?.namedChildren.find((c) =>
|
||||
['slice_type', 'map_type'].includes(c.type),
|
||||
);
|
||||
if (container?.type === 'slice_type') {
|
||||
return (
|
||||
container.namedChildren.find((c) =>
|
||||
['type_identifier', 'qualified_type'].includes(c.type),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
if (container?.type === 'map_type') {
|
||||
const typeChildren = container.namedChildren.filter((c) =>
|
||||
['type_identifier', 'qualified_type'].includes(c.type),
|
||||
);
|
||||
return typeChildren[1] ?? typeChildren[0] ?? null;
|
||||
}
|
||||
}
|
||||
if (fn?.type === 'identifier') return fn;
|
||||
if (fn?.type === 'selector_expression') {
|
||||
return fn.childForFieldName('field') ?? fn;
|
||||
}
|
||||
}
|
||||
if (expr.type === 'type_assertion_expression') {
|
||||
return expr.childForFieldName('type');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -395,6 +395,18 @@ export interface ScopeResolver {
|
|||
*/
|
||||
populateOwners(parsed: ParsedFile): void;
|
||||
|
||||
/**
|
||||
* Optional workspace-wide ownership reconciliation for languages whose
|
||||
* member owner can be declared in a different file from the owner type.
|
||||
* Runs after every file has had `populateOwners(parsed)` applied, but
|
||||
* still before `reconcileOwnership`, so stamped ownerIds are copied into
|
||||
* the semantic model registries.
|
||||
*/
|
||||
readonly populateWorkspaceOwners?: (
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
ctx: { readonly fileContents: ReadonlyMap<string, string> },
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Recognize a `super(...)`-style receiver text. Python returns
|
||||
* `/^super\s*\(/.test(t)`. Java returns `t === 'super'`. C++ may
|
||||
|
|
|
|||
|
|
@ -90,6 +90,14 @@ export function runScopeResolution(
|
|||
const onWarn = input.onWarn ?? (() => {});
|
||||
const PROF = process.env.PROF_SCOPE_RESOLUTION === '1';
|
||||
const tStart = PROF ? process.hrtime.bigint() : 0n;
|
||||
let fileContents: Map<string, string> | undefined;
|
||||
const getFileContents = (): Map<string, string> => {
|
||||
if (fileContents === undefined) {
|
||||
fileContents = new Map<string, string>();
|
||||
for (const f of files) fileContents.set(f.path, f.content);
|
||||
}
|
||||
return fileContents;
|
||||
};
|
||||
|
||||
// ── Phase 1: extract each file → ParsedFile ────────────────────────────
|
||||
const parsedFiles: ParsedFile[] = [];
|
||||
|
|
@ -111,6 +119,7 @@ export function runScopeResolution(
|
|||
provider.populateOwners(parsed);
|
||||
parsedFiles.push(parsed);
|
||||
}
|
||||
provider.populateWorkspaceOwners?.(parsedFiles, { fileContents: getFileContents() });
|
||||
|
||||
// Reconcile scope-resolution's ownership view into the SemanticModel.
|
||||
// See `reconcile-ownership.ts` for the full rationale (Contract
|
||||
|
|
@ -179,15 +188,6 @@ export function runScopeResolution(
|
|||
// class bindings when chasing return-type chains across files.
|
||||
// The hook writes to `bindingAugmentations` only; finalized
|
||||
// `indexes.bindings` remains immutable post-finalize (I8).
|
||||
let fileContents: Map<string, string> | undefined;
|
||||
const getFileContents = (): Map<string, string> => {
|
||||
if (fileContents === undefined) {
|
||||
fileContents = new Map<string, string>();
|
||||
for (const f of files) fileContents.set(f.path, f.content);
|
||||
}
|
||||
return fileContents;
|
||||
};
|
||||
|
||||
if (provider.populateNamespaceSiblings !== undefined) {
|
||||
provider.populateNamespaceSiblings(parsedFiles, indexes, {
|
||||
fileContents: getFileContents(),
|
||||
|
|
|
|||
3
gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/aliasimport
|
||||
|
||||
go 1.21
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/internal/util/log.go
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/internal/util/log.go
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package util
|
||||
|
||||
func Log() {}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/main.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/main.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import util "example.com/aliasimport/internal/util"
|
||||
|
||||
func main() {
|
||||
util.Log()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/samefactory
|
||||
|
||||
go 1.21
|
||||
10
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/main.go
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/main.go
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package main
|
||||
|
||||
func NewUser() *User {
|
||||
return &User{}
|
||||
}
|
||||
|
||||
func processUser() {
|
||||
user := NewUser()
|
||||
user.Save()
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/repo.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/repo.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
type Repo struct{}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/user.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-same-package-factory/user.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
type User struct{}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/splitowner
|
||||
|
||||
go 1.21
|
||||
6
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/main.go
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/main.go
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package main
|
||||
|
||||
func process() {
|
||||
user := User{}
|
||||
user.Save()
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/repo.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/repo.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
type Repo struct{}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/save.go
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/save.go
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package main
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/user.go
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-split-method-owner/user.go
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package main
|
||||
|
||||
type User struct{}
|
||||
|
|
@ -610,6 +610,30 @@ describe('Go return type inference via explicit function return type', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Go same-package factory return type inference', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'go-same-package-factory'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('resolves user.Save() through same-package NewUser() return type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(
|
||||
(c) => c.target === 'Save' && c.source === 'processUser' && c.targetFilePath === 'user.go',
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not resolve user.Save() to Repo.Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const repoSave = calls.find(
|
||||
(c) => c.target === 'Save' && c.source === 'processUser' && c.targetFilePath === 'repo.go',
|
||||
);
|
||||
expect(repoSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Go multi-return factory inference: user, err := NewUser("alice"); user.Save()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1261,6 +1285,47 @@ describe('Go cross-file binding propagation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Go aliased package selector resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'go-aliased-package-import'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('resolves util.Log() through an aliased package import', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const logCall = calls.find(
|
||||
(c) =>
|
||||
c.target === 'Log' && c.source === 'main' && c.targetFilePath === 'internal/util/log.go',
|
||||
);
|
||||
expect(logCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Go method owner resolution across package files', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'go-split-method-owner'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('resolves user.Save() to the method whose receiver type is declared in another package file', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(
|
||||
(c) => c.target === 'Save' && c.source === 'process' && c.targetFilePath === 'save.go',
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not resolve user.Save() to Repo.Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const repoSave = calls.find(
|
||||
(c) => c.target === 'Save' && c.source === 'process' && c.targetFilePath === 'repo.go',
|
||||
);
|
||||
expect(repoSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Go cmd/ helper files should NOT get entry-point multiplier (P0-1 fix)
|
||||
// Only main.go files should get the 3.0 entry-point boost, not arbitrary
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@ const tagNames = (matches: readonly Record<string, unknown>[]) =>
|
|||
matches.flatMap((m) => Object.keys(m));
|
||||
|
||||
describe('Go scope captures — smoke', () => {
|
||||
it('emits grouped imports once per import spec', () => {
|
||||
const src = `
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
`;
|
||||
const matches = emitGoScopeCaptures(src, 'main.go');
|
||||
const imports = matches
|
||||
.filter((m) => m['@import.source'] !== undefined)
|
||||
.map((m) => m['@import.source']!.text);
|
||||
|
||||
expect(imports).toEqual(['fmt', 'os']);
|
||||
});
|
||||
|
||||
it('emits module, struct, interface, function, method, import, call, read, write captures', () => {
|
||||
const src = `
|
||||
package main
|
||||
|
|
|
|||
|
|
@ -82,10 +82,9 @@ describe('Go import interpretation', () => {
|
|||
'@import.source': capt('@import.source', 'example.com/pkg/util'),
|
||||
});
|
||||
expect(result).toEqual({
|
||||
kind: 'alias',
|
||||
kind: 'namespace',
|
||||
localName: 'util',
|
||||
importedName: 'util',
|
||||
alias: 'util',
|
||||
targetRaw: 'example.com/pkg/util',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,6 +67,19 @@ describe('Go type binding synthesis — 7 patterns', () => {
|
|||
expect(qMatch).toBeDefined();
|
||||
});
|
||||
|
||||
it('keeps multi-assignment constructor bindings aligned with RHS positions', () => {
|
||||
const src = 'package main\nfunc main() {\n a, b := 42, X{}\n}';
|
||||
const bindings = emitGoScopeCaptures(src, 'main.go')
|
||||
.filter((m) => m['@type-binding.name'] !== undefined)
|
||||
.map((m) => ({
|
||||
name: m['@type-binding.name']!.text,
|
||||
type: m['@type-binding.type']!.text,
|
||||
}));
|
||||
|
||||
expect(bindings).toContainEqual({ name: 'b', type: 'X' });
|
||||
expect(bindings).not.toContainEqual({ name: 'a', type: 'X' });
|
||||
});
|
||||
|
||||
it('interprets assertion type binding', () => {
|
||||
const result = interpretGoTypeBinding({
|
||||
'@type-binding.assertion': {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue