@@ -14,12 +14,15 @@ package scala
14
14
package tools
15
15
package nsc
16
16
17
+ import java .io .IOException
18
+ import java .nio .charset .Charset
19
+ import java .nio .file .{Files , Path , Paths }
17
20
import java .util .regex .PatternSyntaxException
18
- import scala .annotation .nowarn
21
+ import scala .annotation .{ nowarn , tailrec }
19
22
import scala .collection .mutable
20
23
import scala .reflect .internal
21
24
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 }
23
26
import scala .tools .nsc .Reporting .Version .{NonParseableVersion , ParseableVersion }
24
27
import scala .tools .nsc .Reporting ._
25
28
import scala .tools .nsc .settings .NoScalaVersion
@@ -62,13 +65,50 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
62
65
else conf
63
66
}
64
67
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
+
65
103
private val summarizedWarnings : mutable.Map [WarningCategory , mutable.LinkedHashMap [Position , Message ]] = mutable.HashMap .empty
66
104
private val summarizedInfos : mutable.Map [WarningCategory , mutable.LinkedHashMap [Position , Message ]] = mutable.HashMap .empty
67
105
68
106
private val suppressions : mutable.LinkedHashMap [SourceFile , mutable.ListBuffer [Suppression ]] = mutable.LinkedHashMap .empty
69
107
private val suppressionsComplete : mutable.Set [SourceFile ] = mutable.Set .empty
70
108
private val suspendedMessages : mutable.LinkedHashMap [SourceFile , mutable.LinkedHashSet [Message ]] = mutable.LinkedHashMap .empty
71
109
110
+ private val textEdits : mutable.Set [TextEdit ] = mutable.Set .empty
111
+
72
112
// Used in REPL. The old run is used for parsing. Don't discard its suspended warnings.
73
113
def initFrom (old : PerRunReporting ): Unit = {
74
114
suspendedMessages ++= old.suspendedMessages
@@ -100,6 +140,10 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
100
140
sups <- suppressions.remove(source)
101
141
sup <- sups.reverse
102
142
} 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()
103
147
}
104
148
105
149
def reportSuspendedMessages (unit : CompilationUnit ): Unit = {
@@ -119,6 +163,14 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
119
163
}
120
164
121
165
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
+
122
174
def ifNonEmpty (kind : String , filter : String ) = if (filter.nonEmpty) s " , $kind= $filter" else " "
123
175
def filterHelp =
124
176
s " msg=<part of the message>, cat= ${warning.category.name}" +
@@ -133,12 +185,13 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
133
185
" \n Scala 3 migration messages are errors under -Xsource:3. Use -Wconf / @nowarn to filter them or add -Xmigration to demote them to warnings."
134
186
else " "
135
187
def helpMsg (kind : String , isError : Boolean = false ) =
136
- s " ${warning.msg}${scala3migration(isError)}\n Applicable -Wconf / @nowarn filters for this $kind: $filterHelp"
137
- wconf.action(warning) match {
188
+ s " $quickfixed${scala3migration(isError)}\n Applicable -Wconf / @nowarn filters for this $kind: $filterHelp"
189
+
190
+ action match {
138
191
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)
140
193
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)
142
195
case Action .InfoVerbose => reporter.echo(warning.pos, helpMsg(" message" ), warning.actions)
143
196
case a @ (Action .WarningSummary | Action .InfoSummary ) =>
144
197
val m = summaryMap(a, warning.category.summaryCategory)
@@ -299,6 +352,16 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
299
352
def warning (pos : Position , msg : String , category : WarningCategory , site : Symbol , origin : String ): Unit =
300
353
issueIfNotSuppressed(Message .Origin (pos, msg, category, siteName(site), origin, actions = Nil ))
301
354
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
+
302
365
// used by Global.deprecationWarnings, which is used by sbt
303
366
def deprecationWarnings : List [(Position , String )] = summaryMap(Action .WarningSummary , WarningCategory .Deprecation ).toList.map(p => (p._1, p._2.msg))
304
367
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
330
393
if (settings.fatalWarnings.value && reporter.hasWarnings)
331
394
reporter.error(NoPosition , " No warnings can be incurred under -Werror." )
332
395
}
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
+ }
333
481
}
334
482
}
335
483
@@ -532,7 +680,8 @@ object Reporting {
532
680
}
533
681
534
682
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)
536
685
}
537
686
538
687
final case class SitePattern (pattern : Regex ) extends MessageFilter {
@@ -542,10 +691,11 @@ object Reporting {
542
691
final case class SourcePattern (pattern : Regex ) extends MessageFilter {
543
692
private [this ] val cache = mutable.Map .empty[SourceFile , Boolean ]
544
693
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(" \\ " , " /" )
547
696
pattern.findFirstIn(sourcePath).nonEmpty
548
697
})
698
+ def matches (message : Message ): Boolean = check(message.pos)
549
699
}
550
700
551
701
final case class DeprecatedOrigin (pattern : Regex ) extends MessageFilter {
0 commit comments