Skip to content

Commit 41d4762

Browse files
authored
Avoid infinite recursion when looking for suggestions (#22361)
Keep a a set of seen parent symbols between `rootsIn` and `rootsStrictlyIn` to avoid infinitely growing paths that refer to the same value, such as `Collection.this.O.O.O.O.O.O.O....` when computing import suggestions for: ```scala trait Collection: val base: Collection = ??? base.foo() object O extends Collection: def foo(): Int = ??? ``` Fixes #22145.
2 parents 43f8cdb + a2412d8 commit 41d4762

15 files changed

+199
-22
lines changed

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

+25-22
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ trait ImportSuggestions:
6969
&& !(root.name == nme.raw.BAR && ctx.settings.scalajs.value && root == JSDefinitions.jsdefn.PseudoUnionModule)
7070
}
7171

72-
def nestedRoots(site: Type)(using Context): List[Symbol] =
72+
def nestedRoots(site: Type, parentSymbols: Set[Symbol])(using Context): List[Symbol] =
7373
val seenNames = mutable.Set[Name]()
7474
site.baseClasses.flatMap { bc =>
7575
bc.info.decls.filter { dcl =>
@@ -79,34 +79,37 @@ trait ImportSuggestions:
7979
}
8080
}
8181

82-
def rootsStrictlyIn(ref: Type)(using Context): List[TermRef] =
82+
def rootsStrictlyIn(ref: Type, parentSymbols: Set[Symbol] = Set())(using Context): List[TermRef] =
8383
val site = ref.widen
8484
val refSym = site.typeSymbol
85-
val nested =
86-
if refSym.is(Package) then
87-
if refSym == defn.EmptyPackageClass // Don't search the empty package
88-
|| refSym == defn.JavaPackageClass // As an optimization, don't search java...
89-
|| refSym == defn.JavaLangPackageClass // ... or java.lang.
90-
then Nil
91-
else refSym.info.decls.filter(lookInside)
92-
else if refSym.infoOrCompleter.isInstanceOf[StubInfo] then
93-
Nil // Don't chase roots that do not exist
94-
else
95-
if !refSym.is(Touched) then
96-
refSym.ensureCompleted() // JavaDefined is reliably known only after completion
97-
if refSym.is(JavaDefined) then Nil
98-
else nestedRoots(site)
99-
nested
100-
.map(mbr => TermRef(ref, mbr.asTerm))
101-
.flatMap(rootsIn)
102-
.toList
85+
if parentSymbols.contains(refSym) then Nil
86+
else
87+
val nested =
88+
if refSym.is(Package) then
89+
if refSym == defn.EmptyPackageClass // Don't search the empty package
90+
|| refSym == defn.JavaPackageClass // As an optimization, don't search java...
91+
|| refSym == defn.JavaLangPackageClass // ... or java.lang.
92+
then Nil
93+
else refSym.info.decls.filter(lookInside)
94+
else if refSym.infoOrCompleter.isInstanceOf[StubInfo] then
95+
Nil // Don't chase roots that do not exist
96+
else
97+
if !refSym.is(Touched) then
98+
refSym.ensureCompleted() // JavaDefined is reliably known only after completion
99+
if refSym.is(JavaDefined) then Nil
100+
else nestedRoots(site, parentSymbols)
101+
val newParentSymbols = parentSymbols + refSym
102+
nested
103+
.map(mbr => TermRef(ref, mbr.asTerm))
104+
.flatMap(rootsIn(_, newParentSymbols))
105+
.toList
103106

104-
def rootsIn(ref: TermRef)(using Context): List[TermRef] =
107+
def rootsIn(ref: TermRef, parentSymbols: Set[Symbol] = Set())(using Context): List[TermRef] =
105108
if seen.contains(ref) then Nil
106109
else
107110
implicitsDetailed.println(i"search for suggestions in ${ref.symbol.fullName}")
108111
seen += ref
109-
ref :: rootsStrictlyIn(ref)
112+
ref :: rootsStrictlyIn(ref, parentSymbols)
110113

111114
def rootsOnPath(tp: Type)(using Context): List[TermRef] = tp match
112115
case ref: TermRef => rootsIn(ref) ::: rootsOnPath(ref.prefix)

tests/neg/22145.check

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- [E008] Not Found Error: tests/neg/22145.scala:5:7 -------------------------------------------------------------------
2+
5 | base.foo() // error
3+
| ^^^^^^^^
4+
| value foo is not a member of foo.Collection

tests/neg/22145.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package foo
2+
3+
trait Collection:
4+
val base: Collection = ???
5+
base.foo() // error
6+
7+
object O extends Collection:
8+
def foo(): Int = ???

tests/neg/22145b.check

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-- [E008] Not Found Error: tests/neg/22145b.scala:15:19 ----------------------------------------------------------------
2+
15 | require(base.isWithin(p, start, end), "position is out of bounds") // error
3+
| ^^^^^^^^^^^^^
4+
| value isWithin is not a member of Collection.this.Self
5+
-- [E008] Not Found Error: tests/neg/22145b.scala:28:59 ----------------------------------------------------------------
6+
28 | def positionAfter(p: Position): Position = self.base.positionAfter(p) // error
7+
| ^^^^^^^^^^^^^^^^^^^^^^^
8+
|value positionAfter is not a member of Collection.this.Self.
9+
|An extension method was tried, but could not be fully constructed:
10+
|
11+
| this.positionAfter(self.base)
12+
|
13+
| failed with:
14+
|
15+
| Found: (self.base : Collection.this.Self)
16+
| Required: foo.Collection.given_is_Slice_Collection.Self²
17+
|
18+
| where: Self is a type in trait Collection
19+
| Self² is a type in object given_is_Slice_Collection which is an alias of Collection.this.Slice
20+
|
21+
-- [E008] Not Found Error: tests/neg/22145b.scala:29:50 ----------------------------------------------------------------
22+
29 | def apply(p: Position): Element = self.base.apply(p) // error
23+
| ^^^^^^^^^^^^^^^
24+
|value apply is not a member of Collection.this.Self.
25+
|An extension method was tried, but could not be fully constructed:
26+
|
27+
| this.apply(self.base)
28+
|
29+
| failed with:
30+
|
31+
| Found: (self.base : Collection.this.Self)
32+
| Required: foo.Collection.given_is_Slice_Collection.Self²
33+
|
34+
| where: Self is a type in trait Collection
35+
| Self² is a type in object given_is_Slice_Collection which is an alias of Collection.this.Slice
36+
|

tests/neg/22145b.scala

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package foo
2+
3+
import language.experimental.modularity
4+
5+
trait Collection:
6+
me =>
7+
8+
type Self
9+
type Position
10+
type Element
11+
12+
final class Slice(private[Collection] val base: Self, val start: Position, val end: Position):
13+
14+
final def apply(p: Position): Element =
15+
require(base.isWithin(p, start, end), "position is out of bounds") // error
16+
base.apply(p)
17+
18+
end Slice
19+
20+
given Slice is Collection:
21+
22+
type Position = me.Position
23+
type Element = me.Element
24+
25+
extension (self: Self)
26+
def start: Position = self.start
27+
def end: Position = self.end
28+
def positionAfter(p: Position): Position = self.base.positionAfter(p) // error
29+
def apply(p: Position): Element = self.base.apply(p) // error
30+
31+
end given
32+
33+
extension (self: Self)
34+
35+
def start: Position
36+
def end: Position
37+
def positionAfter(p: Position): Position
38+
def apply(p: Position): Element
39+
40+
end extension

tests/neg/22145c.check

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- [E008] Not Found Error: tests/neg/22145c.scala:4:35 -----------------------------------------------------------------
2+
4 | def bar(base: Collection) = base.foo // error
3+
| ^^^^^^^^
4+
| value foo is not a member of foo.Collection

tests/neg/22145c.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package foo
2+
3+
trait Collection:
4+
def bar(base: Collection) = base.foo // error
5+
object a extends Collection:
6+
def foo: Int = 0
7+
object b extends Collection:
8+
def foo: Int = 1

tests/neg/22145d.check

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- [E008] Not Found Error: tests/neg/22145d.scala:10:4 -----------------------------------------------------------------
2+
10 | 2.f() // error
3+
| ^^^
4+
| value f is not a member of Int, but could be made available as an extension method.
5+
|
6+
| The following import might fix the problem:
7+
|
8+
| import foo.O2.f
9+
|

tests/neg/22145d.scala

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package foo
2+
3+
class C[T]:
4+
extension (x: T) def f(): Int = 1
5+
6+
object O1 extends C[String]
7+
object O2 extends C[Int]
8+
9+
def main =
10+
2.f() // error

tests/neg/22145e.check

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- [E008] Not Found Error: tests/neg/22145e.scala:11:4 -----------------------------------------------------------------
2+
11 | 2.f() // error
3+
| ^^^
4+
| value f is not a member of Int, but could be made available as an extension method.
5+
|
6+
| The following import might fix the problem:
7+
|
8+
| import foo.O2.Ext.f
9+
|

tests/neg/22145e.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package foo
2+
3+
class C[T]:
4+
object Ext:
5+
extension (x: T) def f(): Int = 1
6+
7+
object O1 extends C[String]
8+
object O2 extends C[Int]
9+
10+
def main =
11+
2.f() // error

tests/neg/22145f.check

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- [E008] Not Found Error: tests/neg/22145f.scala:11:6 -----------------------------------------------------------------
2+
11 | 2.f() // error
3+
| ^^^
4+
| value f is not a member of Int, but could be made available as an extension method.
5+
|
6+
| One of the following imports might fix the problem:
7+
|
8+
| import C.this.O1.O2.Ext.f
9+
| import C.this.O2.Ext.f
10+
|

tests/neg/22145f.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package foo
2+
3+
class C[T]:
4+
object Ext:
5+
extension (x: T) def f(): Int = 1
6+
7+
object O1 extends C[String]
8+
object O2 extends C[Int]
9+
10+
def g =
11+
2.f() // error

tests/neg/22145g.check

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- [E008] Not Found Error: tests/neg/22145g.scala:10:4 -----------------------------------------------------------------
2+
10 | 2.f() // error
3+
| ^^^
4+
| value f is not a member of Int

tests/neg/22145g.scala

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package foo
2+
3+
class C[T]:
4+
extension (x: T) def f(): Int = 1
5+
6+
object O:
7+
val c0: C[String] = new C[String]
8+
val c1: C[Int] = new C[Int]
9+
// Currently no import suggestions here
10+
2.f() // error

0 commit comments

Comments
 (0)