diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e84ce8b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,19 @@ +name: Release +on: + push: + branches: [cli] + tags: ["*"] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: olafurpg/setup-scala@v10 + - uses: coursier/cache-action@v3 + - uses: olafurpg/setup-gpg@v3 + - run: sbt ci-release + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml new file mode 100644 index 0000000..e473d53 --- /dev/null +++ b/.github/workflows/scala.yml @@ -0,0 +1,10 @@ +name: build +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: olafurpg/setup-scala@v10 + - uses: coursier/cache-action@v3 + - run: sbt clean headerCheck +test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..675ff7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +*.class +*.log + +# these are moved into the doc project by the build +modules/docs/src/main/tut/changelog.md +modules/docs/src/main/tut/license.md + +# sbt specific +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +project/hydra.sbt + +# Scala-IDE specific +.scala_dependencies +.cache +.classpath +.project +.worksheet/ +.settings/ + +# OS X +.DS_Store + +# Ctags +.tags + +# ENSIME +.ensime +.ensime_cache/ + +# IntelliJ +.idea/ + +# Mill +out/ + +# Bloop/Metals/vscode +.bloop/ +.metals/ +.vscode/ +metals.sbt + +# Transient workspace +modules/docs/src/main/scala/ +*.worksheet.sc + +# bsp +.bsp/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7614b25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2020 by Rob Norris + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2258fd4 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# SourcePos + +This is a Scala micro-library that provides: + +- a `SourcePos` class that specifies a source file and line number, and +- an implicit macro that forges a `SourcePos` at the call site if none is available. + +SourcePos is compiled for Scala **2.12**, **2.13**, and **3.0** (see release notes for exact versions). + + +```scala +libraryDependencies += "org.tpolecat" %% "sourcepos" % +``` + +#### Example + +If you want to know where a method call originated, demand an implicit `SourcePos`. It has fields for both `file` and `line`, and a `toString` that prints them as `:`, which most editors will hyperlink for you. + +```scala +import org.tpolecat.sourcepos._ + +object Example { + + def method(n: Int)(implicit sp: SourcePos): String = + s"You called me with $n from $sp" + + def main(args: Array[String]): Unit = + println(method(42)) + +} +``` + +Running this program on my computer yields: + +``` +You called me with 42 from /Users/rnorris/Scala/SourcePos/src/test/scala/Example.scala:9 +``` + + diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..1b5866d --- /dev/null +++ b/build.sbt @@ -0,0 +1,73 @@ + +// Our Scala versions. +lazy val `scala-3.0` = "3.0.0-M3" +lazy val `scala-3.0-prev` = "3.0.0-M2" +lazy val `scala-2.12` = "2.12.12" +lazy val `scala-2.13` = "2.13.3" + +// Publishing +name := "sourcepos" +organization := "org.tpolecat" +licenses ++= Seq(("MIT", url("http://opensource.org/licenses/MIT"))) +homepage := Some(url("https://github.com/tpolecat/sourcepos")) +developers := List( + Developer("tpolecat", "Rob Norris", "rob_norris@mac.com", url("http://www.tpolecat.org")) +) + +// Headers +headerMappings := headerMappings.value + (HeaderFileType.scala -> HeaderCommentStyle.cppStyleLineComment) +headerLicense := Some(HeaderLicense.Custom( + """|Copyright (c) 2020 by Rob Norris + |This software is licensed under the MIT License (MIT). + |For more information see LICENSE or https://opensource.org/licenses/MIT + |""".stripMargin + ) +) + +// Compilation +scalaVersion := `scala-2.13` +crossScalaVersions := Seq(`scala-2.12`, `scala-2.13`, `scala-3.0-prev`, `scala-3.0`) +Compile / doc / scalacOptions --= Seq("-Xfatal-warnings") +Compile / doc / scalacOptions ++= Seq( + "-groups", + "-sourcepath", (baseDirectory in LocalRootProject).value.getAbsolutePath, + "-doc-source-url", "https://github.com/tpolecat/sourcepos/blob/v" + version.value + "€{FILE_PATH}.scala", +) + +// MUnit +libraryDependencies += "org.scalameta" %% "munit" % "0.7.20" % Test +testFrameworks += new TestFramework("munit.Framework") + +// Scala 2 needs scala-reflect +libraryDependencies ++= Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value).filterNot(_ => isDotty.value) + +// Add some more source directories +unmanagedSourceDirectories in Compile ++= { + val sourceDir = (sourceDirectory in Compile).value + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => Seq(sourceDir / "scala-3") + case Some((2, _)) => Seq(sourceDir / "scala-2") + case _ => Seq() + } +} + +// Also for test +unmanagedSourceDirectories in Test ++= { + val sourceDir = (sourceDirectory in Test).value + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => Seq(sourceDir / "scala-3") + case Some((2, _)) => Seq(sourceDir / "scala-2") + case _ => Seq() + } +} + +// dottydoc really doesn't work at all right now +Compile / doc / sources := { + val old = (Compile / doc / sources).value + if (isDotty.value) + Seq() + else + old +} + +enablePlugins(AutomateHeaderPlugin) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..d91c272 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.6 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..76e7d10 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,10 @@ +addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.5") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.1") +addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.16") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.14") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.1") diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt new file mode 100644 index 0000000..8eaec21 --- /dev/null +++ b/project/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.4.3") diff --git a/src/main/scala-2/SourcePosPlatform.scala b/src/main/scala-2/SourcePosPlatform.scala new file mode 100644 index 0000000..1bec024 --- /dev/null +++ b/src/main/scala-2/SourcePosPlatform.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2020 by Rob Norris +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package org.tpolecat.sourcepos + +import scala.reflect.macros.blackbox + +trait SourcePosPlatform { + implicit def instance: SourcePos = + macro SourcePosPlatform.sourcePos_impl +} + +object SourcePosPlatform { + def sourcePos_impl(c: blackbox.Context): c.Tree = { + import c.universe._ + val file = c.enclosingPosition.source.path + val line = c.enclosingPosition.line + q"_root_.org.tpolecat.sourcepos.SourcePos($file, $line)" + } +} diff --git a/src/main/scala-3.0.0-M2/SourcPosPlatform.scala b/src/main/scala-3.0.0-M2/SourcPosPlatform.scala new file mode 100644 index 0000000..877a67c --- /dev/null +++ b/src/main/scala-3.0.0-M2/SourcPosPlatform.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2020 by Rob Norris +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package org.tpolecat.sourcepos + +import scala.quoted._ + +trait SourcePosPlatform { + + implicit inline def instance: SourcePos = + ${SourcePosPlatform.sourcePos_impl} + +} + +object SourcePosPlatform { + + def sourcePos_impl(using ctx: Quotes): Expr[SourcePos] = { + val rootPosition = ctx.reflect.Position.ofMacroExpansion + val file = Expr(rootPosition.sourceFile.jpath.toString) + val line = Expr(rootPosition.startLine + 1) + '{SourcePos($file, $line)} + } + +} diff --git a/src/main/scala-3.0.0-M3/SourcPosPlatform.scala b/src/main/scala-3.0.0-M3/SourcPosPlatform.scala new file mode 100644 index 0000000..877a67c --- /dev/null +++ b/src/main/scala-3.0.0-M3/SourcPosPlatform.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2020 by Rob Norris +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package org.tpolecat.sourcepos + +import scala.quoted._ + +trait SourcePosPlatform { + + implicit inline def instance: SourcePos = + ${SourcePosPlatform.sourcePos_impl} + +} + +object SourcePosPlatform { + + def sourcePos_impl(using ctx: Quotes): Expr[SourcePos] = { + val rootPosition = ctx.reflect.Position.ofMacroExpansion + val file = Expr(rootPosition.sourceFile.jpath.toString) + val line = Expr(rootPosition.startLine + 1) + '{SourcePos($file, $line)} + } + +} diff --git a/src/main/scala/SourcePos.scala b/src/main/scala/SourcePos.scala new file mode 100644 index 0000000..9f66eb1 --- /dev/null +++ b/src/main/scala/SourcePos.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2020 by Rob Norris +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package org.tpolecat.sourcepos + +final case class SourcePos(file: String, line: Int) { + override def toString = + s"$file:$line" +} + +object SourcePos extends SourcePosPlatform \ No newline at end of file diff --git a/src/test/scala/Example.scala b/src/test/scala/Example.scala new file mode 100644 index 0000000..51325d1 --- /dev/null +++ b/src/test/scala/Example.scala @@ -0,0 +1,15 @@ +// Copyright (c) 2020 by Rob Norris +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +import org.tpolecat.sourcepos._ + +object Example { + + def method(n: Int)(implicit sp: SourcePos): String = + s"You called me with $n from $sp" + + def main(args: Array[String]): Unit = + println(method(42)) + +} \ No newline at end of file diff --git a/src/test/scala/Test.scala b/src/test/scala/Test.scala new file mode 100644 index 0000000..e08c588 --- /dev/null +++ b/src/test/scala/Test.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2020 by Rob Norris +// This software is licensed under the MIT License (MIT). +// For more information see LICENSE or https://opensource.org/licenses/MIT + +package test + +import org.tpolecat.sourcepos._ + +class Test extends munit.FunSuite { + + test("pos") { + val pos = implicitly[SourcePos] + assert(pos.file.endsWith("src/test/scala/Test.scala")) + assert(pos.line == 12) + } + +} \ No newline at end of file