blob: c03eeb81baba82498563416e0fd8ae954fe8391c [file] [log] [blame]
David Benjamin9ad98f72017-07-17 20:35:59 -04001// 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"
David Benjaminbda7b9a2017-08-02 13:53:10 -040019 "fmt"
David Benjamin9ad98f72017-07-17 20:35:59 -040020 "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
David Benjaminbda7b9a2017-08-02 13:53:10 -040071// 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
David Benjamin9ad98f72017-07-17 20:35:59 -040088// 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
David Benjaminbda7b9a2017-08-02 13:53:10 -040094func convertComments(path string, in []byte) []byte {
David Benjamin9ad98f72017-07-17 20:35:59 -040095 lines := strings.Split(string(in), "\n")
David Benjamin9ad98f72017-07-17 20:35:59 -040096
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
David Benjaminbda7b9a2017-08-02 13:53:10 -0400102 // First pass: identify all comments to be converted. Group them into
103 // lineGroups with the same column.
104 var groups []lineGroup
105
David Benjamin9ad98f72017-07-17 20:35:59 -0400106 // Find the license block separator.
107 for len(lines) > 0 {
108 line := lines[0]
109 lines = lines[1:]
David Benjaminbda7b9a2017-08-02 13:53:10 -0400110 addLine(&groups, line, -1)
David Benjamin9ad98f72017-07-17 20:35:59 -0400111 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 {
David Benjaminbda7b9a2017-08-02 13:53:10 -0400133 addLine(&groups, l, -1)
David Benjamin9ad98f72017-07-17 20:35:59 -0400134 }
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 {
David Benjaminbda7b9a2017-08-02 13:53:10 -0400144 addLine(&groups, line, -1)
David Benjamin9ad98f72017-07-17 20:35:59 -0400145 }
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 {
David Benjaminbda7b9a2017-08-02 13:53:10 -0400161 addLine(&groups, l, column)
David Benjamin9ad98f72017-07-17 20:35:59 -0400162 }
163 comment = nil
164 continue
165 }
166
167 // Flush the buffered comment unmodified.
168 for _, l := range comment {
David Benjaminbda7b9a2017-08-02 13:53:10 -0400169 addLine(&groups, l, -1)
David Benjamin9ad98f72017-07-17 20:35:59 -0400170 }
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 {
David Benjaminbda7b9a2017-08-02 13:53:10 -0400181 addLine(&groups, line, -1)
David Benjamin9ad98f72017-07-17 20:35:59 -0400182 break
183 }
184
185 endIdx := indexFrom(line, "*/", idx)
186 if endIdx < 0 {
David Benjamin6c545472017-07-29 01:57:34 -0400187 // The comment is, so far, eligible for conversion.
David Benjamin9ad98f72017-07-17 20:35:59 -0400188 inComment = true
David Benjamin6c545472017-07-29 01:57:34 -0400189 column = idx
190 comment = []string{line}
David Benjamin9ad98f72017-07-17 20:35:59 -0400191 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
David Benjaminbda7b9a2017-08-02 13:53:10 -0400200 addLine(&groups, line[:endIdx], idx)
David Benjamin9ad98f72017-07-17 20:35:59 -0400201 break
202 }
203 }
204
David Benjaminbda7b9a2017-08-02 13:53:10 -0400205 // 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 }
David Benjamin9ad98f72017-07-17 20:35:59 -0400245 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 }
David Benjaminbda7b9a2017-08-02 13:53:10 -0400254 if err := ioutil.WriteFile(arg, convertComments(arg, in), 0666); err != nil {
David Benjamin9ad98f72017-07-17 20:35:59 -0400255 panic(err)
256 }
257 }
258}