xref: /aosp_15_r20/external/ktfmt/core/src/main/java/com/facebook/ktfmt/format/KotlinInput.kt (revision 5be3f65c8cf0e6db0a7e312df5006e8e93cdf9ec)
1 /*
2  * 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 package com.facebook.ktfmt.format
18 
19 import com.google.common.collect.DiscreteDomain
20 import com.google.common.collect.ImmutableList
21 import com.google.common.collect.ImmutableMap
22 import com.google.common.collect.ImmutableRangeMap
23 import com.google.common.collect.Iterables.getLast
24 import com.google.common.collect.Range
25 import com.google.common.collect.RangeSet
26 import com.google.common.collect.TreeRangeSet
27 import com.google.googlejavaformat.Input
28 import com.google.googlejavaformat.Newlines
29 import com.google.googlejavaformat.java.FormatterException
30 import com.google.googlejavaformat.java.JavaOutput
31 import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil
32 import org.jetbrains.kotlin.lexer.KtTokens
33 import org.jetbrains.kotlin.psi.KtFile
34 
35 // TODO: share the code with JavaInput instead of copy-pasting here.
36 /**
37  * KotlinInput is for Kotlin what JavaInput is for Java.
38  *
39  * <p>KotlinInput is duplicating most of JavaInput's code, but uses the Kotlin compiler as a lexer
40  * instead of Javac. This is required because some valid Kotlin programs are not valid Java
41  * programs, e.g., "a..b".
42  *
43  * <p>See javadoc for JavaInput.
44  */
45 class KotlinInput(private val text: String, file: KtFile) : Input() {
46   private val tokens: ImmutableList<Token> // The Tokens for this input.
47   private val positionToColumnMap: ImmutableMap<Int, Int> // Map Tok position to column.
48   private val positionTokenMap: ImmutableRangeMap<Int, Token> // Map position to Token.
49   private var kN = 0 // The number of numbered toks (tokens or comments), excluding the EOF.
50   private val kToToken: Array<Token?>
51 
52   init {
53     setLines(ImmutableList.copyOf(Newlines.lineIterator(text)))
54     val toks = buildToks(file, text)
55     positionToColumnMap = makePositionToColumnMap(toks)
56     tokens = buildTokens(toks)
57     positionTokenMap = buildTokenPositionsMap(tokens)
58 
59     // adjust kN for EOF
60     kToToken = arrayOfNulls(kN + 1)
61     for (token in tokens) {
62       for (tok in token.toksBefore) {
63         if (tok.index < 0) {
64           continue
65         }
66         kToToken[tok.index] = token
67       }
68       kToToken[token.tok.index] = token
69       for (tok in token.toksAfter) {
70         if (tok.index < 0) {
71           continue
72         }
73         kToToken[tok.index] = token
74       }
75     }
76   }
77 
78   @Throws(FormatterException::class)
characterRangesToTokenRangesnull79   fun characterRangesToTokenRanges(characterRanges: Collection<Range<Int>>): RangeSet<Int> {
80     val tokenRangeSet = TreeRangeSet.create<Int>()
81     for (characterRange0 in characterRanges) {
82       val characterRange = characterRange0.canonical(DiscreteDomain.integers())
83       tokenRangeSet.add(
84           characterRangeToTokenRange(
85               characterRange.lowerEndpoint(),
86               characterRange.upperEndpoint() - characterRange.lowerEndpoint()))
87     }
88     return tokenRangeSet
89   }
90 
91   /**
92    * Convert from an offset and length flag pair to a token range.
93    *
94    * @param offset the `0`-based offset in characters
95    * @param length the length in characters
96    * @return the `0`-based [Range] of tokens
97    * @throws FormatterException
98    */
99   @Throws(FormatterException::class)
characterRangeToTokenRangenull100   internal fun characterRangeToTokenRange(offset: Int, length: Int): Range<Int> {
101     val requiredLength = offset + length
102     if (requiredLength > text.length) {
103       throw FormatterException(
104           String.format(
105               "error: invalid length %d, offset + length (%d) is outside the file",
106               length,
107               requiredLength))
108     }
109     val expandedLength =
110         when {
111           length < 0 -> return EMPTY_RANGE
112           length == 0 -> 1 // 0 stands for "format the line under the cursor"
113           else -> length
114         }
115     val enclosed =
116         getPositionTokenMap()
117             .subRangeMap(Range.closedOpen(offset, offset + expandedLength))
118             .asMapOfRanges()
119             .values
120     return if (enclosed.isEmpty()) {
121       EMPTY_RANGE
122     } else
123         Range.closedOpen(
124             enclosed.iterator().next().tok.index, getLast(enclosed).getTok().getIndex() + 1)
125   }
126 
makePositionToColumnMapnull127   private fun makePositionToColumnMap(toks: List<KotlinTok>) =
128       ImmutableMap.copyOf(toks.map { it.position to it.column }.toMap())
129 
buildToksnull130   private fun buildToks(file: KtFile, fileText: String): ImmutableList<KotlinTok> {
131     val tokenizer = Tokenizer(fileText, file)
132     file.accept(tokenizer)
133     val toks = tokenizer.toks
134     toks.add(KotlinTok(tokenizer.index, "", "", fileText.length, 0, true, KtTokens.EOF))
135     kN = tokenizer.index
136     computeRanges(toks)
137     return ImmutableList.copyOf(toks)
138   }
139 
buildTokensnull140   private fun buildTokens(toks: List<KotlinTok>): ImmutableList<Token> {
141     val tokens = ImmutableList.builder<Token>()
142     var k = 0
143     val kN = toks.size
144 
145     // Remaining non-tokens before the token go here.
146     var toksBefore: ImmutableList.Builder<KotlinTok> = ImmutableList.builder()
147 
148     OUTERMOST@ while (k < kN) {
149       while (!toks[k].isToken) {
150         val tok = toks[k++]
151         toksBefore.add(tok)
152         if (isParamComment(tok)) {
153           while (toks[k].isNewline) {
154             // drop newlines after parameter comments
155             k++
156           }
157         }
158       }
159       val tok = toks[k++]
160 
161       // Non-tokens starting on the same line go here too.
162       val toksAfter = ImmutableList.builder<KotlinTok>()
163       OUTER@ while (k < kN && !toks[k].isToken) {
164         // Don't attach inline comments to certain leading tokens, e.g. for `f(/*flag1=*/true).
165         //
166         // Attaching inline comments to the right token is hard, and this barely
167         // scratches the surface. But it's enough to do a better job with parameter
168         // name comments.
169         //
170         // TODO(cushon): find a better strategy.
171         if (toks[k].isSlashStarComment && (tok.text == "(" || tok.text == "<" || tok.text == "."))
172             break@OUTER
173         if (toks[k].isJavadocComment && tok.text == ";") break@OUTER
174         if (isParamComment(toks[k])) {
175           tokens.add(KotlinToken(toksBefore.build(), tok, toksAfter.build()))
176           toksBefore = ImmutableList.builder<KotlinTok>().add(toks[k++])
177           // drop newlines after parameter comments
178           while (toks[k].isNewline) {
179             k++
180           }
181           continue@OUTERMOST
182         }
183         val nonTokenAfter = toks[k++]
184         toksAfter.add(nonTokenAfter)
185         if (Newlines.containsBreaks(nonTokenAfter.text)) {
186           break
187         }
188       }
189       tokens.add(KotlinToken(toksBefore.build(), tok, toksAfter.build()))
190       toksBefore = ImmutableList.builder()
191     }
192     return tokens.build()
193   }
194 
buildTokenPositionsMapnull195   private fun buildTokenPositionsMap(tokens: ImmutableList<Token>): ImmutableRangeMap<Int, Token> {
196     val tokenLocations = ImmutableRangeMap.builder<Int, Token>()
197     for (token in tokens) {
198       val end = JavaOutput.endTok(token)
199       val endPosition = end.position + (if (end.text.isNotEmpty()) end.length() - 1 else 0)
200       tokenLocations.put(Range.closed(JavaOutput.startTok(token).position, endPosition), token)
201     }
202 
203     return tokenLocations.build()
204   }
205 
isParamCommentnull206   private fun isParamComment(tok: Tok): Boolean {
207     return tok.isSlashStarComment && tok.text.matches("/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*/".toRegex())
208   }
209 
getkNnull210   override fun getkN(): Int = kN
211 
212   override fun getToken(k: Int): Token? = kToToken[k]
213 
214   override fun getTokens(): ImmutableList<out Token> = tokens
215 
216   override fun getPositionTokenMap(): ImmutableRangeMap<Int, out Token> = positionTokenMap
217 
218   override fun getPositionToColumnMap(): ImmutableMap<Int, Int> = positionToColumnMap
219 
220   override fun getText(): String = text
221 
222   override fun getLineNumber(inputPosition: Int) =
223       StringUtil.offsetToLineColumn(text, inputPosition).line + 1
224 
225   override fun getColumnNumber(inputPosition: Int) =
226       StringUtil.offsetToLineColumn(text, inputPosition).column
227 }
228