diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt index 1bae343464c..11dec691d7c 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt @@ -1234,6 +1234,12 @@ internal val KT_DIAGNOSTIC_CONVERTER = KtDiagnosticConverterBuilder.buildConvert token, ) } + add(FirJsErrors.NAME_CONTAINS_ILLEGAL_CHARS) { firDiagnostic -> + NameContainsIllegalCharsImpl( + firDiagnostic as KtPsiDiagnostic, + token, + ) + } add(FirErrors.OPT_IN_USAGE) { firDiagnostic -> OptInUsageImpl( firDiagnostic.a, diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt index 187a322d8a9..292c3857e9e 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt @@ -886,6 +886,10 @@ sealed interface KtFirDiagnostic : KtDiagnosticWithPsi { val name: String } + interface NameContainsIllegalChars : KtFirDiagnostic { + override val diagnosticClass get() = NameContainsIllegalChars::class + } + interface OptInUsage : KtFirDiagnostic { override val diagnosticClass get() = OptInUsage::class val optInMarkerFqName: FqName diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt index 43673aea7d0..7998c2b1133 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt @@ -1065,6 +1065,11 @@ internal class JsBuiltinNameClashImpl( token: KtLifetimeToken, ) : KtAbstractFirDiagnostic(firDiagnostic, token), KtFirDiagnostic.JsBuiltinNameClash +internal class NameContainsIllegalCharsImpl( + firDiagnostic: KtPsiDiagnostic, + token: KtLifetimeToken, +) : KtAbstractFirDiagnostic(firDiagnostic, token), KtFirDiagnostic.NameContainsIllegalChars + internal class OptInUsageImpl( override val optInMarkerFqName: FqName, override val message: String, diff --git a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt index 441ac793944..2c63c36582c 100644 --- a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt +++ b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt @@ -57,6 +57,7 @@ object JS_DIAGNOSTICS_LIST : DiagnosticList("FirJsErrors") { val JS_BUILTIN_NAME_CLASH by error(PositioningStrategy.DECLARATION_SIGNATURE_OR_DEFAULT) { parameter("name") } + val NAME_CONTAINS_ILLEGAL_CHARS by error(PositioningStrategy.DECLARATION_SIGNATURE_OR_DEFAULT) } val SUPERTYPES by object : DiagnosticGroup("Supertypes") { diff --git a/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt b/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt index 2ff14468a6d..3b22f8831d1 100644 --- a/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt +++ b/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt @@ -51,6 +51,7 @@ object FirJsErrors { val JS_NAME_ON_ACCESSOR_AND_PROPERTY by error0() val JS_NAME_PROHIBITED_FOR_EXTENSION_PROPERTY by error0() val JS_BUILTIN_NAME_CLASH by error1(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) + val NAME_CONTAINS_ILLEGAL_CHARS by error0(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) // Supertypes val WRONG_MULTIPLE_INHERITANCE by error1>(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt index 9a554318644..3d3ecc350f3 100644 --- a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt @@ -43,6 +43,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_NAME_ON_P import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_NAME_PROHIBITED_FOR_EXTENSION_PROPERTY import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_NAME_PROHIBITED_FOR_NAMED_NATIVE import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_NAME_PROHIBITED_FOR_OVERRIDE +import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.NAME_CONTAINS_ILLEGAL_CHARS import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.NATIVE_ANNOTATIONS_ALLOWED_ONLY_ON_MEMBER_OR_EXTENSION_FUN import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.NATIVE_GETTER_RETURN_TYPE_SHOULD_BE_NULLABLE import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.NATIVE_INDEXER_CAN_NOT_HAVE_DEFAULT_ARGUMENTS @@ -199,6 +200,7 @@ object FirJsErrorsDefaultMessages : BaseDiagnosticRendererFactory() { "JavaScript name generated for this declaration clashes with built-in declaration {0}", CommonRenderers.STRING ) + map.put(NAME_CONTAINS_ILLEGAL_CHARS, "Name contains illegal chars that can't appear in JavaScript identifier") map.put(JS_NAME_IS_NOT_ON_ALL_ACCESSORS, "@JsName should be on all the property accessors") map.put(JS_NAME_PROHIBITED_FOR_NAMED_NATIVE, "@JsName is prohibited for external declaration with explicit name") diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsDeclarationCheckers.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsDeclarationCheckers.kt index de7308b788e..c5c66391eb1 100644 --- a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsDeclarationCheckers.kt +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsDeclarationCheckers.kt @@ -23,7 +23,8 @@ object JsDeclarationCheckers : DeclarationCheckers() { FirJsNameChecker, FirJsExportAnnotationChecker, FirJsExportDeclarationChecker, - FirJsBuiltinNameClashChecker + FirJsBuiltinNameClashChecker, + FirJsNameCharsChecker ) override val classCheckers: Set diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt index 6fcd9654d38..ec6619ec04d 100644 --- a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt @@ -22,7 +22,6 @@ object JsExpressionCheckers : ExpressionCheckers() { override val functionCallCheckers: Set get() = setOf( - FirJsDynamicCallChecker, FirJsReifiedExternalChecker ) @@ -38,6 +37,7 @@ object JsExpressionCheckers : ExpressionCheckers() { override val qualifiedAccessExpressionCheckers: Set get() = setOf( - FirJsModuleQualifiedAccessChecker + FirJsModuleQualifiedAccessChecker, + FirJsDynamicCallChecker ) } diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsNameCharsChecker.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsNameCharsChecker.kt new file mode 100644 index 00000000000..5f85f2a2b0c --- /dev/null +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsNameCharsChecker.kt @@ -0,0 +1,47 @@ +/* + * 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.fir.analysis.js.checkers.declaration + +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.diagnostics.DiagnosticReporter +import org.jetbrains.kotlin.diagnostics.reportOn +import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext +import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirBasicDeclarationChecker +import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors +import org.jetbrains.kotlin.fir.analysis.js.checkers.getJsName +import org.jetbrains.kotlin.fir.analysis.js.checkers.getStableNameInJavaScript +import org.jetbrains.kotlin.fir.analysis.js.checkers.isExportedObject +import org.jetbrains.kotlin.fir.analysis.js.checkers.sanitizeName +import org.jetbrains.kotlin.fir.declarations.FirConstructor +import org.jetbrains.kotlin.fir.declarations.FirDeclaration +import org.jetbrains.kotlin.fir.declarations.FirPropertyAccessor +import org.jetbrains.kotlin.fir.declarations.impl.FirPrimaryConstructor + +object FirJsNameCharsChecker : FirBasicDeclarationChecker() { + override fun check(declaration: FirDeclaration, context: CheckerContext, reporter: DiagnosticReporter) { + if (context.languageVersionSettings.supportsFeature(LanguageFeature.JsAllowInvalidCharsIdentifiersEscaping)) { + return + } + + if ((declaration is FirPropertyAccessor || declaration is FirPrimaryConstructor) && + declaration.symbol.getJsName(context.session) == null + ) { + return + } + + if (declaration is FirConstructor && + declaration.symbol.getJsName(context.session) == null && + declaration.symbol.isExportedObject(context.session) + ) { + return + } + + val stableName = declaration.symbol.getStableNameInJavaScript(context.session) ?: return + if ((sanitizeName(stableName) != stableName)) { + reporter.reportOn(declaration.source, FirJsErrors.NAME_CONTAINS_ILLEGAL_CHARS, context) + } + } +} diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/expression/FirJsDynamicCallChecker.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/expression/FirJsDynamicCallChecker.kt index 69aba198ba7..d5530452b3a 100644 --- a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/expression/FirJsDynamicCallChecker.kt +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/expression/FirJsDynamicCallChecker.kt @@ -6,14 +6,18 @@ package org.jetbrains.kotlin.fir.analysis.js.checkers.expression import org.jetbrains.kotlin.KtFakeSourceElementKind +import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.diagnostics.DiagnosticReporter import org.jetbrains.kotlin.diagnostics.reportOn import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext -import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirFunctionCallChecker +import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirBasicExpressionChecker +import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirQualifiedAccessExpressionChecker import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors +import org.jetbrains.kotlin.fir.analysis.js.checkers.sanitizeName import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin import org.jetbrains.kotlin.fir.expressions.* import org.jetbrains.kotlin.fir.references.FirNamedReference +import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference import org.jetbrains.kotlin.fir.references.resolved import org.jetbrains.kotlin.fir.references.toResolvedCallableSymbol import org.jetbrains.kotlin.fir.types.ConeDynamicType @@ -26,8 +30,8 @@ private val nameToOperator = mapOf( OperatorNameConventions.RANGE_UNTIL to "..<", ) -object FirJsDynamicCallChecker : FirFunctionCallChecker() { - override fun check(expression: FirFunctionCall, context: CheckerContext, reporter: DiagnosticReporter) { +object FirJsDynamicCallChecker : FirQualifiedAccessExpressionChecker() { + override fun check(expression: FirQualifiedAccessExpression, context: CheckerContext, reporter: DiagnosticReporter) { val callee = expression.calleeReference.resolved ?: return if (callee.resolvedSymbol.origin !is FirDeclarationOrigin.DynamicScope) { @@ -38,18 +42,19 @@ object FirJsDynamicCallChecker : FirFunctionCallChecker() { ?: error("Resolved call callee without a callable symbol") when { - expression.isArrayAccessWithMultipleIndices -> reporter.reportOn( + expression is FirCall && expression.isArrayAccessWithMultipleIndices -> reporter.reportOn( expression.source, FirJsErrors.WRONG_OPERATION_WITH_DYNAMIC, "indexed access with more than one index", context ) - expression.isInOperator -> reporter.reportOn( + expression is FirFunctionCall && expression.isInOperator -> reporter.reportOn( expression.source, FirJsErrors.WRONG_OPERATION_WITH_DYNAMIC, "`in` operation", context ) - expression.isRangeOperator -> reporter.reportOn( + expression is FirFunctionCall && expression.isRangeOperator -> reporter.reportOn( expression.source, FirJsErrors.WRONG_OPERATION_WITH_DYNAMIC, "`${nameToOperator[symbol.name]}` operation", context ) expression is FirComponentCall -> reporter.reportOn( expression.source, FirJsErrors.WRONG_OPERATION_WITH_DYNAMIC, "`destructuring declaration", context ) + else -> checkIdentifier(callee, reporter, context) } forAllSpreadArgumentsOf(expression) { @@ -83,7 +88,7 @@ object FirJsDynamicCallChecker : FirFunctionCallChecker() { && origin == FirFunctionCallOrigin.Operator } - private fun checkSpreadOperator(expression: FirCall, context: CheckerContext, reporter: DiagnosticReporter) { + private fun checkSpreadOperator(expression: FirQualifiedAccessExpression, context: CheckerContext, reporter: DiagnosticReporter) { forAllSpreadArgumentsOf(expression) { if (it.typeRef.coneType is ConeDynamicType) { reporter.reportOn(it.source, FirJsErrors.WRONG_OPERATION_WITH_DYNAMIC, "spread operator", context) @@ -91,7 +96,8 @@ object FirJsDynamicCallChecker : FirFunctionCallChecker() { } } - private inline fun forAllSpreadArgumentsOf(call: FirCall, callback: (FirExpression) -> Unit) { + private inline fun forAllSpreadArgumentsOf(expression: FirQualifiedAccessExpression, callback: (FirExpression) -> Unit) { + val call = expression as? FirCall ?: return for (argument in call.argumentList.arguments) { if (argument !is FirVarargArgumentsExpression) { continue @@ -104,4 +110,14 @@ object FirJsDynamicCallChecker : FirFunctionCallChecker() { } } } + + private fun checkIdentifier(namedReference: FirResolvedNamedReference, reporter: DiagnosticReporter, context: CheckerContext) { + if (context.languageVersionSettings.supportsFeature(LanguageFeature.JsAllowInvalidCharsIdentifiersEscaping)) { + return + } + val name = namedReference.name.identifierOrNullIfSpecial ?: return + if (sanitizeName(name) != name) { + reporter.reportOn(namedReference.source, FirJsErrors.NAME_CONTAINS_ILLEGAL_CHARS, context) + } + } } diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.fir.kt b/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.fir.kt deleted file mode 100644 index 1c3562930c5..00000000000 --- a/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.fir.kt +++ /dev/null @@ -1,25 +0,0 @@ -private fun ` .private `(): String = TODO("") - -fun ` .public `(): String = TODO("") - -@JsName(" __ ") -fun foo(): String = TODO("") - -@JsName(" ___ ") -private fun bar(): String = TODO("") - -@JsName("validName") -private fun ` .private with @JsName `(): String = TODO("") - -private class ` .private class ` { - val ` .field. ` = "" -} - -val x: Int - @JsName(".") - get() = TODO("") - -fun box(x: dynamic) { - x.`foo-bar`() - x.`ba-z` -} diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.kt b/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.kt index b6106c0fdab..474047119cb 100644 --- a/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.kt +++ b/compiler/testData/diagnostics/testsWithJsStdLib/name/illegalName.kt @@ -1,3 +1,4 @@ +// FIR_IDENTICAL private fun ` .private `(): String = TODO("") fun ` .public `(): String = TODO("")