[JS IR] Implemented a set of JS code checks before klib serialization

The diagnostics cannot be implemented with the FIR frontend checker
because it requires constant evaluation over FIR.
Therefore, the diagnostics are implemented as a set of klib checks over IR.

For the diagnostics, the js() call argument must be
evaluated and inlined as IrConst<String> into IR
in the same way as const val initializers and annotation arguments.

^KT-59388 Fixed
^KT-59399 Fixed
^KT-62425 Fixed
This commit is contained in:
Alexander Korepanov
2023-09-21 20:59:17 +02:00
committed by Space Team
parent 71eaf651e8
commit 629e0628d6
13 changed files with 275 additions and 28 deletions
@@ -6,6 +6,8 @@ plugins {
dependencies {
compileOnly(project(":compiler:ir.tree"))
compileOnly(commonDependency("org.jetbrains.kotlin:kotlin-reflect")) { isTransitive = false }
implementation(project(":core:compiler.common.js"))
}
optInToUnsafeDuringIrConstructionAPI()
@@ -42,14 +42,14 @@ internal abstract class IrConstExpressionTransformer(
override fun visitClass(declaration: IrClass, data: Data): IrStatement {
if (declaration.kind == ClassKind.ANNOTATION_CLASS) {
return super.visitClass(declaration, data.copy(inAnnotation = true))
return super.visitClass(declaration, data.copy(inConstantExpression = true))
}
return super.visitClass(declaration, data)
}
override fun visitCall(expression: IrCall, data: Data): IrElement {
if (expression.canBeInterpreted()) {
return expression.interpret(failAsError = data.inAnnotation)
return expression.interpret(failAsError = data.inConstantExpression)
}
return super.visitCall(expression, data)
}
@@ -68,7 +68,7 @@ internal abstract class IrConstExpressionTransformer(
override fun visitGetField(expression: IrGetField, data: Data): IrExpression {
if (expression.canBeInterpreted()) {
return expression.interpret(failAsError = data.inAnnotation)
return expression.interpret(failAsError = data.inConstantExpression)
}
return super.visitGetField(expression, data)
}
@@ -78,7 +78,7 @@ internal abstract class IrConstExpressionTransformer(
this.startOffset, this.endOffset, expression.type, listOf(this@wrapInStringConcat)
)
fun IrExpression.wrapInToStringConcatAndInterpret(): IrExpression = wrapInStringConcat().interpret(failAsError = data.inAnnotation)
fun IrExpression.wrapInToStringConcatAndInterpret(): IrExpression = wrapInStringConcat().interpret(failAsError = data.inConstantExpression)
fun IrExpression.getConstStringOrEmpty(): String = if (this is IrConst<*>) value.toString() else ""
// If we have some complex expression in arguments (like some `IrComposite`) we will skip it,
@@ -11,16 +11,19 @@ import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.declarations.IrField
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.interpreter.IrInterpreter
import org.jetbrains.kotlin.ir.interpreter.checker.EvaluationMode
import org.jetbrains.kotlin.ir.interpreter.checker.IrInterpreterChecker
import org.jetbrains.kotlin.ir.interpreter.isConst
import org.jetbrains.kotlin.ir.interpreter.property
import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable
import org.jetbrains.kotlin.name.JsStandardClassIds
/**
* This transformer will visit all expressions and will evaluate only those that are necessary. By "necessary" we mean expressions
* that are used in `const val` and inside annotations.
* This transformer will visit all expressions and will evaluate only those that are necessary.
* By "necessary" we mean expressions that are used in `const val`, inside annotations and js() call arguments.
*/
internal class IrConstOnlyNecessaryTransformer(
interpreter: IrInterpreter,
@@ -35,10 +38,13 @@ internal class IrConstOnlyNecessaryTransformer(
) : IrConstExpressionTransformer(
interpreter, irFile, mode, checker, evaluatedConstTracker, inlineConstTracker, onWarning, onError, suppressExceptions
) {
private val jsCodeFqName = JsStandardClassIds.Callables.JsCode.asSingleFqName()
override fun visitCall(expression: IrCall, data: Data): IrElement {
val isConstGetter = expression.symbol.owner.property.isConst
if (!data.inAnnotation && !isConstGetter) {
expression.transformChildren(this, data)
val isJsCodeCall = expression.symbol.owner.fqNameWhenAvailable == jsCodeFqName
if (isJsCodeCall || (!data.inConstantExpression && !isConstGetter)) {
expression.transformChildren(this, data.copy(inConstantExpression = data.inConstantExpression || isJsCodeCall))
return expression
}
return super.visitCall(expression, data)
@@ -46,12 +52,12 @@ internal class IrConstOnlyNecessaryTransformer(
override fun visitGetField(expression: IrGetField, data: Data): IrExpression {
val isConst = expression.symbol.owner.property.isConst
if (!data.inAnnotation && !isConst) return expression
if (!data.inConstantExpression && !isConst) return expression
return super.visitGetField(expression, data)
}
override fun visitStringConcatenation(expression: IrStringConcatenation, data: Data): IrExpression {
if (!data.inAnnotation) {
if (!data.inConstantExpression) {
expression.transformChildren(this, data)
return expression
}
@@ -99,7 +99,7 @@ internal abstract class IrConstTransformer(
private val onError: (IrFile, IrElement, IrErrorExpression) -> Unit,
private val suppressExceptions: Boolean,
) : IrElementTransformer<IrConstTransformer.Data> {
internal data class Data(val inAnnotation: Boolean = false)
internal data class Data(val inConstantExpression: Boolean = false)
private fun IrExpression.warningIfError(original: IrExpression): IrExpression {
if (this is IrErrorExpression) {
@@ -9,10 +9,15 @@ import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.backend.js.checkers.declarations.JsKlibEsModuleExportsChecker
import org.jetbrains.kotlin.ir.backend.js.checkers.declarations.JsKlibOtherModuleExportsChecker
import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.backend.js.checkers.expressions.JsKlibJsCodeCallChecker
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrCall
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.library.SerializedIrFile
object JsKlibCheckers {
@@ -21,17 +26,51 @@ object JsKlibCheckers {
JsKlibOtherModuleExportsChecker
)
private val callCheckers = listOf(
JsKlibJsCodeCallChecker
)
fun check(
cleanFiles: List<SerializedIrFile>,
dirtyFiles: List<IrFile>,
dirtyModule: IrModuleFragment,
exportedNames: Map<IrFile, Map<IrDeclarationWithName, String>>,
diagnosticReporter: DiagnosticReporter,
configuration: CompilerConfiguration
) {
val reporter = KtDiagnosticReporterWithImplicitIrBasedContext(diagnosticReporter, configuration.languageVersionSettings)
val exportedDeclarations = JsKlibExportingDeclaration.collectDeclarations(cleanFiles, dirtyFiles, exportedNames)
for (checker in exportedDeclarationsCheckers) {
checker.check(exportedDeclarations, reporter)
}
dirtyModule.acceptVoid(object : IrElementVisitorVoid {
private val reporter = KtDiagnosticReporterWithImplicitIrBasedContext(diagnosticReporter, configuration.languageVersionSettings)
private val diagnosticContext = JsKlibDiagnosticContext(configuration)
override fun visitElement(element: IrElement) {
if (element is IrDeclaration) {
diagnosticContext.withDeclarationScope(element) {
element.acceptChildrenVoid(this)
}
} else {
element.acceptChildrenVoid(this)
}
}
override fun visitModuleFragment(declaration: IrModuleFragment) {
val exportedDeclarations = JsKlibExportingDeclaration.collectDeclarations(cleanFiles, declaration.files, exportedNames)
for (checker in exportedDeclarationsCheckers) {
checker.check(exportedDeclarations, this.diagnosticContext, reporter)
}
super.visitModuleFragment(declaration)
}
override fun visitFile(declaration: IrFile) {
diagnosticContext.withFileScope(declaration) {
super.visitFile(declaration)
}
}
override fun visitCall(expression: IrCall) {
for (checker in callCheckers) {
checker.check(expression, this.diagnosticContext, reporter)
}
super.visitCall(expression)
}
})
}
}
@@ -8,7 +8,7 @@ package org.jetbrains.kotlin.ir.backend.js.checkers
import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
interface JsKlibDeclarationsChecker<D> {
fun check(declarations: List<D>, reporter: KtDiagnosticReporterWithImplicitIrBasedContext)
fun check(declarations: List<D>, context: JsKlibDiagnosticContext, reporter: KtDiagnosticReporterWithImplicitIrBasedContext)
}
typealias JsKlibExportedDeclarationsChecker = JsKlibDeclarationsChecker<JsKlibExportingDeclaration>
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2023 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.ir.backend.js.checkers
import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.diagnostics.KtDiagnosticReporterWithContext
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrFile
class JsKlibDiagnosticContext(val compilerConfiguration: CompilerConfiguration) {
var containingDeclaration: IrDeclaration? = null
private set
var containingFile: IrFile? = null
private set
fun withDeclarationScope(declaration: IrDeclaration, f: () -> Unit) {
val prevDeclaration = containingDeclaration
try {
containingDeclaration = declaration
f()
} finally {
containingDeclaration = prevDeclaration
}
}
fun withFileScope(file: IrFile, f: () -> Unit) {
val prevFile = containingFile
try {
containingFile = file
f()
} finally {
containingFile = prevFile
}
}
}
fun KtDiagnosticReporterWithImplicitIrBasedContext.at(
declaration: IrDeclaration,
context: JsKlibDiagnosticContext,
): KtDiagnosticReporterWithContext.DiagnosticContextImpl {
return context.containingFile?.let { at(declaration, it) } ?: at(declaration)
}
fun KtDiagnosticReporterWithImplicitIrBasedContext.at(
irElement: IrElement,
context: JsKlibDiagnosticContext,
): KtDiagnosticReporterWithContext.DiagnosticContextImpl {
val file = context.containingFile
if (file != null) {
return at(irElement, file)
}
val declaration = context.containingDeclaration
if (declaration != null) {
return at(irElement, declaration)
}
// Should never happen
error("Cannot find the expression containing declaration")
}
@@ -7,14 +7,23 @@ package org.jetbrains.kotlin.ir.backend.js.checkers
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.error0
import org.jetbrains.kotlin.diagnostics.error1
import org.jetbrains.kotlin.diagnostics.error2
import org.jetbrains.kotlin.diagnostics.rendering.*
import org.jetbrains.kotlin.diagnostics.warning0
import org.jetbrains.kotlin.diagnostics.warning1
import org.jetbrains.kotlin.diagnostics.warning2
object JsKlibErrors {
val EXPORTING_JS_NAME_CLASH by error2<PsiElement, String, List<JsKlibExport>>()
val EXPORTING_JS_NAME_CLASH_ES by warning2<PsiElement, String, List<JsKlibExport>>()
val JSCODE_CAN_NOT_VERIFY_JAVASCRIPT by warning0<PsiElement>()
val JSCODE_NO_JAVASCRIPT_PRODUCED by error0<PsiElement>()
val JSCODE_ERROR by error1<PsiElement, String>()
val JSCODE_WARNING by warning1<PsiElement, String>()
init {
RootDiagnosticRendererFactory.registerFactory(KtDefaultJsKlibErrorMessages)
}
@@ -43,5 +52,23 @@ private object KtDefaultJsKlibErrorMessages : BaseDiagnosticRendererFactory() {
CommonRenderers.STRING,
JS_KLIB_EXPORTS,
)
map.put(
JsKlibErrors.JSCODE_CAN_NOT_VERIFY_JAVASCRIPT,
"Cannot verify JavaScript code because the argument is not a constant string"
)
map.put(
JsKlibErrors.JSCODE_NO_JAVASCRIPT_PRODUCED,
"An argument for the js() function must be non-empty JavaScript code"
)
map.put(
JsKlibErrors.JSCODE_ERROR,
"JavaScript error: {0}",
CommonRenderers.STRING
)
map.put(
JsKlibErrors.JSCODE_WARNING,
"JavaScript warning: {0}",
CommonRenderers.STRING
)
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2010-2023 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.ir.backend.js.checkers
import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
interface JsKlibExpressionChecker<E : IrExpression> {
fun check(expression: E, context: JsKlibDiagnosticContext, reporter: KtDiagnosticReporterWithImplicitIrBasedContext)
}
typealias JsKlibCallChecker = JsKlibExpressionChecker<IrCall>
@@ -6,14 +6,13 @@
package org.jetbrains.kotlin.ir.backend.js.checkers.declarations
import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.ir.backend.js.checkers.JsKlibExportingDeclaration
import org.jetbrains.kotlin.ir.backend.js.checkers.JsKlibExportedDeclarationsChecker
import org.jetbrains.kotlin.ir.backend.js.checkers.JsKlibErrors
import org.jetbrains.kotlin.ir.backend.js.checkers.*
object JsKlibEsModuleExportsChecker : JsKlibExportedDeclarationsChecker {
override fun check(
declarations: List<JsKlibExportingDeclaration>,
reporter: KtDiagnosticReporterWithImplicitIrBasedContext,
context: JsKlibDiagnosticContext,
reporter: KtDiagnosticReporterWithImplicitIrBasedContext
) {
val allExportedNameClashes = declarations.groupBy { it.exportingName }.filterValues { it.size > 1 }
@@ -21,7 +20,11 @@ object JsKlibEsModuleExportsChecker : JsKlibExportedDeclarationsChecker {
for ((index, exportedDeclaration) in exportedDeclarationClashes.withIndex()) {
val declaration = exportedDeclaration.declaration ?: continue
val clashedWith = exportedDeclarationClashes.filterIndexed { i, _ -> i != index }
reporter.at(declaration).report(JsKlibErrors.EXPORTING_JS_NAME_CLASH_ES, exportedDeclaration.exportingName, clashedWith)
reporter.at(declaration, context).report(
JsKlibErrors.EXPORTING_JS_NAME_CLASH_ES,
exportedDeclaration.exportingName,
clashedWith
)
}
}
}
@@ -44,11 +44,19 @@ object JsKlibOtherModuleExportsChecker : JsKlibExportedDeclarationsChecker {
}
}
override fun check(declarations: List<JsKlibExportingDeclaration>, reporter: KtDiagnosticReporterWithImplicitIrBasedContext) {
override fun check(
declarations: List<JsKlibExportingDeclaration>,
context: JsKlibDiagnosticContext,
reporter: KtDiagnosticReporterWithImplicitIrBasedContext
) {
val clashes = collectClashes(declarations)
for ((declaration, clashedWith) in clashes) {
if (declaration.declaration != null) {
reporter.at(declaration.declaration).report(JsKlibErrors.EXPORTING_JS_NAME_CLASH, declaration.exportingName, clashedWith)
reporter.at(declaration.declaration, context).report(
JsKlibErrors.EXPORTING_JS_NAME_CLASH,
declaration.exportingName,
clashedWith
)
}
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2023 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.ir.backend.js.checkers.expressions
import com.google.gwt.dev.js.parserExceptions.AbortParsingException
import com.google.gwt.dev.js.rhino.CodePosition
import com.google.gwt.dev.js.rhino.ErrorReporter
import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.ir.backend.js.checkers.JsKlibCallChecker
import org.jetbrains.kotlin.ir.backend.js.checkers.JsKlibErrors
import org.jetbrains.kotlin.ir.backend.js.checkers.JsKlibDiagnosticContext
import org.jetbrains.kotlin.ir.backend.js.checkers.at
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrConstKind
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable
import org.jetbrains.kotlin.js.backend.ast.JsFunctionScope
import org.jetbrains.kotlin.js.backend.ast.JsProgram
import org.jetbrains.kotlin.js.backend.ast.JsRootScope
import org.jetbrains.kotlin.js.parser.parseExpressionOrStatement
import org.jetbrains.kotlin.name.JsStandardClassIds
object JsKlibJsCodeCallChecker : JsKlibCallChecker {
private val jsCodeFqName = JsStandardClassIds.Callables.JsCode.asSingleFqName()
override fun check(expression: IrCall, context: JsKlibDiagnosticContext, reporter: KtDiagnosticReporterWithImplicitIrBasedContext) {
// Do not check IR from K1, because there are corresponding K1 FE checks in JsCallChecker
if (!context.compilerConfiguration.languageVersionSettings.languageVersion.usesK2) {
return
}
if (expression.symbol.owner.fqNameWhenAvailable != jsCodeFqName) {
return
}
val jsCodeExpr = expression.getValueArgument(0)
// K2 frontend checker FirJsCodeConstantArgumentChecker checks that the passing argument is a constant.
// IrConstOnlyNecessaryTransformer must evaluate an argument expression and fold it into a string constant.
if (jsCodeExpr !is IrConst<*> || jsCodeExpr.kind != IrConstKind.String) {
// Do not ignore the error if the argument does not fit the expectations.
// The IR can be generated in the plugin, avoiding FE checks and IrConstOnlyNecessaryTransformer.
// Bugs are also possible in IrConstOnlyNecessaryTransformer.
reporter.at(jsCodeExpr ?: expression, context).report(JsKlibErrors.JSCODE_CAN_NOT_VERIFY_JAVASCRIPT)
return
}
val jsCodeStr = IrConstKind.String.valueOf(jsCodeExpr)
try {
val parserScope = JsFunctionScope(JsRootScope(JsProgram()), "<js fun>")
val fileName = context.containingFile?.fileEntry?.name ?: "<unknown file>"
val jsErrorReporter = JsErrorReporter(jsCodeExpr, context, reporter)
val statements = parseExpressionOrStatement(jsCodeStr, jsErrorReporter, parserScope, CodePosition(0, 0), fileName)
if (statements.isNullOrEmpty()) {
reporter.at(jsCodeExpr, context).report(JsKlibErrors.JSCODE_NO_JAVASCRIPT_PRODUCED)
}
} catch (e: AbortParsingException) {
// ignore
}
}
private class JsErrorReporter(
val codeExpression: IrExpression,
val context: JsKlibDiagnosticContext,
val reporter: KtDiagnosticReporterWithImplicitIrBasedContext,
) : ErrorReporter {
override fun warning(message: String, startPosition: CodePosition, endPosition: CodePosition) {
reporter.at(codeExpression, context).report(JsKlibErrors.JSCODE_WARNING, message)
}
override fun error(message: String, startPosition: CodePosition, endPosition: CodePosition) {
reporter.at(codeExpression, context).report(JsKlibErrors.JSCODE_ERROR, message)
throw AbortParsingException()
}
}
}
@@ -656,7 +656,7 @@ fun serializeModuleIntoKlib(
if (builtInsPlatform == BuiltInsPlatform.JS) {
val cleanFilesIrData = cleanFiles.map { it.irData }
JsKlibCheckers.check(cleanFilesIrData, moduleFragment.files, moduleExportedNames, diagnosticReporter, configuration)
JsKlibCheckers.check(cleanFilesIrData, moduleFragment, moduleExportedNames, diagnosticReporter, configuration)
}
val serializedIr =