From 7e1f344b03848a7f2aec09ea8277e066b2ba03e5 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Thu, 5 Sep 2024 10:00:55 +0200 Subject: [PATCH 01/15] Adding Macros for scala 2 and scala 3 to allow string literals with multiple segments --- build.sc | 8 +++++ os/src-2/Macros.scala | 27 ++++++++++++++++ os/src-3/Macros.scala | 27 ++++++++++++++++ os/src-3/acyclic.scala | 5 +++ os/src/Path.scala | 16 ++++++++-- os/test/src/PathTests.scala | 61 ++++++++++++++++++++++++++++++------- 6 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 os/src-2/Macros.scala create mode 100644 os/src-3/Macros.scala create mode 100644 os/src-3/acyclic.scala diff --git a/build.sc b/build.sc index de9903f2..5069b80b 100644 --- a/build.sc +++ b/build.sc @@ -24,6 +24,7 @@ object Deps { val geny = ivy"com.lihaoyi::geny::1.1.1" val sourcecode = ivy"com.lihaoyi::sourcecode::0.4.2" val utest = ivy"com.lihaoyi::utest::0.8.4" + def scalaReflect(scalaVersion: String) = ivy"org.scala-lang:scala-reflect:$scalaVersion" def scalaLibrary(version: String) = ivy"org.scala-lang:scala-library:${version}" } @@ -94,6 +95,13 @@ trait OsLibModule trait OsModule extends OsLibModule { outer => def ivyDeps = Agg(Deps.geny) + override def compileIvyDeps = T{ + scalaVersion.zip(super.compileIvyDeps).map{ + case (scalaVer,superDeps) => + val scalaReflectOpt = if (!ZincWorkerUtil.isDottyOrScala3(scalaVer)) Some(Deps.scalaReflect(scalaVer)) else None + superDeps ++ Agg.from(scalaReflectOpt) + } + } def artifactName = "os-lib" diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala new file mode 100644 index 00000000..5beb11a5 --- /dev/null +++ b/os/src-2/Macros.scala @@ -0,0 +1,27 @@ +package os + +import scala.reflect.macros.blackbox +import os.PathChunk.SubPathChunk +import scala.language.experimental.macros +import acyclic.skipped + +trait PathChunkMacros extends ViewBoundImplicit{ + implicit def validatedStringChunk(s: String): PathChunk = macro Macros.validatedStringChunkImpl +} + +object Macros { + + def validatedStringChunkImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[SubPathChunk] = { + import c.universe._ + + s match { + case Expr(Literal(Constant(literal: String))) => + val splitted = literal.splitWithDelimiters("/",-1).filterNot(_ == "/") + splitted.foreach(BasePath.checkSegment) + + c.Expr(q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(${splitted}.toIndexedSeq))") + case nonLiteral => + c.Expr(q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(IndexedSeq($nonLiteral)))") + } + } +} diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala new file mode 100644 index 00000000..21c9b64e --- /dev/null +++ b/os/src-3/Macros.scala @@ -0,0 +1,27 @@ +package os + +import os.Macros.validatedPathChunkImpl +import os.PathChunk.SubPathChunk + +import scala.collection.immutable.IndexedSeq +import scala.quoted.{Expr, Quotes} + +trait PathChunkMacros extends ViewBoundImplicit{ + inline implicit def validatedStringChunk(s:String): PathChunk = ${validatedPathChunkImpl('s)} +} + +object Macros { + def validatedPathChunkImpl(s:Expr[String])(using quotes: Quotes): Expr[SubPathChunk] = { + import quotes.reflect.* + + s.asTerm match { + case Inlined(_, _, Literal(StringConstant(literal))) => + val splitted = literal.splitWithDelimiters("/",-1).filterNot(_ == "/") + splitted.foreach(BasePath.checkSegment) + + '{new SubPathChunk(SubPath.apply(${Expr(splitted)}.toIndexedSeq))} + case _ => + '{{new SubPathChunk(SubPath.apply(IndexedSeq($s)))}} + } + } +} diff --git a/os/src-3/acyclic.scala b/os/src-3/acyclic.scala new file mode 100644 index 00000000..028279bd --- /dev/null +++ b/os/src-3/acyclic.scala @@ -0,0 +1,5 @@ +package os +private[os] object acyclic { + /** Mocks [[\\import acyclic.skipped]] for scala 3 */ + private[os] type skipped +} diff --git a/os/src/Path.scala b/os/src/Path.scala index d9fb0554..0abe7746 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -5,24 +5,33 @@ import java.nio.file.Paths import collection.JavaConverters._ import scala.language.implicitConversions -import java.nio.file +import acyclic.skipped //needed for cross-version defined macros trait PathChunk { def segments: Seq[String] def ups: Int } -object PathChunk { +trait ViewBoundImplicit{ + /**macros are not avaliable as implicit views, so this is needed for [[os.PathChunk.ArrayPathChunk]] to work*/ + implicit def validatedStringFun(s:String): PathChunk = new PathChunk.StringPathChunkInternal(s) +} + +object PathChunk extends PathChunkMacros { + implicit class RelPathChunk(r: RelPath) extends PathChunk { def segments = r.segments def ups = r.ups override def toString() = r.toString } + implicit class SubPathChunk(r: SubPath) extends PathChunk { def segments = r.segments def ups = 0 override def toString() = r.toString } - implicit class StringPathChunk(s: String) extends PathChunk { + + /**this needed for usages of [[/]] inside this project, as we cannot use macros in same compilation unit*/ + private[os] implicit class StringPathChunkInternal(s: String) extends PathChunk { BasePath.checkSegment(s) def segments = Seq(s) def ups = 0 @@ -473,6 +482,7 @@ object Path { trait ReadablePath { def toSource: os.Source + def getInputStream: java.io.InputStream } diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 98aa6ae4..447b2e2f 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -2,16 +2,52 @@ package test.os import java.nio.file.Paths import java.io.File - import os._ -import os.Path.{driveRoot} +import os.Path.driveRoot import utest.{assert => _, _} + import java.net.URI object PathTests extends TestSuite { + private def nonValidPathSegment(chars:String) = s"[$chars] is not a valid path segment." + val tests = Tests { + test("Temp") { +// os.pwd / "." +// Macros.validateLiteralPath("") +// Macros.validateLiteralPath(".") +// Macros.validateLiteralPath("..") +// Macros.validateLiteralPath("./b") + } + + test("Literals"){ + test("Basic"){ + assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") + assert ( root / "core/src/test" == root / "core" / "src"/ "test") + assert ( root / "core/src/test" == root / "core" / "src/test") + } + test("Compile errors"){ + compileError(""" rel / "src" / "" """).check("",nonValidPathSegment("")) + compileError(""" rel / "src" / "." """).check("", nonValidPathSegment(".")) + compileError(""" rel / "src" / ".." """).check("",nonValidPathSegment("..")) + + compileError { """ root / "src/" """}.check("",nonValidPathSegment("")) + compileError { """ root / "src/." """ }.check("", nonValidPathSegment(".")) + compileError { """ root / "src/.." """ }.check("",nonValidPathSegment("..")) + + compileError { """ root / "" """ }.check("", nonValidPathSegment("")) + compileError { """ root / "." """ }.check("", nonValidPathSegment(".")) + compileError { """ root / ".." """ }.check("",nonValidPathSegment("..")) + + + compileError(""" root / "hello" / ".." / "world" """).check("",nonValidPathSegment("..")) + compileError(""" root / "hello" / "../world" """).check("",nonValidPathSegment("..")) + compileError(""" root / "hello/../world" """).check("",nonValidPathSegment("..")) + } + } test("Basic") { val base = rel / "src" / "main" / "scala" val subBase = sub / "src" / "main" / "scala" + test("Transform posix paths") { // verify posix string format of driveRelative path assert(posix(root / "omg") == posix(Paths.get("/omg").toAbsolutePath)) @@ -279,29 +315,32 @@ object PathTests extends TestSuite { } } test("Errors") { + def nonLiteral(s:String) = s + + test("InvalidChars") { - val ex = intercept[PathError.InvalidSegment](rel / "src" / "Main/.scala") + val ex = intercept[PathError.InvalidSegment](rel / "src" / nonLiteral("Main/.scala")) val PathError.InvalidSegment("Main/.scala", msg1) = ex assert(msg1.contains("[/] is not a valid character to appear in a path segment")) - val ex2 = intercept[PathError.InvalidSegment](root / "hello" / ".." / "world") + val ex2 = intercept[PathError.InvalidSegment](root / "hello" / nonLiteral("..") / "world") val PathError.InvalidSegment("..", msg2) = ex2 assert(msg2.contains("use the `up` segment from `os.up`")) } test("InvalidSegments") { - intercept[PathError.InvalidSegment] { root / "core/src/test" } - intercept[PathError.InvalidSegment] { root / "" } - intercept[PathError.InvalidSegment] { root / "." } - intercept[PathError.InvalidSegment] { root / ".." } + intercept[PathError.InvalidSegment] { root / nonLiteral("core/src/test") } + intercept[PathError.InvalidSegment] { root / nonLiteral("") } + intercept[PathError.InvalidSegment] { root / nonLiteral(".") } + intercept[PathError.InvalidSegment] { root / nonLiteral("..") } } test("EmptySegment") { - intercept[PathError.InvalidSegment](rel / "src" / "") - intercept[PathError.InvalidSegment](rel / "src" / ".") - intercept[PathError.InvalidSegment](rel / "src" / "..") + intercept[PathError.InvalidSegment](rel / "src" / nonLiteral("")) + intercept[PathError.InvalidSegment](rel / "src" / nonLiteral(".")) + intercept[PathError.InvalidSegment](rel / "src" / nonLiteral("..")) } test("CannotRelativizeAbsAndRel") { val abs = pwd From e51ee0f0dcd95be009383b6e5a551747d6558438 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Thu, 5 Sep 2024 10:09:40 +0200 Subject: [PATCH 02/15] removing leftovers --- os/test/src/PathTests.scala | 8 -------- 1 file changed, 8 deletions(-) diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 447b2e2f..5e192801 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -11,14 +11,6 @@ object PathTests extends TestSuite { private def nonValidPathSegment(chars:String) = s"[$chars] is not a valid path segment." val tests = Tests { - test("Temp") { -// os.pwd / "." -// Macros.validateLiteralPath("") -// Macros.validateLiteralPath(".") -// Macros.validateLiteralPath("..") -// Macros.validateLiteralPath("./b") - } - test("Literals"){ test("Basic"){ assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") From 4e9f5e53646cb0eeb495c0df6a83ae1fc2804a03 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Thu, 5 Sep 2024 11:05:22 +0200 Subject: [PATCH 03/15] format --- os/src-2/Macros.scala | 12 ++++++++---- os/src-3/acyclic.scala | 1 + os/src/Path.scala | 9 +++++---- os/test/src/PathTests.scala | 32 +++++++++++++++----------------- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala index 5beb11a5..d95b6a66 100644 --- a/os/src-2/Macros.scala +++ b/os/src-2/Macros.scala @@ -5,7 +5,7 @@ import os.PathChunk.SubPathChunk import scala.language.experimental.macros import acyclic.skipped -trait PathChunkMacros extends ViewBoundImplicit{ +trait PathChunkMacros extends ViewBoundImplicit { implicit def validatedStringChunk(s: String): PathChunk = macro Macros.validatedStringChunkImpl } @@ -16,12 +16,16 @@ object Macros { s match { case Expr(Literal(Constant(literal: String))) => - val splitted = literal.splitWithDelimiters("/",-1).filterNot(_ == "/") + val splitted = literal.splitWithDelimiters("/", -1).filterNot(_ == "/") splitted.foreach(BasePath.checkSegment) - c.Expr(q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(${splitted}.toIndexedSeq))") + c.Expr( + q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(${splitted}.toIndexedSeq))" + ) case nonLiteral => - c.Expr(q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(IndexedSeq($nonLiteral)))") + c.Expr( + q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(IndexedSeq($nonLiteral)))" + ) } } } diff --git a/os/src-3/acyclic.scala b/os/src-3/acyclic.scala index 028279bd..4ae8a1d5 100644 --- a/os/src-3/acyclic.scala +++ b/os/src-3/acyclic.scala @@ -1,5 +1,6 @@ package os private[os] object acyclic { + /** Mocks [[\\import acyclic.skipped]] for scala 3 */ private[os] type skipped } diff --git a/os/src/Path.scala b/os/src/Path.scala index 0abe7746..b36355ea 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -11,9 +11,10 @@ trait PathChunk { def segments: Seq[String] def ups: Int } -trait ViewBoundImplicit{ - /**macros are not avaliable as implicit views, so this is needed for [[os.PathChunk.ArrayPathChunk]] to work*/ - implicit def validatedStringFun(s:String): PathChunk = new PathChunk.StringPathChunkInternal(s) +trait ViewBoundImplicit { + + /** macros are not avaliable as implicit views, so this is needed for [[os.PathChunk.ArrayPathChunk]] to work */ + implicit def validatedStringFun(s: String): PathChunk = new PathChunk.StringPathChunkInternal(s) } object PathChunk extends PathChunkMacros { @@ -30,7 +31,7 @@ object PathChunk extends PathChunkMacros { override def toString() = r.toString } - /**this needed for usages of [[/]] inside this project, as we cannot use macros in same compilation unit*/ + /** this needed for usages of [[/]] inside this project, as we cannot use macros in same compilation unit */ private[os] implicit class StringPathChunkInternal(s: String) extends PathChunk { BasePath.checkSegment(s) def segments = Seq(s) diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 5e192801..9794aa6b 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -8,32 +8,31 @@ import utest.{assert => _, _} import java.net.URI object PathTests extends TestSuite { - private def nonValidPathSegment(chars:String) = s"[$chars] is not a valid path segment." + private def nonValidPathSegment(chars: String) = s"[$chars] is not a valid path segment." val tests = Tests { - test("Literals"){ - test("Basic"){ + test("Literals") { + test("Basic") { assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") - assert ( root / "core/src/test" == root / "core" / "src"/ "test") - assert ( root / "core/src/test" == root / "core" / "src/test") + assert(root / "core/src/test" == root / "core" / "src" / "test") + assert(root / "core/src/test" == root / "core" / "src/test") } - test("Compile errors"){ - compileError(""" rel / "src" / "" """).check("",nonValidPathSegment("")) + test("Compile errors") { + compileError(""" rel / "src" / "" """).check("", nonValidPathSegment("")) compileError(""" rel / "src" / "." """).check("", nonValidPathSegment(".")) - compileError(""" rel / "src" / ".." """).check("",nonValidPathSegment("..")) + compileError(""" rel / "src" / ".." """).check("", nonValidPathSegment("..")) - compileError { """ root / "src/" """}.check("",nonValidPathSegment("")) + compileError { """ root / "src/" """ }.check("", nonValidPathSegment("")) compileError { """ root / "src/." """ }.check("", nonValidPathSegment(".")) - compileError { """ root / "src/.." """ }.check("",nonValidPathSegment("..")) + compileError { """ root / "src/.." """ }.check("", nonValidPathSegment("..")) compileError { """ root / "" """ }.check("", nonValidPathSegment("")) compileError { """ root / "." """ }.check("", nonValidPathSegment(".")) - compileError { """ root / ".." """ }.check("",nonValidPathSegment("..")) + compileError { """ root / ".." """ }.check("", nonValidPathSegment("..")) - - compileError(""" root / "hello" / ".." / "world" """).check("",nonValidPathSegment("..")) - compileError(""" root / "hello" / "../world" """).check("",nonValidPathSegment("..")) - compileError(""" root / "hello/../world" """).check("",nonValidPathSegment("..")) + compileError(""" root / "hello" / ".." / "world" """).check("", nonValidPathSegment("..")) + compileError(""" root / "hello" / "../world" """).check("", nonValidPathSegment("..")) + compileError(""" root / "hello/../world" """).check("", nonValidPathSegment("..")) } } test("Basic") { @@ -307,8 +306,7 @@ object PathTests extends TestSuite { } } test("Errors") { - def nonLiteral(s:String) = s - + def nonLiteral(s: String) = s test("InvalidChars") { val ex = intercept[PathError.InvalidSegment](rel / "src" / nonLiteral("Main/.scala")) From 297ef8993e509d35ea928a07f79073ae4076eae9 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Thu, 5 Sep 2024 15:10:54 +0200 Subject: [PATCH 04/15] adding segmentsFromString --- os/src-2/Macros.scala | 9 +++++---- os/src-3/Macros.scala | 8 ++++---- os/src/Path.scala | 6 ++++++ os/test/src/PathTests.scala | 31 +++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala index d95b6a66..d594945d 100644 --- a/os/src-2/Macros.scala +++ b/os/src-2/Macros.scala @@ -1,7 +1,8 @@ package os import scala.reflect.macros.blackbox -import os.PathChunk.SubPathChunk +import os.PathChunk.{SubPathChunk, segmentsFromString} + import scala.language.experimental.macros import acyclic.skipped @@ -16,11 +17,11 @@ object Macros { s match { case Expr(Literal(Constant(literal: String))) => - val splitted = literal.splitWithDelimiters("/", -1).filterNot(_ == "/") - splitted.foreach(BasePath.checkSegment) + val stringSegments = segmentsFromString(literal) + stringSegments.foreach(BasePath.checkSegment) c.Expr( - q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(${splitted}.toIndexedSeq))" + q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(${stringSegments}.toIndexedSeq))" ) case nonLiteral => c.Expr( diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala index 21c9b64e..2f0e17e3 100644 --- a/os/src-3/Macros.scala +++ b/os/src-3/Macros.scala @@ -1,7 +1,7 @@ package os import os.Macros.validatedPathChunkImpl -import os.PathChunk.SubPathChunk +import os.PathChunk.{SubPathChunk, segmentsFromString} import scala.collection.immutable.IndexedSeq import scala.quoted.{Expr, Quotes} @@ -16,10 +16,10 @@ object Macros { s.asTerm match { case Inlined(_, _, Literal(StringConstant(literal))) => - val splitted = literal.splitWithDelimiters("/",-1).filterNot(_ == "/") - splitted.foreach(BasePath.checkSegment) + val stringSegments = segmentsFromString(literal) + stringSegments.foreach(BasePath.checkSegment) - '{new SubPathChunk(SubPath.apply(${Expr(splitted)}.toIndexedSeq))} + '{new SubPathChunk(SubPath.apply(${Expr(stringSegments)}.toIndexedSeq))} case _ => '{{new SubPathChunk(SubPath.apply(IndexedSeq($s)))}} } diff --git a/os/src/Path.scala b/os/src/Path.scala index b36355ea..41ce9dad 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -18,6 +18,12 @@ trait ViewBoundImplicit { } object PathChunk extends PathChunkMacros { + def segmentsFromString(s: String): Array[String] = { + val trailingSeparatorsCount = s.reverseIterator.takeWhile(_ == '/').length + val strNoTrailingSeps = s.dropRight(trailingSeparatorsCount) + val splitted = strNoTrailingSeps.split('/') + splitted ++ Array.fill(trailingSeparatorsCount)("") + } implicit class RelPathChunk(r: RelPath) extends PathChunk { def segments = r.segments diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 9794aa6b..9383b663 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -4,6 +4,7 @@ import java.nio.file.Paths import java.io.File import os._ import os.Path.driveRoot +import os.PathChunk.segmentsFromString import utest.{assert => _, _} import java.net.URI @@ -11,6 +12,36 @@ object PathTests extends TestSuite { private def nonValidPathSegment(chars: String) = s"[$chars] is not a valid path segment." val tests = Tests { + test("segmentsFromString") { + def testSegmentsFromString(s: String, expected: List[String]) = { + assert(segmentsFromString(s).sameElements(expected)) + } + + testSegmentsFromString(" ", " " :: Nil) + + testSegmentsFromString("", "" :: Nil) + + testSegmentsFromString("""foo/bar/baz""", "foo" :: "bar" :: "baz" :: Nil) + + testSegmentsFromString("""/""", "" :: "" :: Nil) + testSegmentsFromString("""//""", "" :: "" :: "" :: Nil) + testSegmentsFromString("""///""", "" :: "" :: "" :: "" :: Nil) + + testSegmentsFromString("""a/""", "a" :: "" :: Nil) + testSegmentsFromString("""a//""", "a" :: "" :: "" :: Nil) + testSegmentsFromString("""a///""", "a" :: "" :: "" :: "" :: Nil) + + testSegmentsFromString("""ahs/""", "ahs" :: "" :: Nil) + testSegmentsFromString("""ahs//""", "ahs" :: "" :: "" :: Nil) + + testSegmentsFromString("""ahs/aa/""", "ahs" :: "aa" :: "" :: Nil) + testSegmentsFromString("""ahs/aa/""", "ahs" :: "aa" :: "" :: Nil) + testSegmentsFromString("""ahs/aa//""", "ahs" :: "aa" :: "" :: "" :: Nil) + + testSegmentsFromString("""/a""", "" :: "a" :: Nil) + testSegmentsFromString("""//a""", "" :: "" :: "a" :: Nil) + testSegmentsFromString("""//a/""", "" :: "" :: "a" :: "" :: Nil) + } test("Literals") { test("Basic") { assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") From 5a49ac2a6fbd25e9baf228c1fe5ac5bd9e089b78 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Thu, 5 Sep 2024 15:32:53 +0200 Subject: [PATCH 05/15] adding more tests --- os/test/src/PathTests.scala | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 9383b663..5ad1c3f6 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -34,7 +34,6 @@ object PathTests extends TestSuite { testSegmentsFromString("""ahs/""", "ahs" :: "" :: Nil) testSegmentsFromString("""ahs//""", "ahs" :: "" :: "" :: Nil) - testSegmentsFromString("""ahs/aa/""", "ahs" :: "aa" :: "" :: Nil) testSegmentsFromString("""ahs/aa/""", "ahs" :: "aa" :: "" :: Nil) testSegmentsFromString("""ahs/aa//""", "ahs" :: "aa" :: "" :: "" :: Nil) @@ -47,8 +46,27 @@ object PathTests extends TestSuite { assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") assert(root / "core/src/test" == root / "core" / "src" / "test") assert(root / "core/src/test" == root / "core" / "src/test") + assert(root / "core/ " == root / "core" / " ") + assert(root / " / " == root / " " / " ") } test("Compile errors") { + + compileError("""root / "/" """).check("", nonValidPathSegment("")) + compileError("""root / "/ " """).check("", nonValidPathSegment("")) + compileError("""root / " /" """).check("", nonValidPathSegment("")) + compileError("""root / "//" """).check("", nonValidPathSegment("")) + + compileError("""root / "foo/" """).check("", nonValidPathSegment("")) + compileError("""root / "foo//" """).check("", nonValidPathSegment("")) + + compileError("""root / "foo/bar/" """).check("", nonValidPathSegment("")) + compileError("""root / "foo/bar//" """).check("", nonValidPathSegment("")) + + compileError("""root / "/foo" """).check("", nonValidPathSegment("")) + compileError("""root / "//foo" """).check("", nonValidPathSegment("")) + + compileError("""root / "//foo/" """).check("", nonValidPathSegment("")) + compileError(""" rel / "src" / "" """).check("", nonValidPathSegment("")) compileError(""" rel / "src" / "." """).check("", nonValidPathSegment(".")) compileError(""" rel / "src" / ".." """).check("", nonValidPathSegment("..")) From e2262afde51ddd496cd5be90b9089238bdd29074 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 07:53:27 +0200 Subject: [PATCH 06/15] removing ill formed paths from tests --- os/test/src/PathTests.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 5ad1c3f6..eb57f51d 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -46,8 +46,6 @@ object PathTests extends TestSuite { assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") assert(root / "core/src/test" == root / "core" / "src" / "test") assert(root / "core/src/test" == root / "core" / "src/test") - assert(root / "core/ " == root / "core" / " ") - assert(root / " / " == root / " " / " ") } test("Compile errors") { From 4eccdb42e5ac3ab065273320d05fa98b5612ea99 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 08:58:11 +0200 Subject: [PATCH 07/15] fixing binary compatibility --- os/src/Path.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/os/src/Path.scala b/os/src/Path.scala index 41ce9dad..bba8b73e 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -44,6 +44,11 @@ object PathChunk extends PathChunkMacros { def ups = 0 override def toString() = s } + // binary compatibility shim + class StringPathChunk(s: String) extends StringPathChunkInternal(s) + // binary compatibility shim + def StringPathChunk(s: String) = new StringPathChunk(s) + implicit class SymbolPathChunk(s: Symbol) extends PathChunk { BasePath.checkSegment(s.name) def segments = Seq(s.name) From 300f198231bc054e97eaec82fa3b866ec707d311 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 08:58:45 +0200 Subject: [PATCH 08/15] changing docs to comments --- os/src/Path.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/os/src/Path.scala b/os/src/Path.scala index bba8b73e..cbd76df7 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -13,8 +13,9 @@ trait PathChunk { } trait ViewBoundImplicit { - /** macros are not avaliable as implicit views, so this is needed for [[os.PathChunk.ArrayPathChunk]] to work */ - implicit def validatedStringFun(s: String): PathChunk = new PathChunk.StringPathChunkInternal(s) + // fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk + implicit def validatedStringFunction(s: String): PathChunk = + new PathChunk.StringPathChunkInternal(s) } object PathChunk extends PathChunkMacros { @@ -37,7 +38,7 @@ object PathChunk extends PathChunkMacros { override def toString() = r.toString } - /** this needed for usages of [[/]] inside this project, as we cannot use macros in same compilation unit */ + // Implicit String => PathChunk conversion used inside os-lib, prevents macro expansion in same compilation unit private[os] implicit class StringPathChunkInternal(s: String) extends PathChunk { BasePath.checkSegment(s) def segments = Seq(s) From 545e5364a0d2361d6dc81e25677086196911b695 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 09:06:04 +0200 Subject: [PATCH 09/15] better naming, moving comments to right place --- os/src-2/Macros.scala | 8 +++++--- os/src-3/Macros.scala | 7 ++++--- os/src/Path.scala | 5 ++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala index d594945d..e9cf590d 100644 --- a/os/src-2/Macros.scala +++ b/os/src-2/Macros.scala @@ -6,13 +6,15 @@ import os.PathChunk.{SubPathChunk, segmentsFromString} import scala.language.experimental.macros import acyclic.skipped -trait PathChunkMacros extends ViewBoundImplicit { - implicit def validatedStringChunk(s: String): PathChunk = macro Macros.validatedStringChunkImpl +// StringPathChunkConversion is a fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk +trait PathChunkMacros extends StringPathChunkConversion { + implicit def stringPathChunkValidated(s: String): PathChunk = + macro Macros.stringPathChunkValidatedImpl } object Macros { - def validatedStringChunkImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[SubPathChunk] = { + def stringPathChunkValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[SubPathChunk] = { import c.universe._ s match { diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala index 2f0e17e3..292dc41a 100644 --- a/os/src-3/Macros.scala +++ b/os/src-3/Macros.scala @@ -6,12 +6,13 @@ import os.PathChunk.{SubPathChunk, segmentsFromString} import scala.collection.immutable.IndexedSeq import scala.quoted.{Expr, Quotes} -trait PathChunkMacros extends ViewBoundImplicit{ - inline implicit def validatedStringChunk(s:String): PathChunk = ${validatedPathChunkImpl('s)} +// StringPathChunkConversion is a fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk +trait PathChunkMacros extends StringPathChunkConversion{ + inline implicit def stringPathChunkValidated(s:String): PathChunk = ${stringPathChunkValidatedImpl('s)} } object Macros { - def validatedPathChunkImpl(s:Expr[String])(using quotes: Quotes): Expr[SubPathChunk] = { + def stringPathChunkValidatedImpl(s:Expr[String])(using quotes: Quotes): Expr[SubPathChunk] = { import quotes.reflect.* s.asTerm match { diff --git a/os/src/Path.scala b/os/src/Path.scala index cbd76df7..9c9416fa 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -11,10 +11,9 @@ trait PathChunk { def segments: Seq[String] def ups: Int } -trait ViewBoundImplicit { +trait StringPathChunkConversion { - // fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk - implicit def validatedStringFunction(s: String): PathChunk = + implicit def stringToPathChunk(s: String): PathChunk = new PathChunk.StringPathChunkInternal(s) } From 924a02ed07047699dd4866f4a9d313812691fa66 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 09:12:55 +0200 Subject: [PATCH 10/15] remove unnecessary imports, format scala3 --- os/src-3/Macros.scala | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala index 292dc41a..27cb2b23 100644 --- a/os/src-3/Macros.scala +++ b/os/src-3/Macros.scala @@ -1,18 +1,17 @@ package os -import os.Macros.validatedPathChunkImpl import os.PathChunk.{SubPathChunk, segmentsFromString} -import scala.collection.immutable.IndexedSeq import scala.quoted.{Expr, Quotes} // StringPathChunkConversion is a fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk -trait PathChunkMacros extends StringPathChunkConversion{ - inline implicit def stringPathChunkValidated(s:String): PathChunk = ${stringPathChunkValidatedImpl('s)} +trait PathChunkMacros extends StringPathChunkConversion { + inline implicit def stringPathChunkValidated(s: String): PathChunk = + ${ Macros.stringPathChunkValidatedImpl('s) } } object Macros { - def stringPathChunkValidatedImpl(s:Expr[String])(using quotes: Quotes): Expr[SubPathChunk] = { + def stringPathChunkValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[SubPathChunk] = { import quotes.reflect.* s.asTerm match { @@ -20,9 +19,9 @@ object Macros { val stringSegments = segmentsFromString(literal) stringSegments.foreach(BasePath.checkSegment) - '{new SubPathChunk(SubPath.apply(${Expr(stringSegments)}.toIndexedSeq))} + '{ new SubPathChunk(SubPath.apply(${ Expr(stringSegments) }.toIndexedSeq)) } case _ => - '{{new SubPathChunk(SubPath.apply(IndexedSeq($s)))}} + '{ { new SubPathChunk(SubPath.apply(IndexedSeq($s))) } } } } } From 0e04b4fcdb91d737d3c7e0af71d8fa6988cce5e8 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 11:31:03 +0200 Subject: [PATCH 11/15] using ArrayPathChunk and StringPathChunk instead of SubPathChunk in macros, generalizing macro return type to PathChunk --- os/src-2/Macros.scala | 8 ++++---- os/src-3/Macros.scala | 8 ++++---- os/src/Path.scala | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala index e9cf590d..a46ba215 100644 --- a/os/src-2/Macros.scala +++ b/os/src-2/Macros.scala @@ -1,7 +1,7 @@ package os import scala.reflect.macros.blackbox -import os.PathChunk.{SubPathChunk, segmentsFromString} +import os.PathChunk.segmentsFromString import scala.language.experimental.macros import acyclic.skipped @@ -14,7 +14,7 @@ trait PathChunkMacros extends StringPathChunkConversion { object Macros { - def stringPathChunkValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[SubPathChunk] = { + def stringPathChunkValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[PathChunk] = { import c.universe._ s match { @@ -23,11 +23,11 @@ object Macros { stringSegments.foreach(BasePath.checkSegment) c.Expr( - q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(${stringSegments}.toIndexedSeq))" + q"new _root_.os.PathChunk.ArrayPathChunk[String]($stringSegments)(_root_.os.PathChunk.stringToPathChunk)" ) case nonLiteral => c.Expr( - q"new _root_.os.PathChunk.SubPathChunk(_root_.os.SubPath.apply(IndexedSeq($nonLiteral)))" + q"new _root_.os.PathChunk.StringPathChunk($nonLiteral)" ) } } diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala index 27cb2b23..03d06f8e 100644 --- a/os/src-3/Macros.scala +++ b/os/src-3/Macros.scala @@ -1,6 +1,6 @@ package os -import os.PathChunk.{SubPathChunk, segmentsFromString} +import os.PathChunk.{ArrayPathChunk, StringPathChunk, segmentsFromString, stringToPathChunk} import scala.quoted.{Expr, Quotes} @@ -11,7 +11,7 @@ trait PathChunkMacros extends StringPathChunkConversion { } object Macros { - def stringPathChunkValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[SubPathChunk] = { + def stringPathChunkValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[PathChunk] = { import quotes.reflect.* s.asTerm match { @@ -19,9 +19,9 @@ object Macros { val stringSegments = segmentsFromString(literal) stringSegments.foreach(BasePath.checkSegment) - '{ new SubPathChunk(SubPath.apply(${ Expr(stringSegments) }.toIndexedSeq)) } + '{ new ArrayPathChunk[String](${ Expr(stringSegments) })(using stringToPathChunk) } case _ => - '{ { new SubPathChunk(SubPath.apply(IndexedSeq($s))) } } + '{ { new StringPathChunk($s) } } } } } diff --git a/os/src/Path.scala b/os/src/Path.scala index 9c9416fa..867203cb 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -46,8 +46,9 @@ object PathChunk extends PathChunkMacros { } // binary compatibility shim class StringPathChunk(s: String) extends StringPathChunkInternal(s) + // binary compatibility shim - def StringPathChunk(s: String) = new StringPathChunk(s) + def StringPathChunk(s: String): StringPathChunk = new StringPathChunk(s) implicit class SymbolPathChunk(s: Symbol) extends PathChunk { BasePath.checkSegment(s.name) From 8b06c112014e1b75fed48564122a071052c4423a Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 6 Sep 2024 11:35:47 +0200 Subject: [PATCH 12/15] format --- os/src/Path.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/os/src/Path.scala b/os/src/Path.scala index 867203cb..88c98fb9 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -44,6 +44,7 @@ object PathChunk extends PathChunkMacros { def ups = 0 override def toString() = s } + // binary compatibility shim class StringPathChunk(s: String) extends StringPathChunkInternal(s) From c8e20053392aa9c1f9e8b2558aeea8e21ec7b965 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 9 Sep 2024 12:24:28 +0200 Subject: [PATCH 13/15] review suggestions --- build.sc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build.sc b/build.sc index 5069b80b..f3f82ea9 100644 --- a/build.sc +++ b/build.sc @@ -96,11 +96,10 @@ trait OsLibModule trait OsModule extends OsLibModule { outer => def ivyDeps = Agg(Deps.geny) override def compileIvyDeps = T{ - scalaVersion.zip(super.compileIvyDeps).map{ - case (scalaVer,superDeps) => - val scalaReflectOpt = if (!ZincWorkerUtil.isDottyOrScala3(scalaVer)) Some(Deps.scalaReflect(scalaVer)) else None - superDeps ++ Agg.from(scalaReflectOpt) - } + val scalaReflectOpt = Option.when(!ZincWorkerUtil.isDottyOrScala3(scalaVersion())) ( + Deps.scalaReflect(scalaVersion()) + ) + super.compileIvyDeps() ++ scalaReflectOpt } def artifactName = "os-lib" From dc62021e33e88cd69cfeb4d442b12046e47bd2f0 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 9 Sep 2024 12:34:14 +0200 Subject: [PATCH 14/15] review suggestions allowing [..] in literal paths adding literal dedicated compile-errors --- os/src-2/Macros.scala | 11 ++--- os/src-3/Macros.scala | 24 +++++++--- os/src/Path.scala | 35 +++++++++++++- os/test/src/PathTests.scala | 94 ++++++++++++++++++++----------------- 4 files changed, 106 insertions(+), 58 deletions(-) diff --git a/os/src-2/Macros.scala b/os/src-2/Macros.scala index a46ba215..71cae483 100644 --- a/os/src-2/Macros.scala +++ b/os/src-2/Macros.scala @@ -1,9 +1,9 @@ package os -import scala.reflect.macros.blackbox -import os.PathChunk.segmentsFromString +import os.PathChunk.segmentsFromStringLiteralValidation import scala.language.experimental.macros +import scala.reflect.macros.blackbox import acyclic.skipped // StringPathChunkConversion is a fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk @@ -15,15 +15,14 @@ trait PathChunkMacros extends StringPathChunkConversion { object Macros { def stringPathChunkValidatedImpl(c: blackbox.Context)(s: c.Expr[String]): c.Expr[PathChunk] = { - import c.universe._ + import c.universe.{Try => _, _} s match { case Expr(Literal(Constant(literal: String))) => - val stringSegments = segmentsFromString(literal) - stringSegments.foreach(BasePath.checkSegment) + val stringSegments = segmentsFromStringLiteralValidation(literal) c.Expr( - q"new _root_.os.PathChunk.ArrayPathChunk[String]($stringSegments)(_root_.os.PathChunk.stringToPathChunk)" + q"""new _root_.os.PathChunk.RelPathChunk(_root_.os.RelPath.fromStringSegments($stringSegments))""" ) case nonLiteral => c.Expr( diff --git a/os/src-3/Macros.scala b/os/src-3/Macros.scala index 03d06f8e..edcea44b 100644 --- a/os/src-3/Macros.scala +++ b/os/src-3/Macros.scala @@ -1,13 +1,17 @@ package os -import os.PathChunk.{ArrayPathChunk, StringPathChunk, segmentsFromString, stringToPathChunk} +import os.PathChunk.{RelPathChunk, StringPathChunk, segmentsFromStringLiteralValidation} +import os.RelPath.fromStringSegments import scala.quoted.{Expr, Quotes} +import acyclic.skipped // StringPathChunkConversion is a fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk trait PathChunkMacros extends StringPathChunkConversion { inline implicit def stringPathChunkValidated(s: String): PathChunk = - ${ Macros.stringPathChunkValidatedImpl('s) } + ${ + Macros.stringPathChunkValidatedImpl('s) + } } object Macros { @@ -16,12 +20,18 @@ object Macros { s.asTerm match { case Inlined(_, _, Literal(StringConstant(literal))) => - val stringSegments = segmentsFromString(literal) - stringSegments.foreach(BasePath.checkSegment) - - '{ new ArrayPathChunk[String](${ Expr(stringSegments) })(using stringToPathChunk) } + val stringSegments = segmentsFromStringLiteralValidation(literal) + '{ + new RelPathChunk(fromStringSegments(${ + Expr(stringSegments) + })) + } case _ => - '{ { new StringPathChunk($s) } } + '{ + { + new StringPathChunk($s) + } + } } } } diff --git a/os/src/Path.scala b/os/src/Path.scala index 88c98fb9..a9941fe6 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -2,10 +2,12 @@ package os import java.net.URI import java.nio.file.Paths - import collection.JavaConverters._ import scala.language.implicitConversions -import acyclic.skipped //needed for cross-version defined macros +import acyclic.skipped +import os.PathError.{InvalidSegment, NonCanonicalLiteral} + +import scala.util.Try //needed for cross-version defined macros trait PathChunk { def segments: Seq[String] @@ -25,6 +27,25 @@ object PathChunk extends PathChunkMacros { splitted ++ Array.fill(trailingSeparatorsCount)("") } + private[os] def segmentsFromStringLiteralValidation(literal: String) = { + val stringSegments = segmentsFromString(literal) + val validSegmnts = validLiteralSegments(stringSegments) + val sanitizedLiteral = validSegmnts.mkString("/") + if (validSegmnts.isEmpty) throw InvalidSegment( + literal, + s"Literal path sequence [$literal] doesn't affect path being formed, please remove it" + ) + if (literal != sanitizedLiteral) throw NonCanonicalLiteral(literal, sanitizedLiteral) + stringSegments + } + private def validLiteralSegments(segments: Array[String]): Array[String] = { + val AllowedLiteralSegment = ".." + segments.collect { + case AllowedLiteralSegment => AllowedLiteralSegment + case segment if Try(BasePath.checkSegment(segment)).isSuccess => segment + } + } + implicit class RelPathChunk(r: RelPath) extends PathChunk { def segments = r.segments def ups = r.ups @@ -250,6 +271,11 @@ object PathError { case class LastOnEmptyPath() extends IAE("empty path has no last segment") + + case class NonCanonicalLiteral(providedLiteral: String, sanitizedLiteral: String) + extends IAE( + s"Literal path sequence [$providedLiteral] used in OS-Lib must be in a canonical form, please use [$sanitizedLiteral] instead" + ) } /** @@ -320,6 +346,7 @@ class RelPath private[os] (segments0: Array[String], val ups: Int) } object RelPath { + def apply[T: PathConvertible](f0: T): RelPath = { val f = implicitly[PathConvertible[T]].apply(f0) @@ -342,6 +369,10 @@ object RelPath { val up: RelPath = new RelPath(Internals.emptyStringArray, 1) val rel: RelPath = new RelPath(Internals.emptyStringArray, 0) implicit def SubRelPath(p: SubPath): RelPath = new RelPath(p.segments0, 0) + def fromStringSegments(segments: Array[String]): RelPath = segments.foldLeft(RelPath.rel) { + case (agg, "..") => agg / up + case (agg, seg) => agg / seg + } } /** diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index eb57f51d..046b57ac 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -9,7 +9,10 @@ import utest.{assert => _, _} import java.net.URI object PathTests extends TestSuite { - private def nonValidPathSegment(chars: String) = s"[$chars] is not a valid path segment." + private def nonCanonicalLiteral(providedLiteral: String, sanitizedLiteral: String) = + s"Literal path sequence [$providedLiteral] used in OS-Lib must be in a canonical form, please use [$sanitizedLiteral] instead" + private def removeLiteralErr(literal: String) = + s"Literal path sequence [$literal] doesn't affect path being formed, please remove it" val tests = Tests { test("segmentsFromString") { @@ -17,29 +20,29 @@ object PathTests extends TestSuite { assert(segmentsFromString(s).sameElements(expected)) } - testSegmentsFromString(" ", " " :: Nil) + testSegmentsFromString(" ", List(" ")) - testSegmentsFromString("", "" :: Nil) + testSegmentsFromString("", List("")) - testSegmentsFromString("""foo/bar/baz""", "foo" :: "bar" :: "baz" :: Nil) + testSegmentsFromString("""foo/bar/baz""", List("foo", "bar", "baz")) - testSegmentsFromString("""/""", "" :: "" :: Nil) - testSegmentsFromString("""//""", "" :: "" :: "" :: Nil) - testSegmentsFromString("""///""", "" :: "" :: "" :: "" :: Nil) + testSegmentsFromString("""/""", List("", "")) + testSegmentsFromString("""//""", List("", "", "")) + testSegmentsFromString("""///""", List("", "", "", "")) - testSegmentsFromString("""a/""", "a" :: "" :: Nil) - testSegmentsFromString("""a//""", "a" :: "" :: "" :: Nil) - testSegmentsFromString("""a///""", "a" :: "" :: "" :: "" :: Nil) + testSegmentsFromString("""a/""", List("a", "")) + testSegmentsFromString("""a//""", List("a", "", "")) + testSegmentsFromString("""a///""", List("a", "", "", "")) - testSegmentsFromString("""ahs/""", "ahs" :: "" :: Nil) - testSegmentsFromString("""ahs//""", "ahs" :: "" :: "" :: Nil) + testSegmentsFromString("""ahs/""", List("ahs", "")) + testSegmentsFromString("""ahs//""", List("ahs", "", "")) - testSegmentsFromString("""ahs/aa/""", "ahs" :: "aa" :: "" :: Nil) - testSegmentsFromString("""ahs/aa//""", "ahs" :: "aa" :: "" :: "" :: Nil) + testSegmentsFromString("""ahs/aa/""", List("ahs", "aa", "")) + testSegmentsFromString("""ahs/aa//""", List("ahs", "aa", "", "")) - testSegmentsFromString("""/a""", "" :: "a" :: Nil) - testSegmentsFromString("""//a""", "" :: "" :: "a" :: Nil) - testSegmentsFromString("""//a/""", "" :: "" :: "a" :: "" :: Nil) + testSegmentsFromString("""/a""", List("", "a")) + testSegmentsFromString("""//a""", List("", "", "a")) + testSegmentsFromString("""//a/""", List("", "", "a", "")) } test("Literals") { test("Basic") { @@ -47,39 +50,44 @@ object PathTests extends TestSuite { assert(root / "core/src/test" == root / "core" / "src" / "test") assert(root / "core/src/test" == root / "core" / "src/test") } - test("Compile errors") { - - compileError("""root / "/" """).check("", nonValidPathSegment("")) - compileError("""root / "/ " """).check("", nonValidPathSegment("")) - compileError("""root / " /" """).check("", nonValidPathSegment("")) - compileError("""root / "//" """).check("", nonValidPathSegment("")) - - compileError("""root / "foo/" """).check("", nonValidPathSegment("")) - compileError("""root / "foo//" """).check("", nonValidPathSegment("")) + test("literals with [..]") { + assert(rel / "src" / ".." == rel / "src" / os.up) + assert(root / "src/.." == root / "src" / os.up) + assert(root / "src" / ".." == root / "src" / os.up) + assert(root / "hello" / ".." / "world" == root / "hello" / os.up / "world") + assert(root / "hello" / "../world" == root / "hello" / os.up / "world") + assert(root / "hello/../world" == root / "hello" / os.up / "world") + } - compileError("""root / "foo/bar/" """).check("", nonValidPathSegment("")) - compileError("""root / "foo/bar//" """).check("", nonValidPathSegment("")) + test("Compile errors") { + compileError("""root / "/" """).check("", removeLiteralErr("/")) + compileError("""root / "/ " """).check("", nonCanonicalLiteral("/ ", " ")) + compileError("""root / " /" """).check("", nonCanonicalLiteral(" /", " ")) + compileError("""root / "//" """).check("", removeLiteralErr("//")) + + compileError("""root / "foo/" """).check("", nonCanonicalLiteral("foo/", "foo")) + compileError("""root / "foo//" """).check("", nonCanonicalLiteral("foo//", "foo")) + + compileError("""root / "foo/bar/" """).check("", nonCanonicalLiteral("foo/bar/", "foo/bar")) + compileError("""root / "foo/bar//" """).check( + "", + nonCanonicalLiteral("foo/bar//", "foo/bar") + ) - compileError("""root / "/foo" """).check("", nonValidPathSegment("")) - compileError("""root / "//foo" """).check("", nonValidPathSegment("")) + compileError("""root / "/foo" """).check("", nonCanonicalLiteral("/foo", "foo")) + compileError("""root / "//foo" """).check("", nonCanonicalLiteral("//foo", "foo")) - compileError("""root / "//foo/" """).check("", nonValidPathSegment("")) + compileError("""root / "//foo/" """).check("", nonCanonicalLiteral("//foo/", "foo")) - compileError(""" rel / "src" / "" """).check("", nonValidPathSegment("")) - compileError(""" rel / "src" / "." """).check("", nonValidPathSegment(".")) - compileError(""" rel / "src" / ".." """).check("", nonValidPathSegment("..")) + compileError(""" rel / "src" / "" """).check("", removeLiteralErr("")) + compileError(""" rel / "src" / "." """).check("", removeLiteralErr(".")) - compileError { """ root / "src/" """ }.check("", nonValidPathSegment("")) - compileError { """ root / "src/." """ }.check("", nonValidPathSegment(".")) - compileError { """ root / "src/.." """ }.check("", nonValidPathSegment("..")) + compileError(""" root / "src/" """).check("", nonCanonicalLiteral("src/", "src")) + compileError(""" root / "src/." """).check("", nonCanonicalLiteral("src/.", "src")) - compileError { """ root / "" """ }.check("", nonValidPathSegment("")) - compileError { """ root / "." """ }.check("", nonValidPathSegment(".")) - compileError { """ root / ".." """ }.check("", nonValidPathSegment("..")) + compileError(""" root / "" """).check("", removeLiteralErr("")) + compileError(""" root / "." """).check("", removeLiteralErr(".")) - compileError(""" root / "hello" / ".." / "world" """).check("", nonValidPathSegment("..")) - compileError(""" root / "hello" / "../world" """).check("", nonValidPathSegment("..")) - compileError(""" root / "hello/../world" """).check("", nonValidPathSegment("..")) } } test("Basic") { From 84ea724dd5a493ad808437f9e41abb8065b0bc3a Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 10 Sep 2024 08:16:24 +0200 Subject: [PATCH 15/15] making segmentsFromString private[os] moving its tests to os package --- os/src/Path.scala | 2 +- os/test/src/PathTests.scala | 30 ----------------- os/test/src/SegmentsFromStringTests.scala | 39 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 os/test/src/SegmentsFromStringTests.scala diff --git a/os/src/Path.scala b/os/src/Path.scala index a9941fe6..7226e0e3 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -20,7 +20,7 @@ trait StringPathChunkConversion { } object PathChunk extends PathChunkMacros { - def segmentsFromString(s: String): Array[String] = { + private[os] def segmentsFromString(s: String): Array[String] = { val trailingSeparatorsCount = s.reverseIterator.takeWhile(_ == '/').length val strNoTrailingSeps = s.dropRight(trailingSeparatorsCount) val splitted = strNoTrailingSeps.split('/') diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index 046b57ac..6e06ca9a 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -4,7 +4,6 @@ import java.nio.file.Paths import java.io.File import os._ import os.Path.driveRoot -import os.PathChunk.segmentsFromString import utest.{assert => _, _} import java.net.URI @@ -15,35 +14,6 @@ object PathTests extends TestSuite { s"Literal path sequence [$literal] doesn't affect path being formed, please remove it" val tests = Tests { - test("segmentsFromString") { - def testSegmentsFromString(s: String, expected: List[String]) = { - assert(segmentsFromString(s).sameElements(expected)) - } - - testSegmentsFromString(" ", List(" ")) - - testSegmentsFromString("", List("")) - - testSegmentsFromString("""foo/bar/baz""", List("foo", "bar", "baz")) - - testSegmentsFromString("""/""", List("", "")) - testSegmentsFromString("""//""", List("", "", "")) - testSegmentsFromString("""///""", List("", "", "", "")) - - testSegmentsFromString("""a/""", List("a", "")) - testSegmentsFromString("""a//""", List("a", "", "")) - testSegmentsFromString("""a///""", List("a", "", "", "")) - - testSegmentsFromString("""ahs/""", List("ahs", "")) - testSegmentsFromString("""ahs//""", List("ahs", "", "")) - - testSegmentsFromString("""ahs/aa/""", List("ahs", "aa", "")) - testSegmentsFromString("""ahs/aa//""", List("ahs", "aa", "", "")) - - testSegmentsFromString("""/a""", List("", "a")) - testSegmentsFromString("""//a""", List("", "", "a")) - testSegmentsFromString("""//a/""", List("", "", "a", "")) - } test("Literals") { test("Basic") { assert(rel / "src" / "Main/.scala" == rel / "src" / "Main" / ".scala") diff --git a/os/test/src/SegmentsFromStringTests.scala b/os/test/src/SegmentsFromStringTests.scala new file mode 100644 index 00000000..a3e72738 --- /dev/null +++ b/os/test/src/SegmentsFromStringTests.scala @@ -0,0 +1,39 @@ +package os + +import os.PathChunk.segmentsFromString +import utest.{assert => _, _} + +object SegmentsFromStringTests extends TestSuite { + + val tests = Tests { + test("segmentsFromString") { + def testSegmentsFromString(s: String, expected: List[String]) = { + assert(segmentsFromString(s).sameElements(expected)) + } + + testSegmentsFromString(" ", List(" ")) + + testSegmentsFromString("", List("")) + + testSegmentsFromString("""foo/bar/baz""", List("foo", "bar", "baz")) + + testSegmentsFromString("""/""", List("", "")) + testSegmentsFromString("""//""", List("", "", "")) + testSegmentsFromString("""///""", List("", "", "", "")) + + testSegmentsFromString("""a/""", List("a", "")) + testSegmentsFromString("""a//""", List("a", "", "")) + testSegmentsFromString("""a///""", List("a", "", "", "")) + + testSegmentsFromString("""ahs/""", List("ahs", "")) + testSegmentsFromString("""ahs//""", List("ahs", "", "")) + + testSegmentsFromString("""ahs/aa/""", List("ahs", "aa", "")) + testSegmentsFromString("""ahs/aa//""", List("ahs", "aa", "", "")) + + testSegmentsFromString("""/a""", List("", "a")) + testSegmentsFromString("""//a""", List("", "", "a")) + testSegmentsFromString("""//a/""", List("", "", "a", "")) + } + } +}