Skip to content

Commit

Permalink
Merge pull request #1728 from bjaglin/scala3unused
Browse files Browse the repository at this point in the history
Support RemoveUnused in Scala 3.4+
  • Loading branch information
bjaglin authored Feb 3, 2024
2 parents d50f8ff + a1097a7 commit e4eb15d
Show file tree
Hide file tree
Showing 32 changed files with 535 additions and 244 deletions.
3 changes: 0 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,6 @@ lazy val input = projectMatrix
noPublishAndNoMima,
scalacOptions ~= (_.filterNot(_ == "-Yno-adapted-args")),
scalacOptions ++= warnAdaptedArgs.value, // For NoAutoTupling
scalacOptions ++= warnUnusedImports.value, // For RemoveUnused
scalacOptions ++= warnUnused.value, // For RemoveUnusedTerms
logLevel := Level.Error, // avoid flood of compiler warnings
libraryDependencies ++= testsDependencies.value,
coverageEnabled := false,
Expand All @@ -213,7 +211,6 @@ lazy val output = projectMatrix
.in(file("scalafix-tests/output"))
.settings(
noPublishAndNoMima,
scalacOptions --= warnUnusedImports.value,
libraryDependencies ++= testsDependencies.value,
coverageEnabled := false,
// mimic dependsOn(shared) but allowing binary Scala version matching
Expand Down
3 changes: 2 additions & 1 deletion docs/developers/symbol-information.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Use `MethodSignature.parameterLists` to look up parameters of a method.
def printMethodParameters(symbol: Symbol): Unit = {
symbol.info.get.signature match {
case signature @ MethodSignature(typeParameters, parameterLists, _) =>
println("signature = " + signature)
if (typeParameters.nonEmpty) {
println("typeParameters")
println(typeParameters.mkString(" ", "\n ", ""))
Expand Down Expand Up @@ -295,7 +296,7 @@ constructor.
```scala mdoc
def getConstructors(symbol: Symbol): List[SymbolInformation] =
symbol.info.get.signature match {
case ClassSignature(_, parents, _, declarations) =>
case ClassSignature(_, _, _, declarations) =>
declarations.filter { declaration =>
declaration.isConstructor
}
Expand Down
70 changes: 50 additions & 20 deletions docs/rules/RemoveUnused.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@ example diff from running `sbt "scalafix RemoveUnused"`.

To use this rule:

- Enable the Scala compiler option `-Ywarn-unused` (or `-Wunused` in 2.13). In
sbt, this is done with `scalacOptions += "-Ywarn-unused"`.
- Enable the Scala compiler option `-Ywarn-unused` (2.12), `-Wunused` (2.13),
or `-Wunused:all` (3.4+).
- Disable `-Xfatal-warnings` if you have it enabled. This is required so the
compiler warnings do not fail the build before running Scalafix. If you are
running 2.12.13+ or 2.13.2+, you may keep `-Xfatal-warnings` by modifying how
specific warnings are handled via `scalacOptions += "-Wconf:cat=unused:info"`.
- This rule **can't work** yet on Scala 3 projects since the compiler option `warn-unused`
is not yet available in Scala 3. You need to remove `RemoveUnused`
from `.scalafix.conf` for Scala 3 projects.

## Examples

Expand Down Expand Up @@ -67,7 +64,7 @@ object Main {
}
```

Remove unused pattern match variables:
Remove unused pattern match variables (requires `-Wunused:unsafe-warn-patvars` on Scala 3):

```scala
case class AB(a: Int, b: String)
Expand All @@ -87,7 +84,7 @@ object Main {
}
```

Remove unused function parameters:
Remove unused function parameters (Scala 2 only):

```scala
// before
Expand Down Expand Up @@ -129,21 +126,54 @@ import scalafix.internal.rule._
println(scalafix.website.rule("RemoveUnused", RemoveUnusedConfig.default))
```

## -Ywarn-unused
## More granular scalac options

Consult `scala -Y` in the command-line for more information about using
`-Ywarn-unused`.
You may request more granular warnings to the compiler if you opt-out
from some rewrites in the rule configuration.

```
$ scala -Ywarn-unused:help
Enable or disable specific `unused' warnings
imports Warn if an import selector is not referenced.
patvars Warn if a variable bound in a pattern is unused.
privates Warn if a private member is unused.
locals Warn if a local definition is unused.
explicits Warn if an explicit parameter is unused.
implicits Warn if an implicit parameter is unused.
params Enable -Ywarn-unused:explicits,implicits.
linted -Xlint:unused.
$ scala212 -Wunused:help
Enable or disable specific `unused` warnings
imports Warn if an import selector is not referenced.
patvars Warn if a variable bound in a pattern is unused.
privates Warn if a private member is unused.
locals Warn if a local definition is unused.
explicits Warn if an explicit parameter is unused.
implicits Warn if an implicit parameter is unused.
synthetics Warn if a synthetic implicit parameter (context bound) is unused.
nowarn Warn if a @nowarn annotation does not suppress any warnings.
params Enable -Wunused:explicits,implicits,synthetics.
linted -Xlint:unused.
Default: All choices are enabled by default.
```

```
$ scala3 -W
...
-Wunused Enable or disable specific `unused` warnings
Choices :
- nowarn,
- all,
- imports :
Warn if an import selector is not referenced.
NOTE : overrided by -Wunused:strict-no-implicit-warn,
- privates :
Warn if a private member is unused,
- locals :
Warn if a local definition is unused,
- explicits :
Warn if an explicit parameter is unused,
- implicits :
Warn if an implicit parameter is unused,
- params :
Enable -Wunused:explicits,implicits,
- linted :
Enable -Wunused:imports,privates,locals,implicits,
- strict-no-implicit-warn :
Same as -Wunused:import, only for imports of explicit named members.
NOTE : This overrides -Wunused:imports and NOT set by -Wunused:all,
- unsafe-warn-patvars :
(UNSAFE) Warn if a variable bound in a pattern is unused.
This warning can generate false positive, as warning cannot be
suppressed yet.
```
2 changes: 1 addition & 1 deletion docs/users/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: Installation

**Java LTS (8, 11, 17 or 21)**

**Scala 2.12, 2.13 or 3.x** (all rules are not available for Scala 3.x)
**Scala 2.12, 2.13 or 3.x**

## sbt

Expand Down
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import sbt._
object Dependencies {
val scala212 = sys.props.getOrElse("scala212.nightly", "2.12.18")
val scala213 = sys.props.getOrElse("scala213.nightly", "2.13.12")
val scala3 = sys.props.getOrElse("scala3.nightly", "3.3.1")
val scala3 = sys.props.getOrElse("scala3.nightly", "3.4.0-RC2")

val bijectionCoreV = "0.9.7"
val collectionCompatV = "2.11.0"
Expand Down
14 changes: 5 additions & 9 deletions project/ScalafixBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,10 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
lazy val isScala212 = Def.setting {
scalaVersion.value.startsWith("2.12")
}
lazy val warnUnusedImports = Def.setting {
if (isScala3.value) Nil
else if (isScala213.value) Seq("-Wunused:imports")
else Seq("-Ywarn-unused-import")
}
lazy val warnUnused = Def.setting {
if (isScala2.value) Seq("-Ywarn-unused")
else Nil
if (isScala3.value) Seq("-Wunused:all", "-Wunused:unsafe-warn-patvars")
else if (isScala213.value) Seq("-Wunused")
else Seq("-Ywarn-unused")
}
lazy val targetJvm = Def.setting {
if (isScala3.value) Seq("-release:8")
Expand All @@ -109,8 +105,8 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
scalaXml +: otherLibs
}
lazy val compilerOptions = Def.setting(
warnUnusedImports.value ++
targetJvm.value ++
targetJvm.value ++
warnUnused.value ++
Seq(
"-encoding",
"UTF-8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,29 @@ class RemoveUnused(config: RemoveUnusedConfig)
def this() = this(RemoveUnusedConfig.default)

override def description: String =
"Removes unused imports and terms that reported by the compiler under -Ywarn-unused"
"Removes unused imports and terms that reported by the compiler under -Wunused"
override def isRewrite: Boolean = true

private def warnUnusedPrefix = List("-Wunused", "-Ywarn-unused")
private def warnUnusedString = List("-Xlint", "-Xlint:unused")
override def withConfiguration(config: Configuration): Configured[Rule] = {
val diagnosticsAvailableInSemanticdb =
Seq("3.0", "3.1", "3.2", "3.3")
.forall(v => !config.scalaVersion.startsWith(v))

val hasWarnUnused = config.scalacOptions.exists(option =>
warnUnusedPrefix.exists(prefix => option.startsWith(prefix)) ||
warnUnusedString.contains(option)
)
if (config.scalaVersion.startsWith("3"))
if (!hasWarnUnused) {
Configured.error(
"This rule is specific to Scala 2, because the compiler option `-Ywarn-unused` is not available yet in scala 3 " +
"To fix this error, remove RemoveUnused from .scalafix.conf"
"""|A Scala compiler option is required to use RemoveUnused. To fix this problem,
|update your build to add -Ywarn-unused (with 2.12), -Wunused (with 2.13), or
|-Wunused:all (with 3.4+)""".stripMargin
)
else if (!hasWarnUnused) {
} else if (!diagnosticsAvailableInSemanticdb) {
Configured.error(
s"""|The Scala compiler option "-Ywarn-unused" is required to use RemoveUnused.
|To fix this problem, update your build to use at least one Scala compiler
|option like -Ywarn-unused, -Xlint:unused (2.12.2 or above), or -Wunused (2.13 only)""".stripMargin
"You must use a more recent version of the Scala 3 compiler (3.4+)"
)
} else {
config.conf
Expand All @@ -64,29 +67,36 @@ class RemoveUnused(config: RemoveUnusedConfig)
raw"^pattern var .* in (value|method) .* is never used".r

doc.diagnostics.foreach { diagnostic =>
if (config.imports && diagnostic.message == "Unused import") {
val msg = diagnostic.message
if (config.imports && diagnostic.message.toLowerCase == "unused import") {
isUnusedImport += diagnostic.position
} else if (
config.privates &&
diagnostic.message.startsWith("private") &&
diagnostic.message.endsWith("is never used")
(
(msg.startsWith("private") && msg.endsWith("is never used")) ||
msg == "unused private member"
)
) {
isUnusedTerm += diagnostic.position
} else if (
config.locals &&
diagnostic.message.startsWith("local") &&
diagnostic.message.endsWith("is never used")
(
(msg.startsWith("local") && msg.endsWith("is never used")) ||
msg == "unused local definition"
)
) {
isUnusedTerm += diagnostic.position
} else if (
config.patternvars &&
unusedPatterExpr.findFirstMatchIn(diagnostic.message).isDefined
(
unusedPatterExpr.findFirstMatchIn(diagnostic.message).isDefined ||
msg == "unused pattern variable"
)
) {
isUnusedPattern += diagnostic.position
} else if (
config.params &&
diagnostic.message.startsWith("parameter") &&
diagnostic.message.endsWith("is never used")
(msg.startsWith("parameter") && msg.endsWith("is never used"))
) {
isUnusedParam += diagnostic.position
}
Expand Down Expand Up @@ -116,29 +126,33 @@ class RemoveUnused(config: RemoveUnusedConfig)
.atomic
}

def isUnusedImportee(importee: Importee): Boolean = {
val pos = importee match {
case Importee.Rename(from, _) => from.pos
case _ => importee.pos
}
isUnusedImport.exists { unused =>
unused.start <= pos.start && pos.end <= unused.end
}
}

doc.tree.collect {
case Importer(_, importees)
if importees.forall(_.is[Importee.Unimport]) =>
importees.map(Patch.removeImportee).asPatch
case Importer(_, importees) =>
val hasUsedWildcard = importees.exists {
case i: Importee.Wildcard => !isUnusedImport(importPosition(i))
case i: Importee.Wildcard => !isUnusedImportee(i)
case _ => false
}
importees.collect {
case i @ Importee.Rename(_, to)
if isUnusedImport(importPosition(i)) && hasUsedWildcard =>
if isUnusedImportee(i) && hasUsedWildcard =>
// Unimport the identifier instead of removing the importee since
// unused renamed may still impact compilation by shadowing an identifier.
// See https://github.com/scalacenter/scalafix/issues/614
Patch.replaceTree(to, "_").atomic
case i
if isUnusedImport
.exists(unused =>
unused.start <= importPosition(i).start && importPosition(
i
).end <= unused.end
) =>
case i if isUnusedImportee(i) =>
Patch.removeImportee(i).atomic
}.asPatch
case i: Defn if isUnusedTerm(i.pos) =>
Expand Down Expand Up @@ -182,11 +196,6 @@ class RemoveUnused(config: RemoveUnusedConfig)
}
}

private def importPosition(importee: Importee): Position = importee match {
case Importee.Rename(from, _) => from.pos
case _ => importee.pos
}

// Given ("val x = 2", "2"), returns "val x = ".
private def leftTokens(t: Tree, right: Tree): Tokens = {
val startT = t.tokens.start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@ case class RemoveUnusedConfig(
privates: Boolean = true,
@Description("Remove unused local definitions")
locals: Boolean = true,
@Description(
"Remove unused pattern match variables (compatible with Scala 2.12 and 2.13)"
)
@Description("Remove unused pattern match variables")
patternvars: Boolean = true,
@Description(
"Remove unused function parameters (compatible with Scala 2.12 and 2.13)"
)
@Description("Remove unused function parameters (Scala 2 only)")
params: Boolean = true
)

Expand Down
Loading

0 comments on commit e4eb15d

Please sign in to comment.