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