FIR LT: fix column calculation with crlf line endings
#KT-57117 fixed also: - refactor the position finder (mostly for testability) - add testing of position finder to the testLightTreeReadLineEndings - refactor the test for readability - add mixed line ending scenario
This commit is contained in:
committed by
Space Team
parent
feca40071d
commit
54218eb77d
+37
-19
@@ -40,9 +40,19 @@ object FirDiagnosticsCompilerResultsReporter {
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
var hasErrors = false
|
var hasErrors = false
|
||||||
for (filePath in diagnosticsCollector.diagnosticsByFilePath.keys) {
|
for (filePath in diagnosticsCollector.diagnosticsByFilePath.keys) {
|
||||||
val positionFinder: SequentialFilePositionFinder? by lazy(LazyThreadSafetyMode.NONE) {
|
// emulating lazy because of the usage pattern in finally block below (should not initialize in finally)
|
||||||
filePath?.let(::File)?.takeIf { it.isFile }?.let(::SequentialFilePositionFinder)
|
var positionFinderInitialized = false
|
||||||
}
|
var positionFinder: SequentialFilePositionFinder? = null
|
||||||
|
|
||||||
|
fun getPositionFinder() =
|
||||||
|
if (positionFinderInitialized) positionFinder
|
||||||
|
else {
|
||||||
|
positionFinderInitialized = true
|
||||||
|
filePath?.let(::File)?.takeIf { it.isFile }?.let(::SequentialFilePositionFinder)?.also {
|
||||||
|
positionFinder = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("ConvertTryFinallyToUseCall")
|
@Suppress("ConvertTryFinallyToUseCall")
|
||||||
try {
|
try {
|
||||||
for (diagnostic in diagnosticsCollector.diagnosticsByFilePath[filePath].orEmpty().sortedWith(InFileDiagnosticsComparator)) {
|
for (diagnostic in diagnosticsCollector.diagnosticsByFilePath[filePath].orEmpty().sortedWith(InFileDiagnosticsComparator)) {
|
||||||
@@ -60,9 +70,10 @@ object FirDiagnosticsCompilerResultsReporter {
|
|||||||
// NOTE: SequentialPositionFinder relies on the ascending order of the input offsets, so the code relies
|
// NOTE: SequentialPositionFinder relies on the ascending order of the input offsets, so the code relies
|
||||||
// on the the appropriate sorting above
|
// on the the appropriate sorting above
|
||||||
// Also the end offset is ignored, as it is irrelevant for the CLI reporting
|
// Also the end offset is ignored, as it is irrelevant for the CLI reporting
|
||||||
positionFinder?.findNextPosition(DiagnosticUtils.firstRange(diagnostic.textRanges).startOffset)?.let { pos ->
|
getPositionFinder()?.findNextPosition(DiagnosticUtils.firstRange(diagnostic.textRanges).startOffset)
|
||||||
MessageUtil.createMessageLocation(filePath, pos.lineContent, pos.line, pos.column, -1, -1)
|
?.let { pos ->
|
||||||
}
|
MessageUtil.createMessageLocation(filePath, pos.lineContent, pos.line, pos.column, -1, -1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}?.let { location ->
|
}?.let { location ->
|
||||||
report(diagnostic, location)
|
report(diagnostic, location)
|
||||||
@@ -136,19 +147,29 @@ fun BaseDiagnosticsCollector.reportToMessageCollector(messageCollector: MessageC
|
|||||||
FirDiagnosticsCompilerResultsReporter.reportToMessageCollector(this, messageCollector, renderDiagnosticName)
|
FirDiagnosticsCompilerResultsReporter.reportToMessageCollector(this, messageCollector, renderDiagnosticName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class KtSourceFilePos(val line: Int, val column: Int, val lineContent: String?) {
|
// public only because of tests
|
||||||
|
class KtSourceFileDiagnosticPos(val line: Int, val column: Int, val lineContent: String?) {
|
||||||
|
|
||||||
// NOTE: This method is used for presenting positions to the user
|
// 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)"
|
override fun toString(): String = if (line < 0) "(offset: $column line unknown)" else "($line,$column)"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val NONE = KtSourceFilePos(-1, -1, null)
|
val NONE = KtSourceFileDiagnosticPos(-1, -1, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SequentialFilePositionFinder(file: File) : Closeable {
|
private class SequentialFilePositionFinder private constructor(private val reader: InputStreamReader)
|
||||||
|
: Closeable, SequentialPositionFinder(reader)
|
||||||
|
{
|
||||||
|
constructor(file: File) : this(file.reader(/* TODO: select proper charset */))
|
||||||
|
|
||||||
private var reader: InputStreamReader = file.reader(/* TODO: select proper charset */)
|
override fun close() {
|
||||||
|
reader.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public only for tests
|
||||||
|
open class SequentialPositionFinder(private val reader: InputStreamReader) {
|
||||||
|
|
||||||
private var currentLineContent: String? = null
|
private var currentLineContent: String? = null
|
||||||
private val buffer = CharArray(255)
|
private val buffer = CharArray(255)
|
||||||
@@ -161,13 +182,13 @@ private class SequentialFilePositionFinder(file: File) : Closeable {
|
|||||||
private var currentLine = 0
|
private var currentLine = 0
|
||||||
|
|
||||||
// assuming that if called multiple times, calls should be sorted by ascending offset
|
// assuming that if called multiple times, calls should be sorted by ascending offset
|
||||||
fun findNextPosition(offset: Int, withLineContents: Boolean = true): KtSourceFilePos {
|
fun findNextPosition(offset: Int, withLineContents: Boolean = true): KtSourceFileDiagnosticPos {
|
||||||
|
|
||||||
fun posInCurrentLine(): KtSourceFilePos? {
|
fun posInCurrentLine(): KtSourceFileDiagnosticPos? {
|
||||||
val col = offset - (charsRead - currentLineContent!!.length - 1)/* beginning of line offset */ + 1 /* col is 1-based */
|
val col = offset - (charsRead - currentLineContent!!.length - 1)/* beginning of line offset */ + 1 /* col is 1-based */
|
||||||
assert(col > 0)
|
assert(col > 0)
|
||||||
return if (col <= currentLineContent!!.length + 1 /* accounting for a report on EOL (e.g. syntax errors) */ )
|
return if (col <= currentLineContent!!.length + 1 /* accounting for a report on EOL (e.g. syntax errors) */)
|
||||||
KtSourceFilePos(currentLine, col, if (withLineContents) currentLineContent else null)
|
KtSourceFileDiagnosticPos(currentLine, col, if (withLineContents) currentLineContent else null)
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +203,7 @@ private class SequentialFilePositionFinder(file: File) : Closeable {
|
|||||||
|
|
||||||
posInCurrentLine()?.let { return@findNextPosition it }
|
posInCurrentLine()?.let { return@findNextPosition it }
|
||||||
|
|
||||||
if (endOfStream) return KtSourceFilePos(-1, offset, if (withLineContents) currentLineContent else null)
|
if (endOfStream) return KtSourceFileDiagnosticPos(-1, offset, if (withLineContents) currentLineContent else null)
|
||||||
|
|
||||||
currentLineContent = null
|
currentLineContent = null
|
||||||
}
|
}
|
||||||
@@ -204,6 +225,7 @@ private class SequentialFilePositionFinder(file: File) : Closeable {
|
|||||||
charsRead++
|
charsRead++
|
||||||
when {
|
when {
|
||||||
c == '\n' && skipNextLf -> {
|
c == '\n' && skipNextLf -> {
|
||||||
|
charsRead--
|
||||||
skipNextLf = false
|
skipNextLf = false
|
||||||
}
|
}
|
||||||
c == '\n' || c == '\r' -> {
|
c == '\n' || c == '\r' -> {
|
||||||
@@ -219,8 +241,4 @@ private class SequentialFilePositionFinder(file: File) : Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
reader.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.lightTree
|
|||||||
import com.intellij.lang.LighterASTNode
|
import com.intellij.lang.LighterASTNode
|
||||||
import com.intellij.openapi.util.Ref
|
import com.intellij.openapi.util.Ref
|
||||||
import com.intellij.util.diff.FlyweightCapableTreeStructure
|
import com.intellij.util.diff.FlyweightCapableTreeStructure
|
||||||
|
import org.jetbrains.kotlin.cli.common.fir.SequentialPositionFinder
|
||||||
import org.jetbrains.kotlin.fir.lightTree.LightTree2Fir
|
import org.jetbrains.kotlin.fir.lightTree.LightTree2Fir
|
||||||
import org.jetbrains.kotlin.readSourceFileWithMapping
|
import org.jetbrains.kotlin.readSourceFileWithMapping
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
@@ -18,33 +19,68 @@ class LightTreeParsingTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLightTreeReadLineEndings() {
|
fun testLightTreeReadLineEndings() {
|
||||||
fun String.makeCodeMappingAndLines() = run {
|
|
||||||
val (code, mapping) = ByteArrayInputStream(toByteArray()).reader().readSourceFileWithMapping()
|
data class LinePos(
|
||||||
val lines =
|
val mappingLine: Int,
|
||||||
LightTree2Fir.buildLightTree(code).getChildrenAsArray().mapNotNull { it?.startOffset }.map { mapping.getLineByOffset(it) }
|
val line: Int,
|
||||||
Triple(code, mapping, lines)
|
val col: Int,
|
||||||
|
val content: String?
|
||||||
|
) {
|
||||||
|
override fun toString(): String = "$mappingLine: \"$content\" ($line:$col)"
|
||||||
}
|
}
|
||||||
|
|
||||||
val (codeFromTextWithLf, mappingFromTextWithLf, linesFromTextWithLf) = MULTILINE_SOURCE.makeCodeMappingAndLines()
|
fun String.makeCodeMappingAndPositions() = run {
|
||||||
|
val (code, mapping) = ByteArrayInputStream(toByteArray()).reader().readSourceFileWithMapping()
|
||||||
|
val positionFinder = SequentialPositionFinder(ByteArrayInputStream(toByteArray()).reader())
|
||||||
|
val linePositions =
|
||||||
|
LightTree2Fir.buildLightTree(code).getChildrenAsArray()
|
||||||
|
.mapNotNull { it?.startOffset }
|
||||||
|
.map {
|
||||||
|
val nextPos = positionFinder.findNextPosition(it)
|
||||||
|
LinePos( mapping.getLineByOffset(it), nextPos.line, nextPos.column, nextPos.lineContent)
|
||||||
|
}
|
||||||
|
Triple(code.toString(), mapping, linePositions)
|
||||||
|
}
|
||||||
|
|
||||||
val (codeFromTextWithCrLf, mappingFromTextWithCrLf, linesFromTextWithCrLf) =
|
val (codeLf, mappingLf, positionsLf) = MULTILINE_SOURCE.makeCodeMappingAndPositions()
|
||||||
MULTILINE_SOURCE.replace("\n", "\r\n").makeCodeMappingAndLines()
|
|
||||||
|
|
||||||
// classic Mac OS line endings are probably not to be found in the wild, but checking the support nevertheless
|
val (codeCrLf, mappingCrLf, positionsCrLf) =
|
||||||
val (codeFromTextWithCr, mappingFromTextWithCr, linesFromTextWithCr) =
|
MULTILINE_SOURCE.replace("\n", "\r\n").makeCodeMappingAndPositions()
|
||||||
MULTILINE_SOURCE.replace("\n", "\r").makeCodeMappingAndLines()
|
|
||||||
|
|
||||||
Assert.assertEquals(codeFromTextWithLf.toString(), codeFromTextWithCrLf.toString())
|
val (codeCrLfMixed, mappingCrLfMixed, positionsCrLfMixed) =
|
||||||
Assert.assertEquals(codeFromTextWithLf.toString(), codeFromTextWithCr.toString())
|
MULTILINE_SOURCE.let {
|
||||||
|
var toReplace = false
|
||||||
|
buildString {
|
||||||
|
it.forEach { c ->
|
||||||
|
if (c == '\n') {
|
||||||
|
if (toReplace) append("\r\n") else append(c)
|
||||||
|
toReplace = !toReplace
|
||||||
|
} else append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also { s ->
|
||||||
|
Assert.assertEquals(s.count { it == '\r' }, s.count { it == '\n' } / 2)
|
||||||
|
}.makeCodeMappingAndPositions()
|
||||||
|
|
||||||
Assert.assertEquals(mappingFromTextWithLf.linesCount, mappingFromTextWithCrLf.linesCount)
|
// classic MacOS line endings are probably not to be found in the wild, but checking the support nevertheless
|
||||||
Assert.assertEquals(mappingFromTextWithLf.linesCount, mappingFromTextWithCr.linesCount)
|
val (codeCr, mappingCr, positionsCr) =
|
||||||
|
MULTILINE_SOURCE.replace("\n", "\r").makeCodeMappingAndPositions()
|
||||||
|
|
||||||
Assert.assertEquals(linesFromTextWithLf, linesFromTextWithCrLf)
|
Assert.assertEquals(codeLf, codeCrLf)
|
||||||
Assert.assertEquals(linesFromTextWithLf, linesFromTextWithCr)
|
Assert.assertEquals(codeLf, codeCrLfMixed)
|
||||||
|
Assert.assertEquals(codeLf, codeCr)
|
||||||
|
|
||||||
Assert.assertEquals(mappingFromTextWithLf.lastOffset, mappingFromTextWithCrLf.lastOffset)
|
Assert.assertEquals(mappingLf.linesCount, mappingCrLf.linesCount)
|
||||||
Assert.assertEquals(mappingFromTextWithLf.lastOffset, mappingFromTextWithCr.lastOffset)
|
Assert.assertEquals(mappingLf.linesCount, mappingCrLfMixed.linesCount)
|
||||||
|
Assert.assertEquals(mappingLf.linesCount, mappingCr.linesCount)
|
||||||
|
|
||||||
|
Assert.assertEquals(positionsLf, positionsCrLf)
|
||||||
|
Assert.assertEquals(positionsLf, positionsCrLfMixed)
|
||||||
|
Assert.assertEquals(positionsLf, positionsCr)
|
||||||
|
|
||||||
|
Assert.assertEquals(mappingLf.lastOffset, mappingCrLf.lastOffset)
|
||||||
|
Assert.assertEquals(mappingLf.lastOffset, mappingCrLfMixed.lastOffset)
|
||||||
|
Assert.assertEquals(mappingLf.lastOffset, mappingCr.lastOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,4 +95,5 @@ val a = 1
|
|||||||
val b = 2
|
val b = 2
|
||||||
val c = 3
|
val c = 3
|
||||||
val d = 4
|
val d = 4
|
||||||
|
val e = 5
|
||||||
"""
|
"""
|
||||||
Reference in New Issue
Block a user