-
Notifications
You must be signed in to change notification settings - Fork 4
feat: Go structural interface matching and C# implements disambiguation (4.3) #522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
51d99b3
a49440c
772b038
72f1fb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ export function extractGoSymbols(tree, _filePath) { | |
|
|
||
| walkGoNode(tree.rootNode, ctx); | ||
| extractGoTypeMap(tree.rootNode, ctx); | ||
| matchGoStructuralInterfaces(ctx); | ||
| return ctx; | ||
| } | ||
|
|
||
|
|
@@ -360,6 +361,64 @@ function extractGoParameters(paramListNode) { | |
| return params; | ||
| } | ||
|
|
||
| // ── Go structural interface matching ───────────────────────────────────── | ||
|
|
||
| /** | ||
| * Go interfaces are satisfied structurally: a struct implements an interface | ||
| * if it has methods matching every method declared in the interface. | ||
| * This performs file-local matching (cross-file matching requires build-edges). | ||
| */ | ||
| function matchGoStructuralInterfaces(ctx) { | ||
| const interfaceMethods = new Map(); | ||
| const structMethods = new Map(); | ||
| const structLines = new Map(); | ||
|
|
||
| // Collect interface and struct definitions | ||
| const interfaceNames = new Set(); | ||
| const structNames = new Set(); | ||
| for (const def of ctx.definitions) { | ||
| if (def.kind === 'interface') interfaceNames.add(def.name); | ||
| if (def.kind === 'struct') { | ||
| structNames.add(def.name); | ||
| structLines.set(def.name, def.line); | ||
| } | ||
| } | ||
|
|
||
| // Collect methods grouped by receiver type | ||
| for (const def of ctx.definitions) { | ||
| if (def.kind !== 'method' || !def.name.includes('.')) continue; | ||
| const dotIdx = def.name.indexOf('.'); | ||
| const receiver = def.name.slice(0, dotIdx); | ||
| const method = def.name.slice(dotIdx + 1); | ||
|
|
||
| if (interfaceNames.has(receiver)) { | ||
| if (!interfaceMethods.has(receiver)) interfaceMethods.set(receiver, new Set()); | ||
| interfaceMethods.get(receiver).add(method); | ||
| } | ||
| if (structNames.has(receiver)) { | ||
| if (!structMethods.has(receiver)) structMethods.set(receiver, new Set()); | ||
| structMethods.get(receiver).add(method); | ||
| } | ||
| } | ||
|
|
||
| // Match: struct satisfies interface if it has all interface methods (name-only; | ||
| // signatures are not verified — treat as candidate match, not definitive). | ||
| // NOTE: embedded interfaces (type_elem nodes) are not resolved — composite | ||
| // interfaces like `type ReadWriter interface { Reader; Writer }` will have an | ||
| // empty method set and be silently excluded from matching. | ||
| for (const [structName, methods] of structMethods) { | ||
| for (const [ifaceName, ifaceMethods] of interfaceMethods) { | ||
| if (ifaceMethods.size > 0 && [...ifaceMethods].every((m) => methods.has(m))) { | ||
| ctx.classes.push({ | ||
| name: structName, | ||
| implements: ifaceName, | ||
| line: structLines.get(structName) || 1, | ||
| }); | ||
| } | ||
| } | ||
|
Comment on lines
+409
to
+418
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The structural matching only compares method names — not signatures. In Go, a method set is defined by both the method name and its signature, so this can generate incorrect For the file-local use case this is a pragmatic trade-off, but it's worth an explicit note in the JSDoc that the check is name-only (not signature-verified), so downstream consumers of the // Match: struct satisfies interface if it has all interface methods (name-only;
// signatures are not verified — use this as a candidate match, not a definitive one)
for (const [structName, methods] of structMethods) {
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added comment noting that matching is name-only (no signature verification) and that results should be treated as candidate matches, not definitive. |
||
| } | ||
| } | ||
|
|
||
| function extractStructFields(structTypeNode) { | ||
| const fields = []; | ||
| const fieldList = findChild(structTypeNode, 'field_declaration_list'); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a Go interface embeds another interface (e.g.
type ReadWriter interface { Reader; Writer }), tree-sitter represents the embedded names astype_elemnodes, notmethod_elemnodes. The extractor only collectsmethod_elemchildren for interface methods (seehandleGoTypeDecl), so embedded interface names are never added tointerfaceMethods.As a result,
ReadWriterwould end up withifaceMethods.size === 0and be treated like an empty interface — skipped silently. A struct that implementsRead()andWrite()would not be matched toReadWriter, even though it should be.This is an edge case that could confuse users of the
implementationscommand for composite interfaces. Consider adding a note in the JSDoc or a TODO tracking this gap.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — added inline note documenting the embedded interface (type_elem) limitation. Composite interfaces like ReadWriter will be silently excluded from matching until this is addressed.