• 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 (c) Tor Norbye.
19  *
20  * Licensed under the Apache License, Version 2.0 (the "License");
21  * you may not use this file except in compliance with the License.
22  * You may obtain a copy of the License at
23  *
24  *     http://www.apache.org/licenses/LICENSE-2.0
25  *
26  * Unless required by applicable law or agreed to in writing, software
27  * distributed under the License is distributed on an "AS IS" BASIS,
28  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29  * See the License for the specific language governing permissions and
30  * limitations under the License.
31  */
32 
33 package com.facebook.ktfmt.kdoc
34 
35 import kotlin.math.min
36 
37 /** Formatter which can reformat KDoc comments. */
38 class KDocFormatter(private val options: KDocFormattingOptions) {
39   /** Reformats the [comment], which follows the given [initialIndent] string. */
reformatCommentnull40   fun reformatComment(comment: String, initialIndent: String): String {
41     return reformatComment(FormattingTask(options, comment, initialIndent))
42   }
43 
reformatCommentnull44   fun reformatComment(task: FormattingTask): String {
45     val indent = task.secondaryIndent
46     val indentSize = getIndentSize(indent, options)
47     val firstIndentSize = getIndentSize(task.initialIndent, options)
48     val comment = task.comment
49     val lineComment = comment.isLineComment()
50     val blockComment = comment.isBlockComment()
51     val paragraphs = ParagraphListBuilder(comment, options, task).scan(indentSize)
52     val commentType = task.type
53     val lineSeparator = "\n$indent${commentType.linePrefix}"
54     val prefix = commentType.prefix
55 
56     // Collapse single line? If alternate is turned on, use the opposite of the
57     // setting
58     val collapseLine = options.collapseSingleLine.let { if (options.alternate) !it else it }
59     if (paragraphs.isSingleParagraph() && collapseLine && !lineComment) {
60       // Does the text fit on a single line?
61       val trimmed = paragraphs.firstOrNull()?.text?.trim() ?: ""
62       // Subtract out space for "/** " and " */" and the indent:
63       val width =
64           min(
65               options.maxLineWidth - firstIndentSize - commentType.singleLineOverhead(),
66               options.maxCommentWidth)
67       val suffix = if (commentType.suffix.isEmpty()) "" else " ${commentType.suffix}"
68       if (trimmed.length <= width) {
69         return "$prefix $trimmed$suffix"
70       }
71       if (indentSize < firstIndentSize) {
72         val nextLineWidth =
73             min(
74                 options.maxLineWidth - indentSize - commentType.singleLineOverhead(),
75                 options.maxCommentWidth)
76         if (trimmed.length <= nextLineWidth) {
77           return "$prefix $trimmed$suffix"
78         }
79       }
80     }
81 
82     val sb = StringBuilder()
83 
84     sb.append(prefix)
85     if (lineComment) {
86       sb.append(' ')
87     } else {
88       sb.append(lineSeparator)
89     }
90 
91     for (paragraph in paragraphs) {
92       if (paragraph.separate) {
93         // Remove trailing spaces which can happen when we have a paragraph
94         // separator
95         stripTrailingSpaces(lineComment, sb)
96         sb.append(lineSeparator)
97       }
98       val text = paragraph.text
99       if (paragraph.preformatted || paragraph.table) {
100         sb.append(text)
101         // Remove trailing spaces which can happen when we have an empty line in a
102         // preformatted paragraph.
103         stripTrailingSpaces(lineComment, sb)
104         sb.append(lineSeparator)
105         continue
106       }
107 
108       val lineWithoutIndent = options.maxLineWidth - commentType.lineOverhead()
109       val quoteAdjustment = if (paragraph.quoted) 2 else 0
110       val maxLineWidth =
111           min(options.maxCommentWidth, lineWithoutIndent - indentSize) - quoteAdjustment
112       val firstMaxLineWidth =
113           if (sb.indexOf('\n') == -1) {
114             min(options.maxCommentWidth, lineWithoutIndent - firstIndentSize) - quoteAdjustment
115           } else {
116             maxLineWidth
117           }
118 
119       val lines = paragraph.reflow(firstMaxLineWidth, maxLineWidth)
120       var first = true
121       val hangingIndent = paragraph.hangingIndent
122       for (line in lines) {
123         sb.append(paragraph.indent)
124         if (first && !paragraph.continuation) {
125           first = false
126         } else {
127           sb.append(hangingIndent)
128         }
129         if (paragraph.quoted) {
130           sb.append("> ")
131         }
132         if (line.isEmpty()) {
133           // Remove trailing spaces which can happen when we have a paragraph
134           // separator
135           stripTrailingSpaces(lineComment, sb)
136         } else {
137           sb.append(line)
138         }
139         sb.append(lineSeparator)
140       }
141     }
142     if (!lineComment) {
143       if (sb.endsWith("* ")) {
144         sb.setLength(sb.length - 2)
145       }
146       sb.append("*/")
147     } else if (sb.endsWith(lineSeparator)) {
148       @Suppress("NoOp", "ReturnValueIgnored") sb.removeSuffix(lineSeparator)
149     }
150 
151     val formatted =
152         if (lineComment) {
153           sb.trim().removeSuffix("//").trim().toString()
154         } else if (blockComment) {
155           sb.toString().replace(lineSeparator + "\n", "\n\n")
156         } else {
157           sb.toString()
158         }
159 
160     val separatorIndex = comment.indexOf('\n')
161     return if (separatorIndex > 0 && comment[separatorIndex - 1] == '\r') {
162       // CRLF separator
163       formatted.replace("\n", "\r\n")
164     } else {
165       formatted
166     }
167   }
168 
stripTrailingSpacesnull169   private fun stripTrailingSpaces(lineComment: Boolean, sb: StringBuilder) {
170     if (!lineComment && sb.endsWith("* ")) {
171       sb.setLength(sb.length - 1)
172     } else if (lineComment && sb.endsWith("// ")) {
173       sb.setLength(sb.length - 1)
174     }
175   }
176 }
177