diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index df2cab2c3375..ff93d31e71fa 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -97,9 +97,9 @@ object Feature: else false - def checkExperimentalFeature(which: String, srcPos: SrcPos)(using Context) = + def checkExperimentalFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) = if !isExperimentalEnabled then - report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler", srcPos) + report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler$note", srcPos) def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) = if !isExperimentalEnabled then diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index effbfe22f0e4..6821aaa9ca96 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3114,10 +3114,6 @@ object Parsers { languageImport(tree) match case Some(prefix) => in.languageImportContext = in.languageImportContext.importContext(imp, NoSymbol) - if prefix == nme.experimental - && selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros) - then - Feature.checkExperimentalFeature("features", imp.srcPos) for case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors if allSourceVersionNames.contains(imported) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index da386525511a..c7e02a5c6837 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -448,6 +448,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase throw ex } + override def transformStats(trees: List[Tree], exprOwner: Symbol)(using Context): List[Tree] = + try super.transformStats(trees, exprOwner) + finally Checking.checkExperimentalImports(trees) + /** Transforms the rhs tree into a its default tree if it is in an `erased` val/def. * Performed to shrink the tree that is known to be erased later. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 5534d0c795fc..86745606d847 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -719,6 +719,44 @@ object Checking { checkValue(tree) case _ => tree + + /** Check that experimental language imports in `trees` + * are done only in experimental scopes, or in scopes where + * all statements are @experimental definitions. + */ + def checkExperimentalImports(trees: List[Tree])(using Context): Unit = + + def nonExperimentalStat(trees: List[Tree]): Tree = trees match + case (_: Import | EmptyTree) :: rest => + nonExperimentalStat(rest) + case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject => + nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest)) + case (tree: PackageDef) :: rest => + nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest)) + case (tree: MemberDef) :: rest => + if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then + nonExperimentalStat(rest) + else + tree + case tree :: rest => + tree + case Nil => + EmptyTree + + for case imp @ Import(qual, selectors) <- trees do + languageImport(qual) match + case Some(nme.experimental) + if !ctx.owner.isInExperimentalScope + && selectors.exists(sel => experimental(sel.name) != scala2macros) => + def check(stable: => String) = + checkExperimentalFeature("features", imp.srcPos, + s"\n\nNote: the scope enclosing the import is not considered experimental because it contains the\nnon-experimental $stable") + nonExperimentalStat(trees) match + case EmptyTree => + case tree: MemberDef => check(i"${tree.symbol}") + case tree => check(i"expression ${tree}") + case _ => + end checkExperimentalImports } trait Checking { @@ -830,6 +868,7 @@ trait Checking { em"Implementation restriction: ${path.tpe.classSymbol} is not a valid prefix " + "for a wildcard export, as it is a package.", path.srcPos) + /** Check that module `sym` does not clash with a class of the same name * that is concurrently compiled in another source file. */ diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index b9e5198b0f24..ae90317bcbea 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -178,7 +178,7 @@ class CompilationTests { compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), - compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), + compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), ).checkExpectedErrors() } diff --git a/tests/neg-custom-args/no-experimental/experimental-erased.scala b/tests/neg-custom-args/no-experimental/experimental-erased.scala new file mode 100644 index 000000000000..91f84ba3f85f --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-erased.scala @@ -0,0 +1,7 @@ +import language.experimental.erasedDefinitions // error +import annotation.experimental + +@experimental +erased class CanThrow[-E <: Exception] + +def other = 1 diff --git a/tests/neg-custom-args/no-experimental/experimental.scala b/tests/neg-custom-args/no-experimental/experimental.scala index 26f9ba3d21c7..4787e68b4a5f 100644 --- a/tests/neg-custom-args/no-experimental/experimental.scala +++ b/tests/neg-custom-args/no-experimental/experimental.scala @@ -25,10 +25,4 @@ class Test2 { class Test6 { import scala.language.experimental // ok -} - -class Test7 { - import scala.language.experimental - import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import - val x: BigInt = 13232202002020202020202 // error } \ No newline at end of file diff --git a/tests/pos/experimental-erased-2.scala b/tests/pos/experimental-erased-2.scala new file mode 100644 index 000000000000..44d644e5db16 --- /dev/null +++ b/tests/pos/experimental-erased-2.scala @@ -0,0 +1,10 @@ +import language.experimental.erasedDefinitions +import annotation.experimental + +@experimental object Test: + + erased class CanThrow[-E <: Exception] + + def other = 1 + + diff --git a/tests/pos/experimental-erased.scala b/tests/pos/experimental-erased.scala new file mode 100644 index 000000000000..041145bbf261 --- /dev/null +++ b/tests/pos/experimental-erased.scala @@ -0,0 +1,15 @@ +import language.experimental.erasedDefinitions +import annotation.experimental + +@experimental +erased class CanThrow[-E <: Exception](val i: Int = 0) + +@experimental +object Foo + +@experimental +def bar = 1 + + + +