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