From 9e42b4946f08f17e99af5c526cf3601c13f7f01b Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 05:47:20 -0500 Subject: [PATCH 1/2] fix(go): a type declared on its own keeps its doc comment tree-sitter-go puts the comment above `type Foo struct{}` before the `type_declaration`, but the node is made from the spec inside it, so both extractors found no docstring for an ungrouped struct, interface, defined type or alias. A type inside a `type ( ... )` group kept its own comment, which sits beside it in the parentheses. Go's docstring is now read from the declaration when the declaration holds one spec and nothing precedes that spec inside it. The leading comment of a group with several members stays the group's. Reading from the declaration exposed comments that trail the line above: `const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package` would have become `type MetricType int32`'s doc. For every Go declaration, a comment that begins after code on its line, and one that begins on the line such a comment ends, is now left out, as go/parser reads them. Seven function docstrings on etcd and prometheus lose one. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/fixtures/kernel-parity/torture.go | 10 +- __tests__/go-type-doc-comments.test.ts | 212 ++++++++++++++++++++ codegraph-kernel/src/docstring.rs | 80 +++++++- codegraph-kernel/src/go.rs | 35 +++- src/extraction/languages/go.ts | 17 ++ src/extraction/tree-sitter-helpers.ts | 38 +++- src/extraction/tree-sitter-types.ts | 11 +- src/extraction/tree-sitter.ts | 3 +- 9 files changed, 388 insertions(+), 19 deletions(-) create mode 100644 __tests__/go-type-doc-comments.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa4c1d08..80707f742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, calls into a module whose `go.mod` is not at the project root now resolve, whether it is a `server/` backend next to a `web/` frontend or one of several modules side by side as in etcd, so calls like `store.New()` and `s.db.CreateItem()` find their targets instead of being treated as calls into a third-party package. A name written through a package, like a `job.OPCommand` result type or a field of type `artifact.Manager`, now links to that package's symbol rather than a same-named one elsewhere, which also corrects links in projects with a single `go.mod`. Re-index Go projects after upgrading. Thanks @GoDiao for the report and @danusha2345 for the fix. (#2322) - In Go, an exported type written without a package, like `Node` in `func Walk(v Visitor, node Node)`, now links to the type of that name in its own package, whether it is a struct, an interface or another kind of type. Before, it could link to a struct of the same name in an unrelated package, so prometheus's PromQL parser functions pointed at the Kubernetes discovery `Node` struct instead of the parser's own `Node` interface, or, for a result type like `Appender` in `func (f *fanout) Appender(…) Appender`, to the method itself. A type from a package outside your project, like `apiv1.Node` or `http.Handler`, no longer links to a struct of the same name in the file that uses it; a variadic parameter like `...storage.Filter` now links to that package's type; a route handler written as a method value, like `h.Follow` in an Echo or Gin app, links to that method rather than a model struct named `Follow`; and a method called on another call's result or on an indexed value, like `err[i].Error()` or a chained `.String()`, is no longer recorded as creating a struct of the same name. Re-index Go projects after upgrading. - In Go, a function or method passed as a value, like `sync.Pool{New: wm.new}`, now shows the function that passes it among its callers and impact even when it is named `new`, `nil`, `None`, `self` or a handful of similar words, or when a line break or comment sits beside the dot. Before, such a value was skipped, so a method used only that way looked unused. Re-index Go projects after upgrading. +- In Go, a type declared on its own, like `type Point struct{…}` under a `// Point is …` comment, now keeps that comment as its documentation, as a type inside a `type ( … )` group already did, so searching for what a type does can find it. A comment at the end of a line of code, like `const sides = 4 // sides of a square.`, is no longer taken as the documentation of the declaration under it. Re-index Go projects after upgrading. - Indexing a project that includes large bundled JavaScript files, such as a copy of pdf.js or d3, is fast again: since 1.6.2, resolving the calls in a JavaScript or TypeScript file re-read the file's text above each call, so a single bundled library could add many seconds to an index. The graph it builds is unchanged. Thanks @bompus for the report. (#2334) - The time `codegraph init` and `codegraph index` print beside the node and edge counts now covers the whole run, resolving references and linking included, and `codegraph sync` reports its whole run the same way. Before, it counted only reading and parsing the files, which can be a small part of an index, so a slow index looked fast. Thanks @bompus for the report. (#2334) - In Rust, code that uses an enum only through its variants, like `mode::Mode::A` in an expression, a `Mode::B =>` match arm or `Self::A` inside the enum's own `impl`, now shows up in that enum's callers and impact, linked to the enum that actually declares the variant rather than to a same-named type elsewhere. Standard-library variants like `Ordering::Less` or `Option::Some` and associated items like `Foo::new()` or `Foo::MAX` don't count as using a project enum. Re-index Rust projects after upgrading. Thanks @mg-mg-mg for the report and @danusha2345. (#2328) diff --git a/__tests__/fixtures/kernel-parity/torture.go b/__tests__/fixtures/kernel-parity/torture.go index 3aada94af..9c8eed406 100644 --- a/__tests__/fixtures/kernel-parity/torture.go +++ b/__tests__/fixtures/kernel-parity/torture.go @@ -6,7 +6,7 @@ import ( pkga "example.com/other/pkga" ) -const MAX_ITEMS = 128 +const MAX_ITEMS = 128 // the line's own comment, not DefaultRegistry's doc var DefaultRegistry = NewRegistry() @@ -14,6 +14,7 @@ var handlerTable = map[string]func(int){ "recv": TargetCb, } +// Widget is documented above its own type declaration. type Widget struct { *Base Queryable @@ -28,6 +29,13 @@ type Stack[T any] struct { items []T } +// Units of time: the group's comment, no member's doc. +type ( + Seconds int + // Minutes is documented inside its group. + Minutes int +) + type Core interface { Reader pkga.Closer // qualified diff --git a/__tests__/go-type-doc-comments.test.ts b/__tests__/go-type-doc-comments.test.ts new file mode 100644 index 000000000..9998aac4b --- /dev/null +++ b/__tests__/go-type-doc-comments.test.ts @@ -0,0 +1,212 @@ +/** + * A Go type declared on its own lost its doc comment in both extractors: + * + * // Foo is documented. + * type Foo struct{} + * + * tree-sitter-go wraps every type in a `type_declaration`, and the comment is + * that declaration's previous sibling. The node is made from the spec inside + * it, whose only predecessor is the `type` keyword, so the docstring walk + * found nothing. A type inside a `type ( … )` group kept its comment, which + * sits beside it in the parentheses. + * + * A declaration holding one spec is now read from outside, as go doc reads + * it. A group's leading comment stays the group's: it documents the group, + * not its first member. + * + * Reading from outside the declaration exposed a gap Go functions already + * had: a comment written after code on the line above (`const sides = 4 // + * sides of a square.`) belongs to that line, not to the declaration below + * it, and the docstring now starts after it. + * + * Runs against the native kernel (when built) and the wasm extractor, which + * must agree. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { ExtractionResult } from '../src/types'; + +const KERNEL_PATH = path.join( + __dirname, + '..', + 'codegraph-kernel', + 'prebuilds', + `${process.platform}-${process.arch}`, + 'codegraph-kernel.node' +); +const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1'; + +const SOURCE = `// Copyright 2026 The Shapes Authors. + +// Package shapes is documented. +package shapes + +type Bare struct{} + +// Point is documented on its own. +type Point struct{ X, Y int } + +// Shape is an interface documented on its own. +type Shape interface { + Area() float64 +} + +// ID is a defined type documented on its own. +type ID int + +// Set is a generic type documented on its own. +type Set[T comparable] map[T]struct{} + +/* +Box is documented in a block comment. +*/ +type Box struct{} + +// Kinds of shapes. The comment documents the group. +type ( + Circle struct{} + + // Square is documented inside the group. + Square struct{} +) + +// Lone is the only member of its group. +type ( + Lone int +) + +// A group of one whose member has its own comment. +type ( + // Own is documented inside its group. + Own int +) + +// Pair groups a type with its alias. +type ( + First int + Second = First +) + +type ( + Celsius float64 // degrees C. + Kelvin float64 +) + +// sides is documented above its line. +const sides = 4 /* four */ // sides of a square. + +// Rect is documented below a constant's line. +type Rect struct{} + +var origin = Point{} // the origin. +type Polygon struct{} + +// Area is documented as before. +func Area(s Shape) float64 { return s.Area() } + +// Norm is documented as before. +func (p Point) Norm() int { return p.X } // Norm's line comment. +func Next() int { return 0 } +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +describe('Go doc comments on types declared on their own', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['go']); + }); + + beforeEach(() => { + savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + resetKernelForTests(); + }); + + afterEach(() => { + for (const k of ENV_KEYS) { + if (savedEnv[k] === undefined) delete process.env[k]; + else process.env[k] = savedEnv[k]; + } + resetKernelForTests(); + }); + + function extract(backend: 'kernel' | 'wasm', source: string): ExtractionResult { + if (backend === 'wasm') { + process.env.CODEGRAPH_KERNEL = '0'; + return extractFromSource('shapes/shapes.go', source, 'go'); + } + delete process.env.CODEGRAPH_KERNEL; + process.env.CODEGRAPH_KERNEL_LANGS = 'all'; + const result = tryKernelExtract('shapes/shapes.go', source, 'go'); + expect(result, 'kernel extraction').not.toBeNull(); + return result!; + } + + const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const); + + for (const crlf of [false, true]) { + it.each(backends)(`%s${crlf ? ' (CRLF)' : ''}`, (backend) => { + const result = extract(backend, crlf ? SOURCE.replace(/\n/g, '\r\n') : SOURCE); + const doc = (kind: string, name: string): string | null => { + const found = result.nodes.filter((n) => n.kind === kind && n.name === name); + expect(found, `${kind} ${name}`).toHaveLength(1); + return found[0].docstring ?? null; + }; + + expect({ + // A type declared on its own takes the comment above `type`, and the + // file's header and package comment stay before the package clause. + Bare: doc('struct', 'Bare'), + Point: doc('struct', 'Point'), + Shape: doc('interface', 'Shape'), + ID: doc('type_alias', 'ID'), + Set: doc('type_alias', 'Set'), + Box: doc('struct', 'Box'), + // A group's comment is the group's; a member's own comment is found + // beside it. A group of one is that type's declaration, as in go doc, + // unless its member has a comment of its own. + Circle: doc('struct', 'Circle'), + Square: doc('struct', 'Square'), + Lone: doc('type_alias', 'Lone'), + Own: doc('type_alias', 'Own'), + // An alias is one of a group's members too. + First: doc('type_alias', 'First'), + // A comment after code is that line's, whatever comes below it. + Celsius: doc('type_alias', 'Celsius'), + Kelvin: doc('type_alias', 'Kelvin'), + sides: doc('constant', 'sides'), + Rect: doc('struct', 'Rect'), + Polygon: doc('struct', 'Polygon'), + Area: doc('function', 'Area'), + Norm: doc('method', 'Norm'), + Next: doc('function', 'Next'), + }).toEqual({ + Bare: null, + Point: 'Point is documented on its own.', + Shape: 'Shape is an interface documented on its own.', + ID: 'ID is a defined type documented on its own.', + Set: 'Set is a generic type documented on its own.', + Box: 'Box is documented in a block comment.', + Circle: null, + Square: 'Square is documented inside the group.', + Lone: 'Lone is the only member of its group.', + Own: 'Own is documented inside its group.', + First: null, + Celsius: null, + Kelvin: null, + sides: 'sides is documented above its line.', + Rect: "Rect is documented below a constant's line.", + Polygon: null, + Area: 'Area is documented as before.', + Norm: 'Norm is documented as before.', + Next: null, + }); + }); + } +}); diff --git a/codegraph-kernel/src/docstring.rs b/codegraph-kernel/src/docstring.rs index 9b307ca83..22e499c23 100644 --- a/codegraph-kernel/src/docstring.rs +++ b/codegraph-kernel/src/docstring.rs @@ -142,6 +142,24 @@ pub fn preceding_docstring_stepping_over( node: Node, src: &str, step_over: &[&str], +) -> Option { + preceding_docstring_with(node, src, step_over, false) +} + +/// getPrecedingDocstring's `skipTrailing` (Go) — the comments the run opens +/// with that belong to the line above are left out: one written after code on +/// its line (`const n = 4 // four.`), and any that begins on the line such a +/// comment ends. Go reads them as that line's comment, never the next +/// declaration's doc. +pub fn preceding_docstring_skipping_trailing(node: Node, src: &str) -> Option { + preceding_docstring_with(node, src, &[], true) +} + +fn preceding_docstring_with( + node: Node, + src: &str, + step_over: &[&str], + skip_trailing: bool, ) -> Option { let mut anchor = node; while let Some(parent) = anchor.parent() { @@ -152,11 +170,11 @@ pub fn preceding_docstring_stepping_over( } } - let mut comments: Vec<&str> = Vec::new(); + let mut comments: Vec = Vec::new(); let mut sibling = anchor.prev_named_sibling(); while let Some(s) = sibling { if is_comment(s.kind()) { - comments.push(&src[s.byte_range()]); + comments.push(s); sibling = s.prev_named_sibling(); } else if step_over.contains(&s.kind()) { sibling = s.prev_named_sibling(); @@ -164,14 +182,31 @@ pub fn preceding_docstring_stepping_over( break; } } - if comments.is_empty() { + comments.reverse(); // collected nearest-first; TS unshifts to keep source order + + let mut first = 0; + if skip_trailing { + let mut end_row = 0; + for c in &comments { + let trails = if first == 0 { + follows_code_on_its_line(*c, src) + } else { + c.start_position().row == end_row + }; + if !trails { + break; + } + end_row = c.end_position().row; + first += 1; + } + } + if first == comments.len() { return None; } - comments.reverse(); // collected nearest-first; TS unshifts to keep source order Some( - comments + comments[first..] .iter() - .map(|c| clean_comment_markers(c)) + .map(|c| clean_comment_markers(&src[c.byte_range()])) .collect::>() .join("\n") .trim() @@ -179,6 +214,13 @@ pub fn preceding_docstring_stepping_over( ) } +/// followsCodeOnItsLine (tree-sitter-helpers.ts) — whether code comes before +/// `node` on the line it starts on. +fn follows_code_on_its_line(node: Node, src: &str) -> bool { + let before = src[..node.start_byte()].trim_end_matches([' ', '\t']); + !before.is_empty() && !before.ends_with(['\n', '\r']) +} + #[cfg(test)] mod tests { use super::*; @@ -231,4 +273,30 @@ mod tests { Some("a\nb") ); } + + /// Skipping trailing comments leaves out one written after code on its + /// line and one beginning on the line it ends; a comment on a line of its + /// own still opens the run. Without it, nothing is left out. + #[test] + fn skips_the_comments_that_trail_code() { + let grammar = crate::langs::grammar_for("go").expect("go grammar"); + let mut parser = tree_sitter::Parser::new(); + parser.set_language(&grammar).unwrap(); + let src = "package p\n\nconst n = 4 /* a */ // b\n// c\nfunc f() {}\n\nvar m = 1 // d\nfunc g() {}\n"; + let tree = parser.parse(src, None).unwrap(); + let root = tree.root_node(); + let func = |name: &str| { + (0..root.named_child_count()) + .filter_map(|i| root.named_child(i)) + .find(|n| { + n.kind() == "function_declaration" + && n.child_by_field_name("name").map(|id| &src[id.byte_range()]) == Some(name) + }) + .expect(name) + }; + assert_eq!(preceding_docstring(func("f"), src).as_deref(), Some("a\nb\nc")); + assert_eq!(preceding_docstring_skipping_trailing(func("f"), src).as_deref(), Some("c")); + assert_eq!(preceding_docstring(func("g"), src).as_deref(), Some("d")); + assert_eq!(preceding_docstring_skipping_trailing(func("g"), src), None); + } } diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index 3a93bacf1..1febed072 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -16,7 +16,7 @@ use crate::buffers::{ build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow, RefRow, StrRef, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR, }; -use crate::docstring::preceding_docstring; +use crate::docstring::preceding_docstring_skipping_trailing; use crate::ids; use crate::textutil as util; use regex::Regex; @@ -380,6 +380,31 @@ impl<'t> Walker<'t> { receiver_re().captures(text).map(|c| c[1].to_string()) } + /// goExtractor.getDeclarationWrapper: a type declared on its own, `type + /// Foo struct{…}`, is a `type_declaration` holding one spec, and its doc + /// comment comes before the declaration, outside the spec. A member of a + /// `type ( … )` group has its comment beside it, and the comment above the + /// group is the group's. A group of one is that type's declaration, as go + /// doc reads it, unless its member has a comment of its own. + fn declaration_wrapper(&self, node: Node<'t>) -> Option> { + let parent = node.parent()?; + if parent.kind() != "type_declaration" || parent.named_child(0)? != node { + return None; + } + let specs = (0..parent.named_child_count()) + .filter_map(|i| parent.named_child(i)) + .filter(|c| matches!(c.kind(), "type_spec" | "type_alias")) + .count(); + (specs == 1).then_some(parent) + } + + /// docstringFor (tree-sitter.ts) — the preceding comment run, looked up + /// from the declaration wrapper when there is one, without the comments + /// that trail the code above it (docstringSkipsTrailingComments). + fn docstring_of(&self, node: Node<'t>) -> Option { + preceding_docstring_skipping_trailing(self.declaration_wrapper(node).unwrap_or(node), self.src) + } + // --- visitNode ------------------------------------------------------------ fn visit_node(&mut self, node: Node<'t>) { @@ -465,7 +490,7 @@ impl<'t> Walker<'t> { return; } let extra = Extra { - docstring: preceding_docstring(node, self.src), + docstring: self.docstring_of(node), signature: self.signature_of(node), is_exported: Some(self.is_exported(node)), return_type: self.return_type_of(node), @@ -487,7 +512,7 @@ impl<'t> Walker<'t> { let receiver_type = self.receiver_type_of(node); let name = self.extract_name(node); let extra = Extra { - docstring: preceding_docstring(node, self.src), + docstring: self.docstring_of(node), signature: self.signature_of(node), return_type: self.return_type_of(node), qualified_name: receiver_type.as_ref().map(|r| format!("{r}::{name}")), @@ -536,7 +561,7 @@ impl<'t> Walker<'t> { if name == "" { return false; } - let docstring = preceding_docstring(node, self.src); + let docstring = self.docstring_of(node); let is_exported = Some(self.is_exported(node)); let type_child = node.child_by_field_name("type"); let resolved = type_child.map(|t| t.kind()); @@ -614,7 +639,7 @@ impl<'t> Walker<'t> { /// extractVariable's Go branch: var/const specs + short_var_declaration. fn extract_variable(&mut self, node: Node<'t>) { - let docstring = preceding_docstring(node, self.src); + let docstring = self.docstring_of(node); let is_const_decl = node.kind() == "const_declaration"; for i in 0..node.named_child_count() { diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts index 1bad7ff22..d5df53824 100644 --- a/src/extraction/languages/go.ts +++ b/src/extraction/languages/go.ts @@ -128,4 +128,21 @@ export const goExtractor: LanguageExtractor = { const match = text.match(/\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*([A-Za-z_]\w*)/); return match?.[1]; }, + // A type declared on its own, `type Foo struct{…}`, is a `type_declaration` + // holding one spec, and its doc comment comes before the declaration, outside + // the spec. A member of a `type ( … )` group has its comment beside it in the + // parentheses, and the comment above the group is the group's. A group of one + // is that type's declaration, as go doc reads it, unless its member has a + // comment of its own. + getDeclarationWrapper: (node) => { + const parent = node.parent; + if (parent?.type !== 'type_declaration' || !parent.firstNamedChild?.equals(node)) return undefined; + const specs = parent.namedChildren.filter( + (c: SyntaxNode) => c.type === 'type_spec' || c.type === 'type_alias' + ); + return specs.length === 1 ? parent : undefined; + }, + // A comment after code on its line (`const sides = 4 // sides of a square.`) + // is that line's, never the doc of the declaration below it. + docstringSkipsTrailingComments: true, }; diff --git a/src/extraction/tree-sitter-helpers.ts b/src/extraction/tree-sitter-helpers.ts index 8ef15f41d..1177f2881 100644 --- a/src/extraction/tree-sitter-helpers.ts +++ b/src/extraction/tree-sitter-helpers.ts @@ -134,11 +134,17 @@ function cleanCommentMarkers(comment: string): string { * declaration without ending the run: Dart's annotations, in `/// Builds it.` * `@override` `Widget build(…)`. They are not part of the docstring, and the * comments on either side of one join as if it weren't there. Default: none. + * + * `skipTrailing` leaves out the comments the run opens with that belong to + * the line above: one written after code on its line (`const n = 4 // four.`), + * and any that begins on the line such a comment ends. Go reads them as that + * line's comment, never the next declaration's doc. Default: off. */ export function getPrecedingDocstring( node: SyntaxNode, source: string, - stepOver: readonly string[] = [] + stepOver: readonly string[] = [], + skipTrailing = false ): string | undefined { // Climb out of any wrapper(s) so a comment preceding the WHOLE construct // (export-, decorator-, or const-arrow-wrapped) is reachable as a sibling. @@ -151,7 +157,7 @@ export function getPrecedingDocstring( } let sibling = anchor.previousNamedSibling; - const comments: string[] = []; + const comments: SyntaxNode[] = []; while (sibling) { if ( @@ -160,7 +166,7 @@ export function getPrecedingDocstring( sibling.type === 'block_comment' || sibling.type === 'documentation_comment' ) { - comments.unshift(getNodeText(sibling, source)); + comments.unshift(sibling); sibling = sibling.previousNamedSibling; } else if (stepOver.includes(sibling.type)) { sibling = sibling.previousNamedSibling; @@ -169,8 +175,30 @@ export function getPrecedingDocstring( } } - if (comments.length === 0) return undefined; + let first = 0; + if (skipTrailing) { + let endRow = -1; + for (const comment of comments) { + const trails = + first === 0 ? followsCodeOnItsLine(comment, source) : comment.startPosition.row === endRow; + if (!trails) break; + endRow = comment.endPosition.row; + first++; + } + } + if (first === comments.length) return undefined; // Strip each comment's syntax markers (language-aware), then join. - return comments.map(cleanCommentMarkers).join('\n').trim(); + return comments + .slice(first) + .map((c) => cleanCommentMarkers(getNodeText(c, source))) + .join('\n') + .trim(); +} + +/** Whether code comes before `node` on the line it starts on. */ +function followsCodeOnItsLine(node: SyntaxNode, source: string): boolean { + let i = node.startIndex; + while (i > 0 && (source[i - 1] === ' ' || source[i - 1] === '\t')) i--; + return i > 0 && source[i - 1] !== '\n' && source[i - 1] !== '\r'; } diff --git a/src/extraction/tree-sitter-types.ts b/src/extraction/tree-sitter-types.ts index bb7c26f78..e665b7d3d 100644 --- a/src/extraction/tree-sitter-types.ts +++ b/src/extraction/tree-sitter-types.ts @@ -171,7 +171,9 @@ export interface LanguageExtractor { * around it, when the grammar adds one. The doc comment and annotations * written before the declaration precede that wrapper, so both are looked up * from it. Dart wraps a member with no body (`Foo._();`, an abstract - * `void m();`) in a `declaration` node. Returns undefined when there is none. + * `void m();`) in a `declaration` node, and Go a type declared on its own + * (`type Foo struct{…}`) in a `type_declaration`. Returns undefined when + * there is none. */ getDeclarationWrapper?: (node: SyntaxNode) => SyntaxNode | undefined; /** @@ -181,6 +183,13 @@ export interface LanguageExtractor { * each one is stepped over, and comments on either side of it still join. */ docstringStepOverTypes?: string[]; + /** + * Leave out of a docstring the comments that belong to the line above it: + * one written after code on its line, and any that begins on the line such + * a comment ends. In Go, `var origin = Point{} // the origin.` comments that + * line and never documents the declaration below it. + */ + docstringSkipsTrailingComments?: boolean; /** * Node types that may stand between a declaration and the decorators written * before it without ending the scan for them. Dart writes comments there diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 997d73517..3f9745d3c 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -567,7 +567,8 @@ export class TreeSitterExtractor { const preceding = getPrecedingDocstring( anchor, this.source, - this.extractor?.docstringStepOverTypes + this.extractor?.docstringStepOverTypes, + this.extractor?.docstringSkipsTrailingComments ); const body = this.extractor?.getBodyDocstring?.(node, this.source); if (preceding && body) return `${preceding}\n\n${body}`; From 9c5aea4bcc6ac22fabcd83603b616aa725d54d9e Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 06:40:03 -0500 Subject: [PATCH 2/2] test(go): an alias declared on its own keeps its doc comment #2417 now extracts `type A = B`, so the test covers an ungrouped alias and an alias that is a member of a commented group. Co-Authored-By: Claude Opus 5.5 --- __tests__/go-type-doc-comments.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/__tests__/go-type-doc-comments.test.ts b/__tests__/go-type-doc-comments.test.ts index 9998aac4b..852c9a793 100644 --- a/__tests__/go-type-doc-comments.test.ts +++ b/__tests__/go-type-doc-comments.test.ts @@ -6,9 +6,10 @@ * * tree-sitter-go wraps every type in a `type_declaration`, and the comment is * that declaration's previous sibling. The node is made from the spec inside - * it, whose only predecessor is the `type` keyword, so the docstring walk - * found nothing. A type inside a `type ( … )` group kept its comment, which - * sits beside it in the parentheses. + * it (a `type_spec`, or a `type_alias` for `type A = B`), whose only + * predecessor is the `type` keyword, so the docstring walk found nothing. A + * type inside a `type ( … )` group kept its comment, which sits beside it in + * the parentheses. * * A declaration holding one spec is now read from outside, as go doc reads * it. A group's leading comment stays the group's: it documents the group, @@ -61,6 +62,9 @@ type ID int // Set is a generic type documented on its own. type Set[T comparable] map[T]struct{} +// Alias is an alias documented on its own. +type Alias = Point + /* Box is documented in a block comment. */ @@ -167,6 +171,7 @@ describe('Go doc comments on types declared on their own', () => { Shape: doc('interface', 'Shape'), ID: doc('type_alias', 'ID'), Set: doc('type_alias', 'Set'), + Alias: doc('type_alias', 'Alias'), Box: doc('struct', 'Box'), // A group's comment is the group's; a member's own comment is found // beside it. A group of one is that type's declaration, as in go doc, @@ -177,6 +182,7 @@ describe('Go doc comments on types declared on their own', () => { Own: doc('type_alias', 'Own'), // An alias is one of a group's members too. First: doc('type_alias', 'First'), + Second: doc('type_alias', 'Second'), // A comment after code is that line's, whatever comes below it. Celsius: doc('type_alias', 'Celsius'), Kelvin: doc('type_alias', 'Kelvin'), @@ -192,12 +198,14 @@ describe('Go doc comments on types declared on their own', () => { Shape: 'Shape is an interface documented on its own.', ID: 'ID is a defined type documented on its own.', Set: 'Set is a generic type documented on its own.', + Alias: 'Alias is an alias documented on its own.', Box: 'Box is documented in a block comment.', Circle: null, Square: 'Square is documented inside the group.', Lone: 'Lone is the only member of its group.', Own: 'Own is documented inside its group.', First: null, + Second: null, Celsius: null, Kelvin: null, sides: 'sides is documented above its line.',