fix(go): a comment a blank line separates from a declaration is not its doc (#2678)

Go's doc comment, the one go doc shows (go/parser's lead comment), is the
comment group that ends on the line right above a declaration: comments
on consecutive lines form one group, and a blank line ends it. Both
extractors joined every comment above a Go declaration instead, so a
section banner, a TODO or commented-out code above a blank line became
part of the declaration's docstring, or its whole docstring when the
declaration had no doc of its own.

Go now keeps only that group, through a Go-only extractor option beside
the trailing-comment one (docstringSkipsDetachedComments in TS,
preceding_docstring_skipping_trailing_and_detached in the kernel). A
comment that ends on the declaration's own line documents nothing, as
in go doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 20:28:12 +00:00
committed by GitHub
parent 22d420ec8a
commit 23f728fac2
9 changed files with 317 additions and 15 deletions
+60 -10
View File
@@ -143,16 +143,19 @@ pub fn preceding_docstring_stepping_over(
src: &str,
step_over: &[&str],
) -> Option<String> {
preceding_docstring_with(node, src, step_over, false)
preceding_docstring_with(node, src, step_over, false, 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<String> {
preceding_docstring_with(node, src, &[], true)
/// getPrecedingDocstring's `skipTrailing` and `skipDetached` (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. Of the rest, only the comments that
/// end on the line right above the declaration, together with the ones written
/// on the lines just before them, are kept: a blank line ends the docstring,
/// and a comment that ends on the declaration's own line documents nothing.
/// That is Go's doc comment, the one go doc shows.
pub fn preceding_docstring_skipping_trailing_and_detached(node: Node, src: &str) -> Option<String> {
preceding_docstring_with(node, src, &[], true, true)
}
fn preceding_docstring_with(
@@ -160,6 +163,7 @@ fn preceding_docstring_with(
src: &str,
step_over: &[&str],
skip_trailing: bool,
skip_detached: bool,
) -> Option<String> {
let mut anchor = node;
while let Some(parent) = anchor.parent() {
@@ -200,6 +204,21 @@ fn preceding_docstring_with(
first += 1;
}
}
if skip_detached {
// Walk back from the declaration: the last comment must end on the
// line above it, and each one before joins while it ends on the line
// the next one starts, or the line before that.
let mut start = comments.len();
if start > first && comments[start - 1].end_position().row + 1 == anchor.start_position().row {
start -= 1;
while start > first
&& comments[start - 1].end_position().row + 1 >= comments[start].start_position().row
{
start -= 1;
}
}
first = start;
}
if first == comments.len() {
return None;
}
@@ -294,9 +313,40 @@ mod tests {
})
.expect(name)
};
let skipping_trailing = |n| preceding_docstring_with(n, src, &[], true, false);
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!(skipping_trailing(func("f")).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);
assert_eq!(skipping_trailing(func("g")), None);
}
/// Skipping detached comments keeps the comments that end on the line
/// above the declaration and those on the lines just before them; a blank
/// line ends the run, and a comment on the declaration's own line is not
/// kept. Without it, every comment is.
#[test]
fn skips_the_comments_a_blank_line_detaches() {
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\n// a\n\n// b\n/*\nc\n*/\n// d\nfunc f() {}\n\n// e\n\nfunc g() {}\n\n/* h */ func h() {}\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)
};
let skipping_detached = |n| preceding_docstring_with(n, src, &[], false, true);
assert_eq!(preceding_docstring(func("f"), src).as_deref(), Some("a\nb\nc\nd"));
assert_eq!(skipping_detached(func("f")).as_deref(), Some("b\nc\nd"));
assert_eq!(preceding_docstring(func("g"), src).as_deref(), Some("e"));
assert_eq!(skipping_detached(func("g")), None);
assert_eq!(preceding_docstring(func("h"), src).as_deref(), Some("h"));
assert_eq!(skipping_detached(func("h")), None);
}
}
+8 -3
View File
@@ -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_skipping_trailing;
use crate::docstring::preceding_docstring_skipping_trailing_and_detached;
use crate::ids;
use crate::textutil as util;
use regex::Regex;
@@ -400,9 +400,14 @@ impl<'t> Walker<'t> {
/// 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).
/// that trail the code above it (docstringSkipsTrailingComments) or that
/// don't end on the line right above it, past a blank line
/// (docstringSkipsDetachedComments).
fn docstring_of(&self, node: Node<'t>) -> Option<String> {
preceding_docstring_skipping_trailing(self.declaration_wrapper(node).unwrap_or(node), self.src)
preceding_docstring_skipping_trailing_and_detached(
self.declaration_wrapper(node).unwrap_or(node),
self.src,
)
}
// --- visitNode ------------------------------------------------------------