// Copyright (c) 2017, Google Inc. // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. package main import ( "bytes" "fmt" "io/ioutil" "os" "strings" ) // convert_comments.go converts C-style block comments to C++-style line // comments. A block comment is converted if all of the following are true: // // * The comment begins after the first blank line, to leave the license // blocks alone. // // * There are no characters between the '*/' and the end of the line. // // * Either one of the following are true: // // - The comment fits on one line. // // - Each line the comment spans begins with N spaces, followed by '/*' for // the initial line or ' *' for subsequent lines, where N is the same for // each line. // // This tool is a heuristic. While it gets almost all cases correct, the final // output should still be looked over and fixed up as needed. // allSpaces returns true if |s| consists entirely of spaces. func allSpaces(s string) bool { return strings.IndexFunc(s, func(r rune) bool { return r != ' ' }) == -1 } // isContinuation returns true if |s| is a continuation line for a multi-line // comment indented to the specified column. func isContinuation(s string, column int) bool { if len(s) < column+2 { return false } if !allSpaces(s[:column]) { return false } return s[column:column+2] == " *" } // indexFrom behaves like strings.Index but only reports matches starting at // |idx|. func indexFrom(s, sep string, idx int) int { ret := strings.Index(s[idx:], sep) if ret < 0 { return -1 } return idx + ret } // A lineGroup is a contiguous group of lines with an eligible comment at the // same column. Any trailing '*/'s will already be removed. type lineGroup struct { // column is the column where the eligible comment begins. line[column] // and line[column+1] will both be replaced with '/'. It is -1 if this // group is not to be converted. column int lines []string } func addLine(groups *[]lineGroup, line string, column int) { if len(*groups) == 0 || (*groups)[len(*groups)-1].column != column { *groups = append(*groups, lineGroup{column, nil}) } (*groups)[len(*groups)-1].lines = append((*groups)[len(*groups)-1].lines, line) } // writeLine writes |line| to |out|, followed by a newline. func writeLine(out *bytes.Buffer, line string) { out.WriteString(line) out.WriteByte('\n') } func convertComments(path string, in []byte) []byte { lines := strings.Split(string(in), "\n") // Account for the trailing newline. if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { lines = lines[:len(lines)-1] } // First pass: identify all comments to be converted. Group them into // lineGroups with the same column. var groups []lineGroup // Find the license block separator. for len(lines) > 0 { line := lines[0] lines = lines[1:] addLine(&groups, line, -1) if len(line) == 0 { break } } // inComment is true if we are in the middle of a comment. var inComment bool // comment is the currently buffered multi-line comment to convert. If // |inComment| is true and it is nil, the current multi-line comment is // not convertable and we copy lines to |out| as-is. var comment []string // column is the column offset of |comment|. var column int for len(lines) > 0 { line := lines[0] lines = lines[1:] var idx int if inComment { // Stop buffering if this comment isn't eligible. if comment != nil && !isContinuation(line, column) { for _, l := range comment { addLine(&groups, l, -1) } comment = nil } // Look for the end of the current comment. idx = strings.Index(line, "*/") if idx < 0 { if comment != nil { comment = append(comment, line) } else { addLine(&groups, line, -1) } continue } inComment = false if comment != nil { if idx == len(line)-2 { // This is a convertable multi-line comment. if idx >= column+2 { // |idx| may be equal to // |column| + 1, if the line is // a '*/' on its own. In that // case, we discard the line. comment = append(comment, line[:idx]) } for _, l := range comment { addLine(&groups, l, column) } comment = nil continue } // Flush the buffered comment unmodified. for _, l := range comment { addLine(&groups, l, -1) } comment = nil } idx += 2 } // Parse starting from |idx|, looking for either a convertable // line comment or a multi-line comment. for { idx = indexFrom(line, "/*", idx) if idx < 0 { addLine(&groups, line, -1) break } endIdx := indexFrom(line, "*/", idx) if endIdx < 0 { // The comment is, so far, eligible for conversion. inComment = true column = idx comment = []string{line} break } if endIdx != len(line)-2 { // Continue parsing for more comments in this line. idx = endIdx + 2 continue } addLine(&groups, line[:endIdx], idx) break } } // Second pass: convert the lineGroups, adjusting spacing as needed. var out bytes.Buffer var lineNo int for _, group := range groups { if group.column < 0 { for _, line := range group.lines { writeLine(&out, line) } } else { // Google C++ style prefers two spaces before a comment // if it is on the same line as code, but clang-format // has been placing one space for block comments. All // comments within a group should be adjusted by the // same amount. var adjust string for _, line := range group.lines { if !allSpaces(line[:group.column]) && line[group.column-1] != '(' { if line[group.column-1] != ' ' { if len(adjust) < 2 { adjust = " " } } else if line[group.column-2] != ' ' { if len(adjust) < 1 { adjust = " " } } } } for i, line := range group.lines { newLine := fmt.Sprintf("%s%s//%s", line[:group.column], adjust, strings.TrimRight(line[group.column+2:], " ")) if len(newLine) > 80 { fmt.Fprintf(os.Stderr, "%s:%d: Line is now longer than 80 characters\n", path, lineNo+i+1) } writeLine(&out, newLine) } } lineNo += len(group.lines) } return out.Bytes() } func main() { for _, arg := range os.Args[1:] { in, err := ioutil.ReadFile(arg) if err != nil { panic(err) } if err := ioutil.WriteFile(arg, convertComments(arg, in), 0666); err != nil { panic(err) } } }