Skip to content

Commit 51dfbaf

Browse files
committed
Eta-expand 0-arity method if expected type is Function0
Reverse course from the deprecation introduced in 2.12. (4.3) condition for eta-expansion by -Xsource level: - until 2.13: - for arity > 0: function or sam type is expected - for arity == 0: Function0 is expected -- SAM types do not eta-expand because it could be an accidental SAM scala/bug#9489 - 2.14: - for arity > 0: unconditional - for arity == 0: a function-ish type of arity 0 is expected (including SAM) - 3.0: auto-application takes precedence, but otherwise we always eta-expand warnings: - 2.12: eta-expansion of zero-arg methods was deprecated (scala/bug#7187) - 2.13: deprecation dropped in favor of setting the scene for uniform eta-expansion in 3.0 - 2.14: expected type is a SAM that is not annotated with `@FunctionalInterface` (4.2) condition for auto-application by -Xsource level: - until 2.14: none (assuming condition for (4.3) was not met) - in 3.0: `meth.isJavaDefined`
1 parent 65567c3 commit 51dfbaf

File tree

10 files changed

+174
-89
lines changed

10 files changed

+174
-89
lines changed

project/ScalaOptionParser.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ object ScalaOptionParser {
108108
"-g" -> List("line", "none", "notailcails", "source", "vars"),
109109
"-target" -> List("jvm-1.5", "jvm-1.6", "jvm-1.7", "jvm-1.8"))
110110
private def multiChoiceSettingNames = Map[String, List[String]](
111-
"-Xlint" -> List("adapted-args", "nullary-unit", "inaccessible", "nullary-override", "infer-any", "missing-interpolator", "doc-detached", "private-shadow", "type-parameter-shadow", "poly-implicit-overload", "option-implicit", "delayedinit-select", "package-object-classes", "stars-align", "constant", "unused"),
111+
"-Xlint" -> List("adapted-args", "nullary-unit", "inaccessible", "nullary-override", "infer-any", "missing-interpolator", "doc-detached", "private-shadow", "type-parameter-shadow", "poly-implicit-overload", "option-implicit", "delayedinit-select", "package-object-classes", "stars-align", "constant", "unused", "eta-zero"),
112112
"-language" -> List("help", "_", "dynamics", "postfixOps", "reflectiveCalls", "implicitConversions", "higherKinds", "existentials", "experimental.macros"),
113113
"-opt" -> List("unreachable-code", "simplify-jumps", "compact-locals", "copy-propagation", "redundant-casts", "box-unbox", "nullness-tracking", "closure-invocations" , "allow-skip-core-module-init", "assume-modules-non-null", "allow-skip-class-loading", "inline", "l:none", "l:default", "l:method", "l:inline", "l:project", "l:classpath"),
114114
"-Ywarn-unused" -> List("imports", "patvars", "privates", "locals", "explicits", "implicits", "params"),

spec/06-expressions.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -1397,16 +1397,19 @@ type $T$ by evaluating the expression to which $m$ is bound.
13971397
If the method takes only implicit parameters, implicit
13981398
arguments are passed following the rules [here](07-implicits.html#implicit-parameters).
13991399

1400-
###### Empty Application
1401-
Otherwise, if $e$ has method type $()T$, it is implicitly applied to the empty
1402-
argument list, yielding $e()$.
1403-
14041400
###### Eta Expansion
14051401
Otherwise, if the method is not a constructor,
1406-
and the expected type $\mathit{pt}$ is a function type (potentially after sam conversion)
1402+
and the expected type $\mathit{pt}$ is a function type, or,
1403+
for methods of non-zero arity, a type [sam-convertible](#sam-conversion) to a function type,
14071404
$(\mathit{Ts}') \Rightarrow T'$, [eta-expansion](#eta-expansion)
14081405
is performed on the expression $e$.
14091406

1407+
(The exception for zero-arity methods is to avoid surprises due to unexpected sam conversion.)
1408+
1409+
###### Empty Application
1410+
Otherwise, if $e$ has method type $()T$, it is implicitly applied to the empty
1411+
argument list, yielding $e()$.
1412+
14101413
### Overloading Resolution
14111414

14121415
If an identifier or selection $e$ references several members of a

src/compiler/scala/tools/nsc/settings/Warnings.scala

+2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ trait Warnings {
109109
val ImplicitNotFound = LintWarning("implicit-not-found", "Check @implicitNotFound and @implicitAmbiguous messages.")
110110
val Serial = LintWarning("serial", "@SerialVersionUID on traits and non-serializable classes.")
111111
val ValPattern = LintWarning("valpattern", "Enable pattern checks in val definitions.")
112+
val EtaZero = LintWarning("eta-zero", "Warn on eta-expansion (rather than auto-application) of zero-ary method.")
112113

113114
def allLintWarnings = values.toSeq.asInstanceOf[Seq[LintWarning]]
114115
}
@@ -134,6 +135,7 @@ trait Warnings {
134135
def lintImplicitNotFound = lint contains ImplicitNotFound
135136
def warnSerialization = lint contains Serial
136137
def lintValPatterns = lint contains ValPattern
138+
def warnEtaZero = lint contains EtaZero
137139

138140
// The Xlint warning group.
139141
val lint = MultiChoiceSetting(

src/compiler/scala/tools/nsc/typechecker/Typers.scala

+92-42
Original file line numberDiff line numberDiff line change
@@ -779,11 +779,9 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
779779
* store these instances in context.undetparams,
780780
* unless followed by explicit type application.
781781
* (4) Do the following to unapplied methods used as values:
782-
* (4.1) If the method has only implicit parameters pass implicit arguments
783-
* (4.2) otherwise, if the method is nullary with a result type compatible to `pt`
784-
* and it is not a constructor, apply it to ()
785-
* (4.3) otherwise, if `pt` is a function type and method is not a constructor,
786-
* convert to function by eta-expansion,
782+
* (4.1) If the method has only implicit parameters, pass implicit arguments (see adaptToImplicitMethod)
783+
* (4.2) otherwise, if the method is 0-ary and it can be auto-applied (see canAutoApply), apply it to ()
784+
* (4.3) otherwise, if the method is not a constructor, and can be eta-expanded (see canEtaExpand), eta-expand
787785
* otherwise issue an error
788786
* (5) Convert constructors in a pattern as follows:
789787
* (5.1) If constructor refers to a case class factory, set tree's type to the unique
@@ -874,46 +872,98 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
874872
)
875873
}
876874

877-
def instantiateToMethodType(mt: MethodType): Tree = {
878-
val meth = tree match {
879-
// a partial named application is a block (see comment in EtaExpansion)
880-
//
881-
// TODO: document why we need to peel off one layer to begin with, and, if we do, why we don't need to recurse??
882-
// (and why we don't need to look at the stats to make sure it's the block created by eta-expansion)
883-
// I don't see how we call etaExpand on a constructor..
884-
//
885-
// I guess we don't need to recurse because the outer block and a nested block will refer to the same method
886-
// after eta-expansion (if that's what generated these blocks), so we can just look at the outer one.
887-
//
888-
// How about user-written blocks? Can they ever have a MethodType?
889-
case Block(_, tree1) => tree1.symbol
890-
case _ => tree.symbol
891-
}
875+
def adaptMethodTypeToExpr(mt: MethodType): Tree = {
876+
val meth =
877+
tree match {
878+
// a partial named application is a block (see comment in EtaExpansion)
879+
// How about user-written blocks? Can they ever have a MethodType?
880+
case Block(_, tree1) => tree1.symbol
881+
case _ => tree.symbol
882+
}
892883

893-
def cantAdapt =
894-
if (context.implicitsEnabled) MissingArgsForMethodTpeError(tree, meth) else UnstableTreeError(tree)
895884

896-
// constructors do not eta-expand
897-
if (meth.isConstructor) cantAdapt
898-
// (4.2) apply to empty argument list
899-
else if (mt.params.isEmpty && (settings.isScala213 || !isFunctionType(pt))) {
900-
// Starting with 2.13, always insert `()`, regardless of expected type.
901-
// On older versions, expected type must not be a FunctionN (can be a SAM)
902-
//
903-
// scala/bug#7187 deprecates eta-expansion of zero-arg method values (since 2.12; was never introduced for SAM types: scala/bug#9536).
904-
// The next step is to also deprecate insertion of `()` (except for java-defined methods), as dotty already requires explicitly writing them.
905-
// Once explicit application to () is required, we can more aggressively eta-expand method references, even if they are 0-arity
906-
adapt(typed(Apply(tree, Nil) setPos tree.pos), mode, pt, original)
885+
val arity = mt.params.length
886+
887+
val sourceLevel2_14 = settings.isScala214
888+
val sourceLevel3 = sourceLevel2_14 && settings.isScala300
889+
890+
def warnTree = original orElse tree
891+
892+
def warnEtaZero() = {
893+
val ptHelp =
894+
if (expectingFunctionOfArity) pt
895+
else s"$pt, which is SAM-equivalent to ${samToFunctionType(pt)}"
896+
897+
reporter.warning(tree.pos, s"An unapplied 0-arity method was eta-expanded (due to the expected type ${ptHelp}), rather than applied to `()`.\n" +
898+
s"Write ${Apply(warnTree, Nil)} to invoke method ${meth.decodedName}, or change the expected type.")
899+
}
900+
901+
def warnEtaSam() = {
902+
val sam = samOf(pt)
903+
if (sam.exists && !sam.owner.hasAnnotation(definitions.FunctionalInterfaceClass))
904+
reporter.warning(tree.pos, s"Eta-expansion performed to meet expected type $pt, which is SAM-equivalent to ${samToFunctionType(pt)},\n" +
905+
s"even though ${sam.owner} is not annotated with `@FunctionalInterface`; add annotation to suppress warning.")
907906
}
908-
// (4.3) eta-expand method value when function or sam type is expected (for experimentation, always eta-expand under 2.14 source level)
909-
else if (isFunctionProto(pt) || settings.isScala214) { // TODO: decide on `settings.isScala214`
910-
if (settings.isScala212 && mt.params.isEmpty) // implies isFunctionType(pt)
911-
currentRun.reporting.deprecationWarning(tree.pos, NoSymbol, "Eta-expansion of zero-argument methods is deprecated. "+
912-
s"To avoid this warning, write ${Function(Nil, Apply(tree, Nil))}.", "2.12.0")
913907

914-
typedEtaExpansion(tree, mode, pt)
908+
// note that isFunctionProto(pt) does not work properly for Function0
909+
lazy val ptUnderlying =
910+
(pt match {
911+
case oapt: OverloadedArgFunProto => oapt.underlying
912+
case pt => pt
913+
}).dealiasWiden
914+
915+
lazy val expectingFunctionOfArity = {
916+
val ptSym = ptUnderlying.typeSymbolDirect
917+
(ptSym eq FunctionClass(arity)) || (arity > 0 && (ptSym eq FunctionClass(1))) // allowing for tupling conversion
918+
}
919+
lazy val expectingSamOfArity = {
920+
val sam = samOf(ptUnderlying)
921+
sam.exists && sam.info.params.lengthCompare(arity) == 0
915922
}
916-
else cantAdapt
923+
924+
// (4.3) condition for eta-expansion by -Xsource level
925+
//
926+
// until 2.13:
927+
// - for arity > 0: function or sam type is expected
928+
// - for arity == 0: Function0 is expected -- SAM types do not eta-expand because it could be an accidental SAM scala/bug#9489
929+
// 2.14:
930+
// - for arity > 0: unconditional
931+
// - for arity == 0: a function-ish type of arity 0 is expected (including SAM)
932+
// 3.0: auto-application takes precedence, but otherwise we always eta-expand
933+
//
934+
// warnings:
935+
// - 2.12: eta-expansion of zero-arg methods was deprecated (scala/bug#7187)
936+
// - 2.13: deprecation dropped in favor of setting the scene for uniform eta-expansion in 3.0
937+
// (arity > 0) expected type is a SAM that is not annotated with `@FunctionalInterface`
938+
// - 2.14: (arity == 0) expected type is a SAM that is not annotated with `@FunctionalInterface`
939+
def canEtaExpand: Boolean =
940+
if (arity == 0) {
941+
val doEtaZero =
942+
if (sourceLevel3) !canAutoApply
943+
else expectingFunctionOfArity || sourceLevel2_14 && expectingSamOfArity
944+
945+
doEtaZero && { if (settings.warnEtaZero) warnEtaZero(); true }
946+
} else sourceLevel2_14 || expectingFunctionOfArity || expectingSamOfArity
947+
948+
949+
// (4.2) condition for auto-application by -Xsource level
950+
//
951+
// until 2.14: none (assuming condition for (4.3) was not met)
952+
// in 3.0: `meth.isJavaDefined`
953+
def canAutoApply: Boolean =
954+
if (sourceLevel3) meth.isJavaDefined
955+
else {
956+
if (sourceLevel2_14 && !meth.isJavaDefined)
957+
context.deprecationWarning(tree.pos, NoSymbol, s"Auto-application to `()` is deprecated. Write ${Apply(warnTree, Nil)} to invoke method ${meth.decodedName}," +
958+
s"or remove the empty argument list from its definition (Java-defined methods are exempt).\n"+
959+
s"In Scala 3, an unapplied method will be eta-expanded, as in: ${Function(Nil, Apply(warnTree, Nil))}.", "2.14.0")
960+
true
961+
}
962+
963+
if (!meth.isConstructor && canEtaExpand) { if (!expectingFunctionOfArity) warnEtaSam(); typedEtaExpansion(tree, mode, pt) }
964+
else if (arity == 0 && canAutoApply) adapt(typed(Apply(tree, Nil) setPos tree.pos), mode, pt, original)
965+
else if (context.implicitsEnabled) MissingArgsForMethodTpeError(tree, meth) // `context.implicitsEnabled` implies we are not in a pattern
966+
else UnstableTreeError(tree)
917967
}
918968

919969
def adaptType(): Tree = {
@@ -1200,8 +1250,8 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
12001250

12011251
case mt: MethodType if mode.typingExprNotFunNotLhs && mt.isImplicit => // (4.1)
12021252
adaptToImplicitMethod(mt)
1203-
case mt: MethodType if mode.typingExprNotFunNotLhs && !hasUndetsInMonoMode && !treeInfo.isMacroApplicationOrBlock(tree) =>
1204-
instantiateToMethodType(mt)
1253+
case mt: MethodType if mode.typingExprNotFunNotLhs && !hasUndetsInMonoMode && !treeInfo.isMacroApplicationOrBlock(tree) => // (4.2) - (4.3)
1254+
adaptMethodTypeToExpr(mt)
12051255
case _ =>
12061256
vanillaAdapt(tree)
12071257
}

src/reflect/scala/reflect/internal/Definitions.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ trait Definitions extends api.StandardDefinitions {
729729
// Are we expecting something function-ish? This considers FunctionN / SAM / ProtoType that matches functions
730730
def isFunctionProto(pt: Type): Boolean =
731731
(isFunctionType(pt)
732-
|| (pt match { case pt: ProtoType => pt.expectsFunctionType case _ => false })
732+
|| (pt match { case pt: ProtoType => pt.expectsFunctionType case _ => false }) // TODO: this does not work for Function0
733733
|| samOf(pt).exists
734734
)
735735

test/files/neg/t7187-2.14.check

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
t7187-2.14.scala:8: error: type mismatch;
1+
t7187-2.14.scala:13: error: type mismatch;
22
found : Int
33
required: () => Any
44
val t1: () => Any = m1 // error
55
^
6-
t7187-2.14.scala:9: error: type mismatch;
7-
found : Int
8-
required: () => Any
9-
val t2: () => Any = m2 // error, no eta-expansion of zero-args methods
6+
t7187-2.14.scala:14: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => Any), rather than applied to `()`.
7+
Write m2() to invoke method m2, or change the expected type.
8+
val t2: () => Any = m2 // eta-expanded with lint warning
9+
^
10+
t7187-2.14.scala:15: warning: An unapplied 0-arity method was eta-expanded (due to the expected type AcciSamZero, which is SAM-equivalent to () => Int), rather than applied to `()`.
11+
Write m2() to invoke method m2, or change the expected type.
12+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
13+
^
14+
t7187-2.14.scala:15: warning: Eta-expansion performed to meet expected type AcciSamZero, which is SAM-equivalent to () => Int,
15+
even though trait AcciSamZero is not annotated with `@FunctionalInterface`; add annotation to suppress warning.
16+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
17+
^
18+
t7187-2.14.scala:16: warning: An unapplied 0-arity method was eta-expanded (due to the expected type SamZero, which is SAM-equivalent to () => Int), rather than applied to `()`.
19+
Write m2() to invoke method m2, or change the expected type.
20+
val t2Sam: SamZero = m2 // eta-expanded with lint warning
1021
^
11-
two errors found
22+
four warnings found
23+
one error found

test/files/neg/t7187-2.14.scala

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
// scalac: -Xsource:2.14
1+
// scalac: -Xsource:2.14 -Xlint:eta-zero
22
//
3+
trait AcciSamZero { def apply(): Int }
4+
5+
@FunctionalInterface
6+
trait SamZero { def apply(): Int }
7+
38
class EtaExpand214 {
49
def m1 = 1
510
def m2() = 1
611
def m3(x: Int) = x
712

813
val t1: () => Any = m1 // error
9-
val t2: () => Any = m2 // error, no eta-expansion of zero-args methods
14+
val t2: () => Any = m2 // eta-expanded with lint warning
15+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
16+
val t2Sam: SamZero = m2 // eta-expanded with lint warning
1017
val t3: Int => Any = m3 // ok
1118

1219
val t4 = m1 // apply

test/files/neg/t7187.check

+25-19
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,46 @@
1-
t7187.scala:8: error: _ must follow method; cannot follow () => String
1+
t7187.scala:12: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => Any), rather than applied to `()`.
2+
Write foo() to invoke method foo, or change the expected type.
3+
val t1b: () => Any = foo // eta-expansion, but lint warning
4+
^
5+
t7187.scala:16: error: _ must follow method; cannot follow () => String
26
val t1f: Any = foo() _ // error: _ must follow method
37
^
4-
t7187.scala:11: error: type mismatch;
8+
t7187.scala:19: error: type mismatch;
59
found : String
610
required: () => Any
7-
val t2a: () => Any = bar // error: no eta-expansion of zero-arglist-methods
11+
val t2a: () => Any = bar // error: no eta-expansion of zero-arglist-methods (nullary methods)
812
^
9-
t7187.scala:12: error: not enough arguments for method apply: (i: Int)Char in class StringOps.
13+
t7187.scala:20: error: not enough arguments for method apply: (i: Int)Char in class StringOps.
1014
Unspecified value parameter i.
1115
val t2b: () => Any = bar() // error: bar doesn't take arguments, so expanded to bar.apply(), which misses an argument
1216
^
13-
t7187.scala:15: error: not enough arguments for method apply: (i: Int)Char in class StringOps.
17+
t7187.scala:23: error: not enough arguments for method apply: (i: Int)Char in class StringOps.
1418
Unspecified value parameter i.
1519
val t2e: Any = bar() _ // error: not enough arguments for method apply
1620
^
17-
t7187.scala:18: error: type mismatch;
18-
found : String
19-
required: () => Any
20-
val t3a: () => Any = baz // eta-expansion (deprecated) in 2.12, error in 2.13
21+
t7187.scala:26: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => Any), rather than applied to `()`.
22+
Write baz() to invoke method baz, or change the expected type.
23+
val t3a: () => Any = baz // eta-expansion, but lint warning
2124
^
22-
t7187.scala:21: error: _ must follow method; cannot follow String
25+
t7187.scala:29: error: _ must follow method; cannot follow String
2326
val t3d: Any = baz() _ // error: _ must follow method
2427
^
25-
t7187.scala:24: error: type mismatch;
26-
found : String
27-
required: () => Any
28-
val t4a: () => Any = zap // eta-expansion (deprecated) in 2.12, error in 2.13
28+
t7187.scala:32: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => Any), rather than applied to `()`.
29+
Write zap() to invoke method zap, or change the expected type.
30+
val t4a: () => Any = zap // eta-expansion, but lint warning
2931
^
30-
t7187.scala:25: error: type mismatch;
31-
found : String
32-
required: () => Any
32+
t7187.scala:33: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => Any), rather than applied to `()`.
33+
Write zap()() to invoke method zap, or change the expected type.
3334
val t4b: () => Any = zap() // ditto
3435
^
35-
t7187.scala:30: error: missing argument list for method zup in class EtaExpandZeroArg
36+
t7187.scala:38: error: missing argument list for method zup in class EtaExpandZeroArg
3637
Unapplied methods are only converted to functions when a function type is expected.
3738
You can make this conversion explicit by writing `zup _` or `zup(_)` instead of `zup`.
3839
val t5a = zup // error in 2.13, eta-expansion in 2.14
3940
^
40-
9 errors found
41+
t7187.scala:40: warning: Eta-expansion performed to meet expected type AcciSamOne, which is SAM-equivalent to Int => Int,
42+
even though trait AcciSamOne is not annotated with `@FunctionalInterface`; add annotation to suppress warning.
43+
val t5AcciSam: AcciSamOne = zup // ok, but warning
44+
^
45+
5 warnings found
46+
6 errors found

0 commit comments

Comments
 (0)