diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 210cc32f96f2..b9d412579a15 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -30,6 +30,8 @@ import printing._ import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease} import classfile.ReusableDataReader import StdNames.nme +import parsing.Parsers.EnclosingSpan +import util.Spans.NoSpan import scala.annotation.internal.sharable @@ -482,7 +484,7 @@ object Contexts: /** A new context that summarizes an import statement */ def importContext(imp: Import[?], sym: Symbol, enteringSyms: Boolean = false): FreshContext = - fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr).tap(ii => if enteringSyms && ctx.settings.WunusedHas.imports then usages += ii)) + fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr, imp.attachmentOrElse(EnclosingSpan, NoSpan)).tap(ii => if enteringSyms && ctx.settings.WunusedHas.imports then usages += ii)) /** Is the debug option set? */ def debug: Boolean = base.settings.Ydebug.value diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 28f02b7db2a0..0401b6c47914 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -7,7 +7,7 @@ import scala.language.unsafeNulls import scala.annotation.internal.sharable import scala.collection.mutable.ListBuffer import scala.collection.immutable.BitSet -import util.{ SourceFile, SourcePosition, NoSourcePosition } +import util.{Property, SourceFile, SourcePosition, NoSourcePosition} import Tokens._ import Scanners._ import xml.MarkupParsers.MarkupParser @@ -64,6 +64,8 @@ object Parsers { val QuotedPattern = 1 << 2 } + val EnclosingSpan: Property.Key[Span] = Property.Key() + extension (buf: ListBuffer[Tree]) def +++=(x: Tree) = x match { case x: Thicket => buf ++= x.trees @@ -3182,7 +3184,7 @@ object Parsers { /** Import ::= `import' ImportExpr {‘,’ ImportExpr} * Export ::= `export' ImportExpr {‘,’ ImportExpr} */ - def importOrExportClause(leading: Token, mkTree: ImportConstr): List[Tree] = { + def importOrExportClause(leading: Token, mkTree: ImportConstr): List[Tree] = val offset = accept(leading) commaSeparated(importExpr(mkTree)) match { case t :: rest => @@ -3190,10 +3192,12 @@ object Parsers { val firstPos = if (t.span.exists) t.span.withStart(offset) else Span(offset, in.lastOffset) - t.withSpan(firstPos) :: rest + val imports = t.withSpan(firstPos) :: rest + val enclosing = imports.head.span union imports.last.span + imports.foreach(_.putAttachment(EnclosingSpan, enclosing)) + imports case nil => nil } - } def exportClause() = importOrExportClause(EXPORT, Export(_,_)) diff --git a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index 2868155d0013..34d771063995 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -6,6 +6,7 @@ import ast.{tpd, untpd} import core._ import printing.{Printer, Showable} import util.SimpleIdentityMap +import util.Spans.* import Symbols._, Names._, Types._, Contexts._, StdNames._, Flags._ import Implicits.{ImportedImplicitRef, RenamedImplicitRef} import StdNames.nme @@ -32,7 +33,7 @@ object ImportInfo { val expr = tpd.Ident(ref.refFn()) // refFn must be called in the context of ImportInfo.sym tpd.Import(expr, selectors).symbol - ImportInfo(sym, selectors, untpd.EmptyTree, isRootImport = true) + ImportInfo(sym, selectors, untpd.EmptyTree, NoSpan, isRootImport = true) extension (c: Context) def withRootImports(rootRefs: List[RootRef])(using Context): Context = @@ -48,12 +49,14 @@ object ImportInfo { * @param selectors The selector clauses * @param qualifier The import qualifier, or EmptyTree for root imports. * Defined for all explicit imports from ident or select nodes. + * @param enclosingSpan Span of the enclosing import statement * @param isRootImport true if this is one of the implicit imports of scala, java.lang, * scala.Predef in the start context, false otherwise. */ class ImportInfo(symf: Context ?=> Symbol, val selectors: List[untpd.ImportSelector], val qualifier: untpd.Tree, + val enclosingSpan: Span, val isRootImport: Boolean = false) extends Showable { private def symNameOpt = qualifier match { diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index da4ef331da8d..49c1fd26eeaf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -6,7 +6,8 @@ import core._ import ast._ import Trees._, StdNames._, Scopes._, Denotations._, NamerOps._, ContextOps._ import Contexts._, Symbols._, Types._, SymDenotations._, Names._, NameOps._, Flags._ -import Decorators._, Comments.{_, given} +import Decorators._ +import Comments.{_, given} import NameKinds.DefaultGetterName import ast.desugar, ast.desugar._ import ProtoTypes._ diff --git a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala index 2ff1d6f5eaf0..f5f54d3d847f 100644 --- a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala +++ b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala @@ -2,7 +2,6 @@ package dotty.tools package dotc package typer -import ast.untpd import core._ import Phases._ import Contexts._ @@ -57,18 +56,55 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase { JavaChecks.check(unit.tpdTree) } + /** Report unused imports. + * + * If `-Yrewrite-imports`, emit patches instead. + * Patches are applied if `-rewrite` and no errors. + */ def emitDiagnostics(using Context): Unit = - ctx.usages.unused.foreach { (info, owner, selectors) => - import rewrites.Rewrites.patch - import parsing.Parsers - import util.SourceFile - import ast.Trees.* - def reportSelectors() = selectors.foreach(selector => report.warning(s"Unused import", pos = selector.srcPos)) - if ctx.settings.YrewriteImports.value then + import ast.NavigateAST.untypedPath + import ast.Trees.* + import ast.untpd + import parsing.Parsers + import rewrites.Rewrites.patch + import util.SourceFile + def reportSelectors(sels: List[untpd.ImportSelector]) = sels.foreach(sel => report.warning(s"Unused import", pos = sel.srcPos)) + // format the selectors without braces, as replacement text + def toText(sels: List[untpd.ImportSelector]): String = + def selected(sel: untpd.ImportSelector) = + if sel.isGiven then "given" + else if sel.isWildcard then "*" + else if sel.name == sel.rename then sel.name.show + else s"${sel.name.show} as ${sel.rename.show}" + sels.map(selected).mkString(", ") + // begin + val unused = ctx.usages.unused + if ctx.settings.YrewriteImports.value then + val byLocation = unused.groupBy((info, owner, selectors) => info.qualifier.sourcePos.withSpan(info.enclosingSpan)) + byLocation.foreach { (enclosingPos, grouped) => + val importText = enclosingPos.spanText + val lineSource = SourceFile.virtual(name = "import-line.scala", content = importText) + val PackageDef(_, pieces) = Parsers.Parser(lineSource).parse(): @unchecked + println(s"pieces are $pieces") + grouped match { + case (info, owners, selectors) :: rest => + println(s"info enclosing ${info.enclosingSpan} has qual ${info.qualifier.sourcePos}") + println(s"untyped path from qual\n${ untypedPath(info.qualifier.sourcePos.span).mkString("\n") }") + //println(s"untyped path\n${ untypedPath(info.enclosingSpan).mkString("\n") }") + case _ => + println(s"I got nothing") + } + } + /* + ctx.usages.unused.foreach { (info, owner, selectors) => + println(s"PATCH enclosing ${info.enclosingSpan} in ${info.qualifier.sourcePos.withSpan(info.enclosingSpan).spanText}") val src = ctx.compilationUnit.source val infoPos = info.qualifier.sourcePos - val lineSource = SourceFile.virtual(name = "import-line.scala", content = infoPos.lineContent) + //val importText = infoPos.lineContent + val importText = infoPos.withSpan(info.enclosingSpan).spanText + val lineSource = SourceFile.virtual(name = "import-line.scala", content = importText) val PackageDef(_, pieces) = Parsers.Parser(lineSource).parse(): @unchecked + println(s"ENCLOSING has ${pieces.length} parts") // patch if there's just one import on the line, i.e., not import a.b, c.d if pieces.length == 1 then val retained = info.selectors.filterNot(selectors.contains) @@ -86,19 +122,12 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase { patch(src, widened, toText(retained)) // try to remove braces else patch(src, selectorSpan, toText(retained)) - else - reportSelectors() - else - reportSelectors() - } - // just the selectors, no need to add braces - private def toText(retained: List[untpd.ImportSelector])(using Context): String = - def selected(sel: untpd.ImportSelector) = - if sel.isGiven then "given" - else if sel.isWildcard then "*" - else if sel.name == sel.rename then sel.name.show - else s"${sel.name.show} as ${sel.rename.show}" - retained.map(selected).mkString(", ") + } + */ + else + ctx.usages.unused.foreach { (info, owner, selectors) => reportSelectors(selectors) } + end emitDiagnostics + def clearDiagnostics()(using Context): Unit = ctx.usages.clear() diff --git a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala index 29f9a34d2292..d6f273a1661c 100644 --- a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala +++ b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala @@ -33,6 +33,12 @@ extends SrcPos, interfaces.SourcePosition, Showable { def linesSlice: Array[Char] = source.content.slice(source.startOfLine(start), source.nextLine(end)) + /** Extract exactly the span from the source file. */ + def spanSlice: Array[Char] = source.content.slice(start, end) + + /** Extract exactly the span from the source file as a String. */ + def spanText: String = String(spanSlice) + /** The lines of the position */ def lines: Range = { val startOffset = source.offsetToLine(start) diff --git a/project/Build.scala b/project/Build.scala index 4e9606b476bd..9b9917c4cec5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -83,6 +83,11 @@ object Build { val referenceVersion = "3.2.0-RC2" val baseVersion = "3.2.1-RC1" + //val referenceVersion = "3.1.3-RC2" + //val referenceVersion = "3.2.0-RC1-bin-SNAPSHOT" + + //val baseVersion = "3.2.0-RC1" + //val baseVersion = "3.2.0-RC2" // Versions used by the vscode extension to create a new project // This should be the latest published releases. @@ -790,10 +795,11 @@ object Build { "-Ddotty.tests.classes.dottyTastyInspector=" + jars("scala3-tasty-inspector"), ) }, - //scalacOptions ++= Seq( - // "-Wunused:imports", - // "-rewrite", - //), + scalacOptions ++= Seq( + //"-Wunused:imports", + //"-rewrite", + //"-Yrewrite-imports", + ), packageAll := { (`scala3-compiler` / packageAll).value ++ Seq( "scala3-compiler" -> (Compile / packageBin).value.getAbsolutePath,