summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/util/Position.scala
blob: bc74717366f82da2c312ba6b37b9ce6c7c2f121a (plain) (blame)
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
/* NSC -- new Scala compiler
 * Copyright 2005-2011 LAMP/EPFL
 * @author  Martin Odersky
 *
 */

package scala.tools.nsc
package util

object Position {
  val tabInc = 8

  /** Prints the message with the given position indication. */
  def formatMessage(posIn: Position, msg: String, shortenFile: Boolean): String = {
    val pos = (
      if (posIn eq null) NoPosition
      else if (posIn.isDefined) posIn.inUltimateSource(posIn.source)
      else posIn
    )
    def file   = pos.source.file
    def prefix = if (shortenFile) file.name else file.path

    pos match {
      case FakePos(fmsg) => fmsg+" "+msg
      case NoPosition    => msg
      case _             =>
        List(
          "%s:%s: %s".format(prefix, pos.line, msg),
          pos.lineContent.stripLineEnd,
          " " * (pos.column - 1) + "^"
        ) mkString "\n"
    }
  }
}

/**
 * A tree does not directly store a Position. It stores a TreeAnnotation, which /typically/ is a Position.
 *
 * A TreeAnnotion may encompass more than just a Position, though, depending on the exact subclass of TreeAnnotation.
 */
trait TreeAnnotation {
  def pos: Position
}


/** The Position class and its subclasses represent positions of ASTs and symbols.
 *  Except for NoPosition and FakePos, every position refers to a SourceFile
 *  and to an offset in the sourcefile (its `point`). For batch compilation,
 *  that's all. For interactive IDE's there are also RangePositions
 *  and TransparentPositions. A RangePosition indicates a start and an end
 *  in addition to its point. TransparentPositions are a subclass of RangePositions.
 *  Range positions that are not transparent are called opaque.
 *  Trees with RangePositions need to satisfy the following invariants.
 *
 *  INV1: A tree with an offset position never contains a child
 *        with a range position
 *  INV2: If the child of a tree with a range position also has a range position,
 *        then the child's range is contained in the parent's range.
 *  INV3: Opaque range positions of children of the same node are non-overlapping
 *        (this means their overlap is at most a single point).
 *
 *  The following tests are useful on positions:
 *
 *  pos.isDefined     true if position is not a NoPosition nor a FakePosition
 *  pos.isRange       true if position is a range
 *  pos.isOpaqueRange true if position is an opaque range
 *
 *  The following accessor methods are provided:
 *
 *  pos.source        The source file of the position, which must be defined
 *  pos.point         The offset of the position's point, which must be defined
 *  pos.start         The start of the position, which must be a range
 *  pos.end           The end of the position, which must be a range
 *
 *  There are also convenience methods, such as
 *
 *  pos.startOrPoint
 *  pos.endOrPoint
 *  pos.pointOrElse(default)
 *
 *  These are less strict about the kind of position on which they can be applied.
 *
 *  The following conversion methods are often used:
 *
 *  pos.focus           converts a range position to an offset position, keeping its point;
 *                      returns all other positions unchanged.
 *  pos.makeTransparent converts an opaque range position into a transparent one.
 *                      returns all other positions unchanged.
 */
trait Position extends TreeAnnotation {
  def pos: Position = this

  /** An optional value containing the source file referred to by this position, or
   *  None if not defined.
   */
  def source: SourceFile = throw new UnsupportedOperationException("Position.source")

  /** Is this position neither a NoPosition nor a FakePosition?
   *  If isDefined is true, offset and source are both defined.
   */
  def isDefined: Boolean = false

  /** Is this position a transparent position? */
  def isTransparent: Boolean = false

  /** Is this position a range position? */
  def isRange: Boolean = false

  /** Is this position a non-transparent range position? */
  def isOpaqueRange: Boolean = false

  /** if opaque range, make this position transparent */
  def makeTransparent: Position = this

  /** The start of the position's range, error if not a range position */
  def start: Int = throw new UnsupportedOperationException("Position.start")

  /** The start of the position's range, or point if not a range position */
  def startOrPoint: Int = point

  /**  The point (where the ^ is) of the position */
  def point: Int = throw new UnsupportedOperationException("Position.point")

  /**  The point (where the ^ is) of the position, or else `default` if undefined */
  def pointOrElse(default: Int): Int = default

  /** The end of the position's range, error if not a range position */
  def end: Int = throw new UnsupportedOperationException("Position.end")

  /** The end of the position's range, or point if not a range position */
  def endOrPoint: Int = point

  @deprecated("use point instead", "2.9.0")
  def offset: Option[Int] = if (isDefined) Some(point) else None

  /** The same position with a different start value (if a range) */
  def withStart(off: Int) = this

  /** The same position with a different end value (if a range) */
  def withEnd(off: Int) = this

  /** The same position with a different point value (if a range or offset) */
  def withPoint(off: Int) = this

  /** The same position with a different source value, and its values shifted by given offset */
  def withSource(source: SourceFile, shift: Int) = this

  /** If this is a range, the union with the other range, with the point of this position.
   *  Otherwise, this position
   */
  def union(pos: Position) = this

  /** If this is a range position, the offset position of its start.
   *  Otherwise the position itself
   */
  def focusStart = this

  /** If this is a range position, the offset position of its point.
   *  Otherwise the position itself
   */
  def focus = this

  /** If this is a range position, the offset position of its end.
   *  Otherwise the position itself
   */
  def focusEnd = this

  /** Does this position include the given position `pos`.
   *  This holds if `this` is a range position and its range [start..end]
   *  is the same or covers the range of the given position, which may or may not be a range position.
   */
  def includes(pos: Position) = false

  /** Does this position properly include the given position `pos` ("properly" meaning their
   *  ranges are not the same)?
   */
  def properlyIncludes(pos: Position) =
    includes(pos) && (start < pos.startOrPoint || pos.endOrPoint < end)

  /** Does this position precede that position?
   *  This holds if both positions are defined and the end point of this position
   *  is not larger than the start point of the given position.
   */
  def precedes(pos: Position) =
    isDefined && pos.isDefined && endOrPoint <= pos.startOrPoint

  /** Does this position properly precede the given position `pos` ("properly" meaning their ranges
   *  do not share a common point).
   */
  def properlyPrecedes(pos: Position) =
    isDefined && pos.isDefined && endOrPoint < pos.startOrPoint

  /** Does this position overlap with that position?
   *  This holds if both positions are ranges and there is an interval of
   *  non-zero length that is shared by both position ranges.
   */
  def overlaps(pos: Position) =
    isRange && pos.isRange &&
    ((pos.start < end && start < pos.end) || (start < pos.end && pos.start < end))

  /** Does this position cover the same range as that position?
   *  Holds only if both position are ranges
   */
  def sameRange(pos: Position) =
    isRange && pos.isRange && start == pos.start && end == pos.end

  def line: Int = throw new UnsupportedOperationException("Position.line")

  def column: Int = throw new UnsupportedOperationException("Position.column")

  /** Convert this to a position around `point` that spans a single source line */
  def toSingleLine: Position = this

  def lineContent: String =
    if (isDefined) source.lineToString(line - 1)
    else "NO_LINE"

  /** Map this position to a position in an original source
   * file.  If the SourceFile is a normal SourceFile, simply
   * return this.
   */
  def inUltimateSource(source : SourceFile) =
    if (source == null) this else source.positionInUltimateSource(this)

  def dbgString = toString
  def safeLine = try line catch { case _: UnsupportedOperationException => -1 }

  def show: String = "["+toString+"]"
}

case object NoPosition extends Position {
  override def dbgString = toString
}

case class FakePos(msg: String) extends Position {
  override def toString = msg
}

class OffsetPosition(override val source: SourceFile, override val point: Int) extends Position {
  override def isDefined = true
  override def pointOrElse(default: Int): Int = point
  override def withPoint(off: Int) = new OffsetPosition(source, off)
  override def withSource(source: SourceFile, shift: Int) = new OffsetPosition(source, point + shift)

  override def line: Int = source.offsetToLine(point) + 1

  override def column: Int = {
    var idx = source.lineToOffset(source.offsetToLine(point))
    var col = 0
    while (idx != point) {
      col += (if (source.content(idx) == '\t') Position.tabInc - col % Position.tabInc else 1)
      idx += 1
    }
    col + 1
  }

  override def union(pos: Position) =
    if (pos.isRange) pos else this

  override def equals(that : Any) = that match {
    case that : OffsetPosition => point == that.point && source.file == that.source.file
    case that => false
  }
  override def hashCode = point * 37 + source.file.hashCode

  override def toString = {
    val pointmsg = if (point > source.length) "out-of-bounds-" else "offset="
    "source-%s,line-%s,%s%s".format(source.path, line, pointmsg, point)
  }
  override def show = "["+point+"]"
}

/** new for position ranges */
class RangePosition(source: SourceFile, override val start: Int, point: Int, override val end: Int)
extends OffsetPosition(source, point) {
  if (start > end) assert(false, "bad position: "+show)
  override def isRange: Boolean = true
  override def isOpaqueRange: Boolean = true
  override def startOrPoint: Int = start
  override def endOrPoint: Int = end
  override def withStart(off: Int) = new RangePosition(source, off, point, end)
  override def withEnd(off: Int) = new RangePosition(source, start, point, off)
  override def withPoint(off: Int) = new RangePosition(source, start, off, end)
  override def withSource(source: SourceFile, shift: Int) = new RangePosition(source, start + shift, point + shift, end + shift)
  override def focusStart = new OffsetPosition(source, start)
  override def focus = {
    if (focusCache eq NoPosition) focusCache = new OffsetPosition(source, point)
    focusCache
  }
  override def focusEnd = new OffsetPosition(source, end)
  override def makeTransparent = new TransparentPosition(source, start, point, end)
  override def includes(pos: Position) = pos.isDefined && start <= pos.startOrPoint && pos.endOrPoint <= end
  override def union(pos: Position) =
    if (pos.isRange) new RangePosition(source, start min pos.start, point, end max pos.end) else this

  override def toSingleLine: Position = source match {
    case bs: BatchSourceFile
    if end > 0 && bs.offsetToLine(start) < bs.offsetToLine(end - 1) =>
      val pointLine = bs.offsetToLine(point)
      new RangePosition(source, bs.lineToOffset(pointLine), point, bs.lineToOffset(pointLine + 1))
    case _ => this
  }

  override def toString = "RangePosition("+source+", "+start+", "+point+", "+end+")"
  override def show = "["+start+":"+end+"]"
  private var focusCache: Position = NoPosition
}

class TransparentPosition(source: SourceFile, start: Int, point: Int, end: Int) extends RangePosition(source, start, point, end) {
  override def isOpaqueRange: Boolean = false
  override def isTransparent = true
  override def makeTransparent = this
  override def show = "<"+start+":"+end+">"
}