diff --git a/util/convert_comments.go b/util/convert_comments.go new file mode 100644 index 00000000..f5171c35 --- /dev/null +++ b/util/convert_comments.go @@ -0,0 +1,216 @@ +// 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" + "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 +} + +// writeLine writes |line| to |out|, followed by a newline. +func writeLine(out *bytes.Buffer, line string) { + out.WriteString(line) + out.WriteByte('\n') +} + +func convertComments(in []byte) []byte { + lines := strings.Split(string(in), "\n") + var out bytes.Buffer + + // Account for the trailing newline. + if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { + lines = lines[:len(lines)-1] + } + + // Find the license block separator. + for len(lines) > 0 { + line := lines[0] + lines = lines[1:] + writeLine(&out, line) + 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 { + writeLine(&out, l) + } + 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 { + writeLine(&out, line) + } + 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 { + out.WriteString(l[:column]) + out.WriteString("//") + writeLine(&out, strings.TrimRight(l[column+2:], " ")) + } + comment = nil + continue + } + + // Flush the buffered comment unmodified. + for _, l := range comment { + writeLine(&out, l) + } + 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 { + writeLine(&out, line) + break + } + + endIdx := indexFrom(line, "*/", idx) + if endIdx < 0 { + inComment = true + if allSpaces(line[:idx]) { + // The comment is, so far, eligible for conversion. + column = idx + comment = []string{line} + } + break + } + + if endIdx != len(line)-2 { + // Continue parsing for more comments in this line. + idx = endIdx + 2 + continue + } + + out.WriteString(line[:idx]) + + // 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. Fix this. + if !allSpaces(line[:idx]) { + if line[idx-1] != ' ' { + out.WriteString(" ") + } else if line[idx-2] != ' ' { + out.WriteString(" ") + } + } + + out.WriteString("//") + writeLine(&out, strings.TrimRight(line[idx+2:endIdx], " ")) + break + } + } + + 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(in), 0666); err != nil { + panic(err) + } + } +}