-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathMessage.scala
442 lines (384 loc) · 18.2 KB
/
Message.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
package dotty.tools
package dotc
package reporting
import core.*
import Contexts.*, Decorators.*, Symbols.*, Types.*, Flags.*
import printing.{RefinedPrinter, MessageLimiter, ErrorMessageLimiter}
import printing.Texts.Text
import printing.Formatting.hl
import config.SourceVersion
import scala.language.unsafeNulls
import scala.annotation.threadUnsafe
/** ## Tips for error message generation
*
* - You can use the `em` interpolator for error messages. It's defined in core.Decorators.
* - You can also use a simple string argument for `error` or `warning` (not for the other variants),
* but the string should not be interpolated or composed of objects that require a
* Context for evaluation.
* - When embedding interpolated substrings defined elsewhere in error messages,
* use `i` and make sure they are defined as def's instead of vals. That way, the
* possibly expensive interpolation will performed only in the case where the message
* is eventually printed. Note: At least during typer, it's common for messages
* to be discarded without being printed. Also, by making them defs, you ensure that
* they will be evaluated in the Message context, which makes formatting safer
* and more robust.
* - For common messages, or messages that might require explanation, prefer defining
* a new `Message` class in file `messages.scala` and use that instead. The advantage is that these
* messages have unique IDs that can be referenced elsewhere.
*/
object Message:
def rewriteNotice(what: String, version: SourceVersion | Null = null, options: String = "")(using Context): String =
if !ctx.mode.is(Mode.Interactive) then
val sourceStr = if version != null then i"-source $version" else ""
val optionStr =
if options.isEmpty then sourceStr
else if sourceStr.isEmpty then options
else i"$sourceStr $options"
i"\n$what can be rewritten automatically under -rewrite $optionStr."
else ""
private type Recorded = Symbol | ParamRef | SkolemType
private case class SeenKey(str: String, isType: Boolean)
/** A class that records printed items of one of the types in `Recorded`,
* adds superscripts for disambiguations, and can explain recorded symbols
* in ` where` clause
*/
private class Seen(disambiguate: Boolean):
/** The set of lambdas that were opened at some point during printing. */
private val openedLambdas = new collection.mutable.HashSet[LambdaType]
/** Register that `tp` was opened during printing. */
def openLambda(tp: LambdaType): Unit =
openedLambdas += tp
val seen = new collection.mutable.HashMap[SeenKey, List[Recorded]].withDefaultValue(Nil)
var nonSensical = false
/** If false, stop all recordings */
private var recordOK = disambiguate
/** Clear all entries and stop further entries to be added */
def disable() =
seen.clear()
recordOK = false
/** Record an entry `entry` with given String representation `str` and a
* type/term namespace identified by `isType`.
* If the entry was not yet recorded, allocate the next superscript corresponding
* to the same string in the same name space. The first recording is the string proper
* and following recordings get consecutive superscripts starting with 2.
* @return The possibly superscripted version of `str`.
*/
def record(str: String, isType: Boolean, entry: Recorded)(using Context): String = if !recordOK then str else
//println(s"recording $str, $isType, $entry")
/** If `e1` is an alias of another class of the same name, return the other
* class symbol instead. This normalization avoids recording e.g. scala.List
* and scala.collection.immutable.List as two different types
*/
def followAlias(e1: Recorded): Recorded = e1 match {
case e1: Symbol if e1.isAliasType =>
val underlying = e1.typeRef.underlyingClassRef(refinementOK = false).typeSymbol
if (underlying.name == e1.name) underlying else e1.namedType.dealias.typeSymbol
case _ => e1
}
val key = SeenKey(str, isType)
val existing = seen(key)
lazy val dealiased = followAlias(entry)
/** All lambda parameters with the same name are given the same superscript as
* long as their corresponding binder has been printed.
* See tests/neg/lambda-rename.scala for test cases.
*/
def sameSuperscript(cur: Recorded, existing: Recorded) =
(cur eq existing) ||
(cur, existing).match
case (cur: ParamRef, existing: ParamRef) =>
(cur.paramName eq existing.paramName) &&
openedLambdas.contains(cur.binder) &&
openedLambdas.contains(existing.binder)
case _ =>
false
// The length of alts corresponds to the number of superscripts we need to print.
var alts = existing.dropWhile(alt => !sameSuperscript(dealiased, followAlias(alt)))
if alts.isEmpty then
alts = entry :: existing
seen(key) = alts
val suffix = alts.length match {
case 1 => ""
case n => n.toString.toCharArray.map {
case '0' => '⁰'
case '1' => '¹'
case '2' => '²'
case '3' => '³'
case '4' => '⁴'
case '5' => '⁵'
case '6' => '⁶'
case '7' => '⁷'
case '8' => '⁸'
case '9' => '⁹'
}.mkString
}
str + suffix
end record
/** Create explanation for single `Recorded` type or symbol */
private def explanation(entry: AnyRef)(using Context): String =
def boundStr(bound: Type, default: ClassSymbol, cmp: String) =
if (bound.isRef(default)) "" else i"$cmp $bound"
def boundsStr(bounds: TypeBounds): String = {
val lo = boundStr(bounds.lo, defn.NothingClass, ">:")
val hi = boundStr(bounds.hi, defn.AnyClass, "<:")
if (lo.isEmpty) hi
else if (hi.isEmpty) lo
else s"$lo and $hi"
}
def addendum(cat: String, info: Type): String = info match {
case bounds @ TypeBounds(lo, hi) if !(bounds =:= TypeBounds.empty) && !bounds.isErroneous =>
if (lo eq hi) i" which is an alias of $lo"
else i" with $cat ${boundsStr(bounds)}"
case _ =>
""
}
entry match {
case param: TypeParamRef =>
s"is a type variable${addendum("constraint", TypeComparer.bounds(param))}"
case param: TermParamRef =>
s"is a reference to a value parameter"
case sym: Symbol =>
val info =
if (ctx.gadt.contains(sym))
sym.info & ctx.gadt.fullBounds(sym)
else
sym.info
s"is a ${ctx.printer.kindString(sym)}${sym.showExtendedLocation}${addendum("bounds", info)}"
case tp: SkolemType =>
s"is an unknown value of type ${tp.widen.show}"
}
end explanation
/** Produce a where clause with explanations for recorded iterms.
*/
def explanations(using Context): String =
def needsExplanation(entry: Recorded) = entry match {
case param: TypeParamRef => ctx.typerState.constraint.contains(param)
case param: ParamRef => false
case skolem: SkolemType => true
case sym: Symbol => ctx.gadt.contains(sym) && ctx.gadt.fullBounds(sym) != TypeBounds.empty
}
val toExplain: List[(String, Recorded)] = seen.toList.flatMap { kvs =>
val res: List[(String, Recorded)] = kvs match {
case (key, entry :: Nil) =>
if (needsExplanation(entry)) (key.str, entry) :: Nil else Nil
case (key, entries) =>
for (alt <- entries) yield {
val tickedString = record(key.str, key.isType, alt)
(tickedString, alt)
}
}
res // help the inferencer out
}.sortBy(_._1)
def columnar(parts: List[(String, String)]): List[String] = {
lazy val maxLen = parts.map(_._1.length).max
parts.map {
case (leader, trailer) =>
val variable = hl(leader)
s"""$variable${" " * (maxLen - leader.length)} $trailer"""
}
}
val explainParts = toExplain.map { case (str, entry) => (str, explanation(entry)) }
val explainLines = columnar(explainParts)
if (explainLines.isEmpty) "" else i"where: $explainLines%\n %\n"
end explanations
end Seen
/** Printer to be used when formatting messages */
private class Printer(val seen: Seen, _ctx: Context) extends RefinedPrinter(_ctx):
/** True if printer should a show source module instead of its module class */
private def useSourceModule(sym: Symbol): Boolean =
sym.is(ModuleClass, butNot = Package) && sym.sourceModule.exists && !_ctx.settings.YdebugNames.value
override def simpleNameString(sym: Symbol): String =
if useSourceModule(sym) then simpleNameString(sym.sourceModule)
else seen.record(super.simpleNameString(sym), sym.isType, sym)
override def ParamRefNameString(param: ParamRef): String =
seen.record(super.ParamRefNameString(param), param.isInstanceOf[TypeParamRef], param)
override def toTextRef(tp: SingletonType): Text = tp match
case tp: SkolemType => seen.record(tp.repr.toString, isType = true, tp)
case _ => super.toTextRef(tp)
override def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text): Text =
info match
case info: LambdaType =>
seen.openLambda(info)
case _ =>
super.toTextMethodAsFunction(info, isPure, refs)
override def toText(tp: Type): Text =
if !tp.exists || tp.isErroneous then seen.nonSensical = true
tp match
case tp: TypeRef if useSourceModule(tp.symbol) => Str("object ") ~ super.toText(tp)
case tp: LambdaType =>
seen.openLambda(tp)
super.toText(tp)
case _ => super.toText(tp)
override def toText(sym: Symbol): Text =
sym.infoOrCompleter match
case _: ErrorType | TypeAlias(_: ErrorType) | NoType => seen.nonSensical = true
case _ =>
super.toText(sym)
end Printer
end Message
/** A `Message` contains all semantic information necessary to easily
* comprehend what caused the message to be logged. Each message can be turned
* into a `Diagnostic` which contains the log level and can later be
* consumed by a subclass of `Reporter`. However, the error position is only
* part of `Diagnostic`, not `Message`.
*
* NOTE: you should not persist a message directly, because most messages take
* an implicit `Context` and these contexts weigh in at about 4mb per instance.
* Therefore, persisting these will result in a memory leak.
*
* Instead use the `persist` method to create an instance that does not keep a
* reference to these contexts.
*
* @param errorId a unique id identifying the message, this will be
* used to reference documentation online
*
* Messages modify the rendendering of interpolated strings in several ways:
*
* 1. The size of the printed code is limited with a MessageLimiter. If the message
* would get too large or too deeply nested, a `...` is printed instead.
* 2. References to module classes are prefixed with `object` for better recognizability.
* 3. A where clause is sometimes added which contains the following additional explanations:
* - References are disambiguated: If a message contains occurrences of the same identifier
* representing different symbols, the duplicates are printed with superscripts
* and the where-clause explains where each symbol is located.
* - Uninstantiated variables are explained in the where-clause with additional
* info about their bounds.
* - Skolems are explained with additional info about their underlying type.
*
* Messages inheriting from the NoDisambiguation trait or returned from the
* `noDisambiguation()` method skip point (3) above. This makes sense if the
* message already exolains where different occurrences of the same identifier
* are located. Examples are NamingMsgs such as double definition errors,
* overriding errors, and ambiguous implicit errors.
*
* We consciously made the design decision to disambiguate by default and disable
* disambiguation as an opt-in. The reason is that one usually does not consider all
* fine-grained details when writing an error message. If disambiguation is the default,
* some tests will show where clauses that look too noisy and that then can be disabled
* when needed. But if silence is the default, one usually does not realize that
* better info could be obtained by turning disambiguation on.
*/
abstract class Message(val errorId: ErrorMessageID)(using Context) { self =>
import Message.*
/** The kind of the error message, e.g. "Syntax" or "Type Mismatch".
* This will be printed as "$kind Error", "$kind Warning", etc, on the first
* line of the message.
*/
def kind: MessageKind
/** The `msg` contains the diagnostic message e.g:
*
* > expected: String
* > found: Int
*
* This message will be placed underneath the position given by the enclosing
* `Diagnostic`. The message is given in raw form, with possible embedded
* <nonsensical> tags.
*/
protected def msg(using Context): String
/** The explanation should provide a detailed description of why the error
* occurred and use examples from the user's own code to illustrate how to
* avoid these errors. It might contain embedded <nonsensical> tags.
*/
protected def explain(using Context): String
/** What gets printed after the message proper */
protected def msgPostscript(using Context): String =
if ctx eq NoContext then ""
else ctx.printer match
case msgPrinter: Message.Printer =>
myIsNonSensical = msgPrinter.seen.nonSensical
val addendum = msgPrinter.seen.explanations
msgPrinter.seen.disable()
// Clear entries and stop futher recording so that messages containing the current
// one don't repeat the explanations or use explanations from the msgPostscript.
if addendum.isEmpty then "" else "\n\n" ++ addendum
case _ =>
""
/** Does this message have an explanation?
* This is normally the same as `explain.nonEmpty` but can be overridden
* if we need a way to return `true` without actually calling the
* `explain` method.
*/
def canExplain: Boolean = explain.nonEmpty
private var myIsNonSensical: Boolean = false
/** A message is non-sensical if it contains references to internally
* generated error types. Normally we want to suppress error messages
* referring to types like this because they look weird and are normally
* follow-up errors to something that was diagnosed before.
*/
def isNonSensical: Boolean = { message; myIsNonSensical }
private var disambiguate: Boolean = true
def withoutDisambiguation(): this.type =
disambiguate = false
this
private def inMessageContext(disambiguate: Boolean)(op: Context ?=> String): String =
if ctx eq NoContext then op
else
val msgContext = ctx.printer match
case _: Message.Printer => ctx
case _ =>
val seen = Seen(disambiguate)
val ctx1 = ctx.fresh.setPrinterFn(Message.Printer(seen, _))
if !ctx1.property(MessageLimiter).isDefined then
ctx1.setProperty(MessageLimiter, ErrorMessageLimiter())
ctx1
op(using msgContext)
/** The message to report. <nonsensical> tags are filtered out */
@threadUnsafe lazy val message: String =
inMessageContext(disambiguate)(msg + msgPostscript)
/** The explanation to report. <nonsensical> tags are filtered out */
@threadUnsafe lazy val explanation: String =
inMessageContext(disambiguate = false)(explain)
/** The implicit `Context` in messages is a large thing that we don't want
* persisted. This method gets around that by duplicating the message,
* forcing its `msg` and `explanation` vals and dropping the implicit context
* that was captured in the original message.
*/
def persist: Message = new Message(errorId)(using NoContext):
val kind = self.kind
private val persistedMsg = self.message
private val persistedExplain = self.explanation
def msg(using Context) = persistedMsg
def explain(using Context) = persistedExplain
override val canExplain = self.canExplain
override def isNonSensical = self.isNonSensical
def append(suffix: => String): Message = mapMsg(_ ++ suffix)
def prepend(prefix: => String): Message = mapMsg(prefix ++ _)
def mapMsg(f: String => String): Message = new Message(errorId):
val kind = self.kind
def msg(using Context) = f(self.msg)
def explain(using Context) = self.explain
override def canExplain = self.canExplain
def appendExplanation(suffix: => String): Message = new Message(errorId):
val kind = self.kind
def msg(using Context) = self.msg
def explain(using Context) = self.explain ++ suffix
override def canExplain = true
/** Override with `true` for messages that should always be shown even if their
* position overlaps another message of a different class. On the other hand
* multiple messages of the same class with overlapping positions will lead
* to only a single message of that class to be issued.
*/
def showAlways = false
/** A list of actions attached to this message to address the issue this
* message represents.
*/
def actions(using Context): List[CodeAction] = List.empty
override def toString = msg
}
/** A marker trait that suppresses generation of `where` clause for disambiguations */
trait NoDisambiguation extends Message:
withoutDisambiguation()
/** The fallback `Message` containing no explanation and having no `kind` */
final class NoExplanation(msgFn: Context ?=> String)(using Context) extends Message(ErrorMessageID.NoExplanationID) {
def msg(using Context): String = msgFn
def explain(using Context): String = ""
val kind: MessageKind = MessageKind.NoKind
override def toString(): String = msg
}
/** The extractor for `NoExplanation` can be used to check whether any error
* lacks an explanation
*/
object NoExplanation {
def unapply(m: Message): Option[Message] =
if (m.explanation == "") Some(m)
else None
}