All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dotty.tools.dotc.util.SourceFile.scala Maven / Gradle / Ivy

The newest version!
package dotty.tools
package dotc
package util

import scala.collection.mutable.ArrayBuffer
import dotty.tools.io._
import java.util.regex.Pattern
import java.io.IOException
import scala.tasty.util.Chars._
import Spans._
import scala.io.Codec
import core.Names.TermName
import core.Contexts.Context
import scala.annotation.internal.sharable
import core.Decorators.PreNamedString
import java.util.concurrent.atomic.AtomicInteger
import scala.collection.mutable

import java.util.Optional

object ScriptSourceFile {
  @sharable private val headerPattern = Pattern.compile("""^(::)?!#.*(\r|\n|\r\n)""", Pattern.MULTILINE)
  private val headerStarts  = List("#!", "::#!")

  def apply(file: AbstractFile, content: Array[Char]): SourceFile = {
    /** Length of the script header from the given content, if there is one.
     *  The header begins with "#!" or "::#!" and ends with a line starting
     *  with "!#" or "::!#".
     */
    val headerLength =
      if (headerStarts exists (content startsWith _)) {
        val matcher = headerPattern matcher content.mkString
        if (matcher.find) matcher.end
        else throw new IOException("script file does not close its header with !# or ::!#")
      } else 0
    new SourceFile(file, content drop headerLength) {
      override val underlying = new SourceFile(file, content)
    }
  }
}

class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends interfaces.SourceFile {
  import SourceFile._

  private var myContent: Array[Char] = null

  def content(): Array[Char] = {
    if (myContent == null) myContent = computeContent
    myContent
  }

  def this(file: AbstractFile, codec: Codec) = this(file, new String(file.toByteArray, codec.charSet).toCharArray)

  /** Tab increment; can be overridden */
  def tabInc: Int = 8

  override def name: String = file.name
  override def path: String = file.path
  override def jfile: Optional[JFile] = Optional.ofNullable(file.file)

  def pathName: PathName = file.absolutePath.toTermName

  override def equals(that: Any): Boolean =
    (this `eq` that.asInstanceOf[AnyRef]) || {
      that match {
        case that : SourceFile => file == that.file && start == that.start
        case _ => false
      }
    }

  override def hashCode: Int = file.hashCode * 41 + start.hashCode

  def apply(idx: Int): Char = content().apply(idx)

  def length: Int = content().length

  /** true for all source files except `NoSource` */
  def exists: Boolean = true

  /** The underlying source file */
  def underlying: SourceFile = this

  /** The start of this file in the underlying source file */
  def start: Int = 0

  def atSpan(span: Span): SourcePosition =
    if (span.exists) SourcePosition(underlying, span)
    else NoSourcePosition

  def isSelfContained: Boolean = underlying eq this

  /** Map a position to a position in the underlying source file.
   *  For regular source files, simply return the argument.
   */
  def positionInUltimateSource(position: SourcePosition): SourcePosition =
    SourcePosition(underlying, position.span shift start)

  private def isLineBreak(idx: Int) =
    if (idx >= length) false else {
      val ch = content()(idx)
      // don't identify the CR in CR LF as a line break, since LF will do.
      if (ch == CR) (idx + 1 == length) || (content()(idx + 1) != LF)
      else isLineBreakChar(ch)
    }

  private def calculateLineIndices(cs: Array[Char]) = {
    val buf = new ArrayBuffer[Int]
    buf += 0
    for (i <- 0 until cs.length) if (isLineBreak(i)) buf += i + 1
    buf += cs.length // sentinel, so that findLine below works smoother
    buf.toArray
  }
  private lazy val lineIndices: Array[Int] = calculateLineIndices(content())

  /** Map line to offset of first character in line */
  def lineToOffset(index: Int): Int = lineIndices(index)

  /** Like `lineToOffset`, but doesn't crash if the index is out of bounds. */
  def lineToOffsetOpt(index: Int): Option[Int] =
    if (index < 0 || index >= lineIndices.length)
      None
    else
      Some(lineToOffset(index))

  /** A cache to speed up offsetToLine searches to similar lines */
  private[this] var lastLine = 0

  /** Convert offset to line in this source file
   *  Lines are numbered from 0
   */
  def offsetToLine(offset: Int): Int = {
    lastLine = Util.bestFit(lineIndices, lineIndices.length, offset, lastLine)
    if (offset >= length) lastLine -= 1 // compensate for the sentinel
    lastLine
  }

  /** The index of the first character of the line containing position `offset` */
  def startOfLine(offset: Int): Int = {
    require(offset >= 0)
    lineToOffset(offsetToLine(offset))
  }

  /** The start index of the line following the one containing position `offset` */
  def nextLine(offset: Int): Int =
    lineToOffset(offsetToLine(offset) + 1 min lineIndices.length - 1)

  /** The content of the line containing position `offset` */
  def lineContent(offset: Int): String =
    content.slice(startOfLine(offset), nextLine(offset)).mkString

  /** The column corresponding to `offset`, starting at 0 */
  def column(offset: Int): Int = {
    var idx = startOfLine(offset)
    var col = 0
    while (idx != offset) {
      col += (if (idx < length && content()(idx) == '\t') (tabInc - col) % tabInc else 1)
      idx += 1
    }
    col
  }

  /** The padding of the column corresponding to `offset`, includes tabs */
  def startColumnPadding(offset: Int): String = {
    var idx = startOfLine(offset)
    val pad = new StringBuilder
    while (idx != offset) {
      pad.append(if (idx < length && content()(idx) == '\t') '\t' else ' ')
      idx += 1
    }
    pad.result()
  }

  override def toString: String = file.toString

  // Positioned ids

  private[this] val ctr = new AtomicInteger

  def nextId: Int = {
    val id = ctr.get
    if (id % ChunkSize == 0) newChunk
    else if (ctr.compareAndSet(id, id + 1)) id
    else nextId
  }

  private def newChunk: Int = sourceOfChunk.synchronized {
    val id = chunks << ChunkSizeLog
    if (chunks == sourceOfChunk.length) {
      val a = new Array[SourceFile](chunks * 2)
      System.arraycopy(sourceOfChunk, 0, a, 0, chunks)
      sourceOfChunk = a
    }
    sourceOfChunk(chunks) = this
    chunks += 1
    ctr.set(id + 1)
    id
  }
}
object SourceFile {
  implicit def eqSource: Eql[SourceFile, SourceFile] = Eql.derived

  implicit def fromContext(implicit ctx: Context): SourceFile = ctx.source

  type PathName = TermName

  def fromId(id: Int): SourceFile = sourceOfChunk(id >> ChunkSizeLog)

  def virtual(name: String, content: String) = new SourceFile(new VirtualFile(name, content.getBytes), scala.io.Codec.UTF8)

  private final val ChunkSizeLog = 10
  private final val ChunkSize = 1 << ChunkSizeLog

  // These two vars are sharable because they're only used in the synchronized block in newChunk
  @sharable private var chunks: Int = 0
  @sharable private var sourceOfChunk: Array[SourceFile] = new Array[SourceFile](2000)
}

@sharable object NoSource extends SourceFile(NoAbstractFile, Array[Char]()) {
  override def exists: Boolean = false
  override def atSpan(span: Span): SourcePosition = NoSourcePosition
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy