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