1// Copyright (c) 2017, Google Inc. 2// 3// Permission to use, copy, modify, and/or distribute this software for any 4// purpose with or without fee is hereby granted, provided that the above 5// copyright notice and this permission notice appear in all copies. 6// 7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 10// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 12// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 13// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 15//go:build ignore 16 17package main 18 19import ( 20 "bytes" 21 "fmt" 22 "os" 23 "strings" 24) 25 26// convert_comments.go converts C-style block comments to C++-style line 27// comments. A block comment is converted if all of the following are true: 28// 29// * The comment begins after the first blank line, to leave the license 30// blocks alone. 31// 32// * There are no characters between the '*/' and the end of the line. 33// 34// * Either one of the following are true: 35// 36// - The comment fits on one line. 37// 38// - Each line the comment spans begins with N spaces, followed by '/*' for 39// the initial line or ' *' for subsequent lines, where N is the same for 40// each line. 41// 42// This tool is a heuristic. While it gets almost all cases correct, the final 43// output should still be looked over and fixed up as needed. 44 45// allSpaces returns true if |s| consists entirely of spaces. 46func allSpaces(s string) bool { 47 return strings.IndexFunc(s, func(r rune) bool { return r != ' ' }) == -1 48} 49 50// isContinuation returns true if |s| is a continuation line for a multi-line 51// comment indented to the specified column. 52func isContinuation(s string, column int) bool { 53 if len(s) < column+2 { 54 return false 55 } 56 if !allSpaces(s[:column]) { 57 return false 58 } 59 return s[column:column+2] == " *" 60} 61 62// indexFrom behaves like strings.Index but only reports matches starting at 63// |idx|. 64func indexFrom(s, sep string, idx int) int { 65 ret := strings.Index(s[idx:], sep) 66 if ret < 0 { 67 return -1 68 } 69 return idx + ret 70} 71 72// A lineGroup is a contiguous group of lines with an eligible comment at the 73// same column. Any trailing '*/'s will already be removed. 74type lineGroup struct { 75 // column is the column where the eligible comment begins. line[column] 76 // and line[column+1] will both be replaced with '/'. It is -1 if this 77 // group is not to be converted. 78 column int 79 lines []string 80} 81 82func addLine(groups *[]lineGroup, line string, column int) { 83 if len(*groups) == 0 || (*groups)[len(*groups)-1].column != column { 84 *groups = append(*groups, lineGroup{column, nil}) 85 } 86 (*groups)[len(*groups)-1].lines = append((*groups)[len(*groups)-1].lines, line) 87} 88 89// writeLine writes |line| to |out|, followed by a newline. 90func writeLine(out *bytes.Buffer, line string) { 91 out.WriteString(line) 92 out.WriteByte('\n') 93} 94 95func convertComments(path string, in []byte) []byte { 96 lines := strings.Split(string(in), "\n") 97 98 // Account for the trailing newline. 99 if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { 100 lines = lines[:len(lines)-1] 101 } 102 103 // First pass: identify all comments to be converted. Group them into 104 // lineGroups with the same column. 105 var groups []lineGroup 106 107 // Find the license block separator. 108 for len(lines) > 0 { 109 line := lines[0] 110 lines = lines[1:] 111 addLine(&groups, line, -1) 112 if len(line) == 0 { 113 break 114 } 115 } 116 117 // inComment is true if we are in the middle of a comment. 118 var inComment bool 119 // comment is the currently buffered multi-line comment to convert. If 120 // |inComment| is true and it is nil, the current multi-line comment is 121 // not convertable and we copy lines to |out| as-is. 122 var comment []string 123 // column is the column offset of |comment|. 124 var column int 125 for len(lines) > 0 { 126 line := lines[0] 127 lines = lines[1:] 128 129 var idx int 130 if inComment { 131 // Stop buffering if this comment isn't eligible. 132 if comment != nil && !isContinuation(line, column) { 133 for _, l := range comment { 134 addLine(&groups, l, -1) 135 } 136 comment = nil 137 } 138 139 // Look for the end of the current comment. 140 idx = strings.Index(line, "*/") 141 if idx < 0 { 142 if comment != nil { 143 comment = append(comment, line) 144 } else { 145 addLine(&groups, line, -1) 146 } 147 continue 148 } 149 150 inComment = false 151 if comment != nil { 152 if idx == len(line)-2 { 153 // This is a convertable multi-line comment. 154 if idx >= column+2 { 155 // |idx| may be equal to 156 // |column| + 1, if the line is 157 // a '*/' on its own. In that 158 // case, we discard the line. 159 comment = append(comment, line[:idx]) 160 } 161 for _, l := range comment { 162 addLine(&groups, l, column) 163 } 164 comment = nil 165 continue 166 } 167 168 // Flush the buffered comment unmodified. 169 for _, l := range comment { 170 addLine(&groups, l, -1) 171 } 172 comment = nil 173 } 174 idx += 2 175 } 176 177 // Parse starting from |idx|, looking for either a convertable 178 // line comment or a multi-line comment. 179 for { 180 idx = indexFrom(line, "/*", idx) 181 if idx < 0 { 182 addLine(&groups, line, -1) 183 break 184 } 185 186 endIdx := indexFrom(line, "*/", idx) 187 if endIdx < 0 { 188 // The comment is, so far, eligible for conversion. 189 inComment = true 190 column = idx 191 comment = []string{line} 192 break 193 } 194 195 if endIdx != len(line)-2 { 196 // Continue parsing for more comments in this line. 197 idx = endIdx + 2 198 continue 199 } 200 201 addLine(&groups, line[:endIdx], idx) 202 break 203 } 204 } 205 206 // Second pass: convert the lineGroups, adjusting spacing as needed. 207 var out bytes.Buffer 208 var lineNo int 209 for _, group := range groups { 210 if group.column < 0 { 211 for _, line := range group.lines { 212 writeLine(&out, line) 213 } 214 } else { 215 // Google C++ style prefers two spaces before a comment 216 // if it is on the same line as code, but clang-format 217 // has been placing one space for block comments. All 218 // comments within a group should be adjusted by the 219 // same amount. 220 var adjust string 221 for _, line := range group.lines { 222 if !allSpaces(line[:group.column]) && line[group.column-1] != '(' { 223 if line[group.column-1] != ' ' { 224 if len(adjust) < 2 { 225 adjust = " " 226 } 227 } else if line[group.column-2] != ' ' { 228 if len(adjust) < 1 { 229 adjust = " " 230 } 231 } 232 } 233 } 234 235 for i, line := range group.lines { 236 // The OpenSSL style writes multiline block comments with a 237 // blank line at the top and bottom, like so: 238 // 239 // /* 240 // * Some multi-line 241 // * comment 242 // */ 243 // 244 // The trailing lines are already removed above, when buffering. 245 // Remove the leading lines here. (The leading lines cannot be 246 // removed when buffering because we may discover the comment is 247 // not convertible in later lines.) 248 // 249 // Note the leading line cannot be easily removed if there is 250 // code before it, such as the following. Skip those cases. 251 // 252 // foo(); /* 253 // * Some multi-line 254 // * comment 255 // */ 256 if i == 0 && allSpaces(line[:group.column]) && len(line) == group.column+2 { 257 continue 258 } 259 newLine := fmt.Sprintf("%s%s//%s", line[:group.column], adjust, strings.TrimRight(line[group.column+2:], " ")) 260 if len(newLine) > 80 { 261 fmt.Fprintf(os.Stderr, "%s:%d: Line is now longer than 80 characters\n", path, lineNo+i+1) 262 } 263 writeLine(&out, newLine) 264 } 265 266 } 267 lineNo += len(group.lines) 268 } 269 return out.Bytes() 270} 271 272func main() { 273 for _, arg := range os.Args[1:] { 274 in, err := os.ReadFile(arg) 275 if err != nil { 276 panic(err) 277 } 278 if err := os.WriteFile(arg, convertComments(arg, in), 0666); err != nil { 279 panic(err) 280 } 281 } 282} 283