backend: implement simple IR validator
This commit is contained in:
committed by
SvyatoslavScherbina
parent
d4fc79da43
commit
95935921ba
+160
@@ -0,0 +1,160 @@
|
||||
package org.jetbrains.kotlin.backend.common
|
||||
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.ir.IrElement
|
||||
import org.jetbrains.kotlin.ir.expressions.*
|
||||
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.types.typeUtil.makeNullable
|
||||
|
||||
typealias ReportError = (element: IrElement, message: String) -> Unit
|
||||
|
||||
class CheckIrElementVisitor(val builtIns: KotlinBuiltIns, val reportError: ReportError) : IrElementVisitorVoid {
|
||||
|
||||
override fun visitElement(element: IrElement) {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
private fun IrExpression.ensureTypeIs(expectedType: KotlinType) {
|
||||
if (expectedType != type) {
|
||||
reportError(this, "unexpected expression.type: expected $expectedType, got ${type}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> visitConst(expression: IrConst<T>) {
|
||||
super.visitConst(expression)
|
||||
|
||||
val naturalType = when (expression.kind) {
|
||||
IrConstKind.Null -> builtIns.nullableNothingType
|
||||
IrConstKind.Boolean -> builtIns.booleanType
|
||||
IrConstKind.Char -> builtIns.charType
|
||||
IrConstKind.Byte -> builtIns.byteType
|
||||
IrConstKind.Short -> builtIns.shortType
|
||||
IrConstKind.Int -> builtIns.intType
|
||||
IrConstKind.Long -> builtIns.longType
|
||||
IrConstKind.String -> builtIns.stringType
|
||||
IrConstKind.Float -> builtIns.floatType
|
||||
IrConstKind.Double -> builtIns.doubleType
|
||||
}
|
||||
|
||||
expression.ensureTypeIs(naturalType)
|
||||
}
|
||||
|
||||
override fun visitStringConcatenation(expression: IrStringConcatenation) {
|
||||
super.visitStringConcatenation(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.stringType)
|
||||
}
|
||||
|
||||
override fun visitGetObjectValue(expression: IrGetObjectValue) {
|
||||
super.visitGetObjectValue(expression)
|
||||
|
||||
expression.ensureTypeIs(expression.descriptor.defaultType)
|
||||
}
|
||||
|
||||
// TODO: visitGetEnumValue
|
||||
|
||||
override fun visitGetValue(expression: IrGetValue) {
|
||||
super.visitGetValue(expression)
|
||||
|
||||
expression.ensureTypeIs(expression.descriptor.type)
|
||||
}
|
||||
|
||||
override fun visitSetVariable(expression: IrSetVariable) {
|
||||
super.visitSetVariable(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.unitType)
|
||||
}
|
||||
|
||||
override fun visitGetField(expression: IrGetField) {
|
||||
super.visitGetField(expression)
|
||||
|
||||
expression.ensureTypeIs(expression.descriptor.type)
|
||||
}
|
||||
|
||||
override fun visitSetField(expression: IrSetField) {
|
||||
super.visitSetField(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.unitType)
|
||||
}
|
||||
|
||||
override fun visitCall(expression: IrCall) {
|
||||
super.visitCall(expression)
|
||||
|
||||
val returnType = expression.descriptor.returnType
|
||||
if (returnType == null) {
|
||||
reportError(expression, "${expression.descriptor} return type is null")
|
||||
} else {
|
||||
expression.ensureTypeIs(returnType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitDelegatingConstructorCall(expression: IrDelegatingConstructorCall) {
|
||||
super.visitDelegatingConstructorCall(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.unitType)
|
||||
}
|
||||
|
||||
override fun visitEnumConstructorCall(expression: IrEnumConstructorCall) {
|
||||
super.visitEnumConstructorCall(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.unitType)
|
||||
}
|
||||
|
||||
override fun visitInstanceInitializerCall(expression: IrInstanceInitializerCall) {
|
||||
super.visitInstanceInitializerCall(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.unitType)
|
||||
}
|
||||
|
||||
override fun visitTypeOperator(expression: IrTypeOperatorCall) {
|
||||
super.visitTypeOperator(expression)
|
||||
|
||||
val operator = expression.operator
|
||||
val typeOperand = expression.typeOperand
|
||||
|
||||
val naturalType = when (operator) {
|
||||
IrTypeOperator.CAST,
|
||||
IrTypeOperator.IMPLICIT_CAST,
|
||||
IrTypeOperator.IMPLICIT_NOTNULL,
|
||||
IrTypeOperator.IMPLICIT_COERCION_TO_UNIT,
|
||||
IrTypeOperator.IMPLICIT_INTEGER_COERCION -> typeOperand
|
||||
|
||||
IrTypeOperator.SAFE_CAST -> typeOperand.makeNullable()
|
||||
|
||||
IrTypeOperator.INSTANCEOF, IrTypeOperator.NOT_INSTANCEOF -> builtIns.booleanType
|
||||
}
|
||||
|
||||
if (operator == IrTypeOperator.IMPLICIT_COERCION_TO_UNIT && typeOperand != builtIns.unitType) {
|
||||
reportError(expression, "typeOperand is $typeOperand")
|
||||
}
|
||||
|
||||
// TODO: check IMPLICIT_NOTNULL's argument type.
|
||||
|
||||
expression.ensureTypeIs(naturalType)
|
||||
}
|
||||
|
||||
override fun visitLoop(loop: IrLoop) {
|
||||
super.visitLoop(loop)
|
||||
|
||||
loop.ensureTypeIs(builtIns.unitType)
|
||||
}
|
||||
|
||||
override fun visitBreakContinue(jump: IrBreakContinue) {
|
||||
super.visitBreakContinue(jump)
|
||||
|
||||
jump.ensureTypeIs(builtIns.nothingType)
|
||||
}
|
||||
|
||||
override fun visitReturn(expression: IrReturn) {
|
||||
super.visitReturn(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.nothingType)
|
||||
}
|
||||
|
||||
override fun visitThrow(expression: IrThrow) {
|
||||
super.visitThrow(expression)
|
||||
|
||||
expression.ensureTypeIs(builtIns.nothingType)
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package org.jetbrains.kotlin.backend.common
|
||||
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.descriptors.impl.TypeAliasConstructorDescriptor
|
||||
import org.jetbrains.kotlin.ir.IrElement
|
||||
import org.jetbrains.kotlin.ir.declarations.*
|
||||
import org.jetbrains.kotlin.ir.expressions.*
|
||||
import org.jetbrains.kotlin.ir.util.render
|
||||
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
|
||||
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
|
||||
import org.jetbrains.kotlin.ir.visitors.acceptVoid
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.module
|
||||
|
||||
fun validateIrFile(context: BackendContext, irFile: IrFile) {
|
||||
val visitor = IrValidator(context)
|
||||
irFile.acceptVoid(visitor)
|
||||
}
|
||||
|
||||
fun validateIrModule(context: BackendContext, irModule: IrModuleFragment) {
|
||||
val visitor = IrValidator(context)
|
||||
irModule.acceptVoid(visitor)
|
||||
|
||||
val moduleDeclarations = visitor.foundDeclarations
|
||||
|
||||
irModule.acceptVoid(object : IrElementVisitorVoid {
|
||||
lateinit var currentFile: IrFile
|
||||
|
||||
override fun visitFile(declaration: IrFile) {
|
||||
currentFile = declaration
|
||||
declaration.acceptChildrenVoid(this)
|
||||
}
|
||||
|
||||
override fun visitElement(element: IrElement) {
|
||||
element.acceptChildrenVoid(this)
|
||||
}
|
||||
|
||||
override fun visitProperty(declaration: IrProperty) {
|
||||
if (declaration.descriptor.modality == Modality.ABSTRACT) {
|
||||
return // Workaround TODO
|
||||
}
|
||||
|
||||
declaration.acceptChildrenVoid(this)
|
||||
}
|
||||
|
||||
override fun visitDeclarationReference(expression: IrDeclarationReference) {
|
||||
expression.acceptChildrenVoid(this)
|
||||
|
||||
val declarationDescriptor = expression.irDeclarationDescriptor ?: return
|
||||
|
||||
val declarationModule = declarationDescriptor.module
|
||||
|
||||
if (declarationModule == irModule.descriptor && !moduleDeclarations.hasTarget(expression)) {
|
||||
context.reportIrValidationError(
|
||||
"declaration $declarationDescriptor is missing;\n" +
|
||||
"references from:\n" +
|
||||
expression.render(),
|
||||
|
||||
currentFile, expression)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun BackendContext.reportIrValidationError(message: String, irFile: IrFile, irElement: IrElement) {
|
||||
this.reportWarning("[IR VALIDATION] $message", irFile, irElement)
|
||||
// TODO: throw an exception after fixing bugs leading to invalid IR.
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor to be found in IR element declaring target of this reference,
|
||||
* or `null` if it is not supported yet.
|
||||
*/
|
||||
private val IrDeclarationReference.irDeclarationDescriptor: DeclarationDescriptor?
|
||||
get() {
|
||||
when (this) {
|
||||
is IrFieldAccessExpression -> return this.descriptor.original
|
||||
|
||||
is IrMemberAccessExpression -> {
|
||||
val original = this.descriptor.original
|
||||
if (original is CallableMemberDescriptor && !original.kind.isReal) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (original is TypeAliasConstructorDescriptor) {
|
||||
return original.underlyingConstructorDescriptor
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
|
||||
is IrGetValue -> {
|
||||
if (this.descriptor is ParameterDescriptor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.descriptor
|
||||
}
|
||||
|
||||
else -> return this.descriptor
|
||||
}
|
||||
}
|
||||
|
||||
private val IrDeclarationReference.declarationKind: IrDeclarationKind
|
||||
get() = when (this) {
|
||||
is IrClassReference, is IrGetObjectValue -> IrDeclarationKind.CLASS
|
||||
is IrFieldAccessExpression -> IrDeclarationKind.FIELD
|
||||
is IrGetEnumValue -> IrDeclarationKind.ENUM_ENTRY
|
||||
is IrValueAccessExpression -> IrDeclarationKind.VARIABLE
|
||||
is IrMemberAccessExpression -> when (this.descriptor) {
|
||||
is ConstructorDescriptor -> IrDeclarationKind.CONSTRUCTOR
|
||||
is PropertyDescriptor -> IrDeclarationKind.PROPERTY
|
||||
is VariableDescriptorWithAccessors -> IrDeclarationKind.LOCAL_PROPERTY
|
||||
else -> IrDeclarationKind.FUNCTION
|
||||
}
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* The collection of IR declarations.
|
||||
*/
|
||||
private class Declarations {
|
||||
private data class DeclarationKey(val descriptor: DeclarationDescriptor, val kind: IrDeclarationKind)
|
||||
|
||||
private val declarations = mutableSetOf<DeclarationKey>()
|
||||
|
||||
private fun createKey(declaration: IrDeclaration) =
|
||||
DeclarationKey(declaration.descriptor, declaration.declarationKind)
|
||||
|
||||
fun add(declaration: IrDeclaration) {
|
||||
declarations.add(createKey(declaration))
|
||||
}
|
||||
|
||||
fun addParameterOf(aCatch: IrCatch) {
|
||||
val kind = IrDeclarationKind.VARIABLE
|
||||
val key = DeclarationKey(aCatch.parameter, kind)
|
||||
declarations.add(key)
|
||||
}
|
||||
|
||||
fun isAlreadyDeclared(declaration: IrDeclaration): Boolean {
|
||||
return createKey(declaration) in declarations
|
||||
}
|
||||
|
||||
fun hasTarget(reference: IrDeclarationReference): Boolean {
|
||||
val descriptor = reference.irDeclarationDescriptor ?: return false
|
||||
val kind = reference.declarationKind
|
||||
|
||||
return DeclarationKey(descriptor, kind) in declarations
|
||||
}
|
||||
}
|
||||
|
||||
private class IrValidator(val context: BackendContext) : IrElementVisitorVoid {
|
||||
|
||||
val foundDeclarations = Declarations()
|
||||
|
||||
val builtIns = context.builtIns
|
||||
lateinit var currentFile: IrFile
|
||||
|
||||
override fun visitFile(declaration: IrFile) {
|
||||
currentFile = declaration
|
||||
super.visitFile(declaration)
|
||||
}
|
||||
|
||||
private fun error(element: IrElement, message: String) {
|
||||
// TODO: render all element's parents.
|
||||
context.reportIrValidationError(
|
||||
"$message\n" +
|
||||
element.render(),
|
||||
currentFile, element)
|
||||
}
|
||||
|
||||
private val elementChecker = CheckIrElementVisitor(builtIns, this::error)
|
||||
|
||||
override fun visitElement(element: IrElement) {
|
||||
element.acceptVoid(elementChecker)
|
||||
element.acceptChildrenVoid(this)
|
||||
}
|
||||
|
||||
private fun recordDeclaration(declaration: IrDeclaration) {
|
||||
if (foundDeclarations.isAlreadyDeclared(declaration)) {
|
||||
error(declaration, "redeclaration")
|
||||
} else {
|
||||
foundDeclarations.add(declaration)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitDeclaration(declaration: IrDeclaration) {
|
||||
recordDeclaration(declaration)
|
||||
super.visitDeclaration(declaration)
|
||||
}
|
||||
|
||||
override fun visitAnonymousInitializer(declaration: IrAnonymousInitializer) {
|
||||
// Do not treat anonymous initializers as declarations, because they are not unique.
|
||||
super.visitDeclaration(declaration)
|
||||
}
|
||||
|
||||
override fun visitCatch(aCatch: IrCatch) {
|
||||
foundDeclarations.addParameterOf(aCatch)
|
||||
super.visitCatch(aCatch)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package org.jetbrains.kotlin.backend.common
|
||||
|
||||
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
|
||||
import org.jetbrains.kotlin.backend.konan.KonanBackendContext
|
||||
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.ir.IrElement
|
||||
import org.jetbrains.kotlin.ir.declarations.IrFile
|
||||
import org.jetbrains.kotlin.ir.util.getCompilerMessageLocation
|
||||
|
||||
// TODO: declare as a member of BackendContext
|
||||
val BackendContext.compilerConfiguration: CompilerConfiguration
|
||||
get() = when (this) {
|
||||
is KonanBackendContext -> this.config.configuration
|
||||
is JvmBackendContext -> this.state.configuration
|
||||
else -> TODO(this.toString())
|
||||
}
|
||||
|
||||
val BackendContext.messageCollector: MessageCollector
|
||||
get() = this.compilerConfiguration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
|
||||
|
||||
fun BackendContext.reportWarning(message: String, irFile: IrFile, irElement: IrElement) {
|
||||
val location = irElement.getCompilerMessageLocation(irFile)
|
||||
this.messageCollector.report(CompilerMessageSeverity.WARNING, message, location)
|
||||
}
|
||||
+1
-1
@@ -184,7 +184,7 @@ class ReflectionTypes(module: ModuleDescriptor) {
|
||||
}
|
||||
}
|
||||
|
||||
internal class Context(val config: KonanConfig) : KonanBackendContext() {
|
||||
internal class Context(config: KonanConfig) : KonanBackendContext(config) {
|
||||
|
||||
var moduleDescriptor: ModuleDescriptor? = null
|
||||
|
||||
|
||||
+1
-2
@@ -2,9 +2,8 @@ package org.jetbrains.kotlin.backend.konan
|
||||
|
||||
import org.jetbrains.kotlin.backend.common.BackendContext
|
||||
import org.jetbrains.kotlin.backend.konan.descriptors.KonanSharedVariablesManager
|
||||
import org.jetbrains.kotlin.backend.konan.KonanPlatform
|
||||
|
||||
abstract internal class KonanBackendContext : BackendContext {
|
||||
abstract internal class KonanBackendContext(val config: KonanConfig) : BackendContext {
|
||||
override val builtIns = KonanPlatform.builtIns
|
||||
|
||||
override val sharedVariablesManager by lazy {
|
||||
|
||||
+8
-5
@@ -1,6 +1,8 @@
|
||||
package org.jetbrains.kotlin.backend.konan
|
||||
|
||||
import org.jetbrains.kotlin.analyzer.AnalysisResult
|
||||
import org.jetbrains.kotlin.backend.common.messageCollector
|
||||
import org.jetbrains.kotlin.backend.common.validateIrModule
|
||||
import org.jetbrains.kotlin.backend.konan.ir.ModuleIndex
|
||||
import org.jetbrains.kotlin.backend.konan.llvm.emitLLVM
|
||||
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
|
||||
@@ -40,12 +42,10 @@ public fun runTopLevelPhases(konanConfig: KonanConfig, environment: KotlinCoreEn
|
||||
|
||||
if (config.kotlinSourceRoots.isEmpty()) return
|
||||
|
||||
val collector = config.getNotNull(
|
||||
CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
|
||||
|
||||
val analyzerWithCompilerReport = AnalyzerWithCompilerReport(collector)
|
||||
|
||||
val context = Context(konanConfig)
|
||||
|
||||
val analyzerWithCompilerReport = AnalyzerWithCompilerReport(context.messageCollector)
|
||||
|
||||
val phaser = PhaseManager(context)
|
||||
|
||||
phaser.phase(KonanPhase.FRONTEND) {
|
||||
@@ -67,10 +67,13 @@ public fun runTopLevelPhases(konanConfig: KonanConfig, environment: KotlinCoreEn
|
||||
environment.getSourceFiles(), bindingContext)
|
||||
|
||||
context.irModule = module
|
||||
|
||||
validateIrModule(context, module)
|
||||
}
|
||||
phaser.phase(KonanPhase.BACKEND) {
|
||||
phaser.phase(KonanPhase.LOWER) {
|
||||
KonanLower(context).lower()
|
||||
validateIrModule(context, context.ir.irModule)
|
||||
context.ir.moduleIndexForCodegen = ModuleIndex(context.ir.irModule)
|
||||
}
|
||||
phaser.phase(KonanPhase.BITCODE) {
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package org.jetbrains.kotlin.backend.konan
|
||||
|
||||
import org.jetbrains.kotlin.backend.common.runOnFilePostfix
|
||||
import org.jetbrains.kotlin.backend.common.lower.*
|
||||
import org.jetbrains.kotlin.backend.common.validateIrFile
|
||||
import org.jetbrains.kotlin.backend.konan.lower.*
|
||||
import org.jetbrains.kotlin.ir.declarations.IrFile
|
||||
|
||||
@@ -67,6 +68,7 @@ internal class KonanLower(val context: Context) {
|
||||
DirectBridgesCallsLowering(context).runOnFilePostfix(irFile)
|
||||
}
|
||||
phaser.phase(KonanPhase.AUTOBOX) {
|
||||
validateIrFile(context, irFile)
|
||||
Autoboxing(context).lower(irFile)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-19
@@ -1,27 +1,14 @@
|
||||
package org.jetbrains.kotlin.backend.konan
|
||||
|
||||
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
|
||||
import org.jetbrains.kotlin.backend.common.BackendContext
|
||||
import org.jetbrains.kotlin.backend.common.messageCollector
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
import org.jetbrains.kotlin.ir.IrElement
|
||||
import org.jetbrains.kotlin.ir.declarations.IrFile
|
||||
import org.jetbrains.kotlin.ir.util.getCompilerMessageLocation
|
||||
|
||||
internal val Context.messageCollector: MessageCollector
|
||||
get() = this.config.configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
|
||||
|
||||
internal fun getCompilerMessageLocation(irFile: IrFile, irElement: IrElement): CompilerMessageLocation {
|
||||
val sourceRangeInfo = irFile.fileEntry.getSourceRangeInfo(irElement.startOffset, irElement.endOffset)
|
||||
return CompilerMessageLocation.create(
|
||||
path = sourceRangeInfo.filePath,
|
||||
line = sourceRangeInfo.startLineNumber,
|
||||
column = sourceRangeInfo.startColumnNumber,
|
||||
lineContent = null // TODO: retrieve the line content.
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Context.reportCompilationError(message: String, irFile: IrFile, irElement: IrElement): Nothing {
|
||||
val location = getCompilerMessageLocation(irFile, irElement)
|
||||
internal fun BackendContext.reportCompilationError(message: String, irFile: IrFile, irElement: IrElement): Nothing {
|
||||
val location = irElement.getCompilerMessageLocation(irFile)
|
||||
this.messageCollector.report(CompilerMessageSeverity.ERROR, message, location)
|
||||
throw KonanCompilationException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.jetbrains.kotlin.ir.util
|
||||
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
|
||||
import org.jetbrains.kotlin.descriptors.ParameterDescriptor
|
||||
import org.jetbrains.kotlin.ir.IrElement
|
||||
import org.jetbrains.kotlin.ir.declarations.IrFile
|
||||
import org.jetbrains.kotlin.ir.expressions.*
|
||||
|
||||
/**
|
||||
@@ -64,3 +67,13 @@ internal fun IrExpression.isNullConst() = this is IrConst<*> && this.kind == IrC
|
||||
|
||||
fun IrCall.usesDefaultArguments(): Boolean =
|
||||
this.descriptor.valueParameters.any { this.getValueArgument(it) == null }
|
||||
|
||||
fun IrElement.getCompilerMessageLocation(containingFile: IrFile): CompilerMessageLocation {
|
||||
val sourceRangeInfo = containingFile.fileEntry.getSourceRangeInfo(this.startOffset, this.endOffset)
|
||||
return CompilerMessageLocation.create(
|
||||
path = sourceRangeInfo.filePath,
|
||||
line = sourceRangeInfo.startLineNumber,
|
||||
column = sourceRangeInfo.startColumnNumber,
|
||||
lineContent = null // TODO: retrieve the line content.
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user