mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 00:38:18 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user