From df7d1ba37d054f138045f41bca95428f8a539192 Mon Sep 17 00:00:00 2001 From: som-snytt Date: Wed, 3 Sep 2025 23:40:53 -0700 Subject: [PATCH 1/2] Lint function arrow intended context function (#23847) Fixes #21187 If a function literal `x => body` has an expected type `X ?=> ?` then maybe they intended to write `x ?=> body`. As shown in the test, maybe types will misalign in other ways to emit warnings. --- .../tools/dotc/config/ScalaSettings.scala | 2 ++ .../tools/dotc/transform/init/Checker.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 10 +++++++++ .../dotty/tools/dotc/CompilationTests.scala | 10 ++++----- tests/warn/i21187.scala | 22 +++++++++++++++++++ 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 tests/warn/i21187.scala diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index c25e5e0db11c..eb3296ab476c 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -176,6 +176,7 @@ private sealed trait WarningSettings: private val WenumCommentDiscard = BooleanSetting("-Wenum-comment-discard", "Warn when a comment ambiguously assigned to multiple enum cases is discarded.") private val WtoStringInterpolated = BooleanSetting("-Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.") private val WrecurseWithDefault = BooleanSetting("-Wrecurse-with-default", "Warn when a method calls itself with a default argument.") + private val WwrongArrow = BooleanSetting("-Wwrong-arrow", "Warn if function arrow was used instead of context literal ?=>.") private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting( name = "-Wunused", helpArg = "warning", @@ -295,6 +296,7 @@ private sealed trait WarningSettings: def toStringInterpolated(using Context): Boolean = allOr(WtoStringInterpolated) def recurseWithDefault(using Context): Boolean = allOr(WrecurseWithDefault) def checkInit(using Context): Boolean = allOr(YcheckInit) + def wrongArrow(using Context): Boolean = allOr(WwrongArrow) /** -X "Extended" or "Advanced" settings */ private sealed trait XSettings: diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 92f55f30edff..6db759f60ad3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -46,7 +46,7 @@ class Checker extends Phase: cancellable { val classes = traverser.getClasses() - if ctx.settings.Whas.checkInit then + if ctx.settings.Whas.safeInit then Semantic.checkClasses(classes)(using checkCtx) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 94cabdbc7b22..cec2e93d4bf8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3283,6 +3283,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val ifun = desugar.makeContextualFunction(paramTypes, tree, erasedParams) typr.println(i"make contextual function $tree / $pt ---> $ifun") typedFunctionValue(ifun, pt) + .tap: + case tree @ Block((m1: DefDef) :: _, _: Closure) if ctx.settings.Whas.wrongArrow => + m1.rhs match + case Block((m2: DefDef) :: _, _: Closure) if m1.paramss.lengthCompare(m2.paramss) == 0 => + val p1s = m1.symbol.info.asInstanceOf[MethodType].paramInfos + val p2s = m2.symbol.info.asInstanceOf[MethodType].paramInfos + if p1s.corresponds(p2s)(_ =:= _) then + report.warning(em"Context function adapts a lambda with the same parameter types, possibly ?=> was intended.", tree.srcPos) + case _ => + case _ => } /** Typecheck and adapt tree, returning a typed tree. Parameters as for `typedUnadapted` */ diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 9571e80d5ccd..42c2ce99e5ee 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -132,7 +132,7 @@ class CompilationTests { compileFilesInDir("tests/neg-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), compileFile("tests/neg-custom-args/sourcepath/outer/nested/Test1.scala", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath")), - compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Xfatal-warnings")), + compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Werror")), compileList("duplicate source", List( "tests/neg-custom-args/toplevel-samesource/S.scala", "tests/neg-custom-args/toplevel-samesource/nested/S.scala"), @@ -214,7 +214,7 @@ class CompilationTests { compileFilesInDir("tests/init/neg", options).checkExpectedErrors() compileFilesInDir("tests/init/warn", defaultOptions.and("-Ysafe-init")).checkWarnings() compileFilesInDir("tests/init/pos", options).checkCompile() - compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile() + compileFilesInDir("tests/init/crash", options.without("-Werror")).checkCompile() // The regression test for i12128 has some atypical classpath requirements. // The test consists of three files: (a) Reflect_1 (b) Macro_2 (c) Test_3 // which must be compiled separately. In addition: @@ -223,7 +223,7 @@ class CompilationTests { // - the output from (a) _must not_ be on the classpath while compiling (c) locally { val i12128Group = TestGroup("checkInit/i12128") - val i12128Options = options.without("-Xfatal-warnings") + val i12128Options = options.without("-Werror") val outDir1 = defaultOutputDir + i12128Group + "/Reflect_1/i12128/Reflect_1" val outDir2 = defaultOutputDir + i12128Group + "/Macro_2/i12128/Macro_2" @@ -242,7 +242,7 @@ class CompilationTests { * an error when reading the files' TASTy trees. */ locally { val tastyErrorGroup = TestGroup("checkInit/tasty-error/val-or-defdef") - val tastyErrorOptions = options.without("-Xfatal-warnings") + val tastyErrorOptions = options.without("-Werror") val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A" val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A" @@ -265,7 +265,7 @@ class CompilationTests { * an error when reading the files' TASTy trees. This fact is demonstrated by the compilation of Main. */ locally { val tastyErrorGroup = TestGroup("checkInit/tasty-error/typedef") - val tastyErrorOptions = options.without("-Xfatal-warnings").without("-Ycheck:all") + val tastyErrorOptions = options.without("-Werror").without("-Ycheck:all") val classC = defaultOutputDir + tastyErrorGroup + "/C/typedef/C" val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A" diff --git a/tests/warn/i21187.scala b/tests/warn/i21187.scala new file mode 100644 index 000000000000..d6ea131afdfb --- /dev/null +++ b/tests/warn/i21187.scala @@ -0,0 +1,22 @@ +//> using options -Wall + +def oops(msg: String) = sys.error(msg) + +class Zone +object Zone: + inline def apply[T](inline f: Zone ?=> T): T = f(using new Zone) + +inline def zone[A](inline f: Zone ?=> A) = Zone.apply(z => f(using z)) // warn suspicious contextualizing + +def zone_?[A](f: Zone ?=> A) = Zone.apply(z => f(using z)) // warn + +// intended +//inline def zone[A](inline f: Zone ?=> A): A = Zone.apply(z ?=> f(using z)) + +@main def hello = + // this swallows exceptions! + zone(oops("here")) // warn function value is not used + zone_?(oops("here")) // warn + + // this doesn't + Zone(oops("not here")) From 5f6f31c315011fa675f0f1c0371942ddfce8b908 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Fri, 12 Sep 2025 19:01:27 +0200 Subject: [PATCH 2/2] Lint function arrow intended context function (#23847) Fixes #21187 If a function literal `x => body` has an expected type `X ?=> ?` then maybe they intended to write `x ?=> body`. As shown in the test, maybe types will misalign in other ways to emit warnings. [Cherry-picked e0ff32987c6bd608326e9a5eb315150dd0fb7635][modified]