1// 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	"fmt"
20	"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
71// 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
88// 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
94func convertComments(path string, in []byte) []byte {
95	lines := strings.Split(string(in), "\n")
96
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
102	// First pass: identify all comments to be converted. Group them into
103	// lineGroups with the same column.
104	var groups []lineGroup
105
106	// Find the license block separator.
107	for len(lines) > 0 {
108		line := lines[0]
109		lines = lines[1:]
110		addLine(&groups, line, -1)
111		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 {
133					addLine(&groups, l, -1)
134				}
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 {
144					addLine(&groups, line, -1)
145				}
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 {
161						addLine(&groups, l, column)
162					}
163					comment = nil
164					continue
165				}
166
167				// Flush the buffered comment unmodified.
168				for _, l := range comment {
169					addLine(&groups, l, -1)
170				}
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 {
181				addLine(&groups, line, -1)
182				break
183			}
184
185			endIdx := indexFrom(line, "*/", idx)
186			if endIdx < 0 {
187				// The comment is, so far, eligible for conversion.
188				inComment = true
189				column = idx
190				comment = []string{line}
191				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
200			addLine(&groups, line[:endIdx], idx)
201			break
202		}
203	}
204
205	// 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	}
245	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		}
254		if err := ioutil.WriteFile(arg, convertComments(arg, in), 0666); err != nil {
255			panic(err)
256		}
257	}
258}
259