fix(go): a comment directive like //go:embed is not part of a declaration's doc (#2679)

go doc leaves comment directives out of a doc comment's text: a // comment
with no space after the slashes that is //line, //extern or //export
followed by text, or that opens with a lowercase word, a colon and a
lowercase letter or digit, like //go:embed static or //nolint:errcheck
(go/ast's isDirective). Both extractors kept them, so prometheus's
testsFs had the docstring "go:embed testdata", etcd's store.restore
"nolint:unparam", and a doc comment with a directive under it ended in
that directive.

Go now drops directives from the docstring's text through a Go-only
extractor option (docstringSkipsDirectives in TS; the kernel's Go
docstring helper becomes preceding_go_doc_comment, which applies all
three Go rules). A directive still counts as a comment when deciding
where the doc comment starts, as in go/parser, so a declaration whose
only comment is a directive has no docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 20:45:40 +00:00
committed by GitHub
parent 23f728fac2
commit f43432c86a
9 changed files with 299 additions and 26 deletions
+79 -15
View File
@@ -143,19 +143,21 @@ pub fn preceding_docstring_stepping_over(
src: &str,
step_over: &[&str],
) -> Option<String> {
preceding_docstring_with(node, src, step_over, false, false)
preceding_docstring_with(node, src, step_over, false, false, false)
}
/// 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)
/// getPrecedingDocstring's `skipTrailing`, `skipDetached` and `skipDirectives`,
/// which Go turns on together for the doc comment go doc shows. 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. A
/// directive among them (`//go:embed static`, `//nolint:errcheck`) is left out
/// of the text.
pub fn preceding_go_doc_comment(node: Node, src: &str) -> Option<String> {
preceding_docstring_with(node, src, &[], true, true, true)
}
fn preceding_docstring_with(
@@ -164,6 +166,7 @@ fn preceding_docstring_with(
step_over: &[&str],
skip_trailing: bool,
skip_detached: bool,
skip_directives: bool,
) -> Option<String> {
let mut anchor = node;
while let Some(parent) = anchor.parent() {
@@ -219,11 +222,16 @@ fn preceding_docstring_with(
}
first = start;
}
if first == comments.len() {
let doc_comments: Vec<Node> = comments[first..]
.iter()
.copied()
.filter(|c| !skip_directives || !is_directive(&src[c.byte_range()]))
.collect();
if doc_comments.is_empty() {
return None;
}
Some(
comments[first..]
doc_comments
.iter()
.map(|c| clean_comment_markers(&src[c.byte_range()]))
.collect::<Vec<_>>()
@@ -233,6 +241,24 @@ fn preceding_docstring_with(
)
}
/// isDirective (tree-sitter-helpers.ts) — a comment directive Go tools read,
/// which go doc leaves out of a doc comment's text (go/ast's isDirective): a
/// `//` comment with no space after the slashes that is `//line `, `//extern `
/// or `//export ` followed by text, or that opens with a lowercase word, a
/// colon and a lowercase letter or digit (`//go:embed x`, `//nolint:errcheck`).
fn is_directive(comment: &str) -> bool {
let Some(text) = comment.strip_prefix("//") else {
return false;
};
if text.starts_with("line ") || text.starts_with("extern ") || text.starts_with("export ") {
return true;
}
let word_char = |b: &u8| b.is_ascii_lowercase() || b.is_ascii_digit();
let bytes = text.as_bytes();
let word = bytes.iter().take_while(|b| word_char(b)).count();
word > 0 && bytes.get(word) == Some(&b':') && bytes.get(word + 1).is_some_and(word_char)
}
/// 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 {
@@ -313,7 +339,7 @@ mod tests {
})
.expect(name)
};
let skipping_trailing = |n| preceding_docstring_with(n, src, &[], true, false);
let skipping_trailing = |n| preceding_docstring_with(n, src, &[], true, false, false);
assert_eq!(preceding_docstring(func("f"), src).as_deref(), Some("a\nb\nc"));
assert_eq!(skipping_trailing(func("f")).as_deref(), Some("c"));
assert_eq!(preceding_docstring(func("g"), src).as_deref(), Some("d"));
@@ -341,7 +367,7 @@ mod tests {
})
.expect(name)
};
let skipping_detached = |n| preceding_docstring_with(n, src, &[], false, true);
let skipping_detached = |n| preceding_docstring_with(n, src, &[], false, true, false);
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"));
@@ -349,4 +375,42 @@ mod tests {
assert_eq!(preceding_docstring(func("h"), src).as_deref(), Some("h"));
assert_eq!(skipping_detached(func("h")), None);
}
/// The directive shapes go/ast leaves out of a doc comment's text, and
/// the near misses it keeps.
#[test]
fn recognizes_go_directives() {
for directive in ["//go:embed x", "//nolint:errcheck", "//a1:b", "//line f.go:1", "//extern f", "//export F"] {
assert!(is_directive(directive), "{directive}");
}
for comment in ["// nolint:errcheck", "//TODO: x", "//https://go.dev", "//go:", "//:x", "//line", "/*go:embed x*/", "//go-x:y"] {
assert!(!is_directive(comment), "{comment}");
}
}
/// Skipping directives leaves them out of the docstring's text but not
/// out of the comment run: one beside a doc comment drops out of it, and a
/// declaration whose only comment is one has no docstring.
#[test]
fn skips_directives_in_the_text_only() {
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//go:noinline\n// b\nfunc f() {}\n\n//nolint:unparam\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\ngo:noinline\nb"));
assert_eq!(preceding_go_doc_comment(func("f"), src).as_deref(), Some("a\nb"));
assert_eq!(preceding_docstring(func("g"), src).as_deref(), Some("nolint:unparam"));
assert_eq!(preceding_go_doc_comment(func("g"), src), None);
}
}
+4 -6
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_and_detached;
use crate::docstring::preceding_go_doc_comment;
use crate::ids;
use crate::textutil as util;
use regex::Regex;
@@ -402,12 +402,10 @@ impl<'t> Walker<'t> {
/// from the declaration wrapper when there is one, without the comments
/// that trail the code above it (docstringSkipsTrailingComments) or that
/// don't end on the line right above it, past a blank line
/// (docstringSkipsDetachedComments).
/// (docstringSkipsDetachedComments), and without directives like
/// `//go:embed` in its text (docstringSkipsDirectives).
fn docstring_of(&self, node: Node<'t>) -> Option<String> {
preceding_docstring_skipping_trailing_and_detached(
self.declaration_wrapper(node).unwrap_or(node),
self.src,
)
preceding_go_doc_comment(self.declaration_wrapper(node).unwrap_or(node), self.src)
}
// --- visitNode ------------------------------------------------------------