Skip to content

Commit e0ff329

Browse files
authored
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.
1 parent 60e421b commit e0ff329

File tree

10 files changed

+62
-28
lines changed

10 files changed

+62
-28
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class Compiler {
171171
val rctx =
172172
if ctx.settings.Xsemanticdb.value then
173173
ctx.addMode(Mode.ReadPositions)
174-
else if ctx.settings.YcheckInitGlobal.value then
174+
else if ctx.settings.YsafeInitGlobal.value then
175175
ctx.addMode(Mode.ReadPositions)
176176
else
177177
ctx

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private sealed trait WarningSettings:
159159
self: SettingGroup =>
160160

161161
val Whelp: Setting[Boolean] = BooleanSetting(WarningSetting, "W", "Print a synopsis of warning options.")
162-
val XfatalWarnings: Setting[Boolean] = BooleanSetting(WarningSetting, "Werror", "Fail the compilation if there are any warnings.", aliases = List("-Xfatal-warnings"))
162+
val Werror: Setting[Boolean] = BooleanSetting(WarningSetting, "Werror", "Fail the compilation if there are any warnings.", aliases = List("-Xfatal-warnings"))
163163
val Wall: Setting[Boolean] = BooleanSetting(WarningSetting, "Wall", "Enable all warning settings.")
164164
private val WvalueDiscard: Setting[Boolean] = BooleanSetting(WarningSetting, "Wvalue-discard", "Warn when non-Unit expression results are unused.")
165165
private val WNonUnitStatement = BooleanSetting(WarningSetting, "Wnonunit-statement", "Warn when block statements are non-Unit expressions.")
@@ -168,6 +168,7 @@ private sealed trait WarningSettings:
168168
private val WunstableInlineAccessors = BooleanSetting(WarningSetting, "WunstableInlineAccessors", "Warn an inline methods has references to non-stable binary APIs.")
169169
private val WtoStringInterpolated = BooleanSetting(WarningSetting, "Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.")
170170
private val WrecurseWithDefault = BooleanSetting(WarningSetting, "Wrecurse-with-default", "Warn when a method calls itself with a default argument.")
171+
private val WwrongArrow = BooleanSetting(WarningSetting, "Wwrong-arrow", "Warn if function arrow was used instead of context literal ?=>.")
171172
private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting(
172173
WarningSetting,
173174
name = "Wunused",
@@ -299,7 +300,7 @@ private sealed trait WarningSettings:
299300
def typeParameterShadow(using Context) =
300301
allOr("type-parameter-shadow")
301302

302-
val WcheckInit: Setting[Boolean] = BooleanSetting(WarningSetting, "Wsafe-init", "Ensure safe initialization of objects.")
303+
val WsafeInit: Setting[Boolean] = BooleanSetting(WarningSetting, "Wsafe-init", "Ensure safe initialization of objects.")
303304

304305
object Whas:
305306
def allOr(s: Setting[Boolean])(using Context): Boolean =
@@ -311,7 +312,8 @@ private sealed trait WarningSettings:
311312
def unstableInlineAccessors(using Context): Boolean = allOr(WunstableInlineAccessors)
312313
def toStringInterpolated(using Context): Boolean = allOr(WtoStringInterpolated)
313314
def recurseWithDefault(using Context): Boolean = allOr(WrecurseWithDefault)
314-
def checkInit(using Context): Boolean = allOr(WcheckInit)
315+
def wrongArrow(using Context): Boolean = allOr(WwrongArrow)
316+
def safeInit(using Context): Boolean = allOr(WsafeInit)
315317

316318
/** -X "Extended" or "Advanced" settings */
317319
private sealed trait XSettings:
@@ -453,7 +455,7 @@ private sealed trait YSettings:
453455
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism. (This flag has no effect)", deprecation = Deprecation.removed())
454456
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
455457
val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.")
456-
val YcheckInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.")
458+
val YsafeInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.")
457459
val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.")
458460
val YrecheckTest: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrecheck-test", "Run basic rechecking (internal test only).")
459461
val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.")

compiler/src/dotty/tools/dotc/core/Symbols.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ object Symbols extends SymUtils {
8484
ctx.settings.YretainTrees.value ||
8585
denot.owner.isTerm || // no risk of leaking memory after a run for these
8686
denot.isOneOf(InlineOrProxy) || // need to keep inline info
87-
ctx.settings.Whas.checkInit || // initialization check
88-
ctx.settings.YcheckInitGlobal.value
87+
ctx.settings.Whas.safeInit || // initialization check
88+
ctx.settings.YsafeInitGlobal.value
8989

9090
/** The last denotation of this symbol */
9191
private var lastDenot: SymDenotation = uninitialized

compiler/src/dotty/tools/dotc/reporting/ConsoleReporter.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class ConsoleReporter(
2323
super.doReport(dia)
2424
if ctx.settings.Xprompt.value then
2525
dia match
26-
case _: Error => Reporter.displayPrompt(reader, writer)
27-
case _: Warning if ctx.settings.XfatalWarnings.value => Reporter.displayPrompt(reader, writer)
28-
case _ =>
26+
case _: Error => Reporter.displayPrompt(reader, writer)
27+
case _: Warning => if ctx.settings.Werror.value then Reporter.displayPrompt(reader, writer)
28+
case _ =>
2929
}
3030
}
3131

compiler/src/dotty/tools/dotc/reporting/Reporter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ abstract class Reporter extends interfaces.ReporterResult {
224224
incompleteHandler(dia, ctx)
225225

226226
def finalizeReporting()(using Context) =
227-
if (hasWarnings && ctx.settings.XfatalWarnings.value)
227+
if (hasWarnings && ctx.settings.Werror.value)
228228
report(new Error("No warnings can be incurred under -Werror (or -Xfatal-warnings)", NoSourcePosition))
229229

230230
/** Summary of warnings and errors */

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class Checker extends Phase:
2929
override val runsAfter = Set(Pickler.name)
3030

3131
override def isEnabled(using Context): Boolean =
32-
super.isEnabled && (ctx.settings.Whas.checkInit || ctx.settings.YcheckInitGlobal.value)
32+
super.isEnabled && (ctx.settings.Whas.safeInit || ctx.settings.YsafeInitGlobal.value)
3333

3434
def traverse(traverser: InitTreeTraverser)(using Context): Boolean = monitor(phaseName):
3535
val unit = ctx.compilationUnit
@@ -50,10 +50,10 @@ class Checker extends Phase:
5050
cancellable {
5151
val classes = traverser.getClasses()
5252

53-
if ctx.settings.Whas.checkInit then
53+
if ctx.settings.Whas.safeInit then
5454
Semantic.checkClasses(classes)(using checkCtx)
5555

56-
if ctx.settings.YcheckInitGlobal.value then
56+
if ctx.settings.YsafeInitGlobal.value then
5757
val obj = new Objects
5858
obj.checkClasses(classes)(using checkCtx)
5959
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3826,6 +3826,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
38263826
val ifun = desugar.makeContextualFunction(paramTypes, paramNamesOrNil, tree, erasedParams)
38273827
typr.println(i"make contextual function $tree / $pt ---> $ifun")
38283828
typedFunctionValue(ifun, pt)
3829+
.tap:
3830+
case tree @ Block((m1: DefDef) :: _, _: Closure) if ctx.settings.Whas.wrongArrow =>
3831+
m1.rhs match
3832+
case Block((m2: DefDef) :: _, _: Closure) if m1.paramss.lengthCompare(m2.paramss) == 0 =>
3833+
val p1s = m1.symbol.info.asInstanceOf[MethodType].paramInfos
3834+
val p2s = m2.symbol.info.asInstanceOf[MethodType].paramInfos
3835+
if p1s.corresponds(p2s)(_ =:= _) then
3836+
report.warning(em"Context function adapts a lambda with the same parameter types, possibly ?=> was intended.", tree.srcPos)
3837+
case _ =>
3838+
case _ =>
38293839
}
38303840

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

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class CompilationTests {
6262
aggregateTests(
6363
compileFile("tests/rewrites/rewrites.scala", defaultOptions.and("-source", "3.0-migration").and("-rewrite", "-indent")),
6464
compileFile("tests/rewrites/rewrites3x.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
65-
compileFile("tests/rewrites/rewrites3x-fatal-warnings.scala", defaultOptions.and("-rewrite", "-source", "future-migration", "-Xfatal-warnings")),
65+
compileFile("tests/rewrites/rewrites3x-fatal-warnings.scala", defaultOptions.and("-rewrite", "-source", "future-migration", "-Werror")),
6666
compileFile("tests/rewrites/i21394.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
6767
compileFile("tests/rewrites/uninitialized-var.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
6868
compileFile("tests/rewrites/with-type-operator.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
@@ -151,7 +151,7 @@ class CompilationTests {
151151
compileFilesInDir("tests/neg-deep-subtype", allowDeepSubtypes),
152152
compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-language:experimental.separationChecking", "-source", "3.8")),
153153
compileFile("tests/neg-custom-args/sourcepath/outer/nested/Test1.scala", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath")),
154-
compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Xfatal-warnings")),
154+
compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Werror")),
155155
compileList("duplicate source", List(
156156
"tests/neg-custom-args/toplevel-samesource/S.scala",
157157
"tests/neg-custom-args/toplevel-samesource/nested/S.scala"),
@@ -247,21 +247,21 @@ class CompilationTests {
247247
@Test def checkInitGlobal: Unit = {
248248
implicit val testGroup: TestGroup = TestGroup("checkInitGlobal")
249249
compileFilesInDir("tests/init-global/warn", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings()
250-
compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile()
250+
compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global", "-Werror"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile()
251251
if Properties.usingScalaLibraryTasty && !Properties.usingScalaLibraryCCTasty then
252252
compileFilesInDir("tests/init-global/warn-tasty", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings()
253-
compileFilesInDir("tests/init-global/pos-tasty", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile()
253+
compileFilesInDir("tests/init-global/pos-tasty", defaultOptions.and("-Ysafe-init-global", "-Werror"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile()
254254
end if
255255
}
256256

257257
// initialization tests
258-
@Test def checkInit: Unit = {
259-
implicit val testGroup: TestGroup = TestGroup("checkInit")
260-
val options = defaultOptions.and("-Wsafe-init", "-Xfatal-warnings")
258+
@Test def safeInit: Unit = {
259+
given TestGroup = TestGroup("safeInit")
260+
val options = defaultOptions.and("-Wsafe-init", "-Werror")
261261
compileFilesInDir("tests/init/neg", options).checkExpectedErrors()
262262
compileFilesInDir("tests/init/warn", defaultOptions.and("-Wsafe-init")).checkWarnings()
263263
compileFilesInDir("tests/init/pos", options).checkCompile()
264-
compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile()
264+
compileFilesInDir("tests/init/crash", options.without("-Werror")).checkCompile()
265265
// The regression test for i12128 has some atypical classpath requirements.
266266
// The test consists of three files: (a) Reflect_1 (b) Macro_2 (c) Test_3
267267
// which must be compiled separately. In addition:
@@ -270,7 +270,7 @@ class CompilationTests {
270270
// - the output from (a) _must not_ be on the classpath while compiling (c)
271271
locally {
272272
val i12128Group = TestGroup("checkInit/i12128")
273-
val i12128Options = options.without("-Xfatal-warnings")
273+
val i12128Options = options.without("-Werror")
274274
val outDir1 = defaultOutputDir + i12128Group + "/Reflect_1/i12128/Reflect_1"
275275
val outDir2 = defaultOutputDir + i12128Group + "/Macro_2/i12128/Macro_2"
276276

@@ -289,7 +289,7 @@ class CompilationTests {
289289
* an error when reading the files' TASTy trees. */
290290
locally {
291291
val tastyErrorGroup = TestGroup("checkInit/tasty-error/val-or-defdef")
292-
val tastyErrorOptions = options.without("-Xfatal-warnings")
292+
val tastyErrorOptions = options.without("-Werror")
293293

294294
val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A"
295295
val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A"
@@ -312,7 +312,7 @@ class CompilationTests {
312312
* an error when reading the files' TASTy trees. This fact is demonstrated by the compilation of Main. */
313313
locally {
314314
val tastyErrorGroup = TestGroup("checkInit/tasty-error/typedef")
315-
val tastyErrorOptions = options.without("-Xfatal-warnings").without("-Ycheck:all")
315+
val tastyErrorOptions = options.without("-Werror").without("-Ycheck:all")
316316

317317
val classC = defaultOutputDir + tastyErrorGroup + "/C/typedef/C"
318318
val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A"

compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class ScalaSettingsTests:
111111
// createTestCase(settings.YjavaTasty , settings.XjavaTasty),
112112
// createTestCase(settings.YearlyTastyOutput , settings.XearlyTastyOutput, ":./"),
113113
// createTestCase(settings.YallowOutlineFromTasty, settings.XallowOutlineFromTasty),
114-
createTestCase(settings.YcheckInit , settings.WcheckInit),
114+
createTestCase(settings.YcheckInit , settings.WsafeInit),
115115
// createTestCase(settings.Xlint , settings.Wshadow, ":all"), // this setting is not going to be mapped to replacement. Read more in the commit message
116116
).map: (deprecatedArgument, newSetting) =>
117117
val args = List(deprecatedArgument)
@@ -140,7 +140,7 @@ class ScalaSettingsTests:
140140
// createTestCase(settings.YjavaTasty , settings.XjavaTasty),
141141
// createTestCase(settings.YearlyTastyOutput , settings.XearlyTastyOutput),
142142
// createTestCase(settings.YallowOutlineFromTasty, settings.XallowOutlineFromTasty),
143-
createTestCase(settings.YcheckInit , settings.WcheckInit),
143+
createTestCase(settings.YcheckInit , settings.WsafeInit),
144144
createTestCase(settings.Xlint , settings.Wshadow),
145145
).map: (deprecatedArgument, newSetting) =>
146146
val args = List(deprecatedArgument)
@@ -181,7 +181,7 @@ class ScalaSettingsTests:
181181
// createTestCase(settings.YjavaTasty , settings.XjavaTasty),
182182
// createTestCase(settings.YearlyTastyOutput , settings.XearlyTastyOutput, ":./"),
183183
// createTestCase(settings.YallowOutlineFromTasty, settings.XallowOutlineFromTasty),
184-
createTestCase(settings.YcheckInit , settings.WcheckInit),
184+
createTestCase(settings.YcheckInit , settings.WsafeInit),
185185
// createTestCase(settings.Xlint , settings.Wshadow, ":all"), // this setting is not going to be mapped to replacement. Read more in the commit message
186186
).flatten.map: (deprecatedArgument, newSetting) =>
187187
val args = List(deprecatedArgument)

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)