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