convert_comments.go 6.9 KB

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