FIR LT: Introduce source file abstraction, carry it from parsing to IR

along with source lines mapping, allows to "emulate" usage of the
PSI files which allows to extract source file and line mapping info
on every stage from source element.
It makes sense to use this mapping for the error reporting too.
This commit is contained in:
Ilya Chernikov
2022-02-19 21:07:21 +01:00
committed by teamcity
parent bd60d4b2a6
commit 03cbfea737
45 changed files with 539 additions and 239 deletions
@@ -14,9 +14,6 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.tree.IElementType
import com.intellij.util.diff.FlyweightCapableTreeStructure
import java.io.Closeable
import java.io.File
import java.io.InputStreamReader
sealed class KtSourceElementKind
@@ -444,90 +441,3 @@ inline fun LighterASTNode.toKtLightSourceElement(
endOffset: Int = this.endOffset
): KtLightSourceElement = KtLightSourceElement(this, startOffset, endOffset, tree, kind)
class KtSourceFilePosition(val line: Int, val column: Int, val lineContent: String?) {
// NOTE: This method is used for presenting positions to the user
override fun toString(): String = if (line < 0) "(offset: $column line unknown)" else "($line,$column)"
companion object {
val NONE = KtSourceFilePosition(-1, -1, null)
}
}
class SequentialFilePositionFinder(file: File) : Closeable {
private var reader: InputStreamReader = file.reader(/* TODO: select proper charset */)
private var currentLineContent: String? = null
private val buffer = CharArray(255)
private var bufLength = -1
private var bufPos = 0
private var endOfStream = false
private var skipNextLf = false
private var charsRead = 0
private var currentLine = 0
// assuming that if called multiple times, calls should be sorted by ascending offset
fun findNextPosition(offset: Int, withLineContents: Boolean = true): KtSourceFilePosition {
assert(offset >= charsRead - (currentLineContent?.length ?: 0))
fun posInCurrentLine(): KtSourceFilePosition? {
val col = offset - (charsRead - currentLineContent!!.length - 1)/* beginning of line offset */ + 1 /* col is 1-based */
return if (col <= currentLineContent!!.length)
KtSourceFilePosition(currentLine, col, if (withLineContents) currentLineContent else null)
else null
}
if (offset < charsRead) {
return posInCurrentLine()!!
}
while (true) {
if (currentLineContent == null) {
currentLineContent = readNextLine()
}
posInCurrentLine()?.let { return@findNextPosition it }
if (endOfStream) return KtSourceFilePosition(-1, offset, if (withLineContents) currentLineContent else null)
currentLineContent = null
}
}
private fun readNextLine() = buildString {
while (true) {
if (bufPos >= bufLength) {
bufLength = reader.read(buffer)
bufPos = 0
if (bufLength < 0) {
endOfStream = true
break
}
} else {
val c = buffer[bufPos++]
charsRead++
when {
c == '\n' && skipNextLf -> {
skipNextLf = false
}
c == '\n' || c == '\r' -> {
currentLine++
skipNextLf = c == '\r'
break
}
else -> {
append(c)
skipNextLf = false
}
}
}
}
}
override fun close() {
reader.close()
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStream
interface KtSourceFile {
val name: String
val path: String?
fun getContentsAsStream(): InputStream
}
class KtPsiSourceFile(val psiFile: PsiFile) : KtSourceFile {
override val name: String
get() = psiFile.name
override val path: String?
get() = psiFile.virtualFile?.path
override fun getContentsAsStream(): InputStream = psiFile.virtualFile.inputStream
}
class KtVirtualFileSourceFile(val virtualFile: VirtualFile) : KtSourceFile {
override val name: String
get() = virtualFile.name
override val path: String
get() = virtualFile.path
override fun getContentsAsStream(): InputStream = virtualFile.inputStream
}
class KtIoFileSourceFile(val file: File) : KtSourceFile {
override val name: String
get() = file.name
override val path: String
get() = FileUtilRt.toSystemIndependentName(file.path)
override fun getContentsAsStream(): InputStream = file.inputStream()
}
class KtInMemoryTextSourceFile(
override val name: String,
override val path: String?,
val text: CharSequence
) : KtSourceFile {
override fun getContentsAsStream(): InputStream = ByteArrayInputStream(text.toString().toByteArray())
}
@@ -0,0 +1,126 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin
import com.intellij.openapi.editor.Document
import com.intellij.psi.PsiFile
import java.io.InputStreamReader
interface KtSourceFileLinesMapping {
fun getLineStartOffset(line: Int): Int
fun getLineAndColumnByOffset(offset: Int): Pair<Int, Int>
fun getLineByOffset(offset: Int): Int
val lastOffset: Int
val linesCount: Int
}
class KtPsiSourceFileLinesMapping(val psiFile: PsiFile) : KtSourceFileLinesMapping {
private val document: Document? by lazy { psiFile.viewProvider.document }
override fun getLineStartOffset(line: Int): Int =
document?.getLineStartOffset(line) ?: -1
override fun getLineAndColumnByOffset(offset: Int): Pair<Int, Int> =
document?.let {
val lineNumber = it.getLineNumber(offset)
val lineStartOffset = it.getLineStartOffset(lineNumber)
lineNumber to offset - lineStartOffset
} ?: (-1 to -1)
override fun getLineByOffset(offset: Int): Int =
document?.getLineNumber(offset) ?: -1
override val lastOffset: Int
get() = document?.textLength ?: -1
override val linesCount: Int
get() = document?.lineCount ?: 0
}
open class KtSourceFileLinesMappingFromLineStartOffsets(
val lineStartOffsets: IntArray, override val lastOffset: Int
) : KtSourceFileLinesMapping {
override fun getLineStartOffset(line: Int): Int = lineStartOffsets[line]
override fun getLineAndColumnByOffset(offset: Int): Pair<Int, Int> {
val lineNumber = getLineByOffset(offset)
if (lineNumber < 0) return -1 to -1
val lineStartOffset = lineStartOffsets[lineNumber]
return lineNumber to offset - lineStartOffset
}
override fun getLineByOffset(offset: Int): Int {
val index = lineStartOffsets.binarySearch(offset)
return if (index >= 0) index else -index - 2
}
override val linesCount: Int
get() = lineStartOffsets.size
}
/**
* Reads file contents from reader, converts line separators and calculates source lines to file offsets mapping
*
* Returns KtSourceFileLinesMapping and char sequence (StringBuilder to avoid premature copying) containing converted text
* The separators are converted similarly to the com.intellij.openapi.util.text.StringUtilRt algorithms
*/
fun InputStreamReader.readSourceFileWithMapping(): Pair<CharSequence, KtSourceFileLinesMapping> {
val buffer = CharArray(255)
var bufLength = -1
var bufPos = 0
var skipNextLf = false
var charsRead = 0
val lineOffsets = mutableListOf(0) // TODO: consider using implicit first line offset (needs to be handled properly in IR)
val sb = StringBuilder()
while (true) {
if (bufPos >= bufLength) {
bufLength = read(buffer)
bufPos = 0
if (bufLength < 0) {
break
}
} else {
val c = buffer[bufPos++]
charsRead++
when {
c == '\n' && skipNextLf -> {
lineOffsets[lineOffsets.size - 1] = charsRead
skipNextLf = false
}
c == '\n' || c == '\r' -> {
sb.append('\n')
lineOffsets.add(charsRead)
skipNextLf = c == '\r'
}
else -> {
sb.append(c)
skipNextLf = false
}
}
}
}
return sb to KtSourceFileLinesMappingFromLineStartOffsets(lineOffsets.toIntArray(), charsRead)
}
/**
* Extracts source lines to offsets mapping from text
*
* intended for using mainly in tests, so no care is taken about performance or possible corner cases
*/
fun CharSequence.toSourceLinesMapping(): KtSourceFileLinesMapping {
val lineOffsets = mutableListOf(0)
var offset = 0
for (c in this) {
offset++
if (c == '\n') lineOffsets.add(offset)
}
return KtSourceFileLinesMappingFromLineStartOffsets(lineOffsets.toIntArray(), offset)
}