diff --git a/cl/doc.go b/cl/doc.go index 3e331c065..764dd11dc 100644 --- a/cl/doc.go +++ b/cl/doc.go @@ -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) + } + } + // 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) + } + } + 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 diff --git a/cl/doc_test.go b/cl/doc_test.go new file mode 100644 index 000000000..1c6eb4910 --- /dev/null +++ b/cl/doc_test.go @@ -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) + } + }) + } +} diff --git a/tool/_testc/clang-c-22.1.8/Index.go b/tool/_testc/clang-c-22.1.8/Index.go index ebdade2b0..b76fa7df1 100644 --- a/tool/_testc/clang-c-22.1.8/Index.go +++ b/tool/_testc/clang-c-22.1.8/Index.go @@ -3495,7 +3495,6 @@ func (self CursorKind) IsTranslationUnit() c.Uint { return 0 } -// * // Determine whether the given cursor represents a preprocessing // element, such as a preprocessor directive or macro instantiation. // @@ -3504,7 +3503,6 @@ func (self CursorKind) IsPreprocessing() c.Uint { return 0 } -// * // Determine whether the given cursor represents a currently // unexposed piece of the AST (e.g., CXCursor_UnexposedStmt). // diff --git a/tool/_testc/clang-c-22.1.8/Index/out.go b/tool/_testc/clang-c-22.1.8/Index/out.go index 785c4bb45..259d2f7d0 100644 --- a/tool/_testc/clang-c-22.1.8/Index/out.go +++ b/tool/_testc/clang-c-22.1.8/Index/out.go @@ -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. // @@ -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). //