Skip to content

Commit 86f40c2

Browse files
authored
Merge pull request scala#10482 from lrytz/quickfix
2 parents 21c6bbf + cbd18f3 commit 86f40c2

27 files changed

+323
-72
lines changed

src/compiler/scala/tools/nsc/Reporting.scala

Lines changed: 159 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ package scala
1414
package tools
1515
package nsc
1616

17+
import java.io.IOException
18+
import java.nio.charset.Charset
19+
import java.nio.file.{Files, Path, Paths}
1720
import java.util.regex.PatternSyntaxException
18-
import scala.annotation.nowarn
21+
import scala.annotation.{nowarn, tailrec}
1922
import scala.collection.mutable
2023
import scala.reflect.internal
2124
import scala.reflect.internal.util.StringOps.countElementsAsString
22-
import scala.reflect.internal.util.{CodeAction, NoSourceFile, Position, SourceFile}
25+
import scala.reflect.internal.util.{CodeAction, NoSourceFile, Position, SourceFile, TextEdit}
2326
import scala.tools.nsc.Reporting.Version.{NonParseableVersion, ParseableVersion}
2427
import scala.tools.nsc.Reporting._
2528
import scala.tools.nsc.settings.NoScalaVersion
@@ -62,13 +65,50 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
6265
else conf
6366
}
6467

68+
private lazy val quickfixFilters = {
69+
if (settings.quickfix.isSetByUser && settings.quickfix.value.isEmpty) {
70+
globalError(s"Missing message filter for `-quickfix`; see `-quickfix:help` or use `-quickfix:any` to apply all available quick fixes.")
71+
Nil
72+
} else {
73+
val parsed = settings.quickfix.value.map(WConf.parseFilter(_, rootDirPrefix))
74+
val msgs = parsed.collect { case Left(msg) => msg }
75+
if (msgs.nonEmpty) {
76+
globalError(s"Failed to parse `-quickfix` filters: ${settings.quickfix.value.mkString(",")}\n${msgs.mkString("\n")}")
77+
Nil
78+
} else parsed.collect { case Right(f) => f }
79+
}
80+
}
81+
82+
private val skipRewriteAction = Set(Action.WarningSummary, Action.InfoSummary, Action.Silent)
83+
84+
private def registerTextEdit(m: Message): Boolean =
85+
if (quickfixFilters.exists(f => f.matches(m))) {
86+
textEdits.addAll(m.actions.flatMap(_.edits))
87+
true
88+
}
89+
else false
90+
91+
private def registerErrorTextEdit(pos: Position, msg: String, actions: List[CodeAction]): Boolean = {
92+
val matches = quickfixFilters.exists({
93+
case MessageFilter.Any => true
94+
case mp: MessageFilter.MessagePattern => mp.check(msg)
95+
case sp: MessageFilter.SourcePattern => sp.check(pos)
96+
case _ => false
97+
})
98+
if (matches)
99+
textEdits.addAll(actions.flatMap(_.edits))
100+
matches
101+
}
102+
65103
private val summarizedWarnings: mutable.Map[WarningCategory, mutable.LinkedHashMap[Position, Message]] = mutable.HashMap.empty
66104
private val summarizedInfos: mutable.Map[WarningCategory, mutable.LinkedHashMap[Position, Message]] = mutable.HashMap.empty
67105

68106
private val suppressions: mutable.LinkedHashMap[SourceFile, mutable.ListBuffer[Suppression]] = mutable.LinkedHashMap.empty
69107
private val suppressionsComplete: mutable.Set[SourceFile] = mutable.Set.empty
70108
private val suspendedMessages: mutable.LinkedHashMap[SourceFile, mutable.LinkedHashSet[Message]] = mutable.LinkedHashMap.empty
71109

110+
private val textEdits: mutable.Set[TextEdit] = mutable.Set.empty
111+
72112
// Used in REPL. The old run is used for parsing. Don't discard its suspended warnings.
73113
def initFrom(old: PerRunReporting): Unit = {
74114
suspendedMessages ++= old.suspendedMessages
@@ -100,6 +140,10 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
100140
sups <- suppressions.remove(source)
101141
sup <- sups.reverse
102142
} if (!sup.used && !sup.synthetic) issueWarning(Message.Plain(sup.annotPos, "@nowarn annotation does not suppress any warnings", WarningCategory.UnusedNowarn, "", Nil))
143+
144+
// apply quick fixes
145+
quickfix(textEdits)
146+
textEdits.clear()
103147
}
104148

105149
def reportSuspendedMessages(unit: CompilationUnit): Unit = {
@@ -119,6 +163,14 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
119163
}
120164

121165
private def issueWarning(warning: Message): Unit = {
166+
val action = wconf.action(warning)
167+
168+
val quickfixed = {
169+
if (!skipRewriteAction(action) && registerTextEdit(warning)) s"[rewritten by -quickfix] ${warning.msg}"
170+
else if (warning.actions.exists(_.edits.nonEmpty)) s"[quick fix available] ${warning.msg}"
171+
else warning.msg
172+
}
173+
122174
def ifNonEmpty(kind: String, filter: String) = if (filter.nonEmpty) s", $kind=$filter" else ""
123175
def filterHelp =
124176
s"msg=<part of the message>, cat=${warning.category.name}" +
@@ -133,12 +185,13 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
133185
"\nScala 3 migration messages are errors under -Xsource:3. Use -Wconf / @nowarn to filter them or add -Xmigration to demote them to warnings."
134186
else ""
135187
def helpMsg(kind: String, isError: Boolean = false) =
136-
s"${warning.msg}${scala3migration(isError)}\nApplicable -Wconf / @nowarn filters for this $kind: $filterHelp"
137-
wconf.action(warning) match {
188+
s"$quickfixed${scala3migration(isError)}\nApplicable -Wconf / @nowarn filters for this $kind: $filterHelp"
189+
190+
action match {
138191
case Action.Error => reporter.error(warning.pos, helpMsg("fatal warning", isError = true), warning.actions)
139-
case Action.Warning => reporter.warning(warning.pos, warning.msg, warning.actions)
192+
case Action.Warning => reporter.warning(warning.pos, quickfixed, warning.actions)
140193
case Action.WarningVerbose => reporter.warning(warning.pos, helpMsg("warning"), warning.actions)
141-
case Action.Info => reporter.echo(warning.pos, warning.msg, warning.actions)
194+
case Action.Info => reporter.echo(warning.pos, quickfixed, warning.actions)
142195
case Action.InfoVerbose => reporter.echo(warning.pos, helpMsg("message"), warning.actions)
143196
case a @ (Action.WarningSummary | Action.InfoSummary) =>
144197
val m = summaryMap(a, warning.category.summaryCategory)
@@ -299,6 +352,16 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
299352
def warning(pos: Position, msg: String, category: WarningCategory, site: Symbol, origin: String): Unit =
300353
issueIfNotSuppressed(Message.Origin(pos, msg, category, siteName(site), origin, actions = Nil))
301354

355+
// Remember CodeActions that match `-quickfix` and report the error through the reporter
356+
def error(pos: Position, msg: String, actions: List[CodeAction]): Unit = {
357+
val quickfixed = {
358+
if (registerErrorTextEdit(pos, msg, actions)) s"[rewritten by -quickfix] $msg"
359+
else if (actions.exists(_.edits.nonEmpty)) s"[quick fix available] $msg"
360+
else msg
361+
}
362+
reporter.error(pos, quickfixed, actions)
363+
}
364+
302365
// used by Global.deprecationWarnings, which is used by sbt
303366
def deprecationWarnings: List[(Position, String)] = summaryMap(Action.WarningSummary, WarningCategory.Deprecation).toList.map(p => (p._1, p._2.msg))
304367
def uncheckedWarnings: List[(Position, String)] = summaryMap(Action.WarningSummary, WarningCategory.Unchecked).toList.map(p => (p._1, p._2.msg))
@@ -330,6 +393,91 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
330393
if (settings.fatalWarnings.value && reporter.hasWarnings)
331394
reporter.error(NoPosition, "No warnings can be incurred under -Werror.")
332395
}
396+
397+
private object quickfix {
398+
/** Source code at a position. Either a line with caret (offset), else the code at the range position. */
399+
def codeOf(pos: Position, source: SourceFile): String =
400+
if (pos.start < pos.end) new String(source.content.slice(pos.start, pos.end))
401+
else {
402+
val line = source.offsetToLine(pos.point)
403+
val code = source.lines(line).next()
404+
val caret = " " * (pos.point - source.lineToOffset(line)) + "^"
405+
s"$code\n$caret"
406+
}
407+
408+
409+
def checkNoOverlap(patches: List[TextEdit], source: SourceFile): Boolean = {
410+
var ok = true
411+
for (List(p1, p2) <- patches.sliding(2) if p1.position.end > p2.position.start) {
412+
ok = false
413+
val msg =
414+
s"""overlapping quick fixes in ${source.file.file.getAbsolutePath}:
415+
|
416+
|add `${p1.newText}` at
417+
|${codeOf(p1.position, source)}
418+
|
419+
|add `${p2.newText}` at
420+
|${codeOf(p2.position, source)}""".stripMargin.trim
421+
issueWarning(Message.Plain(p1.position, msg, WarningCategory.Other, "", Nil))
422+
}
423+
ok
424+
}
425+
426+
def underlyingFile(source: SourceFile): Option[Path] = {
427+
val fileClass = source.file.getClass.getName
428+
val p = if (fileClass.endsWith("xsbt.ZincVirtualFile")) {
429+
import scala.language.reflectiveCalls
430+
val path = source.file.asInstanceOf[ {def underlying(): {def id(): String}}].underlying().id()
431+
Some(Paths.get(path))
432+
} else
433+
Option(source.file.file).map(_.toPath)
434+
val r = p.filter(Files.exists(_))
435+
if (r.isEmpty)
436+
issueWarning(Message.Plain(NoPosition, s"Failed to apply quick fixes, file does not exist: ${source.file}", WarningCategory.Other, "", Nil))
437+
r
438+
}
439+
440+
val encoding = Charset.forName(settings.encoding.value)
441+
442+
def insertEdits(sourceChars: Array[Char], edits: List[TextEdit], file: Path): Array[Byte] = {
443+
val patchedChars = new Array[Char](sourceChars.length + edits.iterator.map(_.delta).sum)
444+
@tailrec def loop(edits: List[TextEdit], inIdx: Int, outIdx: Int): Unit = {
445+
def copy(upTo: Int): Int = {
446+
val untouched = upTo - inIdx
447+
System.arraycopy(sourceChars, inIdx, patchedChars, outIdx, untouched)
448+
outIdx + untouched
449+
}
450+
edits match {
451+
case e :: es =>
452+
val outNew = copy(e.position.start)
453+
e.newText.copyToArray(patchedChars, outNew)
454+
loop(es, e.position.end, outNew + e.newText.length)
455+
case _ =>
456+
val outNew = copy(sourceChars.length)
457+
if (outNew != patchedChars.length)
458+
issueWarning(Message.Plain(NoPosition, s"Unexpected content length when applying quick fixes; verify the changes to ${file.toFile.getAbsolutePath}", WarningCategory.Other, "", Nil))
459+
}
460+
}
461+
462+
loop(edits, 0, 0)
463+
new String(patchedChars).getBytes(encoding)
464+
}
465+
466+
def apply(edits: mutable.Set[TextEdit]): Unit = {
467+
for ((source, edits) <- edits.groupBy(_.position.source).view.mapValues(_.toList.sortBy(_.position.start))) {
468+
if (checkNoOverlap(edits, source)) {
469+
underlyingFile(source) foreach { file =>
470+
val sourceChars = new String(Files.readAllBytes(file), encoding).toCharArray
471+
try Files.write(file, insertEdits(sourceChars, edits, file))
472+
catch {
473+
case e: IOException =>
474+
issueWarning(Message.Plain(NoPosition, s"Failed to apply quick fixes to ${file.toFile.getAbsolutePath}\n${e.getMessage}", WarningCategory.Other, "", Nil))
475+
}
476+
}
477+
}
478+
}
479+
}
480+
}
333481
}
334482
}
335483

@@ -532,7 +680,8 @@ object Reporting {
532680
}
533681

534682
final case class MessagePattern(pattern: Regex) extends MessageFilter {
535-
def matches(message: Message): Boolean = pattern.findFirstIn(message.msg).nonEmpty
683+
def check(msg: String) = pattern.findFirstIn(msg).nonEmpty
684+
def matches(message: Message): Boolean = check(message.msg)
536685
}
537686

538687
final case class SitePattern(pattern: Regex) extends MessageFilter {
@@ -542,10 +691,11 @@ object Reporting {
542691
final case class SourcePattern(pattern: Regex) extends MessageFilter {
543692
private[this] val cache = mutable.Map.empty[SourceFile, Boolean]
544693

545-
def matches(message: Message): Boolean = cache.getOrElseUpdate(message.pos.source, {
546-
val sourcePath = message.pos.source.file.canonicalPath.replace("\\", "/")
694+
def check(pos: Position) = cache.getOrElseUpdate(pos.source, {
695+
val sourcePath = pos.source.file.canonicalPath.replace("\\", "/")
547696
pattern.findFirstIn(sourcePath).nonEmpty
548697
})
698+
def matches(message: Message): Boolean = check(message.pos)
549699
}
550700

551701
final case class DeprecatedOrigin(pattern: Regex) extends MessageFilter {

src/compiler/scala/tools/nsc/ast/parser/Parsers.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,11 @@ self =>
251251
val syntaxErrors = new ListBuffer[(Int, String, List[CodeAction])]
252252
def showSyntaxErrors() =
253253
for ((offset, msg, actions) <- syntaxErrors)
254-
reporter.error(o2p(offset), msg, actions)
254+
runReporting.error(o2p(offset), msg, actions)
255255

256256
override def syntaxError(offset: Offset, msg: String, actions: List[CodeAction]): Unit = {
257257
if (smartParsing) syntaxErrors += ((offset, msg, actions))
258-
else reporter.error(o2p(offset), msg, actions)
258+
else runReporting.error(o2p(offset), msg, actions)
259259
}
260260

261261
override def incompleteInputError(msg: String, actions: List[CodeAction]): Unit = {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ trait StandardScalaSettings { _: MutableSettings =>
5151
val nowarn = BooleanSetting ("-nowarn", "Generate no warnings.") withAbbreviation "--no-warnings" withPostSetHook { s => if (s.value) maxwarns.value = 0 }
5252
val optimise: BooleanSetting // depends on post hook which mutates other settings
5353
val print = BooleanSetting ("-print", "Print program with Scala-specific features removed.") withAbbreviation "--print"
54+
val quickfix = MultiStringSetting(
55+
"-quickfix",
56+
"filters",
57+
"Apply quick fixes provided by the compiler for warnings and errors to source files",
58+
helpText = Some(
59+
"""Apply quick fixes provided by the compiler for warnings and errors to source files.
60+
|Syntax: -quickfix:<filter>,...,<filter>
61+
|
62+
|<filter> syntax is the same as for configurable warnings, see `-Wconf:help`. Examples:
63+
| -quickfix:any apply all available quick fixes
64+
| -quickfix:msg=Auto-application apply quick fixes where the message contains "Auto-application"
65+
|
66+
|Use `-Wconf:any:warning-verbose` to display applicable message filters with each warning.
67+
|""".stripMargin),
68+
prepend = true)
5469
val release =
5570
ChoiceSetting("-release", "release", "Compile for a version of the Java API and target class file.", AllTargetVersions, normalizeTarget(javaSpecVersion))
5671
.withPostSetHook { setting =>

src/reflect/scala/reflect/internal/util/CodeAction.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ case class CodeAction(title: String, description: Option[String], edits: List[Te
3636
* @groupname Common Commonly used methods
3737
* @group ReflectionAPI
3838
*/
39-
case class TextEdit(position: Position, newText: String)
39+
case class TextEdit(position: Position, newText: String) {
40+
def delta: Int = newText.length - (position.end - position.start)
41+
}

test/files/neg/auto-application.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ auto-application.scala:5: error: Int does not take parameters
77
auto-application.scala:6: error: Int does not take parameters
88
("": Object).##()
99
^
10-
auto-application.scala:9: warning: Auto-application to `()` is deprecated. Supply the empty argument list `()` explicitly to invoke method meth,
10+
auto-application.scala:9: warning: [quick fix available] Auto-application to `()` is deprecated. Supply the empty argument list `()` explicitly to invoke method meth,
1111
or remove the empty argument list from its definition (Java-defined methods are exempt).
1212
In Scala 3, an unapplied method like this will be eta-expanded into a function.
1313
meth // warn, auto-application (of nilary methods) is deprecated

test/files/neg/for-comprehension-old.check

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
for-comprehension-old.scala:6: error: `val` keyword in for comprehension is unsupported: just remove `val`
1+
for-comprehension-old.scala:6: error: [quick fix available] `val` keyword in for comprehension is unsupported: just remove `val`
22
for (val x <- 1 to 5 ; y = x) yield x+y // fail
33
^
4-
for-comprehension-old.scala:7: error: `val` keyword in for comprehension is unsupported: just remove `val`
4+
for-comprehension-old.scala:7: error: [quick fix available] `val` keyword in for comprehension is unsupported: just remove `val`
55
for (val x <- 1 to 5 ; val y = x) yield x+y // fail
66
^
7-
for-comprehension-old.scala:11: error: `val` keyword in for comprehension is unsupported: just remove `val`
7+
for-comprehension-old.scala:11: error: [quick fix available] `val` keyword in for comprehension is unsupported: just remove `val`
88
for (z <- 1 to 2 ; val x <- 1 to 5 ; y = x) yield x+y // fail
99
^
10-
for-comprehension-old.scala:12: error: `val` keyword in for comprehension is unsupported: just remove `val`
10+
for-comprehension-old.scala:12: error: [quick fix available] `val` keyword in for comprehension is unsupported: just remove `val`
1111
for (z <- 1 to 2 ; val x <- 1 to 5 ; val y = x) yield x+y // fail
1212
^
13-
for-comprehension-old.scala:5: warning: `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
13+
for-comprehension-old.scala:5: warning: [quick fix available] `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
1414
for (x <- 1 to 5 ; val y = x) yield x+y // fail
1515
^
16-
for-comprehension-old.scala:7: warning: `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
16+
for-comprehension-old.scala:7: warning: [quick fix available] `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
1717
for (val x <- 1 to 5 ; val y = x) yield x+y // fail
1818
^
19-
for-comprehension-old.scala:10: warning: `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
19+
for-comprehension-old.scala:10: warning: [quick fix available] `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
2020
for (z <- 1 to 2 ; x <- 1 to 5 ; val y = x) yield x+y // fail
2121
^
22-
for-comprehension-old.scala:12: warning: `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
22+
for-comprehension-old.scala:12: warning: [quick fix available] `val` keyword in for comprehension is deprecated: instead, bind the value without `val`
2323
for (z <- 1 to 2 ; val x <- 1 to 5 ; val y = x) yield x+y // fail
2424
^
2525
4 warnings

0 commit comments

Comments
 (0)