Repository navigation
fix(cl): comments format bugfix #933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| } | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The trailing-closer strip removes any The URL case ( |
||
| 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 | ||
|
|
||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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.