Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Import missing extension method #4141

Merged
merged 5 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import org.eclipse.{lsp4j => l}

object ScalacDiagnostic {

object NotAMember {
private val regex = """(?s)value (.+) is not a member of.*""".r
def unapply(d: l.Diagnostic): Option[String] =
d.getMessage() match {
case regex(name) => Some(name)
case _ => None
}
}

object SymbolNotFound {
private val regex = """(n|N)ot found: (value|type)?\s?(\w+)""".r
def unapply(d: l.Diagnostic): Option[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ class ImportMissingSymbol(compilers: Compilers) extends CodeAction {
case diag @ ScalacDiagnostic.SymbolNotFound(name)
if params.getRange().overlapsWith(diag.getRange()) =>
importMissingSymbol(diag, name)
case d @ ScalacDiagnostic.NotAMember(name)
if params.getRange().overlapsWith(d.getRange()) =>
importMissingSymbol(d, name)
}
)
.map { actions =>
Expand Down
23 changes: 12 additions & 11 deletions mtags/src/main/scala/scala/meta/internal/mtags/MtagsIndexer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,18 @@ trait MtagsIndexer {
)
}
}
def term(name: String, pos: m.Position, kind: Kind, properties: Int): Unit =
def term(name: String, pos: m.Position, kind: Kind, properties: Int): String =
addSignature(Descriptor.Term(name), pos, kind, properties)
def term(name: Term.Name, kind: Kind, properties: Int): Unit =
def term(name: Term.Name, kind: Kind, properties: Int): String =
addSignature(Descriptor.Term(name.value), name.pos, kind, properties)
def tparam(name: Name, kind: Kind, properties: Int): Unit =
def tparam(name: Name, kind: Kind, properties: Int): String =
addSignature(
Descriptor.TypeParameter(name.value),
name.pos,
kind,
properties
)
def param(name: Name, kind: Kind, properties: Int): Unit =
def param(name: Name, kind: Kind, properties: Int): String =
addSignature(
Descriptor.Parameter(name.value),
name.pos,
Expand All @@ -86,7 +86,7 @@ trait MtagsIndexer {
disambiguator: String,
pos: m.Position,
properties: Int
): Unit =
): String =
addSignature(
Descriptor.Method(Names.Constructor.value, disambiguator),
pos,
Expand All @@ -98,7 +98,7 @@ trait MtagsIndexer {
disambiguator: String,
pos: m.Position,
properties: Int
): Unit =
): String =
addSignature(
Descriptor.Method(name, disambiguator),
pos,
Expand All @@ -110,7 +110,7 @@ trait MtagsIndexer {
disambiguator: String,
kind: Kind,
properties: Int
): Unit = {
): String = {
val methodName = name match {
case Name.Anonymous() => Names.Constructor.value
case _ => name.value
Expand All @@ -122,11 +122,11 @@ trait MtagsIndexer {
properties
)
}
def tpe(name: String, pos: m.Position, kind: Kind, properties: Int): Unit =
def tpe(name: String, pos: m.Position, kind: Kind, properties: Int): String =
addSignature(Descriptor.Type(name), pos, kind, properties)
def tpe(name: Name, kind: Kind, properties: Int): Unit =
def tpe(name: Name, kind: Kind, properties: Int): String =
addSignature(Descriptor.Type(name.value), name.pos, kind, properties)
def pkg(name: String, pos: m.Position): Unit = {
def pkg(name: String, pos: m.Position): String = {
addSignature(Descriptor.Package(name), pos, Kind.PACKAGE, 0)
}
def pkg(ref: Term): Unit =
Expand All @@ -142,7 +142,7 @@ trait MtagsIndexer {
definition: m.Position,
kind: s.SymbolInformation.Kind,
properties: Int
): Unit = {
): String = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed addSignature to return the added symbol so we can use those symbols as a next owner.

val previousOwner = currentOwner
currentOwner = symbol(signature)
myLastCurrentOwner = currentOwner
Expand All @@ -163,6 +163,7 @@ trait MtagsIndexer {
displayName = signature.name.value
)
visitOccurrence(occ, info, previousOwner)
syntax
}
def symbol(signature: Descriptor): String =
if (currentOwner.eq(Symbols.EmptyPackage) && signature.isPackage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,28 @@ class ScalaToplevelMtags(
expectTemplate: Option[ExpectTemplate]
): Unit = {
def newExpectTemplate: Some[ExpectTemplate] =
Some(ExpectTemplate(indent, currentOwner, false))
Some(ExpectTemplate(indent, currentOwner, false, false))
def newExpectPkgTemplate: Some[ExpectTemplate] =
Some(ExpectTemplate(indent, currentOwner, true))
Some(ExpectTemplate(indent, currentOwner, true, false))
def newExpectExtensionTemplate(owner: String): Some[ExpectTemplate] =
Some(ExpectTemplate(indent, owner, false, true))
def newExpectIgnoreBody: Some[ExpectTemplate] =
Some(
ExpectTemplate(
indent = indent,
owner = currentOwner,
isPackageBody = false,
isExtension = false,
ignoreBody = true
)
)

def needEmitFileOwner(region: Region): Boolean =
!sourceTopLevelAdded && region.produceSourceToplevel
def needToParseBody(expect: ExpectTemplate): Boolean =
includeInnerClasses || expect.isPackageBody
(includeInnerClasses || expect.isPackageBody) && !expect.ignoreBody
def needToParseExtension(expect: ExpectTemplate): Boolean =
includeInnerClasses && expect.isExtension && !expect.ignoreBody
def nextIsNL: Boolean = {
scanner.nextToken()
isNewline
Expand Down Expand Up @@ -114,10 +129,39 @@ class ScalaToplevelMtags(
)
} else
loop(indent, false, currRegion, newExpectTemplate)
case IDENTIFIER
tgodzik marked this conversation as resolved.
Show resolved Hide resolved
if dialect.allowExtensionMethods && data.name == "extension" =>
val nextOwner =
if (
dialect.allowToplevelStatements &&
needEmitFileOwner(currRegion)
) {
sourceTopLevelAdded = true
val pos = newPosition
val srcName = input.filename.stripSuffix(".scala")
val name = s"$srcName$$package"
withOwner(currRegion.owner) {
term(name, pos, Kind.OBJECT, 0)
}
} else currentOwner
scanner.nextToken()
loop(
indent,
isAfterNewline = false,
currRegion,
newExpectExtensionTemplate(nextOwner)
)
case CLASS | TRAIT | OBJECT | ENUM if needEmitMember(currRegion) =>
emitMember(false, currRegion.owner)
loop(indent, isAfterNewline = false, currRegion, newExpectTemplate)
// also covers extension methods because of `def` inside
case DEF if dialect.allowExtensionMethods && currRegion.isExtension =>
acceptTrivia()
val name = newIdentifier
withOwner(currRegion.owner) {
term(name.name, name.pos, Kind.OBJECT, 0)
}
loop(indent, isAfterNewline = false, region, newExpectIgnoreBody)
case DEF | VAL | VAR | GIVEN | TYPE
if dialect.allowToplevelStatements &&
needEmitFileOwner(currRegion) =>
Expand All @@ -139,8 +183,23 @@ class ScalaToplevelMtags(
loop(indent, isAfterNewline = false, region, expectTemplate)
case WHITESPACE if dialect.allowSignificantIndentation =>
if (isNewline) {
scanner.nextToken()
loop(0, isAfterNewline = true, region, expectTemplate)
expectTemplate match {
// extension (x: Int)|
// def foo() = ...
case Some(expect) if needToParseExtension(expect) =>
val next =
expect.startIndentedRegion(currRegion, expect.isExtension)
resetRegion(next)
scanner.nextToken()
loop(0, isAfterNewline = true, next, None)
// basically for braceless def
case Some(expect) if expect.ignoreBody =>
val nextIndent = acceptWhileIndented(expect.indent)
loop(nextIndent, isAfterNewline = false, currRegion, None)
case _ =>
scanner.nextToken()
loop(0, isAfterNewline = true, region, expectTemplate)
}
} else {
val nextIndentLevel =
if (isAfterNewline) indent + 1 else indent
Expand All @@ -163,8 +222,10 @@ class ScalaToplevelMtags(
}
case LBRACE =>
expectTemplate match {
case Some(expect) if needToParseBody(expect) =>
val next = expect.startInBraceRegion(currRegion)
case Some(expect)
if needToParseBody(expect) || needToParseExtension(expect) =>
val next =
expect.startInBraceRegion(currRegion, expect.isExtension)
resetRegion(next)
scanner.nextToken()
loop(indent, isAfterNewline = false, next, None)
Expand All @@ -175,7 +236,7 @@ class ScalaToplevelMtags(
}
case RBRACE =>
val nextRegion = currRegion match {
case Region.InBrace(_, prev) => resetRegion(prev)
case Region.InBrace(_, prev, _) => resetRegion(prev)
case r => r
}
scanner.nextToken()
Expand Down Expand Up @@ -389,7 +450,9 @@ object ScalaToplevelMtags {
final case class ExpectTemplate(
indent: Int,
owner: String,
isPackageBody: Boolean
isPackageBody: Boolean,
isExtension: Boolean = false,
ignoreBody: Boolean = false
) {

/**
Expand All @@ -402,11 +465,11 @@ object ScalaToplevelMtags {
private def adjustRegion(r: Region): Region =
if (isPackageBody) r.prev else r

def startInBraceRegion(prev: Region): Region =
Region.InBrace(owner, adjustRegion(prev))
def startInBraceRegion(prev: Region, extension: Boolean = false): Region =
Region.InBrace(owner, adjustRegion(prev), extension)

def startIndentedRegion(prev: Region): Region =
Region.Indented(owner, indent, adjustRegion(prev))
def startIndentedRegion(prev: Region, extension: Boolean = false): Region =
Region.Indented(owner, indent, adjustRegion(prev), extension)

}

Expand All @@ -415,6 +478,7 @@ object ScalaToplevelMtags {
def owner: String
def acceptMembers: Boolean
def produceSourceToplevel: Boolean
def isExtension: Boolean = false
Copy link
Member Author

@tanishiking tanishiking Jul 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Added isExntension flag to ExpectTemplate (and Region).
  • if ExpectTemplate.isExtension is true on newline or lbrace, as needsToParseExtension says, enter to new Region where extension is true.
  • In those regions, we will index the symbol of DEFs.

}

object Region {
Expand All @@ -431,16 +495,26 @@ object ScalaToplevelMtags {
val produceSourceToplevel: Boolean = true
}

final case class InBrace(owner: String, prev: Region) extends Region {
final case class InBrace(
owner: String,
prev: Region,
extension: Boolean = false
) extends Region {
def acceptMembers: Boolean =
owner.endsWith("/")
val produceSourceToplevel: Boolean = false
override def isExtension = extension
}
final case class Indented(owner: String, exitIndent: Int, prev: Region)
extends Region {
final case class Indented(
owner: String,
exitIndent: Int,
prev: Region,
extension: Boolean = false
) extends Region {
def acceptMembers: Boolean =
owner.endsWith("/")
val produceSourceToplevel: Boolean = false
override def isExtension = extension
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package tests.feature

import scala.meta.internal.metals.codeactions.ConvertToNamedArguments
import scala.meta.internal.metals.codeactions.CreateNewSymbol
import scala.meta.internal.metals.codeactions.ExtractValueCodeAction
import scala.meta.internal.metals.codeactions.ImportMissingSymbol
import scala.meta.internal.metals.{BuildInfo => V}

Expand Down Expand Up @@ -41,4 +43,73 @@ class ImportMissingSymbolCrossLspSuite
)
} yield ()
}

checkEdit(
"extension-import",
s"""|/metals.json
|{
| "a":{"scalaVersion" : "${V.scala3}"},
| "b":{
| "scalaVersion" : "${V.scala3}",
| "dependsOn": ["a"]
| }
|}
|/a/src/main/scala/example/A.scala
|package example
|object IntEnrichment:
| extension (num: Int)
| def incr = num + 1
|
|/b/src/main/scala/x/B.scala
|package x
|def main =
| println(1.<<incr>>)
|""".stripMargin,
s"""|${ImportMissingSymbol.title("incr", "example.IntEnrichment")}
|${ExtractValueCodeAction.title("1.incr")}
|${ConvertToNamedArguments.title("println(...)")}
|""".stripMargin,
s"""|package x
|
|import example.IntEnrichment.incr
|def main =
| println(1.incr)
|""".stripMargin,
)

checkEdit(
"toplevel-extension-import",
s"""|/metals.json
|{
| "a":{"scalaVersion" : "${V.scala3}"},
| "b":{
| "scalaVersion" : "${V.scala3}",
| "dependsOn": ["a"]
| }
|}
|/a/src/main/scala/example/A.scala
|package example
|
|extension (str: String)
| def identity = str
|
|extension (num: Int)
| def incr = num + 1
|
|/b/src/main/scala/x/B.scala
|package x
|def main =
| println(1.<<incr>>)
|""".stripMargin,
s"""|${ImportMissingSymbol.title("incr", "example.A$package")}
|${ExtractValueCodeAction.title("1.incr")}
|${ConvertToNamedArguments.title("println(...)")}
|""".stripMargin,
s"""|package x
|
|import example.incr
|def main =
| println(1.incr)
|""".stripMargin,
)
}
Loading