• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /*
18  * Copyright 2015 Google Inc.
19  *
20  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
21  * in compliance with the License. You may obtain a copy of the License at
22  *
23  *     http://www.apache.org/licenses/LICENSE-2.0
24  *
25  * Unless required by applicable law or agreed to in writing, software distributed under the License
26  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
27  * or implied. See the License for the specific language governing permissions and limitations under
28  * the License.
29  */
30 
31 /*
32  * This was copied from https://github.com/google/google-java-format and modified extensively to
33  * work for Kotlin formatting
34  */
35 
36 package com.facebook.ktfmt.kdoc
37 
38 import com.google.common.base.CharMatcher
39 import com.google.common.base.Strings
40 import com.google.googlejavaformat.CommentsHelper
41 import com.google.googlejavaformat.Input.Tok
42 import com.google.googlejavaformat.Newlines
43 import java.util.ArrayList
44 import java.util.regex.Pattern
45 
46 /** `KDocCommentsHelper` extends [CommentsHelper] to rewrite KDoc comments. */
47 class KDocCommentsHelper(private val lineSeparator: String, private val maxLineLength: Int) :
48     CommentsHelper {
49 
50   private val kdocFormatter =
51       KDocFormatter(
<lambda>null52           KDocFormattingOptions(maxLineLength, maxLineLength).apply {
53             allowParamBrackets = true // TODO Do we want this?
54             convertMarkup = false
55             nestedListIndent = 4
56             optimal = false // Use greedy line breaking for predictability.
57           })
58 
rewritenull59   override fun rewrite(tok: Tok, maxWidth: Int, column0: Int): String {
60     if (!tok.isComment) {
61       return tok.originalText
62     }
63     var text = tok.originalText
64     if (tok.isJavadocComment) {
65       text = kdocFormatter.reformatComment(text, " ".repeat(column0))
66     }
67     val lines = ArrayList<String>()
68     val it = Newlines.lineIterator(text)
69     while (it.hasNext()) {
70       lines.add(CharMatcher.whitespace().trimTrailingFrom(it.next()))
71     }
72     return if (tok.isSlashSlashComment) {
73       indentLineComments(lines, column0)
74     } else if (javadocShaped(lines)) {
75       indentJavadoc(lines, column0)
76     } else {
77       preserveIndentation(lines, column0)
78     }
79   }
80 
81   // For non-javadoc-shaped block comments, shift the entire block to the correct
82   // column, but do not adjust relative indentation.
preserveIndentationnull83   private fun preserveIndentation(lines: List<String>, column0: Int): String {
84     val builder = StringBuilder()
85 
86     // find the leftmost non-whitespace character in all trailing lines
87     var startCol = -1
88     for (i in 1 until lines.size) {
89       val lineIdx = CharMatcher.whitespace().negate().indexIn(lines[i])
90       if (lineIdx >= 0 && (startCol == -1 || lineIdx < startCol)) {
91         startCol = lineIdx
92       }
93     }
94 
95     // output the first line at the current column
96     builder.append(lines[0])
97 
98     // output all trailing lines with plausible indentation
99     for (i in 1 until lines.size) {
100       builder.append(lineSeparator).append(Strings.repeat(" ", column0))
101       // check that startCol is valid index, e.g. for blank lines
102       if (lines[i].length >= startCol) {
103         builder.append(lines[i].substring(startCol))
104       } else {
105         builder.append(lines[i])
106       }
107     }
108     return builder.toString()
109   }
110 
111   // Wraps and re-indents line comments.
indentLineCommentsnull112   private fun indentLineComments(lines: List<String>, column0: Int): String {
113     val wrappedLines = wrapLineComments(lines, column0)
114     val builder = StringBuilder()
115     builder.append(wrappedLines[0].trim())
116     val indentString = Strings.repeat(" ", column0)
117     for (i in 1 until wrappedLines.size) {
118       builder.append(lineSeparator).append(indentString).append(wrappedLines[i].trim())
119     }
120     return builder.toString()
121   }
122 
wrapLineCommentsnull123   private fun wrapLineComments(lines: List<String>, column0: Int): List<String> {
124     val result = ArrayList<String>()
125     for (originalLine in lines) {
126       var line = originalLine
127       // Add missing leading spaces to line comments: `//foo` -> `// foo`.
128       val matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line)
129       if (matcher.find()) {
130         val length = matcher.group(1).length
131         line = Strings.repeat("/", length) + " " + line.substring(length)
132       }
133       if (line.startsWith("// MOE:")) {
134         // don't wrap comments for https://github.com/google/MOE
135         result.add(line)
136         continue
137       }
138       while (line.length + column0 > maxLineLength) {
139         var idx = maxLineLength - column0
140         // only break on whitespace characters, and ignore the leading `// `
141         while (idx >= 2 && !CharMatcher.whitespace().matches(line[idx])) {
142           idx--
143         }
144         if (idx <= 2) {
145           break
146         }
147         result.add(line.substring(0, idx))
148         line = "//" + line.substring(idx)
149       }
150       result.add(line)
151     }
152     return result
153   }
154 
155   // Remove leading whitespace (trailing was already removed), and re-indent.
156   // Add a +1 indent before '*', and add the '*' if necessary.
indentJavadocnull157   private fun indentJavadoc(lines: List<String>, column0: Int): String {
158     val builder = StringBuilder()
159     builder.append(lines[0].trim())
160     val indent = column0 + 1
161     val indentString = Strings.repeat(" ", indent)
162     for (i in 1 until lines.size) {
163       builder.append(lineSeparator).append(indentString)
164       val line = lines[i].trim()
165       if (!line.startsWith("*")) {
166         builder.append("* ")
167       }
168       builder.append(line)
169     }
170     return builder.toString()
171   }
172 
173   // Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
174   // contain leading spaces.
175   private val LINE_COMMENT_MISSING_SPACE_PREFIX =
176       Pattern.compile("^(//+)(?!noinspection|\\\$NON-NLS-\\d+\\$)[^\\s/]")
177 
178   // Returns true if the comment looks like javadoc
javadocShapednull179   private fun javadocShaped(lines: List<String>): Boolean {
180     val it = lines.iterator()
181     if (!it.hasNext()) {
182       return false
183     }
184     val first = it.next().trim()
185     // if it's actually javadoc, we're done
186     if (first.startsWith("/**")) {
187       return true
188     }
189     // if it's a block comment, check all trailing lines for '*'
190     if (!first.startsWith("/*")) {
191       return false
192     }
193     while (it.hasNext()) {
194       if (!it.next().trim().startsWith("*")) {
195         return false
196       }
197     }
198     return true
199   }
200 }
201