blob: ca8ebcfd0dca4dd9e2b29916e6ab555f35d79c6e [file] [log] [blame]
// Copyright (c) Facebook, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.facebook.ktfmt
import com.google.common.base.MoreObjects
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableRangeMap
import com.google.common.collect.Range
import com.google.googlejavaformat.Input
import com.google.googlejavaformat.Newlines
import com.google.googlejavaformat.java.JavaOutput
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import java.util.regex.Pattern
import org.jetbrains.kotlin.lexer.KtToken
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
// TODO: share the code with JavaInput instead of copy-pasting here.
/**
* KotlinInput is for Kotlin what JavaInput is for Java.
*
* <p>KotlinInput is duplicating most of JavaInput's code, but uses the Kotlin compiler as a lexer instead of Javac.
* This is required because some valid Kotlin programs are not valid Java programs, e.g., "a..b".
*
* <p>See javadoc for JavaInput.
*/
class KotlinInput(private val text: String, file: KtFile) : Input() {
private val tokens: ImmutableList<Token> // The Tokens for this input.
private val positionToColumnMap: ImmutableMap<Int, Int> // Map Tok position to column.
private val positionTokenMap: ImmutableRangeMap<Int, Token> // Map position to Token.
private var kN = 0 // The number of numbered toks (tokens or comments), excluding the EOF.
private val kToToken: Array<Token?>
init {
setLines(ImmutableList.copyOf(Newlines.lineIterator(text)))
val toks = buildToks(file, text)
positionToColumnMap = makePositionToColumnMap(toks)
tokens = buildTokens(toks)
val tokenLocations = ImmutableRangeMap.builder<Int, Token>()
for (token in tokens) {
val end = JavaOutput.endTok(token)
var upper = end.position
if (end.text.isNotEmpty()) {
upper += end.length() - 1
}
tokenLocations.put(Range.closed(JavaOutput.startTok(token).position, upper), token)
}
positionTokenMap = tokenLocations.build()
// adjust kN for EOF
kToToken = arrayOfNulls(kN + 1)
for (token in tokens) {
for (tok in token.toksBefore) {
if (tok.index < 0) {
continue
}
kToToken[tok.index] = token
}
kToToken[token.tok.index] = token
for (tok in token.toksAfter) {
if (tok.index < 0) {
continue
}
kToToken[tok.index] = token
}
}
}
private fun makePositionToColumnMap(toks: List<KotlinTok>): ImmutableMap<Int, Int> {
val builder = ImmutableMap.builder<Int, Int>()
for (tok in toks) {
builder.put(tok.position, tok.column)
}
return builder.build()
}
private fun buildToks(file: KtFile, fileText: String): ImmutableList<KotlinTok> {
val tokenizer = Tokenizer(fileText, file)
file.accept(tokenizer)
val toks = tokenizer.toks
toks.add(KotlinTok(tokenizer.index, "", "", fileText.length, 0, true, KtTokens.EOF))
kN = tokenizer.index
computeRanges(toks)
return ImmutableList.copyOf(toks)
}
private fun buildTokens(toks: List<KotlinTok>): ImmutableList<Token> {
val tokens = ImmutableList.builder<Token>()
var k = 0
val kN = toks.size
// Remaining non-tokens before the token go here.
var toksBefore: ImmutableList.Builder<KotlinTok> = ImmutableList.builder()
OUTERMOST@ while (k < kN) {
while (!toks[k].isToken) {
val tok = toks[k++]
toksBefore.add(tok)
if (isParamComment(tok)) {
while (toks[k].isNewline) {
// drop newlines after parameter comments
k++
}
}
}
val tok = toks[k++]
// Non-tokens starting on the same line go here too.
val toksAfter = ImmutableList.builder<KotlinTok>()
OUTER@ while (k < kN && !toks[k].isToken) {
// Don't attach inline comments to certain leading tokens, e.g. for `f(/*flag1=*/true).
//
// Attaching inline comments to the right token is hard, and this barely
// scratches the surface. But it's enough to do a better job with parameter
// name comments.
//
// TODO(cushon): find a better strategy.
if (toks[k].isSlashStarComment && (tok.text == "(" || tok.text == "<" || tok.text == ".")) break@OUTER
if (toks[k].isJavadocComment && tok.text == ";") break@OUTER
if (isParamComment(toks[k])) {
tokens.add(KotlinToken(toksBefore.build(), tok, toksAfter.build()))
toksBefore = ImmutableList.builder<KotlinTok>().add(toks[k++])
// drop newlines after parameter comments
while (toks[k].isNewline) {
k++
}
continue@OUTERMOST
}
val nonTokenAfter = toks[k++]
toksAfter.add(nonTokenAfter)
if (Newlines.containsBreaks(nonTokenAfter.text)) {
break
}
}
tokens.add(KotlinToken(toksBefore.build(), tok, toksAfter.build()))
toksBefore = ImmutableList.builder()
}
return tokens.build()
}
private fun isParamComment(tok: Tok): Boolean {
return tok.isSlashStarComment && tok.text.matches("/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*/".toRegex())
}
override fun getkN(): Int = kN
override fun getToken(k: Int): Token? = kToToken[k]
override fun getTokens(): ImmutableList<out Token> = tokens
override fun getPositionTokenMap(): ImmutableRangeMap<Int, out Token> = positionTokenMap
override fun getPositionToColumnMap(): ImmutableMap<Int, Int> = positionToColumnMap
override fun getText(): String = text
override fun getLineNumber(inputPosition: Int) =
StringUtil.offsetToLineColumn(text, inputPosition).line + 1
override fun getColumnNumber(inputPosition: Int) =
StringUtil.offsetToLineColumn(text, inputPosition).column
}
class KotlinTok(
private val index: Int,
private val originalText: String,
private val text: String,
private val position: Int,
private val columnI: Int,
val isToken: Boolean,
private val kind: KtToken
) : Input.Tok {
override fun getIndex(): Int = index
override fun getText(): String = text
override fun getOriginalText(): String = originalText
override fun length(): Int = originalText.length
override fun getPosition(): Int = position
override fun getColumn(): Int = columnI
override fun isNewline(): Boolean = Newlines.isNewline(text)
override fun isSlashSlashComment(): Boolean = text.startsWith("//")
override fun isSlashStarComment(): Boolean = text.startsWith("/*")
override fun isJavadocComment(): Boolean = text.startsWith("/**") && text.length > 4
override fun isComment(): Boolean = isSlashSlashComment || isSlashStarComment
fun kind(): KtToken = kind
override fun toString(): String {
return MoreObjects.toStringHelper(this)
.add("index", index)
.add("text", text)
.add("position", position)
.add("columnI", columnI)
.add("isToken", isToken)
.toString()
}
}
class KotlinToken(
private val toksBefore: ImmutableList<KotlinTok>,
private val kotlinTok: KotlinTok,
private val toksAfter: ImmutableList<KotlinTok>
) : Input.Token {
override fun getTok(): KotlinTok = kotlinTok
override fun getToksBefore(): ImmutableList<out Input.Tok> = toksBefore
override fun getToksAfter(): ImmutableList<out Input.Tok> = toksAfter
override fun toString(): String {
return MoreObjects.toStringHelper(this)
.add("tok", kotlinTok)
.add("toksBefore", toksBefore)
.add("toksAfter", toksAfter)
.toString()
}
}
internal val WHITESPACE_NEWLINE_REGEX: Pattern = Pattern.compile("\\R|( )+")
/**
* Tokenizer traverses a Kotlin parse tree (which blessedly contains whitespaces and comments, unlike Javac) and
* constructs a list of 'Tok's.
*
* <p>The google-java-format infra expects newline Toks to be separate from maximal-whitespace Toks, but Kotlin emits
* them together. So, we split them using Java's \R regex matcher. We don't use 'split' et al. because we want Toks for
* the newlines themselves.
*/
class Tokenizer(private val fileText: String, val file: KtFile) : KtTreeVisitorVoid() {
val toks = mutableListOf<KotlinTok>()
var index = 0
override fun visitElement(element: PsiElement) {
when (element) {
is PsiComment -> {
val startIndex = element.startOffset
toks.add(
KotlinTok(
index,
fileText.substring(startIndex, element.endOffset),
element.text,
startIndex,
0,
false,
KtTokens.EOF))
index++
return
}
is LeafPsiElement -> {
val elementText = element.text
val startIndex = element.startOffset
val endIndex = element.endOffset
if (element is PsiWhiteSpace) {
val matcher = WHITESPACE_NEWLINE_REGEX.matcher(elementText)
while (matcher.find()) {
val text = matcher.group()
toks.add(
KotlinTok(
-1,
fileText.substring(startIndex + matcher.start(), startIndex + matcher.end()),
text,
startIndex + matcher.start(),
0,
false,
KtTokens.EOF))
}
} else {
toks.add(
KotlinTok(
index,
fileText.substring(startIndex, endIndex),
elementText,
startIndex,
0,
true,
KtTokens.EOF))
index++
}
}
}
super.visitElement(element)
}
}