| // 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) |
| } |
| } |
| } |