feat: add WARNING on usage top-level exportable declarations with non-consumable identifiers.

This commit is contained in:
Artem Kobzar
2022-05-04 11:22:57 +00:00
committed by Space
parent 0ea9c1fbce
commit a2b40acc98
9 changed files with 170 additions and 22 deletions
@@ -0,0 +1,42 @@
// !OPT_IN: kotlin.js.ExperimentalJsExport
// !RENDER_DIAGNOSTICS_MESSAGES
package foo
@JsExport
fun <!NON_CONSUMABLE_EXPORTED_IDENTIFIER!>delete<!>() {}
@JsExport
val <!NON_CONSUMABLE_EXPORTED_IDENTIFIER!>instanceof<!> = 4
@JsExport
class <!NON_CONSUMABLE_EXPORTED_IDENTIFIER!>eval<!>
@JsExport
@JsName(<!NON_CONSUMABLE_EXPORTED_IDENTIFIER!>"await"<!>)
fun foo() {}
@JsExport
@JsName(<!NON_CONSUMABLE_EXPORTED_IDENTIFIER!>"this"<!>)
val bar = 4
@JsExport
@JsName(<!NON_CONSUMABLE_EXPORTED_IDENTIFIER!>"super"<!>)
class Baz
@JsExport
class Test {
fun instanceof() {}
@JsName("eval")
fun test() {}
}
@JsExport
object NaN
@JsExport
enum class Nums {
Infinity,
undefined
}
@@ -0,0 +1,57 @@
package
package foo {
@kotlin.js.JsExport @kotlin.js.JsName(name = "this") public val bar: kotlin.Int = 4
@kotlin.js.JsExport public val instanceof: kotlin.Int = 4
@kotlin.js.JsExport public fun delete(): kotlin.Unit
@kotlin.js.JsExport @kotlin.js.JsName(name = "await") public fun foo(): kotlin.Unit
@kotlin.js.JsExport @kotlin.js.JsName(name = "super") public final class Baz {
public constructor Baz()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.js.JsExport public object NaN {
private constructor NaN()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.js.JsExport public final enum class Nums : kotlin.Enum<foo.Nums> {
enum entry Infinity
enum entry undefined
private constructor Nums()
public final override /*1*/ /*fake_override*/ val name: kotlin.String
public final override /*1*/ /*fake_override*/ val ordinal: kotlin.Int
protected final override /*1*/ /*fake_override*/ fun clone(): kotlin.Any
public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: foo.Nums): kotlin.Int
public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
// Static members
public final /*synthesized*/ fun valueOf(/*0*/ value: kotlin.String): foo.Nums
public final /*synthesized*/ fun values(): kotlin.Array<foo.Nums>
}
@kotlin.js.JsExport public final class Test {
public constructor Test()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final fun instanceof(): kotlin.Unit
@kotlin.js.JsName(name = "eval") public final fun test(): kotlin.Unit
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@kotlin.js.JsExport public final class eval {
public constructor eval()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
@@ -401,6 +401,12 @@ public class DiagnosticsTestWithJsStdLibGenerated extends AbstractDiagnosticsTes
runTest("compiler/testData/diagnostics/testsWithJsStdLib/export/jsExportOnNestedDeclarations.kt"); runTest("compiler/testData/diagnostics/testsWithJsStdLib/export/jsExportOnNestedDeclarations.kt");
} }
@Test
@TestMetadata("nonConsumableIdentifiers.kt")
public void testNonConsumableIdentifiers() throws Exception {
runTest("compiler/testData/diagnostics/testsWithJsStdLib/export/nonConsumableIdentifiers.kt");
}
@Test @Test
@TestMetadata("secondaryConstructorWithoutJsName.kt") @TestMetadata("secondaryConstructorWithoutJsName.kt")
public void testSecondaryConstructorWithoutJsName() throws Exception { public void testSecondaryConstructorWithoutJsName() throws Exception {
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.js.backend.ast package org.jetbrains.kotlin.js.backend.ast
import org.jetbrains.kotlin.js.common.RESERVED_KEYWORDS
import java.util.* import java.util.*
class JsObjectScope(parent: JsScope, description: String) : JsScope(parent, description) class JsObjectScope(parent: JsScope, description: String) : JsScope(parent, description)
@@ -70,23 +71,7 @@ open class JsDeclarationScope(parent: JsScope, description: String, useParentSco
} }
companion object { companion object {
val RESERVED_WORDS: Set<String> = setOf( val RESERVED_WORDS: Set<String> = RESERVED_KEYWORDS + setOf(
// keywords
"await", "break", "case", "catch", "continue", "debugger", "default", "delete", "do", "else", "finally", "for", "function", "if",
"in", "instanceof", "new", "return", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with",
// future reserved words
"class", "const", "enum", "export", "extends", "import", "super",
// as future reserved words in strict mode
"implements", "interface", "let", "package", "private", "protected", "public", "static", "yield",
// additional reserved words
"null", "true", "false",
// disallowed as variable names in strict mode
"eval", "arguments",
// global identifiers usually declared in a typical JS interpreter // global identifiers usually declared in a typical JS interpreter
"NaN", "isNaN", "Infinity", "undefined", "NaN", "isNaN", "Infinity", "undefined",
"Error", "Object", "Math", "String", "Number", "Boolean", "Date", "Array", "RegExp", "JSON", "Error", "Object", "Math", "String", "Number", "Boolean", "Date", "Array", "RegExp", "JSON",
@@ -49,4 +49,22 @@ fun String.isValidES5Identifier(): Boolean {
if (!get(idx).isES5IdentifierPart()) return false if (!get(idx).isES5IdentifierPart()) return false
} }
return true return true
} }
val RESERVED_KEYWORDS: Set<String> = setOf(
// keywords
"await", "break", "case", "catch", "continue", "debugger", "default", "delete", "do", "else", "finally", "for", "function", "if",
"in", "instanceof", "new", "return", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with",
// future reserved words
"class", "const", "enum", "export", "extends", "import", "super",
// as future reserved words in strict mode
"implements", "interface", "let", "package", "private", "protected", "public", "static", "yield",
// additional reserved words
"null", "true", "false",
// disallowed as variable names in strict mode
"eval", "arguments",
)
@@ -106,6 +106,8 @@ private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy {
put(ErrorsJs.WRONG_EXPORTED_DECLARATION, "Declaration of such kind ({0}) can''t be exported to JS", STRING) put(ErrorsJs.WRONG_EXPORTED_DECLARATION, "Declaration of such kind ({0}) can''t be exported to JS", STRING)
put(ErrorsJs.NON_EXPORTABLE_TYPE, "Exported declaration uses non-exportable {0} type: {1}", STRING, RENDER_TYPE) put(ErrorsJs.NON_EXPORTABLE_TYPE, "Exported declaration uses non-exportable {0} type: {1}", STRING, RENDER_TYPE)
put(ErrorsJs.NON_CONSUMABLE_EXPORTED_IDENTIFIER, "Exported declaration contains non-consumable identifier '${0}', that can't be represented inside TS definitions and ESM", STRING)
this this
} }
} }
@@ -110,6 +110,8 @@ public interface ErrorsJs {
DiagnosticFactory1<KtExpression, String> WRONG_EXPORTED_DECLARATION = DiagnosticFactory1.create(ERROR, DECLARATION_SIGNATURE_OR_DEFAULT); DiagnosticFactory1<KtExpression, String> WRONG_EXPORTED_DECLARATION = DiagnosticFactory1.create(ERROR, DECLARATION_SIGNATURE_OR_DEFAULT);
DiagnosticFactory2<PsiElement, String, KotlinType> NON_EXPORTABLE_TYPE = DiagnosticFactory2.create(WARNING, DECLARATION_SIGNATURE_OR_DEFAULT); DiagnosticFactory2<PsiElement, String, KotlinType> NON_EXPORTABLE_TYPE = DiagnosticFactory2.create(WARNING, DECLARATION_SIGNATURE_OR_DEFAULT);
DiagnosticFactory1<PsiElement, String> NON_CONSUMABLE_EXPORTED_IDENTIFIER = DiagnosticFactory1.create(WARNING, DEFAULT);
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() { Object _initializer = new Object() {
{ {
@@ -5,15 +5,19 @@
package org.jetbrains.kotlin.js.resolve.diagnostics package org.jetbrains.kotlin.js.resolve.diagnostics
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.builtins.isFunctionType import org.jetbrains.kotlin.builtins.isFunctionType
import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.ClassKind.* import org.jetbrains.kotlin.descriptors.ClassKind.*
import org.jetbrains.kotlin.js.resolve.diagnostics.JsExportDeclarationChecker.isExportable import org.jetbrains.kotlin.js.common.RESERVED_KEYWORDS
import org.jetbrains.kotlin.js.naming.NameSuggestion
import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
@@ -22,6 +26,7 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.isExtensionProperty
import org.jetbrains.kotlin.resolve.descriptorUtil.isInsideInterface import org.jetbrains.kotlin.resolve.descriptorUtil.isInsideInterface
import org.jetbrains.kotlin.resolve.inline.isInlineWithReified import org.jetbrains.kotlin.resolve.inline.isInlineWithReified
import org.jetbrains.kotlin.resolve.isInlineClass import org.jetbrains.kotlin.resolve.isInlineClass
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.isDynamic import org.jetbrains.kotlin.types.isDynamic
import org.jetbrains.kotlin.types.typeUtil.* import org.jetbrains.kotlin.types.typeUtil.*
@@ -61,6 +66,8 @@ object JsExportDeclarationChecker : DeclarationChecker {
reportWrongExportedDeclaration("expect") reportWrongExportedDeclaration("expect")
} }
validateDeclarationOnConsumableName(declaration, descriptor, trace)
when (descriptor) { when (descriptor) {
is FunctionDescriptor -> { is FunctionDescriptor -> {
for (typeParameter in descriptor.typeParameters) { for (typeParameter in descriptor.typeParameters) {
@@ -201,4 +208,33 @@ object JsExportDeclarationChecker : DeclarationChecker {
return descriptor.isEffectivelyExternal() || AnnotationsUtils.isExportedObject(descriptor, bindingContext) return descriptor.isEffectivelyExternal() || AnnotationsUtils.isExportedObject(descriptor, bindingContext)
} }
private fun validateDeclarationOnConsumableName(
declaration: KtDeclaration,
declarationDescriptor: DeclarationDescriptor,
trace: BindingTrace
) {
if (!declarationDescriptor.isTopLevelInPackage() || declarationDescriptor.name.isSpecial) return
val name = declarationDescriptor.getKotlinOrJsName()
if (name !in RESERVED_KEYWORDS && NameSuggestion.sanitizeName(name) == name) return
val reportTarget = declarationDescriptor.getJsNameArgument() ?: declaration.getIdentifier()
trace.report(ErrorsJs.NON_CONSUMABLE_EXPORTED_IDENTIFIER.on(reportTarget, name))
}
private fun DeclarationDescriptor.getKotlinOrJsName(): String {
return AnnotationsUtils.getJsName(this) ?: name.identifier
}
private fun KtDeclaration.getIdentifier(): PsiElement {
return (this as KtNamedDeclaration).nameIdentifier!!
}
private fun DeclarationDescriptor.getJsNameArgument(): PsiElement? {
val jsNameAnnotation = AnnotationsUtils.getJsNameAnnotation(this) ?: return null
return (jsNameAnnotation.source.getPsi() as KtAnnotationEntry).valueArgumentList?.arguments?.first()
}
} }
@@ -39,7 +39,7 @@ import java.util.List;
import static org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilsKt.isEffectivelyExternal; import static org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilsKt.isEffectivelyExternal;
public final class AnnotationsUtils { public final class AnnotationsUtils {
private static final String JS_NAME = "kotlin.js.JsName"; public static final FqName JS_NAME = new FqName("kotlin.js.JsName");
private static final FqName JS_EXPORT = new FqName("kotlin.js.JsExport"); private static final FqName JS_EXPORT = new FqName("kotlin.js.JsExport");
public static final FqName JS_MODULE_ANNOTATION = new FqName("kotlin.js.JsModule"); public static final FqName JS_MODULE_ANNOTATION = new FqName("kotlin.js.JsModule");
private static final FqName JS_NON_MODULE_ANNOTATION = new FqName("kotlin.js.JsNonModule"); private static final FqName JS_NON_MODULE_ANNOTATION = new FqName("kotlin.js.JsNonModule");
@@ -172,7 +172,7 @@ public final class AnnotationsUtils {
@Nullable @Nullable
public static AnnotationDescriptor getJsNameAnnotation(@NotNull DeclarationDescriptor descriptor) { public static AnnotationDescriptor getJsNameAnnotation(@NotNull DeclarationDescriptor descriptor) {
return descriptor.getAnnotations().findAnnotation(new FqName(JS_NAME)); return descriptor.getAnnotations().findAnnotation(JS_NAME);
} }
@Nullable @Nullable