Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 61 additions & 30 deletions cl/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,44 +44,75 @@ func (p *pkgCtx) docComments(decl clang.Cursor) []*ast.Comment {
return toLineComments(raw)
}

// cleanCommentLine strips the C/C++ comment markers and Doxygen/Javadoc
// decoration that may appear on a single physical line of a raw comment,
// returning just the human-readable content.
//
// Markers can appear at either end of the line because a raw comment reported
// by libclang may concatenate multiple comment blocks (a plain "/* ... */"
// banner immediately followed by a "/** ... */" doc block), which puts an
// opener or closer mid-stream rather than only at the ends of the whole string.
// The order matters: block openers/closers are removed first (so "Declarations
// */" and "/**" collapse correctly), then line-comment markers, then a single
// leading "*" decoration.
func cleanCommentLine(line string) string {
line = strings.TrimSpace(line)
// Strip a leading block-opener: "/" followed by a run of "*" ("/*", "/**",
// "/***", banner "/*****").
if s, ok := strings.CutPrefix(line, "/"); ok {
if t := strings.TrimLeft(s, "*"); len(t) < len(s) {
line = strings.TrimSpace(t)
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symmetric to the trailing-closer case: a content line beginning with / followed by a run of * is treated as an opener and stripped, even when it's genuine content (e.g. prose starting with /*note*/ → /* stripped then */ stripped → note). The heuristic assumes the first/last */-shaped token on a line is always a marker, never content. Consider noting this assumption in the doc comment.

// Strip a trailing block-closer: a run of "*" followed by "/" ("*/", "**/",
// banner "*****/").
if s, ok := strings.CutSuffix(line, "/"); ok {
if t := strings.TrimRight(s, "*"); len(t) < len(s) {
line = strings.TrimSpace(t)
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trailing-closer strip removes any *-run + / suffix from a line regardless of whether it is a true block closer or genuine content. A content line ending in **/ (e.g. a Doxygen line mentioning the glob pattern **/) is silently truncated: CutSuffix("/") → "...pattern **", then TrimRight(.,"*") → "...pattern ", dropping the **/.

The URL case (http://a/b/) is safe because no * precedes the final /, but this heuristic can't distinguish a terminator from content shaped like one. Rare in practice, but worth either guarding (only strip when the line is recognizably a terminator) or documenting the assumption in the function comment.

switch {
case strings.HasPrefix(line, "//"):
// Line-comment markers: "//", "///", "//!".
s := strings.TrimPrefix(line, "//")
s = strings.TrimPrefix(s, "/")
s = strings.TrimPrefix(s, "!")
line = strings.TrimPrefix(s, " ")
case strings.TrimRight(line, "*") == "":
// A line that is only "*" decoration (a blank Doxygen line) or a run
// of asterisks (a banner separator) carries no content.
line = ""
default:
// Drop a single Doxygen/Javadoc leading "*" decoration.
if s, ok := strings.CutPrefix(line, "* "); ok {
line = s
} else if s, ok := strings.CutPrefix(line, "*"); ok {
line = s
}
}
return line
}

// toLineComments converts a raw C/C++ documentation comment into a slice of Go
// "//" line comments. It handles both block comments ("/* ... */", "/** ... */")
// and consecutive line comments ("//", "///", "//!"), stripping the markers and
// any common leading " * " decoration from block comments. It returns nil when
// there is no meaningful content.
// "//" line comments. It handles block comments ("/* ... */", "/** ... */"),
// consecutive line comments ("//", "///", "//!"), and raw comments that
// concatenate several blocks (e.g. "/* Declarations */\n/** ... */", which
// libclang reports when a plain comment immediately precedes a doc comment on
// the same declaration). The markers and any Doxygen/Javadoc "*" decoration are
// stripped. It returns nil when there is no meaningful content.
func toLineComments(raw string) []*ast.Comment {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}

// Process the raw comment line by line. Because a single raw comment may
// contain more than one comment block, the opening ("/*", "/**", ...) and
// closing ("*/", "**/", ...) markers can appear on interior lines, not just
// at the very ends of the string, so each line is cleaned independently.
var lines []string
switch {
case strings.HasPrefix(raw, "/*"):
body := strings.TrimPrefix(raw, "/*")
body = strings.TrimSuffix(body, "*/")
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
// Drop a Doxygen/Javadoc-style leading "*" decoration.
if line == "*" {
line = ""
} else if s, ok := strings.CutPrefix(line, "* "); ok {
line = s
} else if s, ok := strings.CutPrefix(line, "*"); ok {
line = s
}
lines = append(lines, line)
}
default:
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
line = strings.TrimPrefix(line, "//")
// Doxygen line-comment markers: "///" and "//!".
line = strings.TrimPrefix(line, "/")
line = strings.TrimPrefix(line, "!")
line = strings.TrimPrefix(line, " ")
lines = append(lines, line)
}
for _, line := range strings.Split(raw, "\n") {
lines = append(lines, cleanCommentLine(line))
}

// Trim leading/trailing blank lines that come from the markers being on
Expand Down
125 changes: 125 additions & 0 deletions cl/doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cl

import (
"strings"
"testing"
)

func lineCommentsText(raw string) string {
comments := toLineComments(raw)
lines := make([]string, len(comments))
for i, c := range comments {
lines[i] = c.Text
}
return strings.Join(lines, "\n")
}

func TestToLineComments(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{
name: "empty",
raw: "",
want: "",
},
{
name: "line_comment",
raw: "/// A documented enum type.",
want: "// A documented enum type.",
},
{
name: "bang_line_comment",
raw: "//! A bang documented item.",
want: "// A bang documented item.",
},
{
name: "plain_line_comment",
raw: "// plain comment",
want: "// plain comment",
},
{
name: "single_line_block",
raw: "/* simple */",
want: "// simple",
},
{
name: "doxygen_block",
raw: "/**\n * A documented function.\n *\n * It adds two integers.\n */",
want: "// A documented function.\n//\n// It adds two integers.",
},
{
// A "/***" opener must not leak a stray "*" into a "// *" line.
name: "triple_star_opener",
raw: "/***\n * Determine whether the given cursor represents a preprocessing\n * element.\n */",
want: "// Determine whether the given cursor represents a preprocessing\n// element.",
},
{
// Interior indentation after the "* " decoration is preserved.
name: "triple_star_preserves_indent",
raw: "/***\n * Determine whether the given cursor represents a currently\n * unexposed piece of the AST.\n */",
want: "// Determine whether the given cursor represents a currently\n// unexposed piece of the AST.",
},
{
// Banner-style separators must collapse to blank lines, never
// emit rows of asterisks or a trailing "**/".
name: "banner_block",
raw: "/**************\nSymbols and macros to supply platform-independent interfaces.\n**************/",
want: "// Symbols and macros to supply platform-independent interfaces.",
},
{
name: "block_with_star_prefix",
raw: "/* uintptr_t is the C9X name for a type such that a\n * void* can be cast to uintptr_t and back.\n */",
want: "// uintptr_t is the C9X name for a type such that a\n// void* can be cast to uintptr_t and back.",
},
{
name: "block_no_star_prefix",
raw: "/* line one\nline two */",
want: "// line one\n// line two",
},
{
name: "whitespace_only_block",
raw: "/**\n *\n */",
want: "",
},
{
// libclang concatenates a plain banner and the following doc
// block into one raw comment; neither the interior "*/" nor the
// interior "/**" may leak into the output.
name: "multi_block_plain_then_doc",
raw: "/* Declarations */\n /**\n * A declaration whose specific kind is not exposed.\n */",
want: "// Declarations\n//\n// A declaration whose specific kind is not exposed.",
},
{
name: "inline_block_comment",
raw: "/* Decl references */",
want: "// Decl references",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := lineCommentsText(tt.raw)
if got != tt.want {
t.Errorf("toLineComments(%q):\n got:\n%s\nwant:\n%s", tt.raw, got, tt.want)
}
})
}
}
2 changes: 0 additions & 2 deletions tool/_testc/clang-c-22.1.8/Index.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions tool/_testc/clang-c-22.1.8/Index/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -3500,7 +3500,6 @@ func (_llcppg_param1 CursorKind) IsTranslationUnit() c.Uint {
return 0
}

// *
// Determine whether the given cursor represents a preprocessing
// element, such as a preprocessor directive or macro instantiation.
//
Expand All @@ -3509,7 +3508,6 @@ func (_llcppg_param1 CursorKind) IsPreprocessing() c.Uint {
return 0
}

// *
// Determine whether the given cursor represents a currently
// unexposed piece of the AST (e.g., CXCursor_UnexposedStmt).
//
Expand Down
Loading