diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index a5810f04780e..1a4f3e6d7ec2 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -323,7 +323,6 @@ class JSCodeGen()(using genCtx: Context) { // Generate members (constructor + methods) val generatedNonFieldMembers = new mutable.ListBuffer[js.MemberDef] - val exportedSymbols = new mutable.ListBuffer[Symbol] val tpl = td.rhs.asInstanceOf[Template] for (tree <- tpl.constr :: tpl.body) { @@ -336,19 +335,11 @@ class JSCodeGen()(using genCtx: Context) { case dd: DefDef => val sym = dd.symbol - val isExport = false //jsInterop.isExport(sym) - if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) generatedNonFieldMembers += genJSNativeMemberDef(dd) else generatedNonFieldMembers ++= genMethod(dd) - if (isExport) { - // We add symbols that we have to export here. This way we also - // get inherited stuff that is implemented in this class. - exportedSymbols += sym - } - case _ => throw new FatalError("Illegal tree in body of genScalaClass(): " + tree) } @@ -357,21 +348,11 @@ class JSCodeGen()(using genCtx: Context) { // Generate fields and add to methods + ctors val generatedMembers = genClassFields(td) ++ generatedNonFieldMembers.toList - // Generate the exported members, constructors and accessors - val exports = { - /* - // Generate the exported members - val memberExports = genMemberExports(sym, exportedSymbols.toList) + // Generate member exports + val memberExports = jsExportsGen.genMemberExports(sym) - // Generate exported constructors or accessors - val exportedConstructorsOrAccessors = - if (isStaticModule(sym)) genModuleAccessorExports(sym) - else genConstructorExports(sym) - - memberExports ++ exportedConstructorsOrAccessors - */ - Nil - } + // Generate top-level export definitions + val topLevelExportDefs = jsExportsGen.genTopLevelExports(sym) // Static initializer val optStaticInitializer = { @@ -383,20 +364,27 @@ class JSCodeGen()(using genCtx: Context) { } } if (enableReflectiveInstantiation) - genRegisterReflectiveInstantiation(sym) + genRegisterReflectiveInstantiation(sym).toList else - None + Nil } - val staticInitializerStats = reflectInit.toList + // Initialization of the module because of field exports + val needsStaticModuleInit = + topLevelExportDefs.exists(_.isInstanceOf[js.TopLevelFieldExportDef]) + val staticModuleInit = + if (!needsStaticModuleInit) Nil + else List(genLoadModule(sym)) + + val staticInitializerStats = reflectInit ::: staticModuleInit if (staticInitializerStats.nonEmpty) - List(genStaticInitializerWithStats(js.Block(staticInitializerStats))) + List(genStaticConstructorWithStats(ir.Names.StaticInitializerName, js.Block(staticInitializerStats))) else Nil } val allMemberDefsExceptStaticForwarders = - generatedMembers ::: exports ::: optStaticInitializer + generatedMembers ::: memberExports ::: optStaticInitializer // Add static forwarders val allMemberDefs = if (!isCandidateForForwarders(sym)) { @@ -452,7 +440,7 @@ class JSCodeGen()(using genCtx: Context) { None, None, hashedDefs, - Nil)( + topLevelExportDefs)( optimizerHints) classDefinition @@ -511,6 +499,28 @@ class JSCodeGen()(using genCtx: Context) { } } + // Static members (exported from the companion object) + val staticMembers = { + val module = sym.companionModule + if (!module.exists) { + Nil + } else { + val companionModuleClass = module.moduleClass + val exports = withScopedVars(currentClassSym := companionModuleClass) { + jsExportsGen.genStaticExports(companionModuleClass) + } + if (exports.exists(_.isInstanceOf[js.JSFieldDef])) { + val classInitializer = + genStaticConstructorWithStats(ir.Names.ClassInitializerName, genLoadModule(companionModuleClass)) + exports :+ classInitializer + } else { + exports + } + } + } + + val topLevelExports = jsExportsGen.genTopLevelExports(sym) + val (jsClassCaptures, generatedConstructor) = genJSClassCapturesAndConstructor(sym, constructorTrees.toList) @@ -525,7 +535,8 @@ class JSCodeGen()(using genCtx: Context) { genClassFields(td) ::: generatedConstructor :: jsExportsGen.genJSClassDispatchers(sym, dispatchMethodNames.result().distinct) ::: - generatedMethods.toList + generatedMethods.toList ::: + staticMembers } // Hashed definitions of the class @@ -546,7 +557,7 @@ class JSCodeGen()(using genCtx: Context) { jsSuperClass, None, hashedMemberDefs, - Nil)( + topLevelExports)( OptimizerHints.empty) classDefinition @@ -785,10 +796,15 @@ class JSCodeGen()(using genCtx: Context) { !f.isOneOf(Method | Module) && f.isTerm && !f.hasAnnotation(jsdefn.JSNativeAnnot) && !f.hasAnnotation(jsdefn.JSOptionalAnnot) + && !f.hasAnnotation(jsdefn.JSExportStaticAnnot) }.flatMap({ f => implicit val pos = f.span - val isStaticField = f.is(JavaStatic).ensuring(isStatic => !(isStatic && isJSClass)) + val isTopLevelExport = f.hasAnnotation(jsdefn.JSExportTopLevelAnnot) + val isJavaStatic = f.is(JavaStatic) + assert(!(isTopLevelExport && isJavaStatic), + em"found ${f.fullName} which is both a top-level export and a Java static") + val isStaticField = isTopLevelExport || isJavaStatic val namespace = if isStaticField then js.MemberNamespace.PublicStatic else js.MemberNamespace.Public val mutable = isStaticField || f.is(Mutable) @@ -797,6 +813,7 @@ class JSCodeGen()(using genCtx: Context) { val irTpe = if (isJSClass) genExposedFieldIRType(f) + else if (isTopLevelExport) jstpe.AnyType else toIRType(f.info) if (isJSClass && f.isJSExposed) @@ -806,7 +823,7 @@ class JSCodeGen()(using genCtx: Context) { val originalName = originalNameOfField(f) val fieldDef = js.FieldDef(flags, fieldIdent, originalName, irTpe) val optionalStaticFieldGetter = - if isStaticField then + if isJavaStatic then // Here we are generating a public static getter for the static field, // this is its API for other units. This is necessary for singleton // enum values, which are backed by static fields. @@ -824,7 +841,7 @@ class JSCodeGen()(using genCtx: Context) { }).toList } - private def genExposedFieldIRType(f: Symbol): jstpe.Type = { + def genExposedFieldIRType(f: Symbol): jstpe.Type = { val tpeEnteringPosterasure = atPhase(elimErasedValueTypePhase)(f.info) tpeEnteringPosterasure match { case tpe: ErasedValueType => @@ -850,11 +867,11 @@ class JSCodeGen()(using genCtx: Context) { // Static initializers ----------------------------------------------------- - private def genStaticInitializerWithStats(stats: js.Tree)( + private def genStaticConstructorWithStats(name: MethodName, stats: js.Tree)( implicit pos: Position): js.MethodDef = { js.MethodDef( js.MemberFlags.empty.withNamespace(js.MemberNamespace.StaticConstructor), - js.MethodIdent(ir.Names.StaticInitializerName), + js.MethodIdent(name), NoOriginalName, Nil, jstpe.NoType, @@ -3916,19 +3933,17 @@ class JSCodeGen()(using genCtx: Context) { } (f, true) - } else /*if (jsInterop.topLevelExportsOf(sym).nonEmpty) { - val f = js.SelectStatic(encodeClassName(sym.owner), - encodeFieldSym(sym))(jstpe.AnyType) + } else if (sym.hasAnnotation(jsdefn.JSExportTopLevelAnnot)) { + val f = js.SelectStatic(encodeClassName(sym.owner), encodeFieldSym(sym))(jstpe.AnyType) (f, true) - } else if (jsInterop.staticExportsOf(sym).nonEmpty) { - val exportInfo = jsInterop.staticExportsOf(sym).head - val companionClass = patchedLinkedClassOfClass(sym.owner) - val f = js.JSSelect( - genLoadJSConstructor(companionClass), - js.StringLiteral(exportInfo.jsName)) - + } else if (sym.hasAnnotation(jsdefn.JSExportStaticAnnot)) { + val jsName = sym.getAnnotation(jsdefn.JSExportStaticAnnot).get.argumentConstantString(0).getOrElse { + sym.defaultJSName + } + val companionClass = sym.owner.linkedClass + val f = js.JSSelect(genLoadJSConstructor(companionClass), js.StringLiteral(jsName)) (f, true) - } else*/ { + } else { val f = val className = encodeClassName(sym.owner) val fieldIdent = encodeFieldSym(sym) diff --git a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala index 8142d940ee01..2308efd6a0b1 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala @@ -22,18 +22,19 @@ import Types._ import TypeErasure.ErasedValueType import dotty.tools.dotc.transform.Erasure -import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.util.{SourcePosition, SrcPos} import dotty.tools.dotc.util.Spans.Span import dotty.tools.dotc.report import org.scalajs.ir import org.scalajs.ir.{ClassKind, Position, Names => jsNames, Trees => js, Types => jstpe} -import org.scalajs.ir.Names.{ClassName, MethodName, SimpleMethodName} +import org.scalajs.ir.Names.{ClassName, DefaultModuleID, MethodName, SimpleMethodName} import org.scalajs.ir.OriginalName import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Position.NoPosition import org.scalajs.ir.Trees.OptimizerHints +import dotty.tools.dotc.transform.sjs.JSExportUtils._ import dotty.tools.dotc.transform.sjs.JSSymUtils._ import JSEncoding._ @@ -42,6 +43,254 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { import jsCodeGen._ import positionConversions._ + /** Info for a non-member export. */ + sealed trait ExportInfo { + val pos: SourcePosition + } + + final case class TopLevelExportInfo(moduleID: String, jsName: String)(val pos: SourcePosition) extends ExportInfo + final case class StaticExportInfo(jsName: String)(val pos: SourcePosition) extends ExportInfo + + private sealed trait ExportKind + + private object ExportKind { + case object Module extends ExportKind + case object JSClass extends ExportKind + case object Constructor extends ExportKind + case object Method extends ExportKind + case object Property extends ExportKind + case object Field extends ExportKind + + def apply(sym: Symbol): ExportKind = { + if (sym.is(Flags.Module) && sym.isStatic) Module + else if (sym.isClass) JSClass + else if (sym.isConstructor) Constructor + else if (!sym.is(Flags.Method)) Field + else if (sym.isJSProperty) Property + else Method + } + } + + private def topLevelExportsOf(sym: Symbol): List[TopLevelExportInfo] = { + def isScalaClass(sym: Symbol): Boolean = + sym.isClass && !sym.isOneOf(Module | Trait) && !sym.isJSType + + if (isScalaClass(sym)) { + // Scala classes are never exported; their constructors are + Nil + } else if (sym.is(Accessor) || sym.is(Module, butNot = ModuleClass)) { + /* - Accessors receive the `@JSExportTopLevel` annotation of their associated field, + * but only the field is really exported. + * - Module values are not exported; their module class takes care of the export. + */ + Nil + } else { + val symForAnnot = + if (sym.isConstructor && isScalaClass(sym.owner)) sym.owner + else sym + + symForAnnot.annotations.collect { + case annot if annot.symbol == jsdefn.JSExportTopLevelAnnot => + val jsName = annot.argumentConstantString(0).get + val moduleID = annot.argumentConstantString(1).getOrElse(DefaultModuleID) + TopLevelExportInfo(moduleID, jsName)(annot.tree.sourcePos) + } + } + } + + private def staticExportsOf(sym: Symbol): List[StaticExportInfo] = { + if (sym.is(Accessor)) { + Nil + } else { + sym.annotations.collect { + case annot if annot.symbol == jsdefn.JSExportStaticAnnot => + val jsName = annot.argumentConstantString(0).getOrElse { + sym.defaultJSName + } + StaticExportInfo(jsName)(annot.tree.sourcePos) + } + } + } + + private def checkSameKind(tups: List[(ExportInfo, Symbol)]): Option[ExportKind] = { + assert(tups.nonEmpty, "must have at least one export") + + val firstSym = tups.head._2 + val overallKind = ExportKind(firstSym) + var bad = false + + for ((info, sym) <- tups.tail) { + val kind = ExportKind(sym) + + if (kind != overallKind) { + bad = true + report.error( + em"export overload conflicts with export of $firstSym: they are of different types ($kind / $overallKind)", + info.pos) + } + } + + if (bad) None + else Some(overallKind) + } + + private def checkSingleField(tups: List[(ExportInfo, Symbol)]): Symbol = { + assert(tups.nonEmpty, "must have at least one export") + + val firstSym = tups.head._2 + + for ((info, _) <- tups.tail) { + report.error( + em"export overload conflicts with export of $firstSym: " + + "a field may not share its exported name with another export", + info.pos) + } + + firstSym + } + + def genTopLevelExports(classSym: ClassSymbol): List[js.TopLevelExportDef] = { + val exports = for { + sym <- classSym :: classSym.info.decls.toList + info <- topLevelExportsOf(sym) + } yield { + (info, sym) + } + + (for { + (info, tups) <- exports.groupBy(_._1) + kind <- checkSameKind(tups) + } yield { + import ExportKind._ + + implicit val pos = info.pos + + kind match { + case Module => + js.TopLevelModuleExportDef(info.moduleID, info.jsName) + + case JSClass => + assert(classSym.isNonNativeJSClass, "found export on non-JS class") + js.TopLevelJSClassExportDef(info.moduleID, info.jsName) + + case Constructor | Method => + val exported = tups.map(t => Exported(t._2)) + + val methodDef = withNewLocalNameScope { + genExportMethod(exported, JSName.Literal(info.jsName), static = true) + } + + js.TopLevelMethodExportDef(info.moduleID, methodDef) + + case Property => + throw new AssertionError("found top-level exported property") + + case Field => + val sym = checkSingleField(tups) + js.TopLevelFieldExportDef(info.moduleID, info.jsName, encodeFieldSym(sym)) + } + }).toList + } + + def genStaticExports(classSym: Symbol): List[js.MemberDef] = { + val exports = for { + sym <- classSym.info.decls.toList + info <- staticExportsOf(sym) + } yield { + (info, sym) + } + + (for { + (info, tups) <- exports.groupBy(_._1) + kind <- checkSameKind(tups) + } yield { + def alts = tups.map(_._2) + + implicit val pos = info.pos + + import ExportKind._ + + kind match { + case Method => + genMemberExportOrDispatcher(JSName.Literal(info.jsName), isProp = false, alts, static = true) + + case Property => + genMemberExportOrDispatcher(JSName.Literal(info.jsName), isProp = true, alts, static = true) + + case Field => + val sym = checkSingleField(tups) + + // static fields must always be mutable + val flags = js.MemberFlags.empty + .withNamespace(js.MemberNamespace.PublicStatic) + .withMutable(true) + val name = js.StringLiteral(info.jsName) + val irTpe = genExposedFieldIRType(sym) + js.JSFieldDef(flags, name, irTpe) + + case kind => + throw new AssertionError(s"unexpected static export kind: $kind") + } + }).toList + } + + /** Generates exported methods and properties for a class. + * + * @param classSym symbol of the class we export for + */ + def genMemberExports(classSym: ClassSymbol): List[js.MemberDef] = { + val classInfo = classSym.info + val allExports = classInfo.memberDenots(takeAllFilter, { (name, buf) => + if (isExportName(name)) + buf ++= classInfo.member(name).alternatives + }) + + val newlyDeclaredExports = if (classSym.superClass == NoSymbol) { + allExports + } else { + allExports.filterNot { denot => + classSym.superClass.info.member(denot.name).hasAltWith(_.info =:= denot.info) + } + } + + val newlyDeclaredExportNames = newlyDeclaredExports.map(_.name.toTermName).toList.distinct + + newlyDeclaredExportNames.map(genMemberExport(classSym, _)) + } + + private def genMemberExport(classSym: ClassSymbol, name: TermName): js.MemberDef = { + /* This used to be `.member(name)`, but it caused #3538, since we were + * sometimes selecting mixin forwarders, whose type history does not go + * far enough back in time to see varargs. We now explicitly exclude + * mixed-in members in addition to bridge methods (the latter are always + * excluded by `.member(name)`). + */ + val alts = classSym + .findMemberNoShadowingBasedOnFlags(name, classSym.appliedRef, required = Method, excluded = Bridge | MixedIn) + .alternatives + + assert(!alts.isEmpty, + em"Ended up with no alternatives for ${classSym.fullName}::$name. " + + em"Original set was ${alts} with types ${alts.map(_.info)}") + + val (jsName, isProp) = exportNameInfo(name) + + // Check if we have a conflicting export of the other kind + val conflicting = classSym.info.member(makeExportName(jsName, !isProp)) + + if (conflicting.exists) { + val kind = if (isProp) "property" else "method" + val conflictingMember = conflicting.alternatives.head.symbol.fullName + val errorPos: SrcPos = alts.map(_.symbol).filter(_.owner == classSym) match { + case Nil => classSym + case altsInClass => altsInClass.minBy(_.span.point) + } + report.error(em"Exported $kind $jsName conflicts with $conflictingMember", errorPos) + } + + genMemberExportOrDispatcher(JSName.Literal(jsName), isProp, alts.map(_.symbol), static = false) + } + def genJSClassDispatchers(classSym: Symbol, dispatchMethodsNames: List[JSName]): List[js.MemberDef] = { dispatchMethodsNames.map(genJSClassDispatcher(classSym, _)) } @@ -566,7 +815,9 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) { def apply(methodSym: Symbol, infoAtElimRepeated: Type, infoAtElimEVT: Type, methodHasDefaultParams: Boolean, paramIndex: Int): ParamSpec = { val isRepeated = infoAtElimRepeated.isRepeatedParam - val info = if (isRepeated) infoAtElimRepeated.repeatedToSingle else infoAtElimEVT + val info = + if (isRepeated) atPhase(elimRepeatedPhase)(infoAtElimRepeated.repeatedToSingle.widenDealias) + else infoAtElimEVT val hasDefault = methodHasDefaultParams && defaultGetterDenot(methodSym, paramIndex).exists new ParamSpec(info, isRepeated, hasDefault) } diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0b8cfa01f369..402e0d6d7e14 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -793,10 +793,8 @@ object desugar { val originalVparamsIt = originalVparamss.iterator.flatten derivedVparamss match { case first :: rest => - // Annotations on the class _value_ parameters are not set on the parameter accessors - def mods(vdef: ValDef) = vdef.mods.withAnnotations(Nil) - first.map(_.withMods(mods(originalVparamsIt.next()) | caseAccessor)) ++ - rest.flatten.map(_.withMods(mods(originalVparamsIt.next()))) + first.map(_.withMods(originalVparamsIt.next().mods | caseAccessor)) ++ + rest.flatten.map(_.withMods(originalVparamsIt.next().mods)) case _ => Nil } diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index a407c25feada..25a116009f47 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -933,6 +933,7 @@ class Definitions { @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val FieldMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.field") @tu lazy val GetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.getter") + @tu lazy val ParamMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.param") @tu lazy val SetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.setter") @tu lazy val ShowAsInfixAnnot: ClassSymbol = requiredClass("scala.annotation.showAsInfix") @tu lazy val FunctionalInterfaceAnnot: ClassSymbol = requiredClass("java.lang.FunctionalInterface") @@ -940,6 +941,10 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") + // A list of meta-annotations that are relevant for fields and accessors + @tu lazy val FieldAccessorMetaAnnots: Set[Symbol] = + Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot) + // A list of annotations that are commonly used to indicate that a field/method argument or return // type is not null. These annotations are used by the nullification logic in JavaNullInterop to // improve the precision of type nullification. diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 9643617c3631..48ff13852156 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1889,10 +1889,20 @@ object SymDenotations { * The elements of the returned pre-denotation all have existing symbols. */ final def nonPrivateMembersNamed(name: Name)(using Context): PreDenotation = + membersNamedNoShadowingBasedOnFlags(name, excluded = Private) + + /** All members of this class that have the given name and match the + * `required` and `excluded` flag sets; members excluded based on the + * flag sets do not shadow inherited members that would not be excluded. + * + * The elements of the returned pre-denotation all have existing symbols. + */ + final def membersNamedNoShadowingBasedOnFlags(name: Name, + required: FlagSet = EmptyFlags, excluded: FlagSet = EmptyFlags)(using Context): PreDenotation = val mbr = membersNamed(name) - val nonPrivate = mbr.filterWithFlags(EmptyFlags, Private) - if nonPrivate eq mbr then mbr - else addInherited(name, nonPrivate) + val filtered = mbr.filterWithFlags(required, excluded) + if filtered eq mbr then mbr + else addInherited(name, filtered, required, excluded) private[core] def computeMembersNamed(name: Name)(using Context): PreDenotation = Stats.record("computeMembersNamed") @@ -1901,13 +1911,14 @@ object SymDenotations { println(s"$this.member($name), ownDenots = $ownDenots") addInherited(name, ownDenots) - private def addInherited(name: Name, ownDenots: PreDenotation)(using Context): PreDenotation = + private def addInherited(name: Name, ownDenots: PreDenotation, + required: FlagSet = EmptyFlags, excluded: FlagSet = EmptyFlags)(using Context): PreDenotation = def collect(denots: PreDenotation, parents: List[Type]): PreDenotation = parents match case p :: ps => val denots1 = collect(denots, ps) p.classSymbol.denot match case parentd: ClassDenotation => - val inherited = parentd.nonPrivateMembersNamed(name) + val inherited = parentd.membersNamedNoShadowingBasedOnFlags(name, required, excluded | Private) denots1.union(inherited.mapInherited(ownDenots, denots1, thisType)) case _ => denots1 @@ -1919,6 +1930,10 @@ object SymDenotations { val raw = if excluded.is(Private) then nonPrivateMembersNamed(name) else membersNamed(name) raw.filterWithFlags(required, excluded).asSeenFrom(pre).toDenot(pre) + final def findMemberNoShadowingBasedOnFlags(name: Name, pre: Type, + required: FlagSet = EmptyFlags, excluded: FlagSet = EmptyFlags)(using Context): Denotation = + membersNamedNoShadowingBasedOnFlags(name, required, excluded).asSeenFrom(pre).toDenot(pre) + /** Compute tp.baseType(this) */ final def baseTypeOf(tp: Type)(using Context): Type = { val btrCache = baseTypeCache diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 5ed99adaff37..34bd406d69ba 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -147,6 +147,14 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase case tree: ValOrDefDef if !tree.symbol.is(Synthetic) => checkInferredWellFormed(tree.tpt) val sym = tree.symbol + if sym.is(Method) then + if sym.isSetter then + removeUnwantedAnnotations(sym, defn.SetterMetaAnnot, NoSymbol, keepIfNoRelevantAnnot = false) + else + if sym.is(Param) then + removeUnwantedAnnotations(sym, defn.ParamMetaAnnot, NoSymbol, keepIfNoRelevantAnnot = true) + else + removeUnwantedAnnotations(sym, defn.GetterMetaAnnot, defn.FieldMetaAnnot, keepIfNoRelevantAnnot = !sym.is(ParamAccessor)) if sym.isScala2Macro && !ctx.settings.XignoreScala2Macros.value then if !sym.owner.unforcedDecls.exists(p => !p.isScala2Macro && p.name == sym.name && p.signature == sym.signature) // Allow scala.reflect.materializeClassTag to be able to compile scala/reflect/package.scala @@ -168,6 +176,18 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase => Checking.checkAppliedTypesIn(tree) case _ => + private def removeUnwantedAnnotations(sym: Symbol, metaAnnotSym: Symbol, + metaAnnotSymBackup: Symbol, keepIfNoRelevantAnnot: Boolean)(using Context): Unit = + def shouldKeep(annot: Annotation): Boolean = + val annotSym = annot.symbol + annotSym.hasAnnotation(metaAnnotSym) + || annotSym.hasAnnotation(metaAnnotSymBackup) + || (keepIfNoRelevantAnnot && { + !annotSym.annotations.exists(metaAnnot => defn.FieldAccessorMetaAnnots.contains(metaAnnot.symbol)) + }) + if sym.annotations.nonEmpty then + sym.filterAnnotations(shouldKeep(_)) + private def transformSelect(tree: Select, targs: List[Tree])(using Context): Tree = { val qual = tree.qualifier qual.symbol.moduleClass.denot match { diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/JSExportUtils.scala b/compiler/src/dotty/tools/dotc/transform/sjs/JSExportUtils.scala new file mode 100644 index 000000000000..6825ddcc1fef --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/sjs/JSExportUtils.scala @@ -0,0 +1,53 @@ +package dotty.tools.dotc +package transform +package sjs + +import core._ +import util.SrcPos +import Annotations._ +import Constants._ +import Contexts._ +import Decorators._ +import DenotTransformers._ +import Flags._ +import NameKinds.DefaultGetterName +import NameOps._ +import Names._ +import Phases._ +import Scopes._ +import StdNames._ +import Symbols._ +import SymDenotations._ +import SymUtils._ +import ast.Trees._ +import Types._ + +import dotty.tools.backend.sjs.JSDefinitions.jsdefn + +/** Utilities for JS exports handling. */ +object JSExportUtils { + private final val ExportPrefix = "$js$exported$" + private final val MethodExportPrefix = ExportPrefix + "meth$" + private final val PropExportPrefix = ExportPrefix + "prop$" + + /** Creates a name for an export specification. */ + def makeExportName(jsName: String, isProp: Boolean): TermName = { + val prefix = if (isProp) PropExportPrefix else MethodExportPrefix + termName(prefix + jsName) + } + + /** Is this symbol an export forwarder? */ + def isExportName(name: Name): Boolean = + name.startsWith(ExportPrefix) && !name.is(DefaultGetterName) + + /** Retrieves the originally assigned jsName of this export and whether it is a property. */ + def exportNameInfo(name: Name): (String, Boolean) = { + val nameString = name.toString() + if (nameString.startsWith(MethodExportPrefix)) + (nameString.substring(MethodExportPrefix.length), false) + else if (nameString.startsWith(PropExportPrefix)) + (nameString.substring(PropExportPrefix.length), true) + else + throw new IllegalArgumentException(s"non-exported name passed to jsExportInfo: $name") + } +} diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala index 4a765e2e69dc..fa2eca8c211d 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSExports.scala @@ -3,47 +3,474 @@ package transform package sjs import dotty.tools.dotc.ast.{Trees, tpd, untpd} -import scala.collection.mutable -import core._ -import dotty.tools.dotc.typer.Checking -import dotty.tools.dotc.typer.Inliner -import dotty.tools.dotc.typer.VarianceChecker -import Types._ +import dotty.tools.dotc.core._ +import Annotations._ import Contexts._ -import Names._ -import Flags._ +import Decorators._ +import Denotations._ import DenotTransformers._ +import Flags._ +import Names._ +import NameKinds.DefaultGetterName +import NameOps._ import Phases._ -import SymDenotations._ -import StdNames._ -import Annotations._ -import Trees._ import Scopes._ -import Decorators._ +import StdNames._ import Symbols._ +import SymDenotations._ import SymUtils._ -import NameOps._ +import Trees._ +import Types._ + import reporting._ +import util.Spans.Span import util.SrcPos import dotty.tools.backend.sjs.JSDefinitions.jsdefn +import JSExportUtils._ +import JSSymUtils._ + +import org.scalajs.ir.Names.DefaultModuleID +import org.scalajs.ir.Trees.TopLevelExportDef.isValidTopLevelExportName object PrepJSExports { import tpd._ + import PrepJSInterop.{checkSetterSignature, isJSAny, isPrivateMaybeWithin} + + private sealed abstract class ExportDestination - def registerClassOrModuleExports(sym: Symbol)(using Context): Unit = { - // TODO + private object ExportDestination { + /** Export in the "normal" way: as an instance member, or at the top-level + * for naturally top-level things (classes and modules). + */ + case object Normal extends ExportDestination + + /** Export at the top-level. */ + final case class TopLevel(moduleID: String) extends ExportDestination + + /** Export as a static member of the companion class. */ + case object Static extends ExportDestination } - /** Generate the exporter for the given DefDef - * or ValDef (abstract val in class, val in trait or lazy val; - * these don't get DefDefs until the fields phase) + private final case class ExportInfo(jsName: String, destination: ExportDestination)(val pos: SrcPos) + + /** Checks a class or module class for export. + * + * Note that non-module Scala classes are never actually exported; their constructors are. + * However, the checks are performed on the class when the class is annotated. + */ + def checkClassOrModuleExports(sym: Symbol)(using Context): Unit = { + val exports = exportsOf(sym) + if (exports.nonEmpty) + checkClassOrModuleExports(sym, exports.head.pos) + } + + /** Generate the exporter for the given DefDef or ValDef. * * If this DefDef is a constructor, it is registered to be exported by * GenJSCode instead and no trees are returned. */ def genExportMember(baseSym: Symbol)(using Context): List[Tree] = { - // TODO - Nil + val clsSym = baseSym.owner + + val exports = exportsOf(baseSym) + + // Helper function for errors + def err(msg: String): List[Tree] = { + report.error(msg, exports.head.pos) + Nil + } + + def memType = if (baseSym.isConstructor) "constructor" else "method" + + if (exports.isEmpty) { + Nil + } else if (!hasLegalExportVisibility(baseSym)) { + err(s"You may only export public and protected ${memType}s") + } else if (baseSym.is(Inline)) { + err("You may not export an inline method") + } else if (isJSAny(clsSym)) { + err(s"You may not export a $memType of a subclass of js.Any") + } else if (baseSym.isLocalToBlock) { + err("You may not export a local definition") + } else if (hasIllegalRepeatedParam(baseSym)) { + err(s"In an exported $memType, a *-parameter must come last (through all parameter lists)") + } else if (hasIllegalDefaultParam(baseSym)) { + err(s"In an exported $memType, all parameters with defaults must be at the end") + } else if (baseSym.isConstructor) { + // Constructors do not need an exporter method. We only perform the checks at this phase. + checkClassOrModuleExports(clsSym, exports.head.pos) + Nil + } else { + assert(!baseSym.is(Bridge), s"genExportMember called for bridge symbol $baseSym") + val normalExports = exports.filter(_.destination == ExportDestination.Normal) + normalExports.flatMap(exp => genExportDefs(baseSym, exp.jsName, exp.pos.span)) + } + } + + /** Check a class or module for export. + * + * There are 2 ways that this method can be reached: + * - via `registerClassExports` + * - via `genExportMember` (constructor of Scala class) + */ + private def checkClassOrModuleExports(sym: Symbol, errPos: SrcPos)(using Context): Unit = { + val isMod = sym.is(ModuleClass) + + def err(msg: String): Unit = + report.error(msg, errPos) + + def hasAnyNonPrivateCtor: Boolean = + sym.info.decl(nme.CONSTRUCTOR).hasAltWith(denot => !isPrivateMaybeWithin(denot.symbol)) + + if (sym.is(Trait)) { + err("You may not export a trait") + } else if (sym.hasAnnotation(jsdefn.JSNativeAnnot)) { + err("You may not export a native JS " + (if (isMod) "object" else "class")) + } else if (!hasLegalExportVisibility(sym)) { + err("You may only export public and protected " + (if (isMod) "objects" else "classes")) + } else if (isJSAny(sym.owner)) { + err("You may not export a " + (if (isMod) "object" else "class") + " in a subclass of js.Any") + } else if (sym.isLocalToBlock) { + err("You may not export a local " + (if (isMod) "object" else "class")) + } else if (!sym.isStatic) { + if (isMod) + err("You may not export a nested object") + else + err("You may not export a nested class. Create an exported factory method in the outer class to work around this limitation.") + } else if (sym.is(Abstract, butNot = Trait) && !isJSAny(sym)) { + err("You may not export an abstract class") + } else if (!isMod && !hasAnyNonPrivateCtor) { + /* This test is only relevant for JS classes but doesn't hurt for Scala + * classes as we could not reach it if there were only private + * constructors. + */ + err("You may not export a class that has only private constructors") + } else { + // OK + } + } + + /** Computes the ExportInfos for sym from its annotations. */ + private def exportsOf(sym: Symbol)(using Context): List[ExportInfo] = { + val trgSym = { + def isOwnerScalaClass = !sym.owner.is(ModuleClass) && !isJSAny(sym.owner) + + // For primary Scala class constructors, look on the class itself + if (sym.isPrimaryConstructor && isOwnerScalaClass) sym.owner + else sym + } + + val JSExportAnnot = jsdefn.JSExportAnnot + val JSExportTopLevelAnnot = jsdefn.JSExportTopLevelAnnot + val JSExportStaticAnnot = jsdefn.JSExportStaticAnnot + val JSExportAllAnnot = jsdefn.JSExportAllAnnot + + // Annotations that are directly on the member + val directMemberAnnots = Set[Symbol](JSExportAnnot, JSExportTopLevelAnnot, JSExportStaticAnnot) + val directAnnots = trgSym.annotations.filter(annot => directMemberAnnots.contains(annot.symbol)) + + // Is this a member export (i.e. not a class or module export)? + val isMember = !sym.isClass && !sym.isConstructor + + // Annotations for this member on the whole unit + val unitAnnots = { + if (isMember && sym.isPublic && !sym.is(Synthetic)) + sym.owner.annotations.filter(_.symbol == JSExportAllAnnot) + else + Nil + } + + val allExportInfos = for { + annot <- directAnnots ++ unitAnnots + } yield { + val isExportAll = annot.symbol == JSExportAllAnnot + val isTopLevelExport = annot.symbol == JSExportTopLevelAnnot + val isStaticExport = annot.symbol == JSExportStaticAnnot + val hasExplicitName = annot.arguments.nonEmpty + + val exportPos: SrcPos = if (isExportAll) sym else annot.tree + + assert(!isTopLevelExport || hasExplicitName, + em"Found a top-level export without an explicit name at ${exportPos.sourcePos}") + + val name = { + if (hasExplicitName) { + annot.argumentConstantString(0).getOrElse { + report.error( + s"The argument to ${annot.symbol.name} must be a literal string", + annot.arguments(0)) + "dummy" + } + } else { + sym.defaultJSName + } + } + + val destination = { + if (isTopLevelExport) { + val moduleID = if (annot.arguments.size == 1) { + DefaultModuleID + } else { + annot.argumentConstantString(1).getOrElse { + report.error("moduleID must be a literal string", annot.arguments(1)) + DefaultModuleID + } + } + + ExportDestination.TopLevel(moduleID) + } else if (isStaticExport) { + ExportDestination.Static + } else { + ExportDestination.Normal + } + } + + // Enforce proper setter signature + if (sym.isJSSetter) + checkSetterSignature(sym, exportPos, exported = true) + + // Enforce no __ in name + if (!isTopLevelExport && name.contains("__")) + report.error("An exported name may not contain a double underscore (`__`)", exportPos) + + /* Illegal function application exports, i.e., method named 'apply' + * without an explicit export name. + */ + if (isMember && !hasExplicitName && sym.name == nme.apply) { + destination match { + case ExportDestination.Normal => + def shouldBeTolerated = { + isExportAll && directAnnots.exists { annot => + annot.symbol == JSExportAnnot && + annot.arguments.nonEmpty && + annot.argumentConstantString(0).contains("apply") + } + } + + // Don't allow apply without explicit name + if (!shouldBeTolerated) { + report.error( + "A member cannot be exported to function application. " + + "Add @JSExport(\"apply\") to export under the name apply.", + exportPos) + } + + case _: ExportDestination.TopLevel => + throw new AssertionError( + em"Found a top-level export without an explicit name at ${exportPos.sourcePos}") + + case ExportDestination.Static => + report.error( + "A member cannot be exported to function application as static. " + + "Use @JSExportStatic(\"apply\") to export it under the name 'apply'.", + exportPos) + } + } + + val symOwner = + if (sym.isConstructor) sym.owner.owner + else sym.owner + + // Destination-specific restrictions + destination match { + case ExportDestination.Normal => + // Make sure we do not override the default export of toString + def isIllegalToString = { + isMember && name == "toString" && sym.name != nme.toString_ && + sym.info.paramInfoss.forall(_.isEmpty) && !sym.isJSGetter + } + if (isIllegalToString) { + report.error( + "You may not export a zero-argument method named other than 'toString' under the name 'toString'", + exportPos) + } + + // Disallow @JSExport at the top-level, as well as on objects and classes + if (symOwner.is(Package) || symOwner.isPackageObject) { + report.error("@JSExport is forbidden on top-level definitions. Use @JSExportTopLevel instead.", exportPos) + } else if (!isMember && !sym.is(Trait)) { + report.error( + "@JSExport is forbidden on objects and classes. Use @JSExport'ed factory methods instead.", + exportPos) + } + + case _: ExportDestination.TopLevel => + if (sym.is(Lazy)) + report.error("You may not export a lazy val to the top level", exportPos) + else if (!sym.is(Accessor) && sym.isTerm && sym.isJSProperty) + report.error("You may not export a getter or a setter to the top level", exportPos) + + /* Disallow non-static methods. + * Note: Non-static classes have more specific error messages in checkClassOrModuleExports. + */ + if (sym.isTerm && (!symOwner.isStatic || !symOwner.is(ModuleClass))) + report.error("Only static objects may export their members to the top level", exportPos) + + // The top-level name must be a valid JS identifier + if (!isValidTopLevelExportName(name)) + report.error("The top-level export name must be a valid JavaScript identifier name", exportPos) + + case ExportDestination.Static => + def companionIsNonNativeJSClass: Boolean = { + val companion = symOwner.companionClass + companion != NoSymbol + && !companion.is(Trait) + && isJSAny(companion) + && !companion.hasAnnotation(jsdefn.JSNativeAnnot) + } + + if (!symOwner.isStatic || !symOwner.is(ModuleClass) || !companionIsNonNativeJSClass) { + report.error( + "Only a static object whose companion class is a non-native JS class may export its members as static.", + exportPos) + } + + if (isMember) { + if (sym.is(Lazy)) + report.error("You may not export a lazy val as static", exportPos) + } else { + if (sym.is(Trait)) + report.error("You may not export a trait as static.", exportPos) + else + report.error("Implementation restriction: cannot export a class or object as static", exportPos) + } + } + + ExportInfo(name, destination)(exportPos) + } + + allExportInfos.filter(_.destination == ExportDestination.Normal) + .groupBy(_.jsName) + .filter { case (jsName, group) => + if (jsName == "apply" && group.size == 2) + // @JSExportAll and single @JSExport("apply") should not be warned. + !unitAnnots.exists(_.symbol == JSExportAllAnnot) + else + group.size > 1 + } + .foreach(_ => report.warning("Found duplicate @JSExport", sym)) + + /* Make sure that no field is exported *twice* as static, nor both as + * static and as top-level (it is possible to export a field several times + * as top-level, though). + */ + if (!sym.is(Method)) { + for (firstStatic <- allExportInfos.find(_.destination == ExportDestination.Static)) { + for (duplicate <- allExportInfos) { + duplicate.destination match { + case ExportDestination.Normal => + // OK + case ExportDestination.Static => + if (duplicate ne firstStatic) { + report.error( + "Fields (val or var) cannot be exported as static more than once", + duplicate.pos) + } + case _: ExportDestination.TopLevel => + report.error( + "Fields (val or var) cannot be exported both as static and at the top-level", + duplicate.pos) + } + } + } + } + + allExportInfos.distinct + } + + /** Generates an exporter for a DefDef including default parameter methods. */ + private def genExportDefs(defSym: Symbol, jsName: String, span: Span)(using Context): List[Tree] = { + val clsSym = defSym.owner.asClass + + // Create symbol for new method + val name = makeExportName(jsName, !defSym.is(Method) || defSym.isJSProperty) + val flags = (defSym.flags | Method | Synthetic) + &~ (Deferred | Accessor | ParamAccessor | CaseAccessor | Mutable | Lazy | Override) + val info = + if (defSym.isConstructor) defSym.info + else if (defSym.is(Method)) finalResultTypeToAny(defSym.info) + else ExprType(defn.AnyType) + val expSym = newSymbol(clsSym, name, flags, info, defSym.privateWithin, span).entered + + // Construct exporter DefDef tree + val exporter = genProxyDefDef(clsSym, defSym, expSym, span) + + // Construct exporters for default getters + val defaultGetters = if (!defSym.hasDefaultParams) { + Nil + } else { + for { + (param, i) <- defSym.paramSymss.flatten.zipWithIndex + if param.is(HasDefault) + } yield { + genExportDefaultGetter(clsSym, defSym, expSym, i, span) + } + } + + exporter :: defaultGetters + } + + private def genExportDefaultGetter(clsSym: ClassSymbol, trgMethod: Symbol, + exporter: Symbol, paramPos: Int, span: Span)(using Context): Tree = { + + // Get default getter method we'll copy + val trgGetterDenot = defaultGetterDenot(clsSym, trgMethod, paramPos) + + assert(trgGetterDenot.exists, em"Cannot find default getter for param $paramPos of $trgMethod") + + // Although the following must be true in a correct program, we cannot + // assert, since a graceful failure message is only generated later + if (!trgGetterDenot.isOverloaded) { + val trgGetter = trgGetterDenot.symbol + val expGetterName = DefaultGetterName(exporter.name.asTermName, paramPos) + val expGetter = newSymbol(clsSym, expGetterName, trgGetter.flags, trgGetter.info, + trgGetter.privateWithin, coord = span).entered + genProxyDefDef(clsSym, trgGetter, expGetter, span) + } else { + EmptyTree + } + } + + private def defaultGetterDenot(targetSym: Symbol, sym: Symbol, paramIndex: Int)(using Context): Denotation = + targetSym.info.member(DefaultGetterName(sym.name.asTermName, paramIndex)) + + /** generate a DefDef tree (from [[proxySym]]) that calls [[trgSym]] */ + private def genProxyDefDef(clsSym: ClassSymbol, trgSym: Symbol, + proxySym: TermSymbol, span: Span)(using Context): Tree = { + + polyDefDef(proxySym, { targs => argss => + This(clsSym).select(trgSym).appliedToTypes(targs).appliedToArgss(argss) + }).withSpan(span) + } + + /** Changes the final result type of a type `tpe` to Any. */ + private def finalResultTypeToAny(tpe: Type)(using Context): Type = tpe match { + case tpe: MethodType => + MethodType(tpe.paramNames, tpe.paramInfos, finalResultTypeToAny(tpe.resultType)) + case _: ExprType => + ExprType(defn.AnyType) + case tpe: PolyType => + PolyType(tpe.paramNames)( + x => tpe.paramInfos.mapConserve(_.subst(tpe, x).bounds), + x => finalResultTypeToAny(tpe.resultType.subst(tpe, x))) + case _ => + defn.AnyType + } + + /** Whether the given symbol has a visibility that allows exporting */ + private def hasLegalExportVisibility(sym: Symbol)(using Context): Boolean = + sym.isPublic || sym.is(Protected, butNot = Local) + + /** Checks whether this type has a repeated parameter elsewhere than at the end of all the params. */ + private def hasIllegalRepeatedParam(sym: Symbol)(using Context): Boolean = { + val paramInfos = sym.info.paramInfoss.flatten + paramInfos.nonEmpty && paramInfos.init.exists(_.isRepeatedParam) + } + + /** Checks whether there are default parameters not at the end of the flattened parameter list. */ + private def hasIllegalDefaultParam(sym: Symbol)(using Context): Boolean = { + sym.hasDefaultParams + && sym.paramSymss.flatten.reverse.dropWhile(_.is(HasDefault)).exists(_.is(HasDefault)) } } diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 55e6d145dfa2..afc6e9d5d479 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -139,12 +139,14 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP } checkJSNameAnnots(sym) + constFoldJSExportTopLevelAndStaticAnnotations(sym) markExposedIfRequired(tree.symbol) tree match { case tree: TypeDef if tree.isClassDef => - registerClassOrModuleExports(sym) + checkClassOrModuleExports(sym) + if (isJSAny(sym)) transformJSClassDef(tree) else @@ -154,14 +156,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP super.transform(tree) case tree: ValOrDefDef => - /* Prepare exports for methods, local defs and local variables. - * Avoid *fields* (non-local non-method) because they all have a - * corresponding getter, which is the one that handles exports. - * (Note that local-to-block can never have exports, but the error - * messages for that are handled by genExportMember). - */ - if (sym.is(Method) || sym.isLocalToBlock) - exporters.getOrElseUpdate(sym.owner, mutable.ListBuffer.empty) ++= genExportMember(sym) + // Prepare exports + exporters.getOrElseUpdate(sym.owner, mutable.ListBuffer.empty) ++= genExportMember(sym) if (sym.isLocalToBlock) super.transform(tree) @@ -231,6 +227,8 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP exporters.get(clsSym).fold { transformedTree } { exports => + checkNoDoubleDeclaration(clsSym) + cpy.Template(transformedTree)( transformedTree.constr, transformedTree.parents, @@ -836,6 +834,45 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP } } + /** Constant-folds arguments to `@JSExportTopLevel` and `@JSExportStatic`. + * + * Unlike scalac, dotc does not constant-fold expressions in annotations. + * Our back-end needs to have access to the arguments to those two + * annotations as literal strings, so we specifically constant-fold them + * here. + */ + private def constFoldJSExportTopLevelAndStaticAnnotations(sym: Symbol)(using Context): Unit = { + val annots = sym.annotations + val newAnnots = annots.mapConserve { annot => + if (annot.symbol == jsdefn.JSExportTopLevelAnnot || annot.symbol == jsdefn.JSExportStaticAnnot) { + annot.tree match { + case app @ Apply(fun, args) => + val newArgs = args.mapConserve { arg => + arg match { + case _: Literal => + arg + case _ => + arg.tpe.widenTermRefExpr.normalized match { + case ConstantType(c) => Literal(c).withSpan(arg.span) + case _ => arg // PrepJSExports will emit an error for those cases + } + } + } + if (newArgs eq args) + annot + else + Annotation(cpy.Apply(app)(fun, newArgs)) + case _ => + annot + } + } else { + annot + } + } + if (newAnnots ne annots) + sym.annotations = newAnnots + } + /** Mark the symbol as exposed if it is a non-private term member of a * non-native JS class. * diff --git a/project/Build.scala b/project/Build.scala index 3754004352b7..c63aa68ff2b1 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1078,7 +1078,7 @@ object Build { scalaJSModuleInitializers in Test ++= build.TestSuiteLinkerOptions.moduleInitializers, // Perform Ycheck after the Scala.js-specific transformation phases - scalacOptions += "-Ycheck:explicitJSClasses,addLocalJSFakeNews", + scalacOptions += "-Ycheck:prepjsinterop,explicitJSClasses,addLocalJSFakeNews", jsEnvInput in Test := { val resourceDir = fetchScalaJSSource.value / "test-suite/js/src/test/resources" @@ -1124,10 +1124,6 @@ object Build { // Putting them here instead of above makes sure that we do not regress on compilation+linking. Test / testOptions += Tests.Filter { name => !Set[String]( - "org.scalajs.testsuite.jsinterop.AsyncTest", // needs JS exports in PromiseMock.scala - "org.scalajs.testsuite.jsinterop.ExportsTest", // JS exports - "org.scalajs.testsuite.jsinterop.JSExportStaticTest", // JS exports - // Not investigated so far "org.scalajs.testsuite.junit.JUnitAbstractClassTestCheck", "org.scalajs.testsuite.junit.JUnitNamesTestCheck", diff --git a/tests/neg-scalajs/jsexport-bad-tostring.scala b/tests/neg-scalajs/jsexport-bad-tostring.scala new file mode 100644 index 000000000000..53853b1f3a65 --- /dev/null +++ b/tests/neg-scalajs/jsexport-bad-tostring.scala @@ -0,0 +1,12 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport("toString") // error + def a(): Int = 5 +} + +class B { + @JSExport("toString") // ok + def a(x: Int): Int = x + 1 +} diff --git a/tests/neg-scalajs/jsexport-conflict-method-prop.scala b/tests/neg-scalajs/jsexport-conflict-method-prop.scala new file mode 100644 index 000000000000..1b879024b9d8 --- /dev/null +++ b/tests/neg-scalajs/jsexport-conflict-method-prop.scala @@ -0,0 +1,23 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A1 { + @JSExport("a") // error + def bar(): Int = 2 + + @JSExport("a") // error + val foo = 1 +} + +class B1 { + @JSExport("a") + def bar(): Int = 2 +} + +class B2 extends B1 { + @JSExport("a") // error + def foo_=(x: Int): Unit = () + + @JSExport("a") + val foo = 1 +} diff --git a/tests/neg-scalajs/jsexport-conflicts.scala b/tests/neg-scalajs/jsexport-conflicts.scala new file mode 100644 index 000000000000..1b9aa5245bfe --- /dev/null +++ b/tests/neg-scalajs/jsexport-conflicts.scala @@ -0,0 +1,40 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport + def rtType(x: js.Any): js.Any = x + + @JSExport // error + def rtType(x: js.Dynamic): js.Dynamic = x +} + +class B { + @JSExport + def foo(x: Int)(ys: Int*): Int = x + + @JSExport // error + def foo(x: Int*): Seq[Int] = x +} + +class C { + @JSExport + def foo(x: Int = 1): Int = x + @JSExport // error + def foo(x: String*): Seq[String] = x +} + +class D { + @JSExport + def foo(x: Double, y: String)(z: Int = 1): Double = x + @JSExport // error + def foo(x: Double, y: String)(z: String*): Double = x +} + +class E { + @JSExport + def a(x: scala.scalajs.js.Any): Int = 1 + + @JSExport // error + def a(x: Any): Int = 2 +} diff --git a/tests/neg-scalajs/jsexport-double-definition.scala b/tests/neg-scalajs/jsexport-double-definition.scala new file mode 100644 index 000000000000..f9ec73439b23 --- /dev/null +++ b/tests/neg-scalajs/jsexport-double-definition.scala @@ -0,0 +1,19 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport("value") // error + def hello: String = "foo" + + @JSExport("value") + def world: String = "bar" +} + +class B { + class Box[T](val x: T) + + @JSExport // error + def ub(x: Box[String]): String = x.x + @JSExport + def ub(x: Box[Int]): Int = x.x +} diff --git a/tests/neg-scalajs/jsexport-double-underscore.scala b/tests/neg-scalajs/jsexport-double-underscore.scala new file mode 100644 index 000000000000..35d31713d9c6 --- /dev/null +++ b/tests/neg-scalajs/jsexport-double-underscore.scala @@ -0,0 +1,18 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport(name = "__") // error + def foo: Int = 1 + + @JSExport // error + def bar__(x: Int): Int = x +} + +object B { + @JSExportTopLevel(name = "__") // ok + val foo: Int = 1 + + @JSExportTopLevel("bar__") // ok + def bar(x: Int): Int = x +} diff --git a/tests/neg-scalajs/jsexport-implicit-apply.scala b/tests/neg-scalajs/jsexport-implicit-apply.scala new file mode 100644 index 000000000000..80400fa6556e --- /dev/null +++ b/tests/neg-scalajs/jsexport-implicit-apply.scala @@ -0,0 +1,24 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport // error + def apply(): Int = 1 +} + +@JSExportAll +class B { + def apply(): Int = 1 // error +} + +@JSExportAll +class C { + @JSExport("foo") + def apply(): Int = 1 // error +} + +@JSExportAll +class D { + @JSExport("apply") + def apply(): Int = 1 // ok +} diff --git a/tests/neg-scalajs/jsexport-js-member.scala b/tests/neg-scalajs/jsexport-js-member.scala new file mode 100644 index 000000000000..4178a54df88d --- /dev/null +++ b/tests/neg-scalajs/jsexport-js-member.scala @@ -0,0 +1,14 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +@js.native +@JSGlobal +class A extends js.Object { + @JSExport // error + def foo: Int = js.native +} + +class B extends js.Object { + @JSExport // error + def foo: Int = js.native +} diff --git a/tests/neg-scalajs/jsexport-local.scala b/tests/neg-scalajs/jsexport-local.scala new file mode 100644 index 000000000000..84c830c4ca5c --- /dev/null +++ b/tests/neg-scalajs/jsexport-local.scala @@ -0,0 +1,27 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + def method(): Unit = { + @JSExport // error + class A + + @JSExport // error + class B extends js.Object + + @JSExport // error + object C + + @JSExport // error + object D extends js.Object + + @JSExport // error + def e = 1 + + @JSExport // error + val f = 1 + + @JSExport // error + var g = 1 + } +} diff --git a/tests/neg-scalajs/jsexport-misplaced-default-or-vararg.scala b/tests/neg-scalajs/jsexport-misplaced-default-or-vararg.scala new file mode 100644 index 000000000000..eea2d0d373fe --- /dev/null +++ b/tests/neg-scalajs/jsexport-misplaced-default-or-vararg.scala @@ -0,0 +1,12 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport // error + def method(x: Int = 1)(y: String): Int = 1 +} + +class B { + @JSExport // error + def method(xs: Int*)(ys: String): Int = 1 +} diff --git a/tests/neg-scalajs/jsexport-non-public.scala b/tests/neg-scalajs/jsexport-non-public.scala new file mode 100644 index 000000000000..f7e2908b8c50 --- /dev/null +++ b/tests/neg-scalajs/jsexport-non-public.scala @@ -0,0 +1,57 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object Container { + + // Classes + + @JSExport // error + private class A1 + + @JSExport // error + protected[this] class A2 + + @JSExport // error + private[Container] class A3 + + @JSExport // error + private class A4 extends js.Object + + @JSExport // error + protected[this] class A5 extends js.Object + + @JSExport // error + private[Container] class A6 extends js.Object + + // Objects + + @JSExport // error + private object B1 + + @JSExport // error + protected[this] object B2 + + @JSExport // error + private[Container] object B3 + + @JSExport // error + private object B4 extends js.Object + + @JSExport // error + protected[this] object B5 extends js.Object + + @JSExport // error + private[Container] object B6 extends js.Object + + // Vals and defs + + @JSExport // error + private val c1: Int = 5 + + @JSExport // error + protected[this] var c2: Int = 5 + + @JSExport // error + private[Container] def x(x: Int): Int = 5 + +} diff --git a/tests/neg-scalajs/jsexport-on-non-toplevel-class-object.scala b/tests/neg-scalajs/jsexport-on-non-toplevel-class-object.scala new file mode 100644 index 000000000000..abb4c383e814 --- /dev/null +++ b/tests/neg-scalajs/jsexport-on-non-toplevel-class-object.scala @@ -0,0 +1,30 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport // error + class A1 { + @JSExport // error + def this(x: Int) = this() + } + + @JSExport // error + class A2 extends js.Object + + @JSExport // error + object A3 + + @JSExport // error + object A4 extends js.Object +} + +object B { + @JSExport // error + class B1 { + @JSExport // error + def this(x: Int) = this() + } + + @JSExport // error + class B2 extends js.Object +} diff --git a/tests/neg-scalajs/jsexport-on-toplevel.scala b/tests/neg-scalajs/jsexport-on-toplevel.scala new file mode 100644 index 000000000000..ca84e2766472 --- /dev/null +++ b/tests/neg-scalajs/jsexport-on-toplevel.scala @@ -0,0 +1,32 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +@JSExport // error +class A1 +@JSExport("A2Named") // error +class A2 + +@JSExport // error +object B1 +@JSExport("B2Named") // error +object B2 + +@JSExport // error +val c1: Int = 5 +@JSExport("c2Named") // error +val c2: Int = 5 + +@JSExport // error +def d1(x: Int): Int = x + 1 +@JSExport("d2Named") // error +def d2(x: Int): Int = x + 1 + +@JSExport // error +def e1: Int = 5 +@JSExport("e2Named") // error +def e2: Int = 5 + +@JSExport // error +def f1_=(x: Int): Unit = () +@JSExport("f2Named") // error +def f2_=(x: Int): Unit = () diff --git a/tests/neg-scalajs/jsexport-setter-with-bad-setter-sig.scala b/tests/neg-scalajs/jsexport-setter-with-bad-setter-sig.scala new file mode 100644 index 000000000000..f98bd56c9a26 --- /dev/null +++ b/tests/neg-scalajs/jsexport-setter-with-bad-setter-sig.scala @@ -0,0 +1,31 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + @JSExport // error + def badParamList_=(x: Int, y: Int): Unit = () + + @JSExport // error + def badResultType_=(x: Int): String = "string" + + @JSExport // error + def varArgs_=(x: Int*): Unit = () + + @JSExport // error + def defaultParam_=(x: Int = 1): Unit = () +} + +class B extends js.Object +object B { + @JSExportStatic // error + def badParamList_=(x: Int, y: Int): Unit = () + + @JSExportStatic // error + def badResultType_=(x: Int): String = "string" + + @JSExportStatic // error + def varArgs_=(x: Int*): Unit = () + + @JSExportStatic // error + def defaultParam_=(x: Int = 1): Unit = () +} diff --git a/tests/neg-scalajs/jsexportall-bad-names.scala b/tests/neg-scalajs/jsexportall-bad-names.scala new file mode 100644 index 000000000000..7e6b8350beda --- /dev/null +++ b/tests/neg-scalajs/jsexportall-bad-names.scala @@ -0,0 +1,8 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +@JSExportAll +class A { + val __f = 1 // error + def a_= = 2 // error +} diff --git a/tests/neg-scalajs/jsexportany-non-literal-arguments.scala b/tests/neg-scalajs/jsexportany-non-literal-arguments.scala new file mode 100644 index 000000000000..199073f0f393 --- /dev/null +++ b/tests/neg-scalajs/jsexportany-non-literal-arguments.scala @@ -0,0 +1,36 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object Names { + val a = "Hello" + final val b = "World" +} + +class A { + @JSExport(Names.a) // error + def foo: Int = 1 + @JSExport(Names.b) // ok + def bar: Int = 1 +} + +object B { + @JSExportTopLevel("foo", Names.a) // error + def foo(): Int = 1 + @JSExportTopLevel("bar", Names.b) // ok + def bar(): Int = 1 +} + +object C { + @JSExportTopLevel(Names.a, "foo") // error + def foo(): Int = 1 + @JSExportTopLevel(Names.b, "bar") // ok + def bar(): Int = 1 +} + +class D extends js.Object +object D { + @JSExportStatic(Names.a) // error + def foo(): Int = 1 + @JSExportStatic(Names.b) // ok + def bar(): Int = 1 +} diff --git a/tests/neg-scalajs/jsexportstatic-after-stat-or-non-static-field.scala b/tests/neg-scalajs/jsexportstatic-after-stat-or-non-static-field.scala new file mode 100644 index 000000000000..d6d956d3cdf5 --- /dev/null +++ b/tests/neg-scalajs/jsexportstatic-after-stat-or-non-static-field.scala @@ -0,0 +1,106 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class AfterVal extends js.Object + +object AfterVal { + val a: Int = 1 + + // --- + + @JSExportStatic + val b: Int = 1 // error + + @JSExportStatic + var c: Int = 1 // error + + @JSExportStatic + def d: Int = 1 + + @JSExportStatic + def d_=(v: Int): Unit = () + + @JSExportStatic + def e(): Int = 1 +} + +class AfterVar extends js.Object + +object AfterVar { + var a: Int = 1 + + // --- + + @JSExportStatic + val b: Int = 1 // error + + @JSExportStatic + var c: Int = 1 // error + + @JSExportStatic + def d: Int = 1 + + @JSExportStatic + def d_=(v: Int): Unit = () + + @JSExportStatic + def e(): Int = 1 +} + +class AfterStat extends js.Object + +object AfterStat { + val a: Int = 1 + + // --- + + @JSExportStatic + val b: Int = 1 // error + + @JSExportStatic + var c: Int = 1 // error + + @JSExportStatic + def d: Int = 1 + + @JSExportStatic + def d_=(v: Int): Unit = () + + @JSExportStatic + def e(): Int = 1 +} + +class OthersValid extends js.Object + +object OthersValid { + @JSExportStatic val a1: Int = 1 + @JSExportStatic var a2: Int = 1 + lazy val a3: Int = 1 + def a4: Int = 1 + def a4_=(v: Int): Unit = () + def a5(): Int = 1 + @JSExportStatic def a6: Int = 1 + @JSExportStatic def a6_=(v: Int): Unit = () + @JSExportStatic def a7(): Int = 1 + class A8 + object A9 + trait A10 + type A11 = Int + + // --- + + @JSExportStatic + val b: Int = 1 + + @JSExportStatic + var c: Int = 1 + + @JSExportStatic + def d: Int = 1 + + @JSExportStatic + def d_=(v: Int): Unit = () + + @JSExportStatic + def e(): Int = 1 +} diff --git a/tests/neg-scalajs/jsexportstatic-conflicts.scala b/tests/neg-scalajs/jsexportstatic-conflicts.scala new file mode 100644 index 000000000000..14974ec92bfc --- /dev/null +++ b/tests/neg-scalajs/jsexportstatic-conflicts.scala @@ -0,0 +1,92 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A extends js.Object +object A { + @JSExportStatic + def foo(x: Int): Int = x + + @JSExportStatic("foo") + def bar(x: Int): Int = x + 1 // error +} + +class B extends js.Object +object B { + @JSExportStatic + def foo: Int = 1 + + @JSExportStatic("foo") + def bar: Int = 2 // error +} + +class C extends js.Object +object C { + @JSExportStatic + def foo_=(v: Int): Unit = () + + @JSExportStatic("foo") + def bar_=(v: Int): Unit = () // error +} + +class D extends js.Object +object D { + @JSExportStatic + val a: Int = 1 + + @JSExportStatic("a") // error + var b: Int = 1 +} + +class E extends js.Object +object E { + @JSExportStatic + val a: Int = 1 + + @JSExportStatic("a") // error + def b(x: Int): Int = x + 1 +} + +class F extends js.Object +object F { + @JSExportStatic + def a(x: Int): Int = x + 1 + + @JSExportStatic("a") // error + val b: Int = 1 +} + +class G extends js.Object +object G { + @JSExportStatic + val a: Int = 1 + + @JSExportStatic("a") // error + def b: Int = 2 +} + +class H extends js.Object +object H { + @JSExportStatic + def a: Int = 1 + + @JSExportStatic("a") // error + val b: Int = 2 +} + +class I extends js.Object +object I { + @JSExportStatic + def a: Int = 1 + + @JSExportStatic("a") // error + def b(x: Int): Int = x + 1 +} + +class J extends js.Object +object J { + @JSExportStatic + def a(x: Int): Int = x + 1 + + @JSExportStatic("a") // error + def b: Int = 1 +} diff --git a/tests/neg-scalajs/jsexportstatic-twice-same-field.scala b/tests/neg-scalajs/jsexportstatic-twice-same-field.scala new file mode 100644 index 000000000000..8764ef5a07b9 --- /dev/null +++ b/tests/neg-scalajs/jsexportstatic-twice-same-field.scala @@ -0,0 +1,26 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class StaticContainer extends js.Object + +object StaticContainer { + // Twice as static + + @JSExportStatic // error + @JSExportStatic("a1") + val a: Int = 1 + + @JSExportStatic // error + @JSExportStatic("b1") + var b: Int = 1 + + // Once as static and once as top-level + + @JSExportStatic + @JSExportTopLevel("c1") // error + val c: Int = 1 + + @JSExportStatic + @JSExportTopLevel("d1") // error + var d: Int = 1 +} diff --git a/tests/neg-scalajs/jsexportstatic-wrong-kind.scala b/tests/neg-scalajs/jsexportstatic-wrong-kind.scala new file mode 100644 index 000000000000..fdf4d47b2380 --- /dev/null +++ b/tests/neg-scalajs/jsexportstatic-wrong-kind.scala @@ -0,0 +1,23 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class StaticContainer extends js.Object + +object StaticContainer { + @JSExportStatic // error + object A + + @JSExportStatic // error + trait B + + @JSExportStatic // error + class C + + class D { + @JSExportStatic // error + def this(x: Int) = this() + } + + @JSExportStatic // error + lazy val e: Int = 1 +} diff --git a/tests/neg-scalajs/jsexportstatic-wrong-place.scala b/tests/neg-scalajs/jsexportstatic-wrong-place.scala new file mode 100644 index 000000000000..c1b649cf39bd --- /dev/null +++ b/tests/neg-scalajs/jsexportstatic-wrong-place.scala @@ -0,0 +1,50 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + class A1 extends js.Object + + object A1 { + @JSExportStatic // error + def a(): Unit = () + } +} + +class B extends js.Object + +object B extends js.Object { + @JSExportStatic // error + def a(): Unit = () +} + +class C extends js.Object + +@js.native +@JSGlobal("Dummy") +object C extends js.Object { + @JSExportStatic // error + def a(): Unit = js.native +} + +class D + +object D { + @JSExportStatic // error + def a(): Unit = () +} + +trait E extends js.Object + +object E { + @JSExportStatic // error + def a(): Unit = () +} + +@js.native +@JSGlobal("Dummy") +class F extends js.Object + +object F { + @JSExportStatic // error + def a(): Unit = () +} diff --git a/tests/neg-scalajs/jsexporttoplevel-abstract-class-or-trait.scala b/tests/neg-scalajs/jsexporttoplevel-abstract-class-or-trait.scala new file mode 100644 index 000000000000..843e1bab7386 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-abstract-class-or-trait.scala @@ -0,0 +1,27 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +@JSExportTopLevel("A") // error +abstract class A + +abstract class B(x: Int) { + @JSExportTopLevel("B") // error + def this() = this(5) +} + +@JSExportTopLevel("C") // error +trait C + +@JSExportTopLevel("D") // ok +abstract class D extends js.Object + +@JSExportTopLevel("E") // error +trait E extends js.Object + +@JSExportTopLevel("F") // error +@js.native @JSGlobal +abstract class F extends js.Object + +@JSExportTopLevel("G") // error +@js.native +trait G extends js.Object diff --git a/tests/neg-scalajs/jsexporttoplevel-conflicts.scala b/tests/neg-scalajs/jsexporttoplevel-conflicts.scala new file mode 100644 index 000000000000..e684b7940ad8 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-conflicts.scala @@ -0,0 +1,26 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object A { + @JSExportTopLevel("A") + val a: Int = 1 + + @JSExportTopLevel("A") // error + var b: Int = 1 +} + +object B { + @JSExportTopLevel("B") + val a: Int = 1 + + @JSExportTopLevel("B") // error + def b(x: Int): Int = x + 1 +} + +object C { + @JSExportTopLevel("C") + def a(x: Int): Int = x + 1 + + @JSExportTopLevel("C") // error + val b: Int = 1 +} diff --git a/tests/neg-scalajs/jsexporttoplevel-in-js-type.scala b/tests/neg-scalajs/jsexporttoplevel-in-js-type.scala new file mode 100644 index 000000000000..fe0d12597a62 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-in-js-type.scala @@ -0,0 +1,25 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object JSContainer extends js.Object { + @JSExportTopLevel("A") // error + object A + + @JSExportTopLevel("B") // error + object B extends js.Object + + @JSExportTopLevel("C") // error + class C + + @JSExportTopLevel("D") // error + class D extends js.Object + + @JSExportTopLevel("e") // error + val e: Int = 5 + + @JSExportTopLevel("f") // error + var f: Int = 5 + + @JSExportTopLevel("g") // error + def g(x: Int): Int = x + 1 +} diff --git a/tests/neg-scalajs/jsexporttoplevel-invalid-js-identifier.scala b/tests/neg-scalajs/jsexporttoplevel-invalid-js-identifier.scala new file mode 100644 index 000000000000..0b97e384d407 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-invalid-js-identifier.scala @@ -0,0 +1,25 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +@JSExportTopLevel("not-a-valid-JS-identifier-1") // error +object A + +@JSExportTopLevel("not-a-valid-JS-identifier-2") // error +class B + +object C { + @JSExportTopLevel("not-a-valid-JS-identifier-3") // error + val a: Int = 1 + + @JSExportTopLevel("not-a-valid-JS-identifier-4") // error + var b: Int = 1 + + @JSExportTopLevel("not-a-valid-JS-identifier-5") // error + def c(): Int = 1 +} + +@JSExportTopLevel("") // error +object D + +@JSExportTopLevel("namespaced.E") // error +object E diff --git a/tests/neg-scalajs/jsexporttoplevel-js-native.scala b/tests/neg-scalajs/jsexporttoplevel-js-native.scala new file mode 100644 index 000000000000..e5b1aa69f85c --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-js-native.scala @@ -0,0 +1,19 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +@JSExportTopLevel("A") // error +@js.native +@JSGlobal +object A extends js.Object + +@JSExportTopLevel("B") // error +@js.native +trait B extends js.Object + +@JSExportTopLevel("C") // error +@js.native +@JSGlobal +class C extends js.Object { + @JSExportTopLevel("C") // error + def this(x: Int) = this() +} diff --git a/tests/neg-scalajs/jsexporttoplevel-local.scala b/tests/neg-scalajs/jsexporttoplevel-local.scala new file mode 100644 index 000000000000..71b89955d6ce --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-local.scala @@ -0,0 +1,27 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class A { + def method(): Unit = { + @JSExportTopLevel("A") // error + class A + + @JSExportTopLevel("B") // error + class B extends js.Object + + @JSExportTopLevel("C") // error + object C + + @JSExportTopLevel("D") // error + object D extends js.Object + + @JSExportTopLevel("e") // error + def e(): Int = 1 + + @JSExportTopLevel("f") // error + val f = 1 + + @JSExportTopLevel("g") // error + var g = 1 + } +} diff --git a/tests/neg-scalajs/jsexporttoplevel-non-public.scala b/tests/neg-scalajs/jsexporttoplevel-non-public.scala new file mode 100644 index 000000000000..96466ab49707 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-non-public.scala @@ -0,0 +1,57 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object Container { + + // Classes + + @JSExportTopLevel("A1") // error + private class A1 + + @JSExportTopLevel("A2") // error + protected[this] class A2 + + @JSExportTopLevel("A3") // error + private[Container] class A3 + + @JSExportTopLevel("A4") // error + private class A4 extends js.Object + + @JSExportTopLevel("A5") // error + protected[this] class A5 extends js.Object + + @JSExportTopLevel("A6") // error + private[Container] class A6 extends js.Object + + // Objects + + @JSExportTopLevel("B1") // error + private object B1 + + @JSExportTopLevel("B2") // error + protected[this] object B2 + + @JSExportTopLevel("B3") // error + private[Container] object B3 + + @JSExportTopLevel("B4") // error + private object B4 extends js.Object + + @JSExportTopLevel("B5") // error + protected[this] object B5 extends js.Object + + @JSExportTopLevel("B6") // error + private[Container] object B6 extends js.Object + + // Vals and defs + + @JSExportTopLevel("c1") // error + private val c1: Int = 5 + + @JSExportTopLevel("c2") // error + protected[this] var c2: Int = 5 + + @JSExportTopLevel("c3") // error + private[Container] def x(x: Int): Int = 5 + +} diff --git a/tests/neg-scalajs/jsexporttoplevel-non-static.scala b/tests/neg-scalajs/jsexporttoplevel-non-static.scala new file mode 100644 index 000000000000..362717649380 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-non-static.scala @@ -0,0 +1,25 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +class ClassContainer { + @JSExportTopLevel("A") // error + object A + + @JSExportTopLevel("B") // error + object B extends js.Object + + @JSExportTopLevel("C") // error + class C + + @JSExportTopLevel("D") // error + class D extends js.Object + + @JSExportTopLevel("e") // error + val e: Int = 5 + + @JSExportTopLevel("f") // error + var f: Int = 5 + + @JSExportTopLevel("g") // error + def g(x: Int): Int = x + 1 +} diff --git a/tests/neg-scalajs/jsexporttoplevel-wrong-kind.scala b/tests/neg-scalajs/jsexporttoplevel-wrong-kind.scala new file mode 100644 index 000000000000..f3c76d2802d8 --- /dev/null +++ b/tests/neg-scalajs/jsexporttoplevel-wrong-kind.scala @@ -0,0 +1,13 @@ +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object A { + @JSExportTopLevel("a1") // error + lazy val a1: Int = 1 + + @JSExportTopLevel("a2") // error + def a2: Int = 1 + + @JSExportTopLevel("a3") // error + def a3_=(v: Int): Unit = () +} diff --git a/tests/run/i9881.scala b/tests/run/i9881.scala new file mode 100644 index 000000000000..a5786aa4d322 --- /dev/null +++ b/tests/run/i9881.scala @@ -0,0 +1,21 @@ +import java.io._ + +class Config(s: String) + +class ConfigException(@transient val config: Config = null) + extends java.io.Serializable + +object Test { + def main(args: Array[String]): Unit = { + val e = new ConfigException(new Config("not serializable")) + val byteStream = new ByteArrayOutputStream() + val objectStream = new ObjectOutputStream(byteStream) + objectStream.writeObject(e) + objectStream.close() + val bytes = byteStream.toByteArray() + val inStream = new ByteArrayInputStream(bytes) + val inObjectStream = new ObjectInputStream(inStream) + val copy = inObjectStream.readObject() + inObjectStream.close() + } +}