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