diff --git a/compiler/src/dotty/tools/io/Path.scala b/compiler/src/dotty/tools/io/Path.scala index e632bf488ec9..208b18078b7e 100644 --- a/compiler/src/dotty/tools/io/Path.scala +++ b/compiler/src/dotty/tools/io/Path.scala @@ -126,9 +126,33 @@ class Path private[io] (val jpath: JPath) { /** * @return The path of the parent directory, or root if path is already root */ - def parent: Directory = jpath.normalize.getParent match { - case null => Directory(jpath) - case parent => Directory(parent) + def parent: Directory = { + // We don't call JPath#normalize here because it may result in resolving + // to a different path than intended, such as when the given path contains + // a `..` component and the preceding name is a symbolic link. + // https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html#normalize-- + // + // Paths ending with `..` or `.` are handled specially here as + // JPath#getParent wants to simply strip away that last element. + // For the `..` case, we should take care not to fall into the above trap + // as the preceding name may be a symbolic link. This leaves little choice + // (since we don't want to access the file system) but to return the given + // path with `..` appended. By contrast, a `.` as the final path element + // is always redundant and should be removed before computing the parent. + if path.isEmpty then + Directory("..") + else if jpath.endsWith("..") then + (this / "..").toDirectory + else if jpath.endsWith(".") then + jpath.getParent match // strip the trailing `.` element + case null => Directory("..") + case p => new Path(p).parent + else jpath.getParent match + case null => + if isAbsolute then toDirectory // it should be a root + else Directory(".") + case x => + Directory(x) } def parents: List[Directory] = { val p = parent diff --git a/compiler/test/dotty/tools/io/PathTest.scala b/compiler/test/dotty/tools/io/PathTest.scala new file mode 100644 index 000000000000..731d29cee8e6 --- /dev/null +++ b/compiler/test/dotty/tools/io/PathTest.scala @@ -0,0 +1,49 @@ +package dotty.tools.io + +import org.junit.Test + +class PathTest { + // Ref https://github.com/lampepfl/dotty/issues/11644#issuecomment-792457275 + @Test def parent(): Unit = { + testParent(Path(""), Directory("..")) + testParent(Path("."), Directory("..")) + testParent(Path(".") / ".", Directory("..")) + testParent(Path(".."), Directory("..") / "..") + testParent(Path("..") / ".", Directory("..") / "..") + testParent(Path("..") / "..", Directory("..") / ".." / "..") + testParent(Path(".") / "..", + Directory("..") / "..", + Directory(".") / ".." / "..") + + testParent(Path("foo") / ".", Directory(".")) + testParent(Path("foo") / ".." / "bar", Directory("foo") / "..") + testParent(Path("foo") / ".." / "." / "bar", Directory("foo") / ".." / ".") + + testParent(Path("foo.txt"), Directory(".")) + testParent(Path(".") / "foo.txt", Directory(".")) + testParent(Path(".") / "baz" / "bar" / "foo.txt", + Directory(".") / "baz" / "bar", + Directory("baz") / "bar") + + for (root <- Path.roots) { + testParent(root, root) + testParent(root / ".", root) + testParent(root / "..", root / ".." / "..") + testParent(root / "foo" / ".", root) + testParent(root / "foo.txt", root) + testParent(root / "baz" / "bar" / "foo.txt", root / "baz" / "bar") + testParent(root / "foo" / "bar" / "..", root / "foo" / "bar" / ".." / "..") + } + } + + /** The parent of a path may have multiple valid non-canonical representations. + * Here we test that the parent of the specified path is among a curated list + * of representations we consider to be valid. + */ + private def testParent(path: Path, expected: Path*): Unit = { + val actual = path.parent + val some = if (expected.length > 1) " one of" else "" + assert(expected.contains(actual), + s"""expected$some: ${expected.mkString("<",">, <",">")} but was: <$actual>""") + } +} diff --git a/project/scripts/bootstrapCmdTests b/project/scripts/bootstrapCmdTests index d4d7313d300c..7683c53f9e82 100755 --- a/project/scripts/bootstrapCmdTests +++ b/project/scripts/bootstrapCmdTests @@ -37,6 +37,7 @@ clear_out "$OUT" # check that `scalac -from-tasty` compiles and `scala` runs it echo "testing ./bin/scalac -from-tasty and scala -classpath" clear_out "$OUT1" +./bin/scalac "$SOURCE" -d "$OUT" ./bin/scalac -from-tasty -d "$OUT1" "$OUT/$TASTY" ./bin/scala -classpath "$OUT1" "$MAIN" > "$tmp" test "$EXPECTED_OUTPUT" = "$(cat "$tmp")" @@ -57,3 +58,9 @@ grep -qe "def main(args: scala.Array\[scala.Predef.String\]): scala.Unit =" "$tm echo "testing ./bin/scaladoc" clear_out "$OUT1" ./bin/scaladoc -project Hello -d "$OUT1" "$OUT/out.jar" + +# check compilation when the class/tasty files already exist in the current directory +echo "testing i11644" +cwd=$(pwd) +clear_out "$OUT" +(cd "$OUT" && "$cwd/bin/scalac" "$cwd/tests/pos/i11644.scala" && "$cwd/bin/scalac" "$cwd/tests/pos/i11644.scala") diff --git a/project/scripts/cmdTestsCommon.inc.sh b/project/scripts/cmdTestsCommon.inc.sh index 1247481aec12..dc81a3e35b52 100644 --- a/project/scripts/cmdTestsCommon.inc.sh +++ b/project/scripts/cmdTestsCommon.inc.sh @@ -15,5 +15,5 @@ tmp=$(mktemp) clear_out() { local out="$1" - rm -rf "$out/*" + rm -rf "$out"/* } diff --git a/tests/pos/i11644.scala b/tests/pos/i11644.scala new file mode 100644 index 000000000000..3e0b75e49e37 --- /dev/null +++ b/tests/pos/i11644.scala @@ -0,0 +1 @@ +@main def hello = println("hello, world")