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 15package main 16 17import ( 18 "bytes" 19 "fmt" 20 "io/ioutil" 21 "os" 22 "strings" 23) 24 25// convert_comments.go converts C-style block comments to C++-style line 26// comments. A block comment is converted if all of the following are true: 27// 28// * The comment begins after the first blank line, to leave the license 29// blocks alone. 30// 31// * There are no characters between the '*/' and the end of the line. 32// 33// * Either one of the following are true: 34// 35// - The comment fits on one line. 36// 37// - Each line the comment spans begins with N spaces, followed by '/*' for 38// the initial line or ' *' for subsequent lines, where N is the same for 39// each line. 40// 41// This tool is a heuristic. While it gets almost all cases correct, the final 42// output should still be looked over and fixed up as needed. 43 44// allSpaces returns true if |s| consists entirely of spaces. 45func allSpaces(s string) bool { 46 return strings.IndexFunc(s, func(r rune) bool { return r != ' ' }) == -1 47} 48 49// isContinuation returns true if |s| is a continuation line for a multi-line 50// comment indented to the specified column. 51func isContinuation(s string, column int) bool { 52 if len(s) < column+2 { 53 return false 54 } 55 if !allSpaces(s[:column]) { 56 return false 57 } 58 return s[column:column+2] == " *" 59} 60 61// indexFrom behaves like strings.Index but only reports matches starting at 62// |idx|. 63func indexFrom(s, sep string, idx int) int { 64 ret := strings.Index(s[idx:], sep) 65 if ret < 0 { 66 return -1 67 } 68 return idx + ret 69} 70 71// A lineGroup is a contiguous group of lines with an eligible comment at the 72// same column. Any trailing '*/'s will already be removed. 73type lineGroup struct { 74 // column is the column where the eligible comment begins. line[column] 75 // and line[column+1] will both be replaced with '/'. It is -1 if this 76 // group is not to be converted. 77 column int 78 lines []string 79} 80 81func addLine(groups *[]lineGroup, line string, column int) { 82 if len(*groups) == 0 || (*groups)[len(*groups)-1].column != column { 83 *groups = append(*groups, lineGroup{column, nil}) 84 } 85 (*groups)[len(*groups)-1].lines = append((*groups)[len(*groups)-1].lines, line) 86} 87 88// writeLine writes |line| to |out|, followed by a newline. 89func writeLine(out *bytes.Buffer, line string) { 90 out.WriteString(line) 91 out.WriteByte('\n') 92} 93 94func convertComments(path string, in []byte) []byte { 95 lines := strings.Split(string(in), "\n") 96 97 // Account for the trailing newline. 98 if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { 99 lines = lines[:len(lines)-1] 100 } 101 102 // First pass: identify all comments to be converted. Group them into 103 // lineGroups with the same column. 104 var groups []lineGroup 105 106 // Find the license block separator. 107 for len(lines) > 0 { 108 line := lines[0] 109 lines = lines[1:] 110 addLine(&groups, line, -1) 111 if len(line) == 0 { 112 break 113 } 114 } 115 116 // inComment is true if we are in the middle of a comment. 117 var inComment bool 118 // comment is the currently buffered multi-line comment to convert. If 119 // |inComment| is true and it is nil, the current multi-line comment is 120 // not convertable and we copy lines to |out| as-is. 121 var comment []string 122 // column is the column offset of |comment|. 123 var column int 124 for len(lines) > 0 { 125 line := lines[0] 126 lines = lines[1:] 127 128 var idx int 129 if inComment { 130 // Stop buffering if this comment isn't eligible. 131 if comment != nil && !isContinuation(line, column) { 132 for _, l := range comment { 133 addLine(&groups, l, -1) 134 } 135 comment = nil 136 } 137 138 // Look for the end of the current comment. 139 idx = strings.Index(line, "*/") 140 if idx < 0 { 141 if comment != nil { 142 comment = append(comment, line) 143 } else { 144 addLine(&groups, line, -1) 145 } 146 continue 147 } 148 149 inComment = false 150 if comment != nil { 151 if idx == len(line)-2 { 152 // This is a convertable multi-line comment. 153 if idx >= column+2 { 154 // |idx| may be equal to 155 // |column| + 1, if the line is 156 // a '*/' on its own. In that 157 // case, we discard the line. 158 comment = append(comment, line[:idx]) 159 } 160 for _, l := range comment { 161 addLine(&groups, l, column) 162 } 163 comment = nil 164 continue 165 } 166 167 // Flush the buffered comment unmodified. 168 for _, l := range comment { 169 addLine(&groups, l, -1) 170 } 171 comment = nil 172 } 173 idx += 2 174 } 175 176 // Parse starting from |idx|, looking for either a convertable 177 // line comment or a multi-line comment. 178 for { 179 idx = indexFrom(line, "/*", idx) 180 if idx < 0 { 181 addLine(&groups, line, -1) 182 break 183 } 184 185 endIdx := indexFrom(line, "*/", idx) 186 if endIdx < 0 { 187 // The comment is, so far, eligible for conversion. 188 inComment = true 189 column = idx 190 comment = []string{line} 191 break 192 } 193 194 if endIdx != len(line)-2 { 195 // Continue parsing for more comments in this line. 196 idx = endIdx + 2 197 continue 198 } 199 200 addLine(&groups, line[:endIdx], idx) 201 break 202 } 203 } 204 205 // Second pass: convert the lineGroups, adjusting spacing as needed. 206 var out bytes.Buffer 207 var lineNo int 208 for _, group := range groups { 209 if group.column < 0 { 210 for _, line := range group.lines { 211 writeLine(&out, line) 212 } 213 } else { 214 // Google C++ style prefers two spaces before a comment 215 // if it is on the same line as code, but clang-format 216 // has been placing one space for block comments. All 217 // comments within a group should be adjusted by the 218 // same amount. 219 var adjust string 220 for _, line := range group.lines { 221 if !allSpaces(line[:group.column]) && line[group.column-1] != '(' { 222 if line[group.column-1] != ' ' { 223 if len(adjust) < 2 { 224 adjust = " " 225 } 226 } else if line[group.column-2] != ' ' { 227 if len(adjust) < 1 { 228 adjust = " " 229 } 230 } 231 } 232 } 233 234 for i, line := range group.lines { 235 newLine := fmt.Sprintf("%s%s//%s", line[:group.column], adjust, strings.TrimRight(line[group.column+2:], " ")) 236 if len(newLine) > 80 { 237 fmt.Fprintf(os.Stderr, "%s:%d: Line is now longer than 80 characters\n", path, lineNo+i+1) 238 } 239 writeLine(&out, newLine) 240 } 241 242 } 243 lineNo += len(group.lines) 244 } 245 return out.Bytes() 246} 247 248func main() { 249 for _, arg := range os.Args[1:] { 250 in, err := ioutil.ReadFile(arg) 251 if err != nil { 252 panic(err) 253 } 254 if err := ioutil.WriteFile(arg, convertComments(arg, in), 0666); err != nil { 255 panic(err) 256 } 257 } 258} 259