Skip to content

Commit df7d1ba

Browse files
som-snytttgodzik
authored andcommitted
Lint function arrow intended context function (scala#23847)
Fixes scala#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.
1 parent d8e1229 commit df7d1ba

File tree

5 files changed

+40
-6
lines changed

5 files changed

+40
-6
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ private sealed trait WarningSettings:
176176
private val WenumCommentDiscard = BooleanSetting("-Wenum-comment-discard", "Warn when a comment ambiguously assigned to multiple enum cases is discarded.")
177177
private val WtoStringInterpolated = BooleanSetting("-Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.")
178178
private val WrecurseWithDefault = BooleanSetting("-Wrecurse-with-default", "Warn when a method calls itself with a default argument.")
179+
private val WwrongArrow = BooleanSetting("-Wwrong-arrow", "Warn if function arrow was used instead of context literal ?=>.")
179180
private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting(
180181
name = "-Wunused",
181182
helpArg = "warning",
@@ -295,6 +296,7 @@ private sealed trait WarningSettings:
295296
def toStringInterpolated(using Context): Boolean = allOr(WtoStringInterpolated)
296297
def recurseWithDefault(using Context): Boolean = allOr(WrecurseWithDefault)
297298
def checkInit(using Context): Boolean = allOr(YcheckInit)
299+
def wrongArrow(using Context): Boolean = allOr(WwrongArrow)
298300

299301
/** -X "Extended" or "Advanced" settings */
300302
private sealed trait XSettings:

compiler/src/dotty/tools/dotc/transform/init/Checker.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class Checker extends Phase:
4646
cancellable {
4747
val classes = traverser.getClasses()
4848

49-
if ctx.settings.Whas.checkInit then
49+
if ctx.settings.Whas.safeInit then
5050
Semantic.checkClasses(classes)(using checkCtx)
5151
}
5252

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3283,6 +3283,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
32833283
val ifun = desugar.makeContextualFunction(paramTypes, tree, erasedParams)
32843284
typr.println(i"make contextual function $tree / $pt ---> $ifun")
32853285
typedFunctionValue(ifun, pt)
3286+
.tap:
3287+
case tree @ Block((m1: DefDef) :: _, _: Closure) if ctx.settings.Whas.wrongArrow =>
3288+
m1.rhs match
3289+
case Block((m2: DefDef) :: _, _: Closure) if m1.paramss.lengthCompare(m2.paramss) == 0 =>
3290+
val p1s = m1.symbol.info.asInstanceOf[MethodType].paramInfos
3291+
val p2s = m2.symbol.info.asInstanceOf[MethodType].paramInfos
3292+
if p1s.corresponds(p2s)(_ =:= _) then
3293+
report.warning(em"Context function adapts a lambda with the same parameter types, possibly ?=> was intended.", tree.srcPos)
3294+
case _ =>
3295+
case _ =>
32863296
}
32873297

32883298
/** Typecheck and adapt tree, returning a typed tree. Parameters as for `typedUnadapted` */

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class CompilationTests {
132132
compileFilesInDir("tests/neg-deep-subtype", allowDeepSubtypes),
133133
compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")),
134134
compileFile("tests/neg-custom-args/sourcepath/outer/nested/Test1.scala", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath")),
135-
compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Xfatal-warnings")),
135+
compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Werror")),
136136
compileList("duplicate source", List(
137137
"tests/neg-custom-args/toplevel-samesource/S.scala",
138138
"tests/neg-custom-args/toplevel-samesource/nested/S.scala"),
@@ -214,7 +214,7 @@ class CompilationTests {
214214
compileFilesInDir("tests/init/neg", options).checkExpectedErrors()
215215
compileFilesInDir("tests/init/warn", defaultOptions.and("-Ysafe-init")).checkWarnings()
216216
compileFilesInDir("tests/init/pos", options).checkCompile()
217-
compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile()
217+
compileFilesInDir("tests/init/crash", options.without("-Werror")).checkCompile()
218218
// The regression test for i12128 has some atypical classpath requirements.
219219
// The test consists of three files: (a) Reflect_1 (b) Macro_2 (c) Test_3
220220
// which must be compiled separately. In addition:
@@ -223,7 +223,7 @@ class CompilationTests {
223223
// - the output from (a) _must not_ be on the classpath while compiling (c)
224224
locally {
225225
val i12128Group = TestGroup("checkInit/i12128")
226-
val i12128Options = options.without("-Xfatal-warnings")
226+
val i12128Options = options.without("-Werror")
227227
val outDir1 = defaultOutputDir + i12128Group + "/Reflect_1/i12128/Reflect_1"
228228
val outDir2 = defaultOutputDir + i12128Group + "/Macro_2/i12128/Macro_2"
229229

@@ -242,7 +242,7 @@ class CompilationTests {
242242
* an error when reading the files' TASTy trees. */
243243
locally {
244244
val tastyErrorGroup = TestGroup("checkInit/tasty-error/val-or-defdef")
245-
val tastyErrorOptions = options.without("-Xfatal-warnings")
245+
val tastyErrorOptions = options.without("-Werror")
246246

247247
val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A"
248248
val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A"
@@ -265,7 +265,7 @@ class CompilationTests {
265265
* an error when reading the files' TASTy trees. This fact is demonstrated by the compilation of Main. */
266266
locally {
267267
val tastyErrorGroup = TestGroup("checkInit/tasty-error/typedef")
268-
val tastyErrorOptions = options.without("-Xfatal-warnings").without("-Ycheck:all")
268+
val tastyErrorOptions = options.without("-Werror").without("-Ycheck:all")
269269

270270
val classC = defaultOutputDir + tastyErrorGroup + "/C/typedef/C"
271271
val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A"

tests/warn/i21187.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//> using options -Wall
2+
3+
def oops(msg: String) = sys.error(msg)
4+
5+
class Zone
6+
object Zone:
7+
inline def apply[T](inline f: Zone ?=> T): T = f(using new Zone)
8+
9+
inline def zone[A](inline f: Zone ?=> A) = Zone.apply(z => f(using z)) // warn suspicious contextualizing
10+
11+
def zone_?[A](f: Zone ?=> A) = Zone.apply(z => f(using z)) // warn
12+
13+
// intended
14+
//inline def zone[A](inline f: Zone ?=> A): A = Zone.apply(z ?=> f(using z))
15+
16+
@main def hello =
17+
// this swallows exceptions!
18+
zone(oops("here")) // warn function value is not used
19+
zone_?(oops("here")) // warn
20+
21+
// this doesn't
22+
Zone(oops("not here"))

0 commit comments

Comments
 (0)