From 3673ccfff795a581fc84fd3578874c95af7e4165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 4 Oct 2021 12:12:45 +0200 Subject: [PATCH 001/121] Create classes for improved main annotations --- .../src/scala/annotation/MainAnnotation.scala | 40 ++++++++ library/src/scala/main.scala | 92 ++++++++++++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 library/src/scala/annotation/MainAnnotation.scala diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala new file mode 100644 index 000000000000..53e720ff8aaa --- /dev/null +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -0,0 +1,40 @@ +package scala.annotation + +/** MainAnnotation provides the functionality for a compiler-generated main class. + * It links a compiler-generated main method (call it compiler-main) to a user + * written main method (user-main). + * The protocol of calls from compiler-main is as follows: + * + * - create a `command` with the command line arguments, + * - for each parameter of user-main, a call to `command.argGetter`, + * or `command.argsGetter` if is a final varargs parameter, + * - a call to `command.run` with the closure of user-main applied to all arguments. + */ +trait MainAnnotation extends StaticAnnotation: + + /** The class used for argument string parsing. E.g. `scala.util.CommandLineParser.FromString`, + * but could be something else + */ + type ArgumentParser[T] + + /** The required result type of the main function */ + type MainResultType + + /** A new command with arguments from `args` */ + def command(args: Array[String]): Command + + /** A class representing a command to run */ + abstract class Command: + + /** The getter for the next argument of type `T` */ + def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + + /** The getter for a final varargs argument of type `T*` */ + def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] + + /** Run `program` if all arguments are valid, + * or print usage information and/or error messages. + */ + def run(program: => MainResultType, progName: String, docComment: String): Unit + end Command +end MainAnnotation diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 057fdad4c2fb..41a8c5e04d83 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -8,6 +8,96 @@ package scala +import collection.mutable + /** An annotation that designates a main function */ -class main extends scala.annotation.Annotation {} +class main extends scala.annotation.MainAnnotation: + type ArgumentParser[T] = util.CommandLineParser.FromString[T] + type MainResultType = Any + + def command(args: Array[String]): Command = new Command: + + /** A buffer of demanded argument names, plus + * "?" if it has a default + * "*" if it is a vararg + * "" otherwise + */ + private var argInfos = new mutable.ListBuffer[(String, String)] + + /** A buffer for all errors */ + private var errors = new mutable.ListBuffer[String] + + /** Issue an error, and return an uncallable getter */ + private def error(msg: String): () => Nothing = + errors += msg + () => throw new AssertionError("trying to get invalid argument") + + /** The next argument index */ + private var argIdx: Int = 0 + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = + argInfos += ((argName, if defaultValue.isDefined then "?" else "")) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => defaultValue match + case Some(t) => () => t + case None => error(s"missing argument for $argName") + + def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = + argInfos += ((argName, "*")) + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(arg, argName, p) :: remainingArgGetters() + case None => Nil + val getters = remainingArgGetters() + () => getters.map(_()) + + def run(f: => MainResultType, progName: String, docComment: String): Unit = + def usage(): Unit = + println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}") + + def explain(): Unit = + if docComment.nonEmpty then println(docComment) // todo: process & format doc comment + + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.isEmpty || args.contains("--help") then + usage() + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else f match + case n: Int if n < 0 => System.exit(-n) + case _ => + end run + end command +end main From 65785d4c52d0c3137e6d580c4b685f8857a672da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 4 Oct 2021 14:49:52 +0200 Subject: [PATCH 002/121] Fix parameter error in argsGetter --- library/src/scala/main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 41a8c5e04d83..c5b2003be955 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -63,7 +63,7 @@ class main extends scala.annotation.MainAnnotation: def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = argInfos += ((argName, "*")) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(arg, argName, p) :: remainingArgGetters() + case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) From e9db3d08a4295a5c0b4bd0e053f50b491bba7a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 4 Oct 2021 14:50:17 +0200 Subject: [PATCH 003/121] Allow no parameters --- library/src/scala/main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index c5b2003be955..c0b981755141 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -87,7 +87,7 @@ class main extends scala.annotation.MainAnnotation: error(s"unknown argument name: $arg") end flagUnused - if args.isEmpty || args.contains("--help") then + if args.contains("--help") then usage() explain() else From 5d3c199eef1bea0077e5e5723be03ce3b33dc98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 4 Oct 2021 14:51:25 +0200 Subject: [PATCH 004/121] Add first tests for new main annotation --- tests/run/main-annotation-default-value.check | 3 + tests/run/main-annotation-default-value.scala | 25 ++++++ tests/run/main-annotation-help.check | 6 ++ tests/run/main-annotation-help.scala | 25 ++++++ tests/run/main-annotation-named-params.check | 2 + tests/run/main-annotation-named-params.scala | 24 ++++++ tests/run/main-annotation-no-parameters.check | 1 + tests/run/main-annotation-no-parameters.scala | 21 +++++ tests/run/main-annotation-simple.check | 1 + tests/run/main-annotation-simple.scala | 76 +++++++++++++++++++ tests/run/main-annotation-types.check | 18 +++++ tests/run/main-annotation-types.scala | 37 +++++++++ tests/run/main-annotation-vararg-1.check | 5 ++ tests/run/main-annotation-vararg-1.scala | 29 +++++++ tests/run/main-annotation-vararg-2.check | 10 +++ tests/run/main-annotation-vararg-2.scala | 34 +++++++++ 16 files changed, 317 insertions(+) create mode 100644 tests/run/main-annotation-default-value.check create mode 100644 tests/run/main-annotation-default-value.scala create mode 100644 tests/run/main-annotation-help.check create mode 100644 tests/run/main-annotation-help.scala create mode 100644 tests/run/main-annotation-named-params.check create mode 100644 tests/run/main-annotation-named-params.scala create mode 100644 tests/run/main-annotation-no-parameters.check create mode 100644 tests/run/main-annotation-no-parameters.scala create mode 100644 tests/run/main-annotation-simple.check create mode 100644 tests/run/main-annotation-simple.scala create mode 100644 tests/run/main-annotation-types.check create mode 100644 tests/run/main-annotation-types.scala create mode 100644 tests/run/main-annotation-vararg-1.check create mode 100644 tests/run/main-annotation-vararg-1.scala create mode 100644 tests/run/main-annotation-vararg-2.check create mode 100644 tests/run/main-annotation-vararg-2.scala diff --git a/tests/run/main-annotation-default-value.check b/tests/run/main-annotation-default-value.check new file mode 100644 index 000000000000..f9f34ca3ad3e --- /dev/null +++ b/tests/run/main-annotation-default-value.check @@ -0,0 +1,3 @@ +2 + 3 = 5 +2 + 1 = 3 +0 + 1 = 1 diff --git a/tests/run/main-annotation-default-value.scala b/tests/run/main-annotation-default-value.scala new file mode 100644 index 000000000000..b3b73a39bb07 --- /dev/null +++ b/tests/run/main-annotation-default-value.scala @@ -0,0 +1,25 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int = 0, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]], Some(0)) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) + add.main(Array("2")) + add.main(Array()) +end Test \ No newline at end of file diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check new file mode 100644 index 000000000000..7e5ef4b359a2 --- /dev/null +++ b/tests/run/main-annotation-help.check @@ -0,0 +1,6 @@ +Usage: add num inc? +Adds two numbers +Usage: add num inc? +Adds two numbers +Usage: add num inc? +Adds two numbers diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala new file mode 100644 index 000000000000..db67aedc9b61 --- /dev/null +++ b/tests/run/main-annotation-help.scala @@ -0,0 +1,25 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("--help")) + add.main(Array("Some", "garbage", "before", "--help")) + add.main(Array("--help", "and", "some", "stuff", "after")) +end Test diff --git a/tests/run/main-annotation-named-params.check b/tests/run/main-annotation-named-params.check new file mode 100644 index 000000000000..fa94c80607d2 --- /dev/null +++ b/tests/run/main-annotation-named-params.check @@ -0,0 +1,2 @@ +2 + 3 = 5 +2 + 3 = 5 diff --git a/tests/run/main-annotation-named-params.scala b/tests/run/main-annotation-named-params.scala new file mode 100644 index 000000000000..b79262f4216c --- /dev/null +++ b/tests/run/main-annotation-named-params.scala @@ -0,0 +1,24 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("--num", "2", "--inc", "3")) + add.main(Array("--inc", "3", "--num", "2")) +end Test \ No newline at end of file diff --git a/tests/run/main-annotation-no-parameters.check b/tests/run/main-annotation-no-parameters.check new file mode 100644 index 000000000000..020c8e62064f --- /dev/null +++ b/tests/run/main-annotation-no-parameters.check @@ -0,0 +1 @@ +I run properly! diff --git a/tests/run/main-annotation-no-parameters.scala b/tests/run/main-annotation-no-parameters.scala new file mode 100644 index 000000000000..a0a967a63704 --- /dev/null +++ b/tests/run/main-annotation-no-parameters.scala @@ -0,0 +1,21 @@ +// Sample main method +object myProgram: + + /** Does nothing, except confirming that it runs */ + @main def run(): Unit = + println("I run properly!") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object run extends main: + def main(args: Array[String]) = + val cmd = command(args) + cmd.run(myProgram.run(), "run", "Does nothing, except confirming that it runs") +end run + +object Test: + def main(args: Array[String]): Unit = + run.main(Array()) +end Test diff --git a/tests/run/main-annotation-simple.check b/tests/run/main-annotation-simple.check new file mode 100644 index 000000000000..56acd5c7a7ab --- /dev/null +++ b/tests/run/main-annotation-simple.check @@ -0,0 +1 @@ +2 + 3 = 5 diff --git a/tests/run/main-annotation-simple.scala b/tests/run/main-annotation-simple.scala new file mode 100644 index 000000000000..d18d6a1f0ea0 --- /dev/null +++ b/tests/run/main-annotation-simple.scala @@ -0,0 +1,76 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) +end Test + +// After implemented, move direct call to `add` in tests/neg + +/** --- Some scenarios ---------------------------------------- + +> java add 2 3 +2 + 3 = 5 +> java add 4 +4 + 1 = 5 +> java add --num 10 --inc -2 +10 + -2 = 8 +> java add --num 10 +10 + 1 = 11 +> java add --help +Usage: add num inc? +Adds two numbers +> java add +Usage: add num inc? +Adds two numbers +> java add 1 2 3 4 +Error: unused argument: 3 +Error: unused argument: 4 +Usage: add num inc? +> java add -n 1 -i 10 +Error: invalid argument for num: -n +Error: unused argument: -i +Error: unused argument: 10 +Usage: add num inc? +> java add --n 1 --i 10 +Error: missing argument for num +Error: unknown argument name: --n +Error: unknown argument name: --i +Usage: add num inc? +> java add true 10 +Error: invalid argument for num: true +Usage: add num inc? +> java add true false +Error: invalid argument for num: true +Error: invalid argument for inc: false +Usage: add num inc? +> java add true false 10 +Error: invalid argument for num: true +Error: invalid argument for inc: false +Error: unused argument: 10 +Usage: add num inc? +> java add --inc 10 --num 20 +20 + 10 = 30 +> java add binary 10 01 +Error: invalid argument for num: binary +Error: unused argument: 01 +Usage: add num inc? + +*/ diff --git a/tests/run/main-annotation-types.check b/tests/run/main-annotation-types.check new file mode 100644 index 000000000000..0b7f741518c7 --- /dev/null +++ b/tests/run/main-annotation-types.check @@ -0,0 +1,18 @@ +Here's what I got: +int - 2 +double - 3.0 +string - 4 +boolean - true + +Here's what I got: +int - -1 +double - 3.4567890987654456E18 +string - false +boolean - false + +Here's what I got: +int - 2147483647 +double - 3.1415926535 +string - Hello world! +boolean - true + diff --git a/tests/run/main-annotation-types.scala b/tests/run/main-annotation-types.scala new file mode 100644 index 000000000000..9d37a27c8a9a --- /dev/null +++ b/tests/run/main-annotation-types.scala @@ -0,0 +1,37 @@ +// Sample main method +object myProgram: + + /** Displays some parameters */ + @main def show( + int: Int, + double: Double, + string: String, + boolean: Boolean + ): Unit = + println("Here's what I got:") + println(s"int - $int") + println(s"double - $double") + println(s"string - $string") + println(s"boolean - $boolean") + println() + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object show extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("int", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Double]("double", summon[ArgumentParser[Double]]) + val arg3 = cmd.argGetter[String]("string", summon[ArgumentParser[String]]) + val arg4 = cmd.argGetter[Boolean]("boolean", summon[ArgumentParser[Boolean]]) + cmd.run(myProgram.show(arg1(), arg2(), arg3(), arg4()), "show", "Displays some parameters") +end show + +object Test: + def main(args: Array[String]): Unit = + show.main(Array("2", "3", "4", "true")) + show.main(Array("-1", "3456789098765445678", "false", "FALSE")) + show.main(Array("2147483647", "3.1415926535", "Hello world!", "True")) +end Test diff --git a/tests/run/main-annotation-vararg-1.check b/tests/run/main-annotation-vararg-1.check new file mode 100644 index 000000000000..c35a83bfd5fa --- /dev/null +++ b/tests/run/main-annotation-vararg-1.check @@ -0,0 +1,5 @@ +2 + 3 = 5 +2 + 3 + -4 = 1 +1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55 +0 = 0 +No number input diff --git a/tests/run/main-annotation-vararg-1.scala b/tests/run/main-annotation-vararg-1.scala new file mode 100644 index 000000000000..06979ee03d87 --- /dev/null +++ b/tests/run/main-annotation-vararg-1.scala @@ -0,0 +1,29 @@ +// Sample main method +object myProgram: + + /** Adds any amount of numbers */ + @main def add(nums: Int*): Unit = + if (nums.isEmpty) + println("No number input") + else + println(s"${nums.mkString(" + ")} = ${nums.sum}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argsGetter[Int]("nums", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(): _*), "add", "Adds any amount of numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) + add.main(Array("2", "3", "-4")) + add.main((1 to 10).toArray.map(_.toString)) + add.main(Array("0")) + add.main(Array()) +end Test diff --git a/tests/run/main-annotation-vararg-2.check b/tests/run/main-annotation-vararg-2.check new file mode 100644 index 000000000000..7a7c9798fab4 --- /dev/null +++ b/tests/run/main-annotation-vararg-2.check @@ -0,0 +1,10 @@ +Correct +Correct +Expected 3 arguments, but got 1 + No 3 elements +Correct +Expected 0 arguments, but got 4 + I, shouldn't, be, here +Expected -2 arguments, but got 1 + How does that make sense? +Correct diff --git a/tests/run/main-annotation-vararg-2.scala b/tests/run/main-annotation-vararg-2.scala new file mode 100644 index 000000000000..631474835b2e --- /dev/null +++ b/tests/run/main-annotation-vararg-2.scala @@ -0,0 +1,34 @@ +// Sample main method +object myProgram: + + /** Checks that the correct amount of parameters were passed */ + @main def count(count: Int, elems: String*): Unit = + if (elems.length == count) + println("Correct") + else + println(s"Expected $count argument${if (count != 1) "s" else ""}, but got ${elems.length}") + println(s" ${elems.mkString(", ")}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object count extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("count", summon[ArgumentParser[Int]]) + val arg2 = cmd.argsGetter[String]("elems", summon[ArgumentParser[String]]) + + cmd.run(myProgram.count(arg1(), arg2(): _*), "count", "Checks that the correct amount of parameters were passed") +end count + +object Test: + def main(args: Array[String]): Unit = + count.main(Array("1", "Hello")) + count.main(Array("2", "Hello", "world!")) + count.main(Array("3", "No 3 elements")) + count.main(Array("0")) + count.main(Array("0", "I", "shouldn't", "be", "here")) + count.main(Array("-2", "How does that make sense?")) + count.main(Array("26") ++ ('a' to 'z').toArray.map(_.toString)) +end Test From 6161b9b41fda4d0ea8b26546bb3c394a9f5c866d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 5 Oct 2021 12:15:26 +0200 Subject: [PATCH 005/121] Add tests for incorrect parameters passing --- tests/run/main-annotation-wrong-param-1.check | 13 +++++++++ tests/run/main-annotation-wrong-param-1.scala | 26 +++++++++++++++++ .../main-annotation-wrong-param-names.check | 9 ++++++ .../main-annotation-wrong-param-names.scala | 24 ++++++++++++++++ .../main-annotation-wrong-param-number.check | 16 +++++++++++ .../main-annotation-wrong-param-number.scala | 26 +++++++++++++++++ .../main-annotation-wrong-param-type.check | 14 ++++++++++ .../main-annotation-wrong-param-type.scala | 28 +++++++++++++++++++ 8 files changed, 156 insertions(+) create mode 100644 tests/run/main-annotation-wrong-param-1.check create mode 100644 tests/run/main-annotation-wrong-param-1.scala create mode 100644 tests/run/main-annotation-wrong-param-names.check create mode 100644 tests/run/main-annotation-wrong-param-names.scala create mode 100644 tests/run/main-annotation-wrong-param-number.check create mode 100644 tests/run/main-annotation-wrong-param-number.scala create mode 100644 tests/run/main-annotation-wrong-param-type.check create mode 100644 tests/run/main-annotation-wrong-param-type.scala diff --git a/tests/run/main-annotation-wrong-param-1.check b/tests/run/main-annotation-wrong-param-1.check new file mode 100644 index 000000000000..3220f4dc201e --- /dev/null +++ b/tests/run/main-annotation-wrong-param-1.check @@ -0,0 +1,13 @@ +Error: invalid argument for inc: true +Error: unused argument: SPAAAAACE +Usage: add num inc +Error: invalid argument for num: add +Error: unused argument: 3 +Usage: add num inc +Error: invalid argument for num: true +Error: invalid argument for inc: false +Error: unused argument: 10 +Usage: add num inc +Error: invalid argument for num: binary +Error: unused argument: 01 +Usage: add num inc diff --git a/tests/run/main-annotation-wrong-param-1.scala b/tests/run/main-annotation-wrong-param-1.scala new file mode 100644 index 000000000000..eecf83edd816 --- /dev/null +++ b/tests/run/main-annotation-wrong-param-1.scala @@ -0,0 +1,26 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "true", "SPAAAAACE")) + add.main(Array("add", "2", "3")) + add.main(Array("true", "false", "10")) + add.main(Array("binary", "10", "01")) +end Test diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check new file mode 100644 index 000000000000..9518b2de5330 --- /dev/null +++ b/tests/run/main-annotation-wrong-param-names.check @@ -0,0 +1,9 @@ +Error: invalid argument for num: -n +Error: unused argument: -i +Error: unused argument: 10 +Usage: add num inc +Error: missing argument for num +Error: missing argument for inc +Error: unknown argument name: --n +Error: unknown argument name: --i +Usage: add num inc diff --git a/tests/run/main-annotation-wrong-param-names.scala b/tests/run/main-annotation-wrong-param-names.scala new file mode 100644 index 000000000000..27fd09925a9f --- /dev/null +++ b/tests/run/main-annotation-wrong-param-names.scala @@ -0,0 +1,24 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("-n", "1", "-i", "10")) + add.main(Array("--n", "1", "--i", "10")) +end Test diff --git a/tests/run/main-annotation-wrong-param-number.check b/tests/run/main-annotation-wrong-param-number.check new file mode 100644 index 000000000000..6becb8d4952c --- /dev/null +++ b/tests/run/main-annotation-wrong-param-number.check @@ -0,0 +1,16 @@ +Error: missing argument for num +Error: missing argument for inc +Usage: add num inc +Error: missing argument for inc +Usage: add num inc +Error: unused argument: 3 +Usage: add num inc +Error: unused argument: 3 +Error: unused argument: 4 +Error: unused argument: 5 +Error: unused argument: 6 +Error: unused argument: 7 +Error: unused argument: 8 +Error: unused argument: 9 +Error: unused argument: 10 +Usage: add num inc diff --git a/tests/run/main-annotation-wrong-param-number.scala b/tests/run/main-annotation-wrong-param-number.scala new file mode 100644 index 000000000000..9159b7e6c171 --- /dev/null +++ b/tests/run/main-annotation-wrong-param-number.scala @@ -0,0 +1,26 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array()) + add.main(Array("1")) + add.main(Array("1", "2", "3")) + add.main(Array((1 to 10).toArray.map(_.toString): _*)) +end Test diff --git a/tests/run/main-annotation-wrong-param-type.check b/tests/run/main-annotation-wrong-param-type.check new file mode 100644 index 000000000000..8b3269a91b2e --- /dev/null +++ b/tests/run/main-annotation-wrong-param-type.check @@ -0,0 +1,14 @@ +Error: invalid argument for inc: true +Usage: add num inc +Error: invalid argument for num: 2.1 +Usage: add num inc +Error: invalid argument for inc: 3.1415921535 +Usage: add num inc +Error: invalid argument for num: 192.168.1.1 +Usage: add num inc +Error: invalid argument for num: false +Error: invalid argument for inc: true +Usage: add num inc +Error: invalid argument for num: Hello +Error: invalid argument for inc: world! +Usage: add num inc diff --git a/tests/run/main-annotation-wrong-param-type.scala b/tests/run/main-annotation-wrong-param-type.scala new file mode 100644 index 000000000000..bc62c4220993 --- /dev/null +++ b/tests/run/main-annotation-wrong-param-type.scala @@ -0,0 +1,28 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "true")) + add.main(Array("2.1", "3")) + add.main(Array("2", "3.1415921535")) + add.main(Array("192.168.1.1", "3")) + add.main(Array("false", "true")) + add.main(Array("Hello", "world!")) +end Test From 959aff90d991439134587c4cfc1db55faee82721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 8 Oct 2021 16:12:54 +0200 Subject: [PATCH 006/121] Update MiMa filters --- project/MiMaFilters.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index cac81eecc141..5b40530237db 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -4,6 +4,12 @@ import com.typesafe.tools.mima.core._ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( // Experimental APIs that can be added in 3.2.0 + ProblemFilters.exclude[MissingTypesProblem]("scala.main"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.init"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.last"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"), ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeReprMethods.substituteTypes"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeReprMethods.substituteTypes"), From 81c88c105d0bb2f0c398318d62a77f76e6054d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 13 Oct 2021 10:07:23 +0200 Subject: [PATCH 007/121] Split argGetter with/out default value --- .../src/scala/annotation/MainAnnotation.scala | 5 ++++- library/src/scala/main.scala | 16 +++++++++++----- tests/run/main-annotation-default-value.scala | 4 ++-- tests/run/main-annotation-help.scala | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 53e720ff8aaa..8b4bf818bf03 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -27,7 +27,10 @@ trait MainAnnotation extends StaticAnnotation: abstract class Command: /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: Option[T] = None): () => T + def argGetter[T](argName: String, fromString: ArgumentParser[T]): () => T + + /** The getter for the next argument of type `T` with a default value */ + def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: T): () => T /** The getter for a final varargs argument of type `T*` */ def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index c0b981755141..904ca0cd6ee4 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -50,15 +50,21 @@ class main extends scala.annotation.MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: Option[T] = None): () => T = - argInfos += ((argName, if defaultValue.isDefined then "?" else "")) + def argGetter[T](argName: String, p: ArgumentParser[T]): () => T = + argInfos += ((argName, "")) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() argOpt match case Some(arg) => convert(argName, arg, p) - case None => defaultValue match - case Some(t) => () => t - case None => error(s"missing argument for $argName") + case None => error(s"missing argument for $argName") + + def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: T): () => T = + argInfos += ((argName, "?")) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => () => defaultValue def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = argInfos += ((argName, "*")) diff --git a/tests/run/main-annotation-default-value.scala b/tests/run/main-annotation-default-value.scala index b3b73a39bb07..e11f9e05f870 100644 --- a/tests/run/main-annotation-default-value.scala +++ b/tests/run/main-annotation-default-value.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]], Some(0)) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]], 0) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], 1) cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index db67aedc9b61..01fd68d5014d 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -13,7 +13,7 @@ object add extends main: def main(args: Array[String]) = val cmd = command(args) val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], Some(1)) + val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], 1) cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add From 2fbdf66b8446dad3edd43326f9331ab77ed89cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 13 Oct 2021 10:34:09 +0200 Subject: [PATCH 008/121] Curry parsers with using --- library/src/scala/annotation/MainAnnotation.scala | 6 +++--- library/src/scala/main.scala | 6 +++--- tests/run/main-annotation-default-value.scala | 4 ++-- tests/run/main-annotation-help.scala | 4 ++-- tests/run/main-annotation-named-params.scala | 4 ++-- tests/run/main-annotation-simple.scala | 4 ++-- tests/run/main-annotation-types.scala | 8 ++++---- tests/run/main-annotation-vararg-1.scala | 2 +- tests/run/main-annotation-vararg-2.scala | 4 ++-- tests/run/main-annotation-wrong-param-1.scala | 4 ++-- tests/run/main-annotation-wrong-param-names.scala | 4 ++-- tests/run/main-annotation-wrong-param-number.scala | 4 ++-- tests/run/main-annotation-wrong-param-type.scala | 4 ++-- 13 files changed, 29 insertions(+), 29 deletions(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 8b4bf818bf03..208d6da82235 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -27,13 +27,13 @@ trait MainAnnotation extends StaticAnnotation: abstract class Command: /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, fromString: ArgumentParser[T]): () => T + def argGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetter[T](argName: String, fromString: ArgumentParser[T], defaultValue: T): () => T + def argGetter[T](argName: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String, fromString: ArgumentParser[T]): () => Seq[T] + def argsGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 904ca0cd6ee4..19a8f02fe186 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -50,7 +50,7 @@ class main extends scala.annotation.MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - def argGetter[T](argName: String, p: ArgumentParser[T]): () => T = + def argGetter[T](argName: String)(using p: ArgumentParser[T]): () => T = argInfos += ((argName, "")) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() @@ -58,7 +58,7 @@ class main extends scala.annotation.MainAnnotation: case Some(arg) => convert(argName, arg, p) case None => error(s"missing argument for $argName") - def argGetter[T](argName: String, p: ArgumentParser[T], defaultValue: T): () => T = + def argGetter[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = argInfos += ((argName, "?")) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() @@ -66,7 +66,7 @@ class main extends scala.annotation.MainAnnotation: case Some(arg) => convert(argName, arg, p) case None => () => defaultValue - def argsGetter[T](argName: String, p: ArgumentParser[T]): () => Seq[T] = + def argsGetter[T](argName: String)(using p: ArgumentParser[T]): () => Seq[T] = argInfos += ((argName, "*")) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() diff --git a/tests/run/main-annotation-default-value.scala b/tests/run/main-annotation-default-value.scala index e11f9e05f870..742a55edda64 100644 --- a/tests/run/main-annotation-default-value.scala +++ b/tests/run/main-annotation-default-value.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]], 0) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], 1) + val arg1 = cmd.argGetter[Int]("num", 0) + val arg2 = cmd.argGetter[Int]("inc", 1) cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index 01fd68d5014d..730fc335c9fb 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]], 1) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc", 1) cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-named-params.scala b/tests/run/main-annotation-named-params.scala index b79262f4216c..e0a61cb54991 100644 --- a/tests/run/main-annotation-named-params.scala +++ b/tests/run/main-annotation-named-params.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-simple.scala b/tests/run/main-annotation-simple.scala index d18d6a1f0ea0..2e176514741e 100644 --- a/tests/run/main-annotation-simple.scala +++ b/tests/run/main-annotation-simple.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-types.scala b/tests/run/main-annotation-types.scala index 9d37a27c8a9a..4d5b97b29dcd 100644 --- a/tests/run/main-annotation-types.scala +++ b/tests/run/main-annotation-types.scala @@ -22,10 +22,10 @@ end myProgram object show extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("int", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Double]("double", summon[ArgumentParser[Double]]) - val arg3 = cmd.argGetter[String]("string", summon[ArgumentParser[String]]) - val arg4 = cmd.argGetter[Boolean]("boolean", summon[ArgumentParser[Boolean]]) + val arg1 = cmd.argGetter[Int]("int") + val arg2 = cmd.argGetter[Double]("double") + val arg3 = cmd.argGetter[String]("string") + val arg4 = cmd.argGetter[Boolean]("boolean") cmd.run(myProgram.show(arg1(), arg2(), arg3(), arg4()), "show", "Displays some parameters") end show diff --git a/tests/run/main-annotation-vararg-1.scala b/tests/run/main-annotation-vararg-1.scala index 06979ee03d87..80fe275893fc 100644 --- a/tests/run/main-annotation-vararg-1.scala +++ b/tests/run/main-annotation-vararg-1.scala @@ -15,7 +15,7 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argsGetter[Int]("nums", summon[ArgumentParser[Int]]) + val arg1 = cmd.argsGetter[Int]("nums") cmd.run(myProgram.add(arg1(): _*), "add", "Adds any amount of numbers") end add diff --git a/tests/run/main-annotation-vararg-2.scala b/tests/run/main-annotation-vararg-2.scala index 631474835b2e..aa363418610d 100644 --- a/tests/run/main-annotation-vararg-2.scala +++ b/tests/run/main-annotation-vararg-2.scala @@ -16,8 +16,8 @@ end myProgram object count extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("count", summon[ArgumentParser[Int]]) - val arg2 = cmd.argsGetter[String]("elems", summon[ArgumentParser[String]]) + val arg1 = cmd.argGetter[Int]("count") + val arg2 = cmd.argsGetter[String]("elems") cmd.run(myProgram.count(arg1(), arg2(): _*), "count", "Checks that the correct amount of parameters were passed") end count diff --git a/tests/run/main-annotation-wrong-param-1.scala b/tests/run/main-annotation-wrong-param-1.scala index eecf83edd816..9e08ad110eac 100644 --- a/tests/run/main-annotation-wrong-param-1.scala +++ b/tests/run/main-annotation-wrong-param-1.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-wrong-param-names.scala b/tests/run/main-annotation-wrong-param-names.scala index 27fd09925a9f..fa4e3ecce197 100644 --- a/tests/run/main-annotation-wrong-param-names.scala +++ b/tests/run/main-annotation-wrong-param-names.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-wrong-param-number.scala b/tests/run/main-annotation-wrong-param-number.scala index 9159b7e6c171..6d9ac44b4ccf 100644 --- a/tests/run/main-annotation-wrong-param-number.scala +++ b/tests/run/main-annotation-wrong-param-number.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add diff --git a/tests/run/main-annotation-wrong-param-type.scala b/tests/run/main-annotation-wrong-param-type.scala index bc62c4220993..7930d8fd6445 100644 --- a/tests/run/main-annotation-wrong-param-type.scala +++ b/tests/run/main-annotation-wrong-param-type.scala @@ -12,8 +12,8 @@ end myProgram object add extends main: def main(args: Array[String]) = val cmd = command(args) - val arg1 = cmd.argGetter[Int]("num", summon[ArgumentParser[Int]]) - val arg2 = cmd.argGetter[Int]("inc", summon[ArgumentParser[Int]]) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") end add From 098fd276eabffc964e810644e5a27f93a0130e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 13 Oct 2021 10:35:09 +0200 Subject: [PATCH 009/121] Add more tests: no parentheses, by-name params --- tests/neg/main-annotation-by-name-param.scala | 20 ++++++++++++++++++ ...n-annotation-no-parameters-no-parens.check | 1 + ...n-annotation-no-parameters-no-parens.scala | 21 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/neg/main-annotation-by-name-param.scala create mode 100644 tests/run/main-annotation-no-parameters-no-parens.check create mode 100644 tests/run/main-annotation-no-parameters-no-parens.scala diff --git a/tests/neg/main-annotation-by-name-param.scala b/tests/neg/main-annotation-by-name-param.scala new file mode 100644 index 000000000000..231bfd87d5bf --- /dev/null +++ b/tests/neg/main-annotation-by-name-param.scala @@ -0,0 +1,20 @@ +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: => Int): Unit = // error + println(s"$num + $inc = ${num + inc}") + +end myProgram + +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-no-parameters-no-parens.check b/tests/run/main-annotation-no-parameters-no-parens.check new file mode 100644 index 000000000000..020c8e62064f --- /dev/null +++ b/tests/run/main-annotation-no-parameters-no-parens.check @@ -0,0 +1 @@ +I run properly! diff --git a/tests/run/main-annotation-no-parameters-no-parens.scala b/tests/run/main-annotation-no-parameters-no-parens.scala new file mode 100644 index 000000000000..0a9b10283a27 --- /dev/null +++ b/tests/run/main-annotation-no-parameters-no-parens.scala @@ -0,0 +1,21 @@ +// Sample main method +object myProgram: + + /** Does nothing, except confirming that it runs */ + @main def run: Unit = + println("I run properly!") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object run extends main: + def main(args: Array[String]) = + val cmd = command(args) + cmd.run(myProgram.run, "run", "Does nothing, except confirming that it runs") +end run + +object Test: + def main(args: Array[String]): Unit = + run.main(Array()) +end Test From 754a6d0db830d7f4c1d2a59e7ce93fd0ce6cb917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 13 Oct 2021 17:28:04 +0200 Subject: [PATCH 010/121] Add override keyword for safety --- library/src/scala/main.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 19a8f02fe186..8ac2281ba567 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -13,10 +13,10 @@ import collection.mutable /** An annotation that designates a main function */ class main extends scala.annotation.MainAnnotation: - type ArgumentParser[T] = util.CommandLineParser.FromString[T] - type MainResultType = Any + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] + override type MainResultType = Any - def command(args: Array[String]): Command = new Command: + override def command(args: Array[String]): Command = new Command: /** A buffer of demanded argument names, plus * "?" if it has a default @@ -50,7 +50,7 @@ class main extends scala.annotation.MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - def argGetter[T](argName: String)(using p: ArgumentParser[T]): () => T = + override def argGetter[T](argName: String)(using p: ArgumentParser[T]): () => T = argInfos += ((argName, "")) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() @@ -58,7 +58,7 @@ class main extends scala.annotation.MainAnnotation: case Some(arg) => convert(argName, arg, p) case None => error(s"missing argument for $argName") - def argGetter[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = + override def argGetter[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = argInfos += ((argName, "?")) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() @@ -66,7 +66,7 @@ class main extends scala.annotation.MainAnnotation: case Some(arg) => convert(argName, arg, p) case None => () => defaultValue - def argsGetter[T](argName: String)(using p: ArgumentParser[T]): () => Seq[T] = + override def argsGetter[T](argName: String)(using p: ArgumentParser[T]): () => Seq[T] = argInfos += ((argName, "*")) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() @@ -74,7 +74,7 @@ class main extends scala.annotation.MainAnnotation: val getters = remainingArgGetters() () => getters.map(_()) - def run(f: => MainResultType, progName: String, docComment: String): Unit = + override def run(f: => MainResultType, progName: String, docComment: String): Unit = def usage(): Unit = println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}") From c8210d5b29de6ed674d921b59b57595f9e8166a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 13 Oct 2021 17:28:37 +0200 Subject: [PATCH 011/121] Test for unknown given parser --- .../neg/main-annotation-unknown-parser.scala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/neg/main-annotation-unknown-parser.scala diff --git a/tests/neg/main-annotation-unknown-parser.scala b/tests/neg/main-annotation-unknown-parser.scala new file mode 100644 index 000000000000..94ab6b5b7716 --- /dev/null +++ b/tests/neg/main-annotation-unknown-parser.scala @@ -0,0 +1,25 @@ +class MyNumber(val value: Int) { + def +(other: MyNumber): MyNumber = MyNumber(value + other.value) +} + +object myProgram: + + // The following line will produce an error when the main class is generated dynamically + @main def add(num: MyNumber, inc: MyNumber): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + // TODO remove errors below when code is generated + val arg1 = cmd.argGetter[MyNumber]("num") // error + val arg2 = cmd.argGetter[MyNumber]("inc") // error + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) +end Test From b727dabfb2653c86f0cb006fbef7e6baa5523114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 14 Oct 2021 13:03:31 +0200 Subject: [PATCH 012/121] Add class for main method exit code --- library/src/scala/main.scala | 9 +++++++- .../main-annotation-return-exit-code.scala | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/pos/main-annotation-return-exit-code.scala diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 8ac2281ba567..79e434ebb3c7 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -10,9 +10,12 @@ package scala import collection.mutable + /** An annotation that designates a main function */ class main extends scala.annotation.MainAnnotation: + import main._ + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any @@ -102,8 +105,12 @@ class main extends scala.annotation.MainAnnotation: for msg <- errors do println(s"Error: $msg") usage() else f match - case n: Int if n < 0 => System.exit(-n) + case ExitCode(n) => System.exit(n) case _ => end run end command end main + +object main: + case class ExitCode(val code: Int) +end main \ No newline at end of file diff --git a/tests/pos/main-annotation-return-exit-code.scala b/tests/pos/main-annotation-return-exit-code.scala new file mode 100644 index 000000000000..b6c474f5c173 --- /dev/null +++ b/tests/pos/main-annotation-return-exit-code.scala @@ -0,0 +1,23 @@ +// Sample main method +object myProgram: + + /** Exits program with error code */ + @main def exit(code: Int): main.ExitCode = + println(f"Exiting with code $code") + main.ExitCode(code) + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object exit extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("code") + cmd.run(myProgram.exit(arg1()), "exit", "Exits program with error code") +end exit + +object Test: + def main(args: Array[String]): Unit = + exit.main(Array("42")) +end Test From ad291f275593a4403f6b692b08d6bf2e019c8e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 14 Oct 2021 13:05:26 +0200 Subject: [PATCH 013/121] Add tests for return types and overloading --- tests/run/main-annotation-overload.check | 1 + tests/run/main-annotation-overload.scala | 31 +++++++++++++++++++ tests/run/main-annotation-return-type-1.check | 6 ++++ tests/run/main-annotation-return-type-1.scala | 27 ++++++++++++++++ tests/run/main-annotation-return-type-2.check | 6 ++++ tests/run/main-annotation-return-type-2.scala | 30 ++++++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 tests/run/main-annotation-overload.check create mode 100644 tests/run/main-annotation-overload.scala create mode 100644 tests/run/main-annotation-return-type-1.check create mode 100644 tests/run/main-annotation-return-type-1.scala create mode 100644 tests/run/main-annotation-return-type-2.check create mode 100644 tests/run/main-annotation-return-type-2.scala diff --git a/tests/run/main-annotation-overload.check b/tests/run/main-annotation-overload.check new file mode 100644 index 000000000000..56acd5c7a7ab --- /dev/null +++ b/tests/run/main-annotation-overload.check @@ -0,0 +1 @@ +2 + 3 = 5 diff --git a/tests/run/main-annotation-overload.scala b/tests/run/main-annotation-overload.scala new file mode 100644 index 000000000000..3bfe91c0c40b --- /dev/null +++ b/tests/run/main-annotation-overload.scala @@ -0,0 +1,31 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** Adds one number (malformed, doesn't work) */ + def add(num: Int): Unit = + ??? + + /** Adds zero numbers (malformed, doesn't work) */ + def add(): Int = + ??? + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-return-type-1.check b/tests/run/main-annotation-return-type-1.check new file mode 100644 index 000000000000..6b91368a3d9c --- /dev/null +++ b/tests/run/main-annotation-return-type-1.check @@ -0,0 +1,6 @@ +Direct call +2 + 3 = 5 +5 +Main call +2 + 3 = 5 +() diff --git a/tests/run/main-annotation-return-type-1.scala b/tests/run/main-annotation-return-type-1.scala new file mode 100644 index 000000000000..a2292757285b --- /dev/null +++ b/tests/run/main-annotation-return-type-1.scala @@ -0,0 +1,27 @@ +// Sample main method +object myProgram: + + /** Adds two numbers and returns them */ + @main def add(num: Int, inc: Int): Int = + println(f"$num + $inc = ${num + inc}") + num + inc + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers and returns them") +end add + +object Test: + def main(args: Array[String]): Unit = + println("Direct call") + println(myProgram.add(2, 3)) + println("Main call") + println(add.main(Array("2", "3"))) +end Test diff --git a/tests/run/main-annotation-return-type-2.check b/tests/run/main-annotation-return-type-2.check new file mode 100644 index 000000000000..6b91368a3d9c --- /dev/null +++ b/tests/run/main-annotation-return-type-2.check @@ -0,0 +1,6 @@ +Direct call +2 + 3 = 5 +5 +Main call +2 + 3 = 5 +() diff --git a/tests/run/main-annotation-return-type-2.scala b/tests/run/main-annotation-return-type-2.scala new file mode 100644 index 000000000000..a39703d1cb51 --- /dev/null +++ b/tests/run/main-annotation-return-type-2.scala @@ -0,0 +1,30 @@ +class MyResult(val result: Int): + override def toString: String = result.toString + +// Sample main method +object myProgram: + + /** Adds two numbers and returns them */ + @main def add(num: Int, inc: Int): MyResult = + println(f"$num + $inc = ${num + inc}") + MyResult(num + inc) + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args) + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers and returns them") +end add + +object Test: + def main(args: Array[String]): Unit = + println("Direct call") + println(myProgram.add(2, 3)) + println("Main call") + println(add.main(Array("2", "3"))) +end Test From c68d6f00a88dddf7eda9bdad17613df6d4911d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 15 Oct 2021 13:18:38 +0200 Subject: [PATCH 014/121] Extract usage and explain into main --- .../src/scala/annotation/MainAnnotation.scala | 6 +- library/src/scala/main.scala | 195 ++++++++++-------- tests/neg/main-annotation-by-name-param.scala | 4 +- .../neg/main-annotation-unknown-parser.scala | 4 +- .../main-annotation-return-exit-code.scala | 4 +- tests/run/main-annotation-default-value.scala | 4 +- tests/run/main-annotation-help.scala | 4 +- tests/run/main-annotation-multiple.check | 2 + tests/run/main-annotation-multiple.scala | 36 ++++ tests/run/main-annotation-named-params.scala | 4 +- ...n-annotation-no-parameters-no-parens.scala | 4 +- tests/run/main-annotation-no-parameters.scala | 4 +- tests/run/main-annotation-overload.scala | 4 +- .../main-annotation-override-explain.check | 17 ++ .../main-annotation-override-explain.scala | 25 +++ .../main-annotation-override-usage-1.check | 2 + .../main-annotation-override-usage-1.scala | 32 +++ .../main-annotation-override-usage-2.check | 2 + .../main-annotation-override-usage-2.scala | 32 +++ tests/run/main-annotation-return-type-1.scala | 4 +- tests/run/main-annotation-return-type-2.scala | 4 +- tests/run/main-annotation-simple.scala | 4 +- tests/run/main-annotation-types.scala | 4 +- tests/run/main-annotation-vararg-1.scala | 4 +- tests/run/main-annotation-vararg-2.scala | 4 +- tests/run/main-annotation-wrong-param-1.scala | 4 +- .../main-annotation-wrong-param-names.scala | 4 +- .../main-annotation-wrong-param-number.scala | 4 +- .../main-annotation-wrong-param-type.scala | 4 +- 29 files changed, 296 insertions(+), 129 deletions(-) create mode 100644 tests/run/main-annotation-multiple.check create mode 100644 tests/run/main-annotation-multiple.scala create mode 100644 tests/run/main-annotation-override-explain.check create mode 100644 tests/run/main-annotation-override-explain.scala create mode 100644 tests/run/main-annotation-override-usage-1.check create mode 100644 tests/run/main-annotation-override-usage-1.scala create mode 100644 tests/run/main-annotation-override-usage-2.check create mode 100644 tests/run/main-annotation-override-usage-2.scala diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 208d6da82235..86499ff50d29 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -21,10 +21,10 @@ trait MainAnnotation extends StaticAnnotation: type MainResultType /** A new command with arguments from `args` */ - def command(args: Array[String]): Command + def command(args: Array[String], commandName: String, docComment: String): Command /** A class representing a command to run */ - abstract class Command: + abstract class Command(val commandName: String, val docComment: String): /** The getter for the next argument of type `T` */ def argGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => T @@ -38,6 +38,6 @@ trait MainAnnotation extends StaticAnnotation: /** Run `program` if all arguments are valid, * or print usage information and/or error messages. */ - def run(program: => MainResultType, progName: String, docComment: String): Unit + def run(program: => MainResultType): Unit end Command end MainAnnotation diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 79e434ebb3c7..3fce65515ece 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -16,98 +16,117 @@ import collection.mutable class main extends scala.annotation.MainAnnotation: import main._ + protected sealed abstract class Argument { + val name: String + } + protected case class SimpleArgument(name: String) extends Argument + protected case class OptionalArgument[T](name: String, val defaultValue: T) extends Argument + protected case class VarArgument(name: String) extends Argument + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - override def command(args: Array[String]): Command = new Command: - - /** A buffer of demanded argument names, plus - * "?" if it has a default - * "*" if it is a vararg - * "" otherwise - */ - private var argInfos = new mutable.ListBuffer[(String, String)] - - /** A buffer for all errors */ - private var errors = new mutable.ListBuffer[String] - - /** Issue an error, and return an uncallable getter */ - private def error(msg: String): () => Nothing = - errors += msg - () => throw new AssertionError("trying to get invalid argument") - - /** The next argument index */ - private var argIdx: Int = 0 - - private def argAt(idx: Int): Option[String] = - if idx < args.length then Some(args(idx)) else None - - private def nextPositionalArg(): Option[String] = - while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 - val result = argAt(argIdx) - argIdx += 1 - result - - private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = - p.fromStringOption(arg) match - case Some(t) => () => t - case None => error(s"invalid argument for $argName: $arg") - - override def argGetter[T](argName: String)(using p: ArgumentParser[T]): () => T = - argInfos += ((argName, "")) - val idx = args.indexOf(s"--$argName") - val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() - argOpt match - case Some(arg) => convert(argName, arg, p) - case None => error(s"missing argument for $argName") - - override def argGetter[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = - argInfos += ((argName, "?")) - val idx = args.indexOf(s"--$argName") - val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() - argOpt match - case Some(arg) => convert(argName, arg, p) - case None => () => defaultValue - - override def argsGetter[T](argName: String)(using p: ArgumentParser[T]): () => Seq[T] = - argInfos += ((argName, "*")) - def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() - case None => Nil - val getters = remainingArgGetters() - () => getters.map(_()) - - override def run(f: => MainResultType, progName: String, docComment: String): Unit = - def usage(): Unit = - println(s"Usage: $progName ${argInfos.map(_ + _).mkString(" ")}") - - def explain(): Unit = - if docComment.nonEmpty then println(docComment) // todo: process & format doc comment - - def flagUnused(): Unit = nextPositionalArg() match - case Some(arg) => - error(s"unused argument: $arg") - flagUnused() - case None => - for - arg <- args - if arg.startsWith("--") && !argInfos.map(_._1).contains(arg.drop(2)) - do - error(s"unknown argument name: $arg") - end flagUnused - - if args.contains("--help") then - usage() - explain() - else - flagUnused() - if errors.nonEmpty then - for msg <- errors do println(s"Error: $msg") + /** Prints the main function's usage */ + def usage(commandName: String, args: Seq[Argument]): Unit = + val argInfos = args map ( + _ match { + case SimpleArgument(name) => name + case OptionalArgument(name, _) => s"$name?" + case VarArgument(name) => s"$name*" + } + ) + println(s"Usage: $commandName ${argInfos.mkString(" ")}") + + /** Prints an explanation about the function */ + def explain(commandName: String, args: Seq[Argument], docComment: String): Unit = + if docComment.nonEmpty then println(docComment) // todo: process & format doc comment + + override def command(args: Array[String], commandName: String, docComment: String): Command = + val self = this + new Command(commandName, docComment): + /** A buffer of demanded arguments */ + private var argInfos = new mutable.ListBuffer[self.Argument] + + /** A buffer for all errors */ + private var errors = new mutable.ListBuffer[String] + + /** Issue an error, and return an uncallable getter */ + private def error(msg: String): () => Nothing = + errors += msg + () => throw new AssertionError("trying to get invalid argument") + + /** The next argument index */ + private var argIdx: Int = 0 + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + private def usage(): Unit = + self.usage(this.commandName, argInfos.toSeq) + + private def explain(): Unit = + self.explain(this.commandName, argInfos.toSeq, this.docComment) + + override def argGetter[T](argName: String)(using p: ArgumentParser[T]): () => T = + argInfos += self.SimpleArgument(argName) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => error(s"missing argument for $argName") + + override def argGetter[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = + argInfos += self.OptionalArgument(argName, defaultValue) + val idx = args.indexOf(s"--$argName") + val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() + argOpt match + case Some(arg) => convert(argName, arg, p) + case None => () => defaultValue + + override def argsGetter[T](argName: String)(using p: ArgumentParser[T]): () => Seq[T] = + argInfos += self.VarArgument(argName) + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case None => Nil + val getters = remainingArgGetters() + () => getters.map(_()) + + override def run(f: => MainResultType): Unit = + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argInfos.map(_.name).contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.contains("--help") then usage() - else f match - case ExitCode(n) => System.exit(n) - case _ => - end run + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else f match + case ExitCode(n) => System.exit(n) + case _ => + end run end command end main diff --git a/tests/neg/main-annotation-by-name-param.scala b/tests/neg/main-annotation-by-name-param.scala index 231bfd87d5bf..3e5cf7204d7c 100644 --- a/tests/neg/main-annotation-by-name-param.scala +++ b/tests/neg/main-annotation-by-name-param.scala @@ -8,10 +8,10 @@ end myProgram object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/neg/main-annotation-unknown-parser.scala b/tests/neg/main-annotation-unknown-parser.scala index 94ab6b5b7716..e46b7d9d4604 100644 --- a/tests/neg/main-annotation-unknown-parser.scala +++ b/tests/neg/main-annotation-unknown-parser.scala @@ -12,11 +12,11 @@ end myProgram object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") // TODO remove errors below when code is generated val arg1 = cmd.argGetter[MyNumber]("num") // error val arg2 = cmd.argGetter[MyNumber]("inc") // error - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/pos/main-annotation-return-exit-code.scala b/tests/pos/main-annotation-return-exit-code.scala index b6c474f5c173..51d0feeb7e5b 100644 --- a/tests/pos/main-annotation-return-exit-code.scala +++ b/tests/pos/main-annotation-return-exit-code.scala @@ -12,9 +12,9 @@ end myProgram // TODO remove once @main generation is operational object exit extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "exit", "Exits program with error code") val arg1 = cmd.argGetter[Int]("code") - cmd.run(myProgram.exit(arg1()), "exit", "Exits program with error code") + cmd.run(myProgram.exit(arg1())) end exit object Test: diff --git a/tests/run/main-annotation-default-value.scala b/tests/run/main-annotation-default-value.scala index 742a55edda64..041a72833561 100644 --- a/tests/run/main-annotation-default-value.scala +++ b/tests/run/main-annotation-default-value.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num", 0) val arg2 = cmd.argGetter[Int]("inc", 1) - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index 730fc335c9fb..a8319fa425fb 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc", 1) - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-multiple.check b/tests/run/main-annotation-multiple.check new file mode 100644 index 000000000000..eba187d5c3a3 --- /dev/null +++ b/tests/run/main-annotation-multiple.check @@ -0,0 +1,2 @@ +2 + 3 = 5 +2 - 3 = -1 diff --git a/tests/run/main-annotation-multiple.scala b/tests/run/main-annotation-multiple.scala new file mode 100644 index 000000000000..06cf2dac9533 --- /dev/null +++ b/tests/run/main-annotation-multiple.scala @@ -0,0 +1,36 @@ +// Sample main method +object myProgram: + + /** Adds two numbers */ + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** Subtracts two numbers */ + @main def sub(num: Int, inc: Int): Unit = + println(s"$num - $inc = ${num - inc}") + +end myProgram + +// Compiler generated code: +// TODO remove once @main generation is operational +object add extends main: + def main(args: Array[String]) = + val cmd = command(args, "add", "Adds two numbers") + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2())) +end add + +object sub extends main: + def main(args: Array[String]) = + val cmd = command(args, "sub", "Subtracts two numbers") + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.sub(arg1(), arg2())) +end sub + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) + sub.main(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-named-params.scala b/tests/run/main-annotation-named-params.scala index e0a61cb54991..dc38760264da 100644 --- a/tests/run/main-annotation-named-params.scala +++ b/tests/run/main-annotation-named-params.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-no-parameters-no-parens.scala b/tests/run/main-annotation-no-parameters-no-parens.scala index 0a9b10283a27..945f827e4ce4 100644 --- a/tests/run/main-annotation-no-parameters-no-parens.scala +++ b/tests/run/main-annotation-no-parameters-no-parens.scala @@ -11,8 +11,8 @@ end myProgram // TODO remove once @main generation is operational object run extends main: def main(args: Array[String]) = - val cmd = command(args) - cmd.run(myProgram.run, "run", "Does nothing, except confirming that it runs") + val cmd = command(args, "run", "Does nothing, except confirming that it runs") + cmd.run(myProgram.run) end run object Test: diff --git a/tests/run/main-annotation-no-parameters.scala b/tests/run/main-annotation-no-parameters.scala index a0a967a63704..c527a085856f 100644 --- a/tests/run/main-annotation-no-parameters.scala +++ b/tests/run/main-annotation-no-parameters.scala @@ -11,8 +11,8 @@ end myProgram // TODO remove once @main generation is operational object run extends main: def main(args: Array[String]) = - val cmd = command(args) - cmd.run(myProgram.run(), "run", "Does nothing, except confirming that it runs") + val cmd = command(args, "run", "Does nothing, except confirming that it runs") + cmd.run(myProgram.run()) end run object Test: diff --git a/tests/run/main-annotation-overload.scala b/tests/run/main-annotation-overload.scala index 3bfe91c0c40b..7df44120670c 100644 --- a/tests/run/main-annotation-overload.scala +++ b/tests/run/main-annotation-overload.scala @@ -19,10 +19,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-override-explain.check b/tests/run/main-annotation-override-explain.check new file mode 100644 index 000000000000..29bae24b0019 --- /dev/null +++ b/tests/run/main-annotation-override-explain.check @@ -0,0 +1,17 @@ +Usage: add num inc? +A +d +d +s + +t +w +o + +n +u +m +b +e +r +s diff --git a/tests/run/main-annotation-override-explain.scala b/tests/run/main-annotation-override-explain.scala new file mode 100644 index 000000000000..d45cc9dc3245 --- /dev/null +++ b/tests/run/main-annotation-override-explain.scala @@ -0,0 +1,25 @@ +class myMain extends main: + override def explain(commandName: String, args: Seq[Argument], docComment: String): Unit = + if docComment.nonEmpty then println(docComment.mkString("\n")) + +object myProgram: + + /** Adds two numbers */ + @myMain def add(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// TODO remove once @main generation is operational +object add extends myMain: + def main(args: Array[String]) = + val cmd = command(args, "add", "Adds two numbers") + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc", 1) + cmd.run(myProgram.add(arg1(), arg2())) +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("--help")) +end Test diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check new file mode 100644 index 000000000000..1b1bc1c50060 --- /dev/null +++ b/tests/run/main-annotation-override-usage-1.check @@ -0,0 +1,2 @@ +My shiny command works like this: add num [inc] +Adds two numbers diff --git a/tests/run/main-annotation-override-usage-1.scala b/tests/run/main-annotation-override-usage-1.scala new file mode 100644 index 000000000000..4de72d1a2ea2 --- /dev/null +++ b/tests/run/main-annotation-override-usage-1.scala @@ -0,0 +1,32 @@ +class myMain extends main: + override def usage(commandName: String, args: Seq[Argument]): Unit = + val argInfos = args map ( + _ match { + case SimpleArgument(name) => name + case OptionalArgument(name, _) => s"[$name]" + case VarArgument(name) => s"[$name [$name [...]]]" + } + ) + println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") + +object myProgram: + + /** Adds two numbers */ + @myMain def add(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + +// TODO remove once @main generation is operational +object add extends myMain: + def main(args: Array[String]) = + val cmd = command(args, "add", "Adds two numbers") + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc", 1) + cmd.run(myProgram.add(arg1(), arg2())) +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("--help")) +end Test diff --git a/tests/run/main-annotation-override-usage-2.check b/tests/run/main-annotation-override-usage-2.check new file mode 100644 index 000000000000..e76f8aa8b815 --- /dev/null +++ b/tests/run/main-annotation-override-usage-2.check @@ -0,0 +1,2 @@ +My shiny command works like this: add num [inc [inc [...]]] +Adds numbers diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala new file mode 100644 index 000000000000..85e6bf9ea017 --- /dev/null +++ b/tests/run/main-annotation-override-usage-2.scala @@ -0,0 +1,32 @@ +class myMain extends main: + override def usage(commandName: String, args: Seq[Argument]): Unit = + val argInfos = args map ( + _ match { + case SimpleArgument(name) => name + case OptionalArgument(name, _) => s"[$name]" + case VarArgument(name) => s"[$name [$name [...]]]" + } + ) + println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") + +object myProgram: + + /** Adds two numbers */ + @myMain def add(num: Int, inc: Int*): Unit = + println(s"$num + ${inc.mkString(" + ")} = ${num + inc.sum}") + +end myProgram + +// TODO remove once @main generation is operational +object add extends myMain: + def main(args: Array[String]) = + val cmd = command(args, "add", "Adds numbers") + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argsGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2(): _*)) +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("--help")) +end Test diff --git a/tests/run/main-annotation-return-type-1.scala b/tests/run/main-annotation-return-type-1.scala index a2292757285b..393fe749da63 100644 --- a/tests/run/main-annotation-return-type-1.scala +++ b/tests/run/main-annotation-return-type-1.scala @@ -12,10 +12,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers and returns them") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers and returns them") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-return-type-2.scala b/tests/run/main-annotation-return-type-2.scala index a39703d1cb51..2113af0912d7 100644 --- a/tests/run/main-annotation-return-type-2.scala +++ b/tests/run/main-annotation-return-type-2.scala @@ -15,10 +15,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers and returns them") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers and returns them") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-simple.scala b/tests/run/main-annotation-simple.scala index 2e176514741e..9d481358b71d 100644 --- a/tests/run/main-annotation-simple.scala +++ b/tests/run/main-annotation-simple.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-types.scala b/tests/run/main-annotation-types.scala index 4d5b97b29dcd..a2eb9039a7aa 100644 --- a/tests/run/main-annotation-types.scala +++ b/tests/run/main-annotation-types.scala @@ -21,12 +21,12 @@ end myProgram // TODO remove once @main generation is operational object show extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "show", "Displays some parameters") val arg1 = cmd.argGetter[Int]("int") val arg2 = cmd.argGetter[Double]("double") val arg3 = cmd.argGetter[String]("string") val arg4 = cmd.argGetter[Boolean]("boolean") - cmd.run(myProgram.show(arg1(), arg2(), arg3(), arg4()), "show", "Displays some parameters") + cmd.run(myProgram.show(arg1(), arg2(), arg3(), arg4())) end show object Test: diff --git a/tests/run/main-annotation-vararg-1.scala b/tests/run/main-annotation-vararg-1.scala index 80fe275893fc..f0447206231b 100644 --- a/tests/run/main-annotation-vararg-1.scala +++ b/tests/run/main-annotation-vararg-1.scala @@ -14,9 +14,9 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds any amount of numbers") val arg1 = cmd.argsGetter[Int]("nums") - cmd.run(myProgram.add(arg1(): _*), "add", "Adds any amount of numbers") + cmd.run(myProgram.add(arg1(): _*)) end add object Test: diff --git a/tests/run/main-annotation-vararg-2.scala b/tests/run/main-annotation-vararg-2.scala index aa363418610d..ea40dce66271 100644 --- a/tests/run/main-annotation-vararg-2.scala +++ b/tests/run/main-annotation-vararg-2.scala @@ -15,11 +15,11 @@ end myProgram // TODO remove once @main generation is operational object count extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "count", "Checks that the correct amount of parameters were passed") val arg1 = cmd.argGetter[Int]("count") val arg2 = cmd.argsGetter[String]("elems") - cmd.run(myProgram.count(arg1(), arg2(): _*), "count", "Checks that the correct amount of parameters were passed") + cmd.run(myProgram.count(arg1(), arg2(): _*)) end count object Test: diff --git a/tests/run/main-annotation-wrong-param-1.scala b/tests/run/main-annotation-wrong-param-1.scala index 9e08ad110eac..9a620c487dec 100644 --- a/tests/run/main-annotation-wrong-param-1.scala +++ b/tests/run/main-annotation-wrong-param-1.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-wrong-param-names.scala b/tests/run/main-annotation-wrong-param-names.scala index fa4e3ecce197..799035beb33f 100644 --- a/tests/run/main-annotation-wrong-param-names.scala +++ b/tests/run/main-annotation-wrong-param-names.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-wrong-param-number.scala b/tests/run/main-annotation-wrong-param-number.scala index 6d9ac44b4ccf..7e1f32a1d02d 100644 --- a/tests/run/main-annotation-wrong-param-number.scala +++ b/tests/run/main-annotation-wrong-param-number.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: diff --git a/tests/run/main-annotation-wrong-param-type.scala b/tests/run/main-annotation-wrong-param-type.scala index 7930d8fd6445..6e315f05284f 100644 --- a/tests/run/main-annotation-wrong-param-type.scala +++ b/tests/run/main-annotation-wrong-param-type.scala @@ -11,10 +11,10 @@ end myProgram // TODO remove once @main generation is operational object add extends main: def main(args: Array[String]) = - val cmd = command(args) + val cmd = command(args, "add", "Adds two numbers") val arg1 = cmd.argGetter[Int]("num") val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2()), "add", "Adds two numbers") + cmd.run(myProgram.add(arg1(), arg2())) end add object Test: From 1ca13aa6a956ce8749f7a2032101dbbea3736716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 15 Oct 2021 16:01:11 +0200 Subject: [PATCH 015/121] Extract run into main --- library/src/scala/main.scala | 11 +++++-- .../run/main-annotation-override-run-1.check | 4 +++ .../run/main-annotation-override-run-1.scala | 31 +++++++++++++++++++ .../run/main-annotation-override-run-2.check | 1 + .../run/main-annotation-override-run-2.scala | 24 ++++++++++++++ 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 tests/run/main-annotation-override-run-1.check create mode 100644 tests/run/main-annotation-override-run-1.scala create mode 100644 tests/run/main-annotation-override-run-2.check create mode 100644 tests/run/main-annotation-override-run-2.scala diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 3fce65515ece..3465c2a26171 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -41,6 +41,12 @@ class main extends scala.annotation.MainAnnotation: def explain(commandName: String, args: Seq[Argument], docComment: String): Unit = if docComment.nonEmpty then println(docComment) // todo: process & format doc comment + /** Runs the command and handles its return value */ + def run(f: => MainResultType): Unit = + f match + case ExitCode(n) => System.exit(n) + case _ => + override def command(args: Array[String], commandName: String, docComment: String): Command = val self = this new Command(commandName, docComment): @@ -123,9 +129,8 @@ class main extends scala.annotation.MainAnnotation: if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() - else f match - case ExitCode(n) => System.exit(n) - case _ => + else + self.run(f) end run end command end main diff --git a/tests/run/main-annotation-override-run-1.check b/tests/run/main-annotation-override-run-1.check new file mode 100644 index 000000000000..f7fa695217ea --- /dev/null +++ b/tests/run/main-annotation-override-run-1.check @@ -0,0 +1,4 @@ +I'm about to run! +2 + 3 = 5 +Exit with 5 +I'm done! diff --git a/tests/run/main-annotation-override-run-1.scala b/tests/run/main-annotation-override-run-1.scala new file mode 100644 index 000000000000..f337f2145077 --- /dev/null +++ b/tests/run/main-annotation-override-run-1.scala @@ -0,0 +1,31 @@ +class myMain extends main: + override def run(f: => MainResultType): Unit = + println("I'm about to run!") + f match { + case main.ExitCode(n) => println(s"Exit with $n") + case _ => println("I should not have printed this...") + } + println("I'm done!") + +object myProgram: + + /** Adds two numbers */ + @myMain def add(num: Int, inc: Int): main.ExitCode = + println(s"$num + $inc = ${num + inc}") + main.ExitCode(num + inc) + +end myProgram + +// TODO remove once @main generation is operational +object add extends myMain: + def main(args: Array[String]) = + val cmd = command(args, "add", "Adds two numbers") + val arg1 = cmd.argGetter[Int]("num") + val arg2 = cmd.argGetter[Int]("inc") + cmd.run(myProgram.add(arg1(), arg2())) +end add + +object Test: + def main(args: Array[String]): Unit = + add.main(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-override-run-2.check b/tests/run/main-annotation-override-run-2.check new file mode 100644 index 000000000000..ae59037e583f --- /dev/null +++ b/tests/run/main-annotation-override-run-2.check @@ -0,0 +1 @@ +Yeah, I won't run that... diff --git a/tests/run/main-annotation-override-run-2.scala b/tests/run/main-annotation-override-run-2.scala new file mode 100644 index 000000000000..60afbca2f2a3 --- /dev/null +++ b/tests/run/main-annotation-override-run-2.scala @@ -0,0 +1,24 @@ +class myMain extends main: + override def run(f: => MainResultType): Unit = + println("Yeah, I won't run that...") + +object myProgram: + + /** Halt and catch fire */ + @myMain def hcf(code: Int): Nothing = + throw new Exception("I should not be executed!!") + +end myProgram + +// TODO remove once @main generation is operational +object hcf extends myMain: + def main(args: Array[String]) = + val cmd = command(args, "hcf", "Halt and catch fire") + val arg1 = cmd.argGetter[Int]("code") + cmd.run(myProgram.hcf(arg1())) +end hcf + +object Test: + def main(args: Array[String]): Unit = + hcf.main(Array("42")) +end Test From 891c6e45b922428386bd8fe34265e3e150391c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 18 Oct 2021 15:52:42 +0200 Subject: [PATCH 016/121] Make Argument a trait, make name a def --- library/src/scala/main.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 3465c2a26171..79d07996c748 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -16,8 +16,8 @@ import collection.mutable class main extends scala.annotation.MainAnnotation: import main._ - protected sealed abstract class Argument { - val name: String + protected sealed abstract trait Argument { + def name: String } protected case class SimpleArgument(name: String) extends Argument protected case class OptionalArgument[T](name: String, val defaultValue: T) extends Argument From d35237e13e6ad2761615aa873122cbdebb483e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 19 Oct 2021 21:41:04 +0200 Subject: [PATCH 017/121] Update main code generation --- .../dotty/tools/dotc/ast/MainProxies.scala | 92 +++++++++++-------- .../dotty/tools/dotc/core/Definitions.scala | 13 +-- .../src/scala/annotation/MainAnnotation.scala | 2 +- library/src/scala/main.scala | 2 +- 4 files changed, 61 insertions(+), 48 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 2eafeca16e39..3afb19f9a3cf 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -5,6 +5,7 @@ import core._ import Symbols._, Types._, Contexts._, Decorators._, util.Spans._, Flags._, Constants._ import StdNames.nme import ast.Trees._ +import Names.TermName /** Generate proxy classes for @main functions. * A function like @@ -13,16 +14,13 @@ import ast.Trees._ * * would be translated to something like * - * import CommandLineParser._ - * class f { + * object f extends main { * @static def main(args: Array[String]): Unit = - * try - * f( - * parseArgument[S](args, 0), - * parseRemainingArguments[T](args, 1): _* - * ) - * catch case err: ParseError => showError(err) - * } + * val cmd = command(args, "f", "") + * val arg1 = cmd.argGetter[S]("x") + * val arg2 = cmd.argsGetter[T]("ys") + * cmd.run(f(arg1(), arg2(): _*)) + * } */ object MainProxies { @@ -40,59 +38,74 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol)(using Context): List[TypeDef] = { + def mainProxy(mainFun: Symbol)(using Context): List[ModuleDef] = { val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span def pos = mainFun.sourcePos - val argsRef = Ident(nme.args) + val mainArgsName: TermName = nme.args + val cmdName: TermName = Names.termName("cmd") - def addArgs(call: untpd.Tree, mt: MethodType, idx: Int): untpd.Tree = + def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[ValDef] = if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) - call + Nil } else { - val args = mt.paramInfos.zipWithIndex map { - (formal, n) => - val (parserSym, formalElem) = - if (formal.isRepeatedParam) (defn.CLP_parseRemainingArguments, formal.argTypes.head) - else (defn.CLP_parseArgument, formal) - val arg = Apply( - TypeApply(ref(parserSym.termRef), TypeTree(formalElem) :: Nil), - argsRef :: Literal(Constant(idx + n)) :: Nil) - if (formal.isRepeatedParam) repeated(arg) else arg + var valArgs: List[ValDef] = Nil + // TODO check & handle default value + val ValDefargs = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { + case ((formal, paramName), n) => + val (getterSym, formalElem) = + if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head) + else (defn.MainAnnotCommand_argGetter, formal) + val valArg = ValDef( + mainArgsName ++ (idx + n).toString, // FIXME + TypeTree(defn.FunctionOf(Nil, formalElem)), + Apply( + TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalElem) :: Nil), + Literal(Constant(paramName.toString)) :: Nil + ), + ) + valArgs = valArgs :+ valArg } - val call1 = Apply(call, args) mt.resType match { case restpe: MethodType => if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam) report.error(s"varargs parameter of @main method must come last", pos) - addArgs(call1, restpe, idx + args.length) + valArgs ::: createValArgs(restpe, cmdName, idx + valArgs.length) case _ => - call1 + valArgs } } - var result: List[TypeDef] = Nil + var result: List[ModuleDef] = Nil if (!mainFun.owner.isStaticOwner) report.error(s"@main method is not statically accessible", pos) else { - var call = ref(mainFun.termRef) + val cmd = ValDef( + cmdName, + TypeTree(), // TODO check if good practice + Apply( + Ident(defn.MainAnnot_command.name), + Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: /* TODO Docstring */ Literal(Constant("")) :: Nil + ) + ) + var args: List[ValDef] = Nil + var mainCall: Tree = ref(mainFun.termRef) + mainFun.info match { case _: ExprType => case mt: MethodType => - call = addArgs(call, mt, 0) + args = createValArgs(mt, cmdName, 0) + mainCall = Apply(mainCall, args map (arg => Apply(Ident(arg.name), Nil))) case _: PolyType => report.error(s"@main method cannot have type parameters", pos) case _ => report.error(s"@main can only annotate a method", pos) } - val errVar = Ident(nme.error) - val handler = CaseDef( - Typed(errVar, TypeTree(defn.CLP_ParseError.typeRef)), - EmptyTree, - Apply(ref(defn.CLP_showError.termRef), errVar :: Nil)) - val body = Try(call, handler :: Nil, EmptyTree) - val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) + + val run = Apply(Select(Ident(cmdName), defn.MainAnnotCommand_run.name), mainCall) + val body = Block(cmd :: args, run) + val mainArg = ValDef(mainArgsName, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) .withFlags(Param) /** Replace typed `Ident`s that have been typed with a TypeSplice with the reference to the symbol. * The annotations will be retype-checked in another scope that may not have the same imports. @@ -106,12 +119,11 @@ object MainProxies { .filterNot(_.matches(defn.MainAnnot)) .map(annot => insertTypeSplices.transform(annot.tree)) val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body) - .withFlags(JavaStatic) + //.withFlags(JavaStatic) // TODO check if necessary .withAnnotations(annots) - val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil) - val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl) - .withFlags(Final | Invisible) - if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil + val mainTempl = Template(emptyConstructor, TypeTree(defn.MainAnnot.typeRef) :: Nil, Nil, EmptyValDef, mainMeth :: Nil) + val mainObj = ModuleDef(mainFun.name.toTermName, mainTempl) + if (!ctx.reporter.hasErrors) result = mainObj.withSpan(mainAnnotSpan.toSynthetic) :: Nil } result } diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 794119cd7a79..ca50bb2c7751 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -853,11 +853,13 @@ class Definitions { @tu lazy val XMLTopScopeModule: Symbol = requiredModule("scala.xml.TopScope") - @tu lazy val CommandLineParserModule: Symbol = requiredModule("scala.util.CommandLineParser") - @tu lazy val CLP_ParseError: ClassSymbol = CommandLineParserModule.requiredClass("ParseError").typeRef.symbol.asClass - @tu lazy val CLP_parseArgument: Symbol = CommandLineParserModule.requiredMethod("parseArgument") - @tu lazy val CLP_parseRemainingArguments: Symbol = CommandLineParserModule.requiredMethod("parseRemainingArguments") - @tu lazy val CLP_showError: Symbol = CommandLineParserModule.requiredMethod("showError") + @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.main") + @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") + @tu lazy val MainAnnotCommand: ClassSymbol = MainAnnot.requiredClass("Command") + @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") + @tu lazy val MainAnnotCommand_argGetterDefault: Symbol = MainAnnotCommand.requiredMethod("argGetterDefault") + @tu lazy val MainAnnotCommand_argsGetter: Symbol = MainAnnotCommand.requiredMethod("argsGetter") + @tu lazy val MainAnnotCommand_run: Symbol = MainAnnotCommand.requiredMethod("run") @tu lazy val TupleTypeRef: TypeRef = requiredClassRef("scala.Tuple") def TupleClass(using Context): ClassSymbol = TupleTypeRef.symbol.asClass @@ -908,7 +910,6 @@ class Definitions { @tu lazy val InlineParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InlineParam") @tu lazy val ErasedParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ErasedParam") @tu lazy val InvariantBetweenAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InvariantBetween") - @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.main") @tu lazy val MigrationAnnot: ClassSymbol = requiredClass("scala.annotation.migration") @tu lazy val NowarnAnnot: ClassSymbol = requiredClass("scala.annotation.nowarn") @tu lazy val TransparentTraitAnnot: ClassSymbol = requiredClass("scala.annotation.transparentTrait") diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 86499ff50d29..e550892899a5 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -30,7 +30,7 @@ trait MainAnnotation extends StaticAnnotation: def argGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetter[T](argName: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ def argsGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => Seq[T] diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 79d07996c748..ea572153bc9c 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -92,7 +92,7 @@ class main extends scala.annotation.MainAnnotation: case Some(arg) => convert(argName, arg, p) case None => error(s"missing argument for $argName") - override def argGetter[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = + override def argGetterDefault[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = argInfos += self.OptionalArgument(argName, defaultValue) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() From 1ebf1e5606f346c25ccdcd7b7c2135a15d0fcf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 21 Oct 2021 13:02:53 +0200 Subject: [PATCH 018/121] Update tests with Java reflection (do not pass yet) --- tests/neg/main-annotation-by-name-param.scala | 15 ++-- .../neg/main-annotation-unknown-parser.scala | 17 +---- .../main-annotation-return-exit-code.scala | 14 ---- tests/run/main-annotation-default-value.scala | 21 +++--- tests/run/main-annotation-help.scala | 21 +++--- tests/run/main-annotation-multiple.scala | 27 ++----- tests/run/main-annotation-named-params.scala | 19 ++--- ...n-annotation-no-parameters-no-parens.scala | 15 ++-- tests/run/main-annotation-no-parameters.scala | 15 ++-- tests/run/main-annotation-overload.scala | 17 ++--- .../main-annotation-override-explain.scala | 16 ++--- .../run/main-annotation-override-run-1.scala | 16 ++--- .../run/main-annotation-override-run-2.scala | 15 ++-- .../main-annotation-override-usage-1.scala | 16 ++--- .../main-annotation-override-usage-2.scala | 16 ++--- tests/run/main-annotation-return-type-1.check | 1 - tests/run/main-annotation-return-type-1.scala | 19 ++--- tests/run/main-annotation-return-type-2.check | 1 - tests/run/main-annotation-return-type-2.scala | 19 ++--- tests/run/main-annotation-simple.scala | 71 ++----------------- tests/run/main-annotation-types.scala | 23 +++--- tests/run/main-annotation-vararg-1.scala | 24 +++---- tests/run/main-annotation-vararg-2.scala | 30 ++++---- tests/run/main-annotation-wrong-param-1.scala | 23 +++--- .../main-annotation-wrong-param-names.check | 6 ++ .../main-annotation-wrong-param-names.scala | 21 +++--- .../main-annotation-wrong-param-number.scala | 23 +++--- .../main-annotation-wrong-param-type.scala | 27 +++---- 28 files changed, 179 insertions(+), 369 deletions(-) diff --git a/tests/neg/main-annotation-by-name-param.scala b/tests/neg/main-annotation-by-name-param.scala index 3e5cf7204d7c..f2a7eb373b46 100644 --- a/tests/neg/main-annotation-by-name-param.scala +++ b/tests/neg/main-annotation-by-name-param.scala @@ -6,15 +6,12 @@ object myProgram: end myProgram -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) + callMain(Array("2", "3")) end Test diff --git a/tests/neg/main-annotation-unknown-parser.scala b/tests/neg/main-annotation-unknown-parser.scala index e46b7d9d4604..686d530d17de 100644 --- a/tests/neg/main-annotation-unknown-parser.scala +++ b/tests/neg/main-annotation-unknown-parser.scala @@ -4,22 +4,7 @@ class MyNumber(val value: Int) { object myProgram: - // The following line will produce an error when the main class is generated dynamically - @main def add(num: MyNumber, inc: MyNumber): Unit = + @main def add(num: MyNumber, inc: MyNumber): Unit = // error println(s"$num + $inc = ${num + inc}") end myProgram - -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - // TODO remove errors below when code is generated - val arg1 = cmd.argGetter[MyNumber]("num") // error - val arg2 = cmd.argGetter[MyNumber]("inc") // error - cmd.run(myProgram.add(arg1(), arg2())) -end add - -object Test: - def main(args: Array[String]): Unit = - add.main(Array("2", "3")) -end Test diff --git a/tests/pos/main-annotation-return-exit-code.scala b/tests/pos/main-annotation-return-exit-code.scala index 51d0feeb7e5b..fcc1fbda426f 100644 --- a/tests/pos/main-annotation-return-exit-code.scala +++ b/tests/pos/main-annotation-return-exit-code.scala @@ -7,17 +7,3 @@ object myProgram: main.ExitCode(code) end myProgram - -// Compiler generated code: -// TODO remove once @main generation is operational -object exit extends main: - def main(args: Array[String]) = - val cmd = command(args, "exit", "Exits program with error code") - val arg1 = cmd.argGetter[Int]("code") - cmd.run(myProgram.exit(arg1())) -end exit - -object Test: - def main(args: Array[String]): Unit = - exit.main(Array("42")) -end Test diff --git a/tests/run/main-annotation-default-value.scala b/tests/run/main-annotation-default-value.scala index 041a72833561..4bb712c86831 100644 --- a/tests/run/main-annotation-default-value.scala +++ b/tests/run/main-annotation-default-value.scala @@ -7,19 +7,14 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num", 0) - val arg2 = cmd.argGetter[Int]("inc", 1) - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) - add.main(Array("2")) - add.main(Array()) + callMain(Array("2", "3")) + callMain(Array("2")) + callMain(Array()) end Test \ No newline at end of file diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index a8319fa425fb..708b3116d401 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -7,19 +7,14 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc", 1) - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("--help")) - add.main(Array("Some", "garbage", "before", "--help")) - add.main(Array("--help", "and", "some", "stuff", "after")) + callMain(Array("--help")) + callMain(Array("Some", "garbage", "before", "--help")) + callMain(Array("--help", "and", "some", "stuff", "after")) end Test diff --git a/tests/run/main-annotation-multiple.scala b/tests/run/main-annotation-multiple.scala index 06cf2dac9533..aebb2cb059dc 100644 --- a/tests/run/main-annotation-multiple.scala +++ b/tests/run/main-annotation-multiple.scala @@ -11,26 +11,13 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - -object sub extends main: - def main(args: Array[String]) = - val cmd = command(args, "sub", "Subtracts two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.sub(arg1(), arg2())) -end sub - object Test: + def callMain(mainMeth: String, args: Array[String]): Unit = + val clazz = Class.forName(mainMeth) + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) - sub.main(Array("2", "3")) + callMain("add", Array("2", "3")) + callMain("sub", Array("2", "3")) end Test diff --git a/tests/run/main-annotation-named-params.scala b/tests/run/main-annotation-named-params.scala index dc38760264da..dbee28ce8277 100644 --- a/tests/run/main-annotation-named-params.scala +++ b/tests/run/main-annotation-named-params.scala @@ -7,18 +7,13 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("--num", "2", "--inc", "3")) - add.main(Array("--inc", "3", "--num", "2")) + callMain(Array("--num", "2", "--inc", "3")) + callMain(Array("--inc", "3", "--num", "2")) end Test \ No newline at end of file diff --git a/tests/run/main-annotation-no-parameters-no-parens.scala b/tests/run/main-annotation-no-parameters-no-parens.scala index 945f827e4ce4..9ba6f150f21b 100644 --- a/tests/run/main-annotation-no-parameters-no-parens.scala +++ b/tests/run/main-annotation-no-parameters-no-parens.scala @@ -7,15 +7,12 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object run extends main: - def main(args: Array[String]) = - val cmd = command(args, "run", "Does nothing, except confirming that it runs") - cmd.run(myProgram.run) -end run - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("run") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - run.main(Array()) + callMain(Array()) end Test diff --git a/tests/run/main-annotation-no-parameters.scala b/tests/run/main-annotation-no-parameters.scala index c527a085856f..978ec6e6373e 100644 --- a/tests/run/main-annotation-no-parameters.scala +++ b/tests/run/main-annotation-no-parameters.scala @@ -7,15 +7,12 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object run extends main: - def main(args: Array[String]) = - val cmd = command(args, "run", "Does nothing, except confirming that it runs") - cmd.run(myProgram.run()) -end run - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("run") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - run.main(Array()) + callMain(Array()) end Test diff --git a/tests/run/main-annotation-overload.scala b/tests/run/main-annotation-overload.scala index 7df44120670c..5c9f263ef647 100644 --- a/tests/run/main-annotation-overload.scala +++ b/tests/run/main-annotation-overload.scala @@ -15,17 +15,12 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) + callMain(Array("2", "3")) end Test diff --git a/tests/run/main-annotation-override-explain.scala b/tests/run/main-annotation-override-explain.scala index d45cc9dc3245..279f979254fc 100644 --- a/tests/run/main-annotation-override-explain.scala +++ b/tests/run/main-annotation-override-explain.scala @@ -10,16 +10,12 @@ object myProgram: end myProgram -// TODO remove once @main generation is operational -object add extends myMain: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc", 1) - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("--help")) + callMain(Array("--help")) end Test diff --git a/tests/run/main-annotation-override-run-1.scala b/tests/run/main-annotation-override-run-1.scala index f337f2145077..9fb6e984cbb8 100644 --- a/tests/run/main-annotation-override-run-1.scala +++ b/tests/run/main-annotation-override-run-1.scala @@ -16,16 +16,12 @@ object myProgram: end myProgram -// TODO remove once @main generation is operational -object add extends myMain: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) + callMain(Array("2", "3")) end Test diff --git a/tests/run/main-annotation-override-run-2.scala b/tests/run/main-annotation-override-run-2.scala index 60afbca2f2a3..c3187ea382bb 100644 --- a/tests/run/main-annotation-override-run-2.scala +++ b/tests/run/main-annotation-override-run-2.scala @@ -10,15 +10,12 @@ object myProgram: end myProgram -// TODO remove once @main generation is operational -object hcf extends myMain: - def main(args: Array[String]) = - val cmd = command(args, "hcf", "Halt and catch fire") - val arg1 = cmd.argGetter[Int]("code") - cmd.run(myProgram.hcf(arg1())) -end hcf - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("hcf") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - hcf.main(Array("42")) + callMain(Array("42")) end Test diff --git a/tests/run/main-annotation-override-usage-1.scala b/tests/run/main-annotation-override-usage-1.scala index 4de72d1a2ea2..e854593ff7d5 100644 --- a/tests/run/main-annotation-override-usage-1.scala +++ b/tests/run/main-annotation-override-usage-1.scala @@ -17,16 +17,12 @@ object myProgram: end myProgram -// TODO remove once @main generation is operational -object add extends myMain: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc", 1) - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("--help")) + callMain(Array("--help")) end Test diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala index 85e6bf9ea017..2ddd9b0888dc 100644 --- a/tests/run/main-annotation-override-usage-2.scala +++ b/tests/run/main-annotation-override-usage-2.scala @@ -17,16 +17,12 @@ object myProgram: end myProgram -// TODO remove once @main generation is operational -object add extends myMain: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argsGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2(): _*)) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("--help")) + callMain(Array("--help")) end Test diff --git a/tests/run/main-annotation-return-type-1.check b/tests/run/main-annotation-return-type-1.check index 6b91368a3d9c..9dd320bd88a1 100644 --- a/tests/run/main-annotation-return-type-1.check +++ b/tests/run/main-annotation-return-type-1.check @@ -1,6 +1,5 @@ Direct call 2 + 3 = 5 -5 Main call 2 + 3 = 5 () diff --git a/tests/run/main-annotation-return-type-1.scala b/tests/run/main-annotation-return-type-1.scala index 393fe749da63..8d363ca830b7 100644 --- a/tests/run/main-annotation-return-type-1.scala +++ b/tests/run/main-annotation-return-type-1.scala @@ -8,20 +8,15 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers and returns them") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = println("Direct call") - println(myProgram.add(2, 3)) + assert(myProgram.add(2, 3) == 5) println("Main call") - println(add.main(Array("2", "3"))) + println(callMain(Array("2", "3"))) end Test diff --git a/tests/run/main-annotation-return-type-2.check b/tests/run/main-annotation-return-type-2.check index 6b91368a3d9c..9dd320bd88a1 100644 --- a/tests/run/main-annotation-return-type-2.check +++ b/tests/run/main-annotation-return-type-2.check @@ -1,6 +1,5 @@ Direct call 2 + 3 = 5 -5 Main call 2 + 3 = 5 () diff --git a/tests/run/main-annotation-return-type-2.scala b/tests/run/main-annotation-return-type-2.scala index 2113af0912d7..4942d86dbae2 100644 --- a/tests/run/main-annotation-return-type-2.scala +++ b/tests/run/main-annotation-return-type-2.scala @@ -11,20 +11,15 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers and returns them") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = println("Direct call") - println(myProgram.add(2, 3)) + assert(myProgram.add(2, 3).result == 5) println("Main call") - println(add.main(Array("2", "3"))) + println(callMain(Array("2", "3"))) end Test diff --git a/tests/run/main-annotation-simple.scala b/tests/run/main-annotation-simple.scala index 9d481358b71d..33eb4cbb0308 100644 --- a/tests/run/main-annotation-simple.scala +++ b/tests/run/main-annotation-simple.scala @@ -1,4 +1,3 @@ -// Sample main method object myProgram: /** Adds two numbers */ @@ -7,70 +6,12 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) + callMain(Array("2", "3")) end Test - -// After implemented, move direct call to `add` in tests/neg - -/** --- Some scenarios ---------------------------------------- - -> java add 2 3 -2 + 3 = 5 -> java add 4 -4 + 1 = 5 -> java add --num 10 --inc -2 -10 + -2 = 8 -> java add --num 10 -10 + 1 = 11 -> java add --help -Usage: add num inc? -Adds two numbers -> java add -Usage: add num inc? -Adds two numbers -> java add 1 2 3 4 -Error: unused argument: 3 -Error: unused argument: 4 -Usage: add num inc? -> java add -n 1 -i 10 -Error: invalid argument for num: -n -Error: unused argument: -i -Error: unused argument: 10 -Usage: add num inc? -> java add --n 1 --i 10 -Error: missing argument for num -Error: unknown argument name: --n -Error: unknown argument name: --i -Usage: add num inc? -> java add true 10 -Error: invalid argument for num: true -Usage: add num inc? -> java add true false -Error: invalid argument for num: true -Error: invalid argument for inc: false -Usage: add num inc? -> java add true false 10 -Error: invalid argument for num: true -Error: invalid argument for inc: false -Error: unused argument: 10 -Usage: add num inc? -> java add --inc 10 --num 20 -20 + 10 = 30 -> java add binary 10 01 -Error: invalid argument for num: binary -Error: unused argument: 01 -Usage: add num inc? - -*/ diff --git a/tests/run/main-annotation-types.scala b/tests/run/main-annotation-types.scala index a2eb9039a7aa..8ac694449f68 100644 --- a/tests/run/main-annotation-types.scala +++ b/tests/run/main-annotation-types.scala @@ -17,21 +17,14 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object show extends main: - def main(args: Array[String]) = - val cmd = command(args, "show", "Displays some parameters") - val arg1 = cmd.argGetter[Int]("int") - val arg2 = cmd.argGetter[Double]("double") - val arg3 = cmd.argGetter[String]("string") - val arg4 = cmd.argGetter[Boolean]("boolean") - cmd.run(myProgram.show(arg1(), arg2(), arg3(), arg4())) -end show - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - show.main(Array("2", "3", "4", "true")) - show.main(Array("-1", "3456789098765445678", "false", "FALSE")) - show.main(Array("2147483647", "3.1415926535", "Hello world!", "True")) + callMain(Array("2", "3", "4", "true")) + callMain(Array("-1", "3456789098765445678", "false", "FALSE")) + callMain(Array("2147483647", "3.1415926535", "Hello world!", "True")) end Test diff --git a/tests/run/main-annotation-vararg-1.scala b/tests/run/main-annotation-vararg-1.scala index f0447206231b..bc8f84a3c6a9 100644 --- a/tests/run/main-annotation-vararg-1.scala +++ b/tests/run/main-annotation-vararg-1.scala @@ -10,20 +10,16 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds any amount of numbers") - val arg1 = cmd.argsGetter[Int]("nums") - cmd.run(myProgram.add(arg1(): _*)) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "3")) - add.main(Array("2", "3", "-4")) - add.main((1 to 10).toArray.map(_.toString)) - add.main(Array("0")) - add.main(Array()) + callMain(Array("2", "3")) + callMain(Array("2", "3", "-4")) + callMain((1 to 10).toArray.map(_.toString)) + callMain(Array("0")) + callMain(Array()) end Test diff --git a/tests/run/main-annotation-vararg-2.scala b/tests/run/main-annotation-vararg-2.scala index ea40dce66271..4f0507268133 100644 --- a/tests/run/main-annotation-vararg-2.scala +++ b/tests/run/main-annotation-vararg-2.scala @@ -11,24 +11,18 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object count extends main: - def main(args: Array[String]) = - val cmd = command(args, "count", "Checks that the correct amount of parameters were passed") - val arg1 = cmd.argGetter[Int]("count") - val arg2 = cmd.argsGetter[String]("elems") - - cmd.run(myProgram.count(arg1(), arg2(): _*)) -end count - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("count") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - count.main(Array("1", "Hello")) - count.main(Array("2", "Hello", "world!")) - count.main(Array("3", "No 3 elements")) - count.main(Array("0")) - count.main(Array("0", "I", "shouldn't", "be", "here")) - count.main(Array("-2", "How does that make sense?")) - count.main(Array("26") ++ ('a' to 'z').toArray.map(_.toString)) + callMain(Array("1", "Hello")) + callMain(Array("2", "Hello", "world!")) + callMain(Array("3", "No 3 elements")) + callMain(Array("0")) + callMain(Array("0", "I", "shouldn't", "be", "here")) + callMain(Array("-2", "How does that make sense?")) + callMain(Array("26") ++ ('a' to 'z').toArray.map(_.toString)) end Test diff --git a/tests/run/main-annotation-wrong-param-1.scala b/tests/run/main-annotation-wrong-param-1.scala index 9a620c487dec..88e7e4c67937 100644 --- a/tests/run/main-annotation-wrong-param-1.scala +++ b/tests/run/main-annotation-wrong-param-1.scala @@ -7,20 +7,15 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "true", "SPAAAAACE")) - add.main(Array("add", "2", "3")) - add.main(Array("true", "false", "10")) - add.main(Array("binary", "10", "01")) + callMain(Array("2", "true", "SPAAAAACE")) + callMain(Array("add", "2", "3")) + callMain(Array("true", "false", "10")) + callMain(Array("binary", "10", "01")) end Test diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check index 9518b2de5330..450dec9440bc 100644 --- a/tests/run/main-annotation-wrong-param-names.check +++ b/tests/run/main-annotation-wrong-param-names.check @@ -7,3 +7,9 @@ Error: missing argument for inc Error: unknown argument name: --n Error: unknown argument name: --i Usage: add num inc +Error: invalid argument for num: -num +Error: unused argument: 1 +Usage: add num inc +Error: invalid argument for inc: -inc +Error: unused argument: 10 +Usage: add num inc diff --git a/tests/run/main-annotation-wrong-param-names.scala b/tests/run/main-annotation-wrong-param-names.scala index 799035beb33f..ce475af0226e 100644 --- a/tests/run/main-annotation-wrong-param-names.scala +++ b/tests/run/main-annotation-wrong-param-names.scala @@ -7,18 +7,15 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("-n", "1", "-i", "10")) - add.main(Array("--n", "1", "--i", "10")) + callMain(Array("-n", "1", "-i", "10")) + callMain(Array("--n", "1", "--i", "10")) + callMain(Array("-num", "1", "--inc", "10")) + callMain(Array("--num", "1", "-inc", "10")) end Test diff --git a/tests/run/main-annotation-wrong-param-number.scala b/tests/run/main-annotation-wrong-param-number.scala index 7e1f32a1d02d..d2025faffd21 100644 --- a/tests/run/main-annotation-wrong-param-number.scala +++ b/tests/run/main-annotation-wrong-param-number.scala @@ -7,20 +7,15 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array()) - add.main(Array("1")) - add.main(Array("1", "2", "3")) - add.main(Array((1 to 10).toArray.map(_.toString): _*)) + callMain(Array()) + callMain(Array("1")) + callMain(Array("1", "2", "3")) + callMain(Array((1 to 10).toArray.map(_.toString): _*)) end Test diff --git a/tests/run/main-annotation-wrong-param-type.scala b/tests/run/main-annotation-wrong-param-type.scala index 6e315f05284f..e4e4b3893334 100644 --- a/tests/run/main-annotation-wrong-param-type.scala +++ b/tests/run/main-annotation-wrong-param-type.scala @@ -7,22 +7,17 @@ object myProgram: end myProgram -// Compiler generated code: -// TODO remove once @main generation is operational -object add extends main: - def main(args: Array[String]) = - val cmd = command(args, "add", "Adds two numbers") - val arg1 = cmd.argGetter[Int]("num") - val arg2 = cmd.argGetter[Int]("inc") - cmd.run(myProgram.add(arg1(), arg2())) -end add - object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - add.main(Array("2", "true")) - add.main(Array("2.1", "3")) - add.main(Array("2", "3.1415921535")) - add.main(Array("192.168.1.1", "3")) - add.main(Array("false", "true")) - add.main(Array("Hello", "world!")) + callMain(Array("2", "true")) + callMain(Array("2.1", "3")) + callMain(Array("2", "3.1415921535")) + callMain(Array("192.168.1.1", "3")) + callMain(Array("false", "true")) + callMain(Array("Hello", "world!")) end Test From 58d5460bff14bf8cde72ad97f63da8ca9616a600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 21 Oct 2021 15:55:36 +0200 Subject: [PATCH 019/121] Add argument type to Argument trait --- .../dotty/tools/dotc/ast/MainProxies.scala | 6 ++--- .../src/scala/annotation/MainAnnotation.scala | 6 ++--- library/src/scala/main.scala | 25 ++++++++++--------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 3afb19f9a3cf..887cc5473d70 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -17,8 +17,8 @@ import Names.TermName * object f extends main { * @static def main(args: Array[String]): Unit = * val cmd = command(args, "f", "") - * val arg1 = cmd.argGetter[S]("x") - * val arg2 = cmd.argsGetter[T]("ys") + * val arg1 = cmd.argGetter[S]("x", "S") + * val arg2 = cmd.argsGetter[T]("ys", "T") * cmd.run(f(arg1(), arg2(): _*)) * } */ @@ -62,7 +62,7 @@ object MainProxies { TypeTree(defn.FunctionOf(Nil, formalElem)), Apply( TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalElem) :: Nil), - Literal(Constant(paramName.toString)) :: Nil + Literal(Constant(paramName.toString)) :: Literal(Constant(formalElem.show)) :: Nil // TODO check if better way to print name of formalElem ), ) valArgs = valArgs :+ valArg diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index e550892899a5..cd7884dc0760 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -27,13 +27,13 @@ trait MainAnnotation extends StaticAnnotation: abstract class Command(val commandName: String, val docComment: String): /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => T + def argGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, argType: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String)(using fromString: ArgumentParser[T]): () => Seq[T] + def argsGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index ea572153bc9c..5895c4978a61 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -18,10 +18,11 @@ class main extends scala.annotation.MainAnnotation: protected sealed abstract trait Argument { def name: String + def typeName: String } - protected case class SimpleArgument(name: String) extends Argument - protected case class OptionalArgument[T](name: String, val defaultValue: T) extends Argument - protected case class VarArgument(name: String) extends Argument + protected case class SimpleArgument(name: String, typeName: String) extends Argument + protected case class OptionalArgument[T](name: String, typeName: String, val defaultValue: T) extends Argument + protected case class VarArgument(name: String, typeName: String) extends Argument override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any @@ -30,9 +31,9 @@ class main extends scala.annotation.MainAnnotation: def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name) => name - case OptionalArgument(name, _) => s"$name?" - case VarArgument(name) => s"$name*" + case SimpleArgument(name, _) => name + case OptionalArgument(name, _, _) => s"$name?" + case VarArgument(name, _) => s"$name*" } ) println(s"Usage: $commandName ${argInfos.mkString(" ")}") @@ -84,24 +85,24 @@ class main extends scala.annotation.MainAnnotation: private def explain(): Unit = self.explain(this.commandName, argInfos.toSeq, this.docComment) - override def argGetter[T](argName: String)(using p: ArgumentParser[T]): () => T = - argInfos += self.SimpleArgument(argName) + override def argGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => T = + argInfos += self.SimpleArgument(argName, argType) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() argOpt match case Some(arg) => convert(argName, arg, p) case None => error(s"missing argument for $argName") - override def argGetterDefault[T](argName: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = - argInfos += self.OptionalArgument(argName, defaultValue) + override def argGetterDefault[T](argName: String, argType: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = + argInfos += self.OptionalArgument(argName, argType, defaultValue) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() argOpt match case Some(arg) => convert(argName, arg, p) case None => () => defaultValue - override def argsGetter[T](argName: String)(using p: ArgumentParser[T]): () => Seq[T] = - argInfos += self.VarArgument(argName) + override def argsGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => Seq[T] = + argInfos += self.VarArgument(argName, argType) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() case None => Nil From 652e1e0c167e99ee7b8a075b947243409b2f6637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 21 Oct 2021 20:45:53 +0200 Subject: [PATCH 020/121] Fix return type of vararg func --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 887cc5473d70..75685f08b93a 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -54,15 +54,15 @@ object MainProxies { // TODO check & handle default value val ValDefargs = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { case ((formal, paramName), n) => - val (getterSym, formalElem) = - if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head) - else (defn.MainAnnotCommand_argGetter, formal) + val (getterSym, formalType, returnType) = + if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head, defn.SeqType.appliedTo(formal.argTypes.head)) + else (defn.MainAnnotCommand_argGetter, formal, formal) val valArg = ValDef( mainArgsName ++ (idx + n).toString, // FIXME - TypeTree(defn.FunctionOf(Nil, formalElem)), + TypeTree(defn.FunctionOf(Nil, returnType)), Apply( - TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalElem) :: Nil), - Literal(Constant(paramName.toString)) :: Literal(Constant(formalElem.show)) :: Nil // TODO check if better way to print name of formalElem + TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), + Literal(Constant(paramName.toString)) :: Literal(Constant(formalType.show)) :: Nil // TODO check if better way to print name of formalElem ), ) valArgs = valArgs :+ valArg From 8ac03b7e9b389af792a689f4030d7a85e633f15f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 22 Oct 2021 17:47:45 +0200 Subject: [PATCH 021/121] Implement Scaladoc parsing --- .../dotty/tools/dotc/ast/MainProxies.scala | 76 +++++++++++++++++-- .../src/scala/annotation/MainAnnotation.scala | 6 +- library/src/scala/main.scala | 39 ++++++---- 3 files changed, 95 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 75685f08b93a..89cca1008762 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -6,19 +6,27 @@ import Symbols._, Types._, Contexts._, Decorators._, util.Spans._, Flags._, Cons import StdNames.nme import ast.Trees._ import Names.TermName +import Comments.Comment /** Generate proxy classes for @main functions. * A function like * + * /** + * * Lorem ipsum dolor sit amet + * * consectetur adipiscing elit. + * * + * * @param x my param x + * * @param ys all my params y + * */ * @main def f(x: S, ys: T*) = ... * * would be translated to something like * * object f extends main { * @static def main(args: Array[String]): Unit = - * val cmd = command(args, "f", "") - * val arg1 = cmd.argGetter[S]("x", "S") - * val arg2 = cmd.argsGetter[T]("ys", "T") + * val cmd = command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") + * val arg1 = cmd.argGetter[S]("x", "S", "my param x") + * val arg2 = cmd.argsGetter[T]("ys", "T", "all my params y") * cmd.run(f(arg1(), arg2(): _*)) * } */ @@ -26,9 +34,9 @@ object MainProxies { def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ - def mainMethods(stats: List[Tree]): List[Symbol] = stats.flatMap { + def mainMethods(stats: List[Tree]): List[(Symbol, Option[Comment])] = stats.flatMap { case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => - stat.symbol :: Nil + (stat.symbol, stat.rawComment) :: Nil case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => mainMethods(impl.body) case _ => @@ -38,12 +46,14 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol)(using Context): List[ModuleDef] = { + def mainProxy(mainFun: Symbol, docComment: Option[Comment])(using Context): List[ModuleDef] = { val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args val cmdName: TermName = Names.termName("cmd") + val documentation = new Documentation(docComment) + def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[ValDef] = if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) @@ -62,7 +72,10 @@ object MainProxies { TypeTree(defn.FunctionOf(Nil, returnType)), Apply( TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), - Literal(Constant(paramName.toString)) :: Literal(Constant(formalType.show)) :: Nil // TODO check if better way to print name of formalElem + Literal(Constant(paramName.toString)) + :: Literal(Constant(formalType.show)) + :: Literal(Constant(documentation.argDocs(paramName.toString))) + :: Nil // TODO check if better way to print name of formalType ), ) valArgs = valArgs :+ valArg @@ -86,7 +99,7 @@ object MainProxies { TypeTree(), // TODO check if good practice Apply( Ident(defn.MainAnnot_command.name), - Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: /* TODO Docstring */ Literal(Constant("")) :: Nil + Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: Literal(Constant(documentation.mainDoc)) :: Nil ) ) var args: List[ValDef] = Nil @@ -127,4 +140,51 @@ object MainProxies { } result } + + private class Documentation(docComment: Option[Comment], val maxLineLength: Int = 120): + import util.CommentParsing._ + + /** The main part of the documentation. */ + lazy val mainDoc: String = _mainDoc + /** The parameters identified by @param. Maps from param name to documentation. */ + lazy val argDocs: Map[String, String] = _argDocs + + private var _mainDoc: String = "" + private var _argDocs: Map[String, String] = Map().withDefaultValue("") + + docComment match { + case Some(comment) => if comment.isDocComment then parseDocComment(comment.raw) else _mainDoc = comment.raw + case None => + } + + private def breakLongLines(s: String): String = + // TODO + s + + private def cleanComment(raw: String): String = + var lines: Seq[String] = raw.trim.split('\n').toSeq + lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).trim) + var s = lines.foldLeft("") { + case ("", s2) => s2 + case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines + case (s1, "") => s1 + '\n' + case (s1, s2) => s1 + ' ' + s2 + } + s.trim + + private def parseDocComment(raw: String): Unit = + // Positions of the sections (@) in the docstring + val tidx: List[(Int, Int)] = tagIndex(raw) + + // Parse main comment + var mainComment: String = raw.substring(skipLineLead(raw, 0), startTag(raw, tidx)) + mainComment = cleanComment(mainComment) + _mainDoc = breakLongLines(mainComment) + + // Parse arguments comments + val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(raw, "@param", tidx) + val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(raw, _)) + val argsCommentsTexts = argsCommentsTextSpans.mapValues({ case (beg, end) => raw.substring(beg, end) }) + _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).mapValues(breakLongLines(_)).toMap.withDefaultValue("") + end Documentation } diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index cd7884dc0760..afc7b4f82907 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -27,13 +27,13 @@ trait MainAnnotation extends StaticAnnotation: abstract class Command(val commandName: String, val docComment: String): /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => T + def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, argType: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => Seq[T] + def argsGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 5895c4978a61..517083cba60a 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -19,10 +19,11 @@ class main extends scala.annotation.MainAnnotation: protected sealed abstract trait Argument { def name: String def typeName: String + def doc: String } - protected case class SimpleArgument(name: String, typeName: String) extends Argument - protected case class OptionalArgument[T](name: String, typeName: String, val defaultValue: T) extends Argument - protected case class VarArgument(name: String, typeName: String) extends Argument + protected case class SimpleArgument(name: String, typeName: String, doc: String) extends Argument + protected case class OptionalArgument[T](name: String, typeName: String, doc: String, val defaultValue: T) extends Argument + protected case class VarArgument(name: String, typeName: String, doc: String) extends Argument override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any @@ -31,16 +32,23 @@ class main extends scala.annotation.MainAnnotation: def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name, _) => name - case OptionalArgument(name, _, _) => s"$name?" - case VarArgument(name, _) => s"$name*" + case SimpleArgument(name, _, _) => name + case OptionalArgument(name, _, _, _) => s"$name?" + case VarArgument(name, _, _) => s"$name*" } ) println(s"Usage: $commandName ${argInfos.mkString(" ")}") /** Prints an explanation about the function */ - def explain(commandName: String, args: Seq[Argument], docComment: String): Unit = - if docComment.nonEmpty then println(docComment) // todo: process & format doc comment + def explain(commandName: String, commandDoc: String, args: Seq[Argument]): Unit = + if (commandDoc.nonEmpty) + println(commandDoc) + if (args.nonEmpty) { + println("Arguments:") + for (arg <- args) + val argDoc = if arg.doc.isEmpty then "" else s" - ${arg.doc}" + println(s"\t${arg.name}, ${arg.typeName}${argDoc}") + } /** Runs the command and handles its return value */ def run(f: => MainResultType): Unit = @@ -83,26 +91,26 @@ class main extends scala.annotation.MainAnnotation: self.usage(this.commandName, argInfos.toSeq) private def explain(): Unit = - self.explain(this.commandName, argInfos.toSeq, this.docComment) + self.explain(this.commandName, this.docComment, argInfos.toSeq) - override def argGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => T = - argInfos += self.SimpleArgument(argName, argType) + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = + argInfos += self.SimpleArgument(argName, argType, argDoc) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() argOpt match case Some(arg) => convert(argName, arg, p) case None => error(s"missing argument for $argName") - override def argGetterDefault[T](argName: String, argType: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = - argInfos += self.OptionalArgument(argName, argType, defaultValue) + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = + argInfos += self.OptionalArgument(argName, argType, argDoc, defaultValue) val idx = args.indexOf(s"--$argName") val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() argOpt match case Some(arg) => convert(argName, arg, p) case None => () => defaultValue - override def argsGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => Seq[T] = - argInfos += self.VarArgument(argName, argType) + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = + argInfos += self.VarArgument(argName, argType, argDoc) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() case None => Nil @@ -124,6 +132,7 @@ class main extends scala.annotation.MainAnnotation: if args.contains("--help") then usage() + println() explain() else flagUnused() From b9cc15649a8c66b5568b192a065d56cfae8afec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 22 Oct 2021 18:51:17 +0200 Subject: [PATCH 022/121] Add tests for top-level main, currying and genericity --- tests/neg/main-annotation-currying.scala | 6 ++++++ tests/neg/main-annotation-generic.scala | 6 ++++++ tests/run/main-annotation-top-level.check | 1 + tests/run/main-annotation-top-level.scala | 13 +++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 tests/neg/main-annotation-currying.scala create mode 100644 tests/neg/main-annotation-generic.scala create mode 100644 tests/run/main-annotation-top-level.check create mode 100644 tests/run/main-annotation-top-level.scala diff --git a/tests/neg/main-annotation-currying.scala b/tests/neg/main-annotation-currying.scala new file mode 100644 index 000000000000..9fdae8dd0954 --- /dev/null +++ b/tests/neg/main-annotation-currying.scala @@ -0,0 +1,6 @@ +object myProgram: + + @main def add(num: Int)(inc: Int): Unit = // error + println(s"$num + $inc = ${num + inc}") + +end myProgram diff --git a/tests/neg/main-annotation-generic.scala b/tests/neg/main-annotation-generic.scala new file mode 100644 index 000000000000..a9d3a63c9aa3 --- /dev/null +++ b/tests/neg/main-annotation-generic.scala @@ -0,0 +1,6 @@ +object myProgram: + + @main def nop[T](t: T): T = // error + t + +end myProgram diff --git a/tests/run/main-annotation-top-level.check b/tests/run/main-annotation-top-level.check new file mode 100644 index 000000000000..56acd5c7a7ab --- /dev/null +++ b/tests/run/main-annotation-top-level.check @@ -0,0 +1 @@ +2 + 3 = 5 diff --git a/tests/run/main-annotation-top-level.scala b/tests/run/main-annotation-top-level.scala new file mode 100644 index 000000000000..f28f09137fb9 --- /dev/null +++ b/tests/run/main-annotation-top-level.scala @@ -0,0 +1,13 @@ +/** Adds two numbers */ +@main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("2", "3")) +end Test From e7d9bbaa759eb99ec2738983ba470e7e06b04696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 24 Oct 2021 17:51:03 +0200 Subject: [PATCH 023/121] Simplify code in createValArgs --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 89cca1008762..54d89ba09805 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -55,19 +55,19 @@ object MainProxies { val documentation = new Documentation(docComment) def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[ValDef] = + println(mt.companion) if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) Nil } else { - var valArgs: List[ValDef] = Nil // TODO check & handle default value - val ValDefargs = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { + var valArgs: List[ValDef] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { case ((formal, paramName), n) => val (getterSym, formalType, returnType) = if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head, defn.SeqType.appliedTo(formal.argTypes.head)) else (defn.MainAnnotCommand_argGetter, formal, formal) - val valArg = ValDef( + ValDef( mainArgsName ++ (idx + n).toString, // FIXME TypeTree(defn.FunctionOf(Nil, returnType)), Apply( @@ -78,7 +78,6 @@ object MainProxies { :: Nil // TODO check if better way to print name of formalType ), ) - valArgs = valArgs :+ valArg } mt.resType match { case restpe: MethodType => From a05c4f198f0a4937ee5073cfc82c353835a87fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 24 Oct 2021 18:21:42 +0200 Subject: [PATCH 024/121] Add rules to MiMa filters --- project/MiMaFilters.scala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 5b40530237db..f368d2657bba 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -3,11 +3,26 @@ import com.typesafe.tools.mima.core._ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( - // Experimental APIs that can be added in 3.2.0 ProblemFilters.exclude[MissingTypesProblem]("scala.main"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.SimpleArgument"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.OptionalArgument"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.VarArgument"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.usage"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.explain"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.run"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$Argument"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$ExitCode"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$ExitCode$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$OptionalArgument"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$OptionalArgument$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$SimpleArgument"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$SimpleArgument$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$VarArgument"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$VarArgument$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.init"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.last"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"), From cb4bb204f93b97459469050f625bd793aa99efe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 24 Oct 2021 18:25:14 +0200 Subject: [PATCH 025/121] Fix tests overriding main methods --- tests/run/main-annotation-override-explain.scala | 2 +- tests/run/main-annotation-override-usage-1.scala | 6 +++--- tests/run/main-annotation-override-usage-2.scala | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/run/main-annotation-override-explain.scala b/tests/run/main-annotation-override-explain.scala index 279f979254fc..1fcfcef08c2e 100644 --- a/tests/run/main-annotation-override-explain.scala +++ b/tests/run/main-annotation-override-explain.scala @@ -1,5 +1,5 @@ class myMain extends main: - override def explain(commandName: String, args: Seq[Argument], docComment: String): Unit = + override def explain(commandName: String, docComment: String, args: Seq[Argument]): Unit = if docComment.nonEmpty then println(docComment.mkString("\n")) object myProgram: diff --git a/tests/run/main-annotation-override-usage-1.scala b/tests/run/main-annotation-override-usage-1.scala index e854593ff7d5..f84fc132d2de 100644 --- a/tests/run/main-annotation-override-usage-1.scala +++ b/tests/run/main-annotation-override-usage-1.scala @@ -2,9 +2,9 @@ class myMain extends main: override def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name) => name - case OptionalArgument(name, _) => s"[$name]" - case VarArgument(name) => s"[$name [$name [...]]]" + case SimpleArgument(name, _, _) => name + case OptionalArgument(name, _, _, _) => s"[$name]" + case VarArgument(name, _, _) => s"[$name [$name [...]]]" } ) println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala index 2ddd9b0888dc..cf8af8b01cd4 100644 --- a/tests/run/main-annotation-override-usage-2.scala +++ b/tests/run/main-annotation-override-usage-2.scala @@ -2,9 +2,9 @@ class myMain extends main: override def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name) => name - case OptionalArgument(name, _) => s"[$name]" - case VarArgument(name) => s"[$name [$name [...]]]" + case SimpleArgument(name, _, _) => name + case OptionalArgument(name, _, _, _) => s"[$name]" + case VarArgument(name, _, _) => s"[$name [$name [...]]]" } ) println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") From c5bed16bceffdd5be2a3279c7743a2448f66cc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 25 Oct 2021 15:43:15 +0200 Subject: [PATCH 026/121] Remove debug println --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 54d89ba09805..5736631e4ede 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -55,7 +55,6 @@ object MainProxies { val documentation = new Documentation(docComment) def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[ValDef] = - println(mt.companion) if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) Nil From c0d80d330f7638240b6a1d92ce9bfb7142440579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 25 Oct 2021 15:46:42 +0200 Subject: [PATCH 027/121] Fix vararg handling in MainProxies --- .../dotty/tools/dotc/ast/MainProxies.scala | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 5736631e4ede..9634f23d9ad1 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -3,7 +3,7 @@ package ast import core._ import Symbols._, Types._, Contexts._, Decorators._, util.Spans._, Flags._, Constants._ -import StdNames.nme +import StdNames.{nme, tpnme} import ast.Trees._ import Names.TermName import Comments.Comment @@ -25,9 +25,9 @@ import Comments.Comment * object f extends main { * @static def main(args: Array[String]): Unit = * val cmd = command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") - * val arg1 = cmd.argGetter[S]("x", "S", "my param x") - * val arg2 = cmd.argsGetter[T]("ys", "T", "all my params y") - * cmd.run(f(arg1(), arg2(): _*)) + * val arg1: () => S = cmd.argGetter[S]("x", "S", "my param x") + * val arg2: () => Seq[T] = cmd.argsGetter[T]("ys", "T", "all my params y") + * cmd.run(f(arg1(), arg2()*)) * } */ object MainProxies { @@ -54,19 +54,21 @@ object MainProxies { val documentation = new Documentation(docComment) - def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[ValDef] = + def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[(ValDef, Boolean)] = if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) Nil } else { // TODO check & handle default value - var valArgs: List[ValDef] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { + var valArgs: List[(ValDef, Boolean)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { case ((formal, paramName), n) => - val (getterSym, formalType, returnType) = - if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head, defn.SeqType.appliedTo(formal.argTypes.head)) - else (defn.MainAnnotCommand_argGetter, formal, formal) - ValDef( + // TODO make me cleaner + val (getterSym, formalType, isVararg) = + if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head, true) + else (defn.MainAnnotCommand_argGetter, formal, false) + val returnType = if isVararg then defn.SeqType.appliedTo(formalType) else formalType + val valDef = ValDef( mainArgsName ++ (idx + n).toString, // FIXME TypeTree(defn.FunctionOf(Nil, returnType)), Apply( @@ -77,6 +79,7 @@ object MainProxies { :: Nil // TODO check if better way to print name of formalType ), ) + (valDef, isVararg) } mt.resType match { case restpe: MethodType => @@ -106,8 +109,14 @@ object MainProxies { mainFun.info match { case _: ExprType => case mt: MethodType => - args = createValArgs(mt, cmdName, 0) - mainCall = Apply(mainCall, args map (arg => Apply(Ident(arg.name), Nil))) + val valArgs = createValArgs(mt, cmdName, 0) + args = valArgs.unzip._1 + mainCall = Apply(mainCall, valArgs map { + case (arg, isVararg) => + var argCall: Tree = Apply(Ident(arg.name), Nil) + if isVararg then argCall = repeated(argCall) + argCall + }) case _: PolyType => report.error(s"@main method cannot have type parameters", pos) case _ => From c22d36053e3200193124ba23a2e4e15deb11ff31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 25 Oct 2021 15:49:44 +0200 Subject: [PATCH 028/121] Cleaner self reference in main.scala --- library/src/scala/main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 517083cba60a..7c180c89b86e 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -14,6 +14,7 @@ import collection.mutable /** An annotation that designates a main function */ class main extends scala.annotation.MainAnnotation: + self => import main._ protected sealed abstract trait Argument { @@ -57,7 +58,6 @@ class main extends scala.annotation.MainAnnotation: case _ => override def command(args: Array[String], commandName: String, docComment: String): Command = - val self = this new Command(commandName, docComment): /** A buffer of demanded arguments */ private var argInfos = new mutable.ListBuffer[self.Argument] From 884ea20546a8d025e6844b72cf7531c23bb03140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 25 Oct 2021 17:32:00 +0200 Subject: [PATCH 029/121] Fix reflection in test --- tests/run/main-annotation-types.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/main-annotation-types.scala b/tests/run/main-annotation-types.scala index 8ac694449f68..8d4fa14761ab 100644 --- a/tests/run/main-annotation-types.scala +++ b/tests/run/main-annotation-types.scala @@ -19,7 +19,7 @@ end myProgram object Test: def callMain(args: Array[String]): Unit = - val clazz = Class.forName("add") + val clazz = Class.forName("show") val method = clazz.getMethod("main", classOf[Array[String]]) method.invoke(null, args) From f18b3a47e73b4f24a8bb30a880976ef8d6934f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 25 Oct 2021 17:33:17 +0200 Subject: [PATCH 030/121] Generate class instead of object, instanciate main - This allows for top-level main methods without name conflict with the companion object - Instead of extending main, instanciate a new main and call command on it - Will need to instanciate the @ class instead of @main (allowing easy overriding of methods) - Will need to check if not extending main is an issue --- .../dotty/tools/dotc/ast/MainProxies.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 9634f23d9ad1..6252bb18228e 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -22,9 +22,9 @@ import Comments.Comment * * would be translated to something like * - * object f extends main { + * final class f { * @static def main(args: Array[String]): Unit = - * val cmd = command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") + * val cmd = (new scala.main).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") * val arg1: () => S = cmd.argGetter[S]("x", "S", "my param x") * val arg2: () => Seq[T] = cmd.argsGetter[T]("ys", "T", "all my params y") * cmd.run(f(arg1(), arg2()*)) @@ -46,7 +46,7 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol, docComment: Option[Comment])(using Context): List[ModuleDef] = { + def mainProxy(mainFun: Symbol, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args @@ -91,7 +91,7 @@ object MainProxies { } } - var result: List[ModuleDef] = Nil + var result: List[TypeDef] = Nil if (!mainFun.owner.isStaticOwner) report.error(s"@main method is not statically accessible", pos) else { @@ -99,7 +99,7 @@ object MainProxies { cmdName, TypeTree(), // TODO check if good practice Apply( - Ident(defn.MainAnnot_command.name), + Select(makeNew(TypeTree(defn.MainAnnot.typeRef)), defn.MainAnnot_command.name), Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: Literal(Constant(documentation.mainDoc)) :: Nil ) ) @@ -139,11 +139,12 @@ object MainProxies { .filterNot(_.matches(defn.MainAnnot)) .map(annot => insertTypeSplices.transform(annot.tree)) val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body) - //.withFlags(JavaStatic) // TODO check if necessary + .withFlags(JavaStatic) .withAnnotations(annots) - val mainTempl = Template(emptyConstructor, TypeTree(defn.MainAnnot.typeRef) :: Nil, Nil, EmptyValDef, mainMeth :: Nil) - val mainObj = ModuleDef(mainFun.name.toTermName, mainTempl) - if (!ctx.reporter.hasErrors) result = mainObj.withSpan(mainAnnotSpan.toSynthetic) :: Nil + val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil) + val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl) + .withFlags(Final | Invisible) + if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil } result } From a3748d0c143fd3634615a5665e1e850ca750bd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 07:21:40 +0200 Subject: [PATCH 031/121] Instanciate correct class of annotation --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 6252bb18228e..21814ed2a38e 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -8,6 +8,11 @@ import ast.Trees._ import Names.TermName import Comments.Comment +/** The symbol of an annotated function. */ +private type FunSymbol = Symbol +/** The symbol of a main annotation. */ +private type MainSymbol = Symbol + /** Generate proxy classes for @main functions. * A function like * @@ -34,9 +39,9 @@ object MainProxies { def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ - def mainMethods(stats: List[Tree]): List[(Symbol, Option[Comment])] = stats.flatMap { + def mainMethods(stats: List[Tree]): List[(FunSymbol, MainSymbol, Option[Comment])] = stats.flatMap { case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => - (stat.symbol, stat.rawComment) :: Nil + (stat.symbol, stat.symbol.getAnnotation(defn.MainAnnot).get.symbol, stat.rawComment) :: Nil case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => mainMethods(impl.body) case _ => @@ -46,7 +51,7 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol, docComment: Option[Comment])(using Context): List[TypeDef] = { + def mainProxy(mainFun: FunSymbol, mainAnnot: MainSymbol, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args @@ -99,7 +104,7 @@ object MainProxies { cmdName, TypeTree(), // TODO check if good practice Apply( - Select(makeNew(TypeTree(defn.MainAnnot.typeRef)), defn.MainAnnot_command.name), + Select(makeNew(TypeTree(mainAnnot.typeRef)), defn.MainAnnot_command.name), Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: Literal(Constant(documentation.mainDoc)) :: Nil ) ) From a60bd1151fbf6c99203e40d3d4b0c29eb72426cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 07:28:28 +0200 Subject: [PATCH 032/121] Fix test outputs with new doc format --- tests/run/main-annotation-help.check | 12 ++++++++++++ tests/run/main-annotation-override-explain.check | 1 + tests/run/main-annotation-override-usage-1.check | 4 ++++ tests/run/main-annotation-override-usage-2.check | 4 ++++ tests/run/main-annotation-override-usage-2.scala | 2 +- 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index 7e5ef4b359a2..babfbb7a6d55 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -1,6 +1,18 @@ Usage: add num inc? + Adds two numbers +Arguments: + num, Int + inc, Int Usage: add num inc? + Adds two numbers +Arguments: + num, Int + inc, Int Usage: add num inc? + Adds two numbers +Arguments: + num, Int + inc, Int diff --git a/tests/run/main-annotation-override-explain.check b/tests/run/main-annotation-override-explain.check index 29bae24b0019..a71e5dc1ccb1 100644 --- a/tests/run/main-annotation-override-explain.check +++ b/tests/run/main-annotation-override-explain.check @@ -1,4 +1,5 @@ Usage: add num inc? + A d d diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check index 1b1bc1c50060..0d9c7f8ec623 100644 --- a/tests/run/main-annotation-override-usage-1.check +++ b/tests/run/main-annotation-override-usage-1.check @@ -1,2 +1,6 @@ My shiny command works like this: add num [inc] + Adds two numbers +Arguments: + num, Int + inc, Int diff --git a/tests/run/main-annotation-override-usage-2.check b/tests/run/main-annotation-override-usage-2.check index e76f8aa8b815..c3159f8a81cf 100644 --- a/tests/run/main-annotation-override-usage-2.check +++ b/tests/run/main-annotation-override-usage-2.check @@ -1,2 +1,6 @@ My shiny command works like this: add num [inc [inc [...]]] + Adds numbers +Arguments: + num, Int + inc, Int diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala index cf8af8b01cd4..44242e045005 100644 --- a/tests/run/main-annotation-override-usage-2.scala +++ b/tests/run/main-annotation-override-usage-2.scala @@ -11,7 +11,7 @@ class myMain extends main: object myProgram: - /** Adds two numbers */ + /** Adds numbers */ @myMain def add(num: Int, inc: Int*): Unit = println(s"$num + ${inc.mkString(" + ")} = ${num + inc.sum}") From 989bcfa0fd5b06dc7990245d41c71a9e6b619439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 07:28:38 +0200 Subject: [PATCH 033/121] Add tests for multiple annotations, non-method --- tests/neg/main-annotation-multiple-annot.scala | 14 ++++++++++++++ tests/neg/main-annotation-nonmethod.scala | 7 +++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/neg/main-annotation-multiple-annot.scala create mode 100644 tests/neg/main-annotation-nonmethod.scala diff --git a/tests/neg/main-annotation-multiple-annot.scala b/tests/neg/main-annotation-multiple-annot.scala new file mode 100644 index 000000000000..bd3715c1d98e --- /dev/null +++ b/tests/neg/main-annotation-multiple-annot.scala @@ -0,0 +1,14 @@ +class myMain extends main + +object myProgram: + + @main @main def add1(num: Int)(inc: Int): Unit = // error + println(s"$num + $inc = ${num + inc}") + + @myMain @main def add2(num: Int)(inc: Int): Unit = // error + println(s"$num + $inc = ${num + inc}") + + @myMain @myMain def add3(num: Int)(inc: Int): Unit = // error + println(s"$num + $inc = ${num + inc}") + +end myProgram diff --git a/tests/neg/main-annotation-nonmethod.scala b/tests/neg/main-annotation-nonmethod.scala new file mode 100644 index 000000000000..9a6cc6cee846 --- /dev/null +++ b/tests/neg/main-annotation-nonmethod.scala @@ -0,0 +1,7 @@ +object myProgram: + + @main val n = 2 // error + + @main class A // error + +end myProgram From 26b2d63e8b224d96e8bab643f06e44d724cff7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 12:11:15 +0200 Subject: [PATCH 034/121] Clarification of arg generation --- .../dotty/tools/dotc/ast/MainProxies.scala | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 21814ed2a38e..e302645c299b 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -29,7 +29,7 @@ private type MainSymbol = Symbol * * final class f { * @static def main(args: Array[String]): Unit = - * val cmd = (new scala.main).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") + * val cmd: main#Command = (new scala.main).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") * val arg1: () => S = cmd.argGetter[S]("x", "S", "my param x") * val arg2: () => Seq[T] = cmd.argsGetter[T]("ys", "T", "all my params y") * cmd.run(f(arg1(), arg2()*)) @@ -59,38 +59,45 @@ object MainProxies { val documentation = new Documentation(docComment) - def createValArgs(mt: MethodType, cmdName: TermName, idx: Int): List[(ValDef, Boolean)] = + def createArgs(mt: MethodType, cmdName: TermName, idx: Int): List[(Tree, ValDef)] = if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) Nil } else { // TODO check & handle default value - var valArgs: List[(ValDef, Boolean)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { + var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { case ((formal, paramName), n) => - // TODO make me cleaner - val (getterSym, formalType, isVararg) = - if (formal.isRepeatedParam) (defn.MainAnnotCommand_argsGetter, formal.argTypes.head, true) - else (defn.MainAnnotCommand_argGetter, formal, false) - val returnType = if isVararg then defn.SeqType.appliedTo(formalType) else formalType - val valDef = ValDef( - mainArgsName ++ (idx + n).toString, // FIXME - TypeTree(defn.FunctionOf(Nil, returnType)), + val argName = mainArgsName ++ (idx + n).toString + val getterSym = if formal.isRepeatedParam then defn.MainAnnotCommand_argsGetter else defn.MainAnnotCommand_argGetter + + var argRef: Tree = Apply(Ident(argName), Nil) + var formalType = formal + //var returnType = formalType + + if (formal.isRepeatedParam) { + argRef = repeated(argRef) + formalType = formalType.argTypes.head + //returnType = defn.SeqType.appliedTo(formalType) + } + + val rawArgs = List(paramName.toString, formalType.show, documentation.argDocs(paramName.toString)) // TODO check if better way to print name of formalType + val argDef = ValDef( + argName, + TypeTree(/*defn.FunctionOf(Nil, returnType)*/), Apply( TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), - Literal(Constant(paramName.toString)) - :: Literal(Constant(formalType.show)) - :: Literal(Constant(documentation.argDocs(paramName.toString))) - :: Nil // TODO check if better way to print name of formalType + rawArgs map (arg => Literal(Constant(arg))) ), ) - (valDef, isVararg) + + (argRef, argDef) } mt.resType match { case restpe: MethodType => if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam) report.error(s"varargs parameter of @main method must come last", pos) - valArgs ::: createValArgs(restpe, cmdName, idx + valArgs.length) + valArgs ::: createArgs(restpe, cmdName, idx + valArgs.length) case _ => valArgs } @@ -114,14 +121,9 @@ object MainProxies { mainFun.info match { case _: ExprType => case mt: MethodType => - val valArgs = createValArgs(mt, cmdName, 0) - args = valArgs.unzip._1 - mainCall = Apply(mainCall, valArgs map { - case (arg, isVararg) => - var argCall: Tree = Apply(Ident(arg.name), Nil) - if isVararg then argCall = repeated(argCall) - argCall - }) + val (argRefs, argVals) = createArgs(mt, cmdName, 0).unzip + args = argVals + mainCall = Apply(mainCall, argRefs) case _: PolyType => report.error(s"@main method cannot have type parameters", pos) case _ => From 45f7ba2f4adb08214a451ed46b9889be03885593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 15:36:48 +0200 Subject: [PATCH 035/121] Add vararg and default to top-level test --- tests/run/main-annotation-top-level.check | 4 ++++ tests/run/main-annotation-top-level.scala | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/run/main-annotation-top-level.check b/tests/run/main-annotation-top-level.check index 56acd5c7a7ab..1e5864820d78 100644 --- a/tests/run/main-annotation-top-level.check +++ b/tests/run/main-annotation-top-level.check @@ -1 +1,5 @@ 2 + 3 = 5 +2 + 3 = 5 +2 = 2 +0 = 0 +1 + 2 + 3 + 4 = 10 diff --git a/tests/run/main-annotation-top-level.scala b/tests/run/main-annotation-top-level.scala index f28f09137fb9..932b74b690be 100644 --- a/tests/run/main-annotation-top-level.scala +++ b/tests/run/main-annotation-top-level.scala @@ -2,12 +2,31 @@ @main def add(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") +/** Adds any amount of numbers */ +@main def addAll(num: Int = 0, incs: Int*): Unit = + print(num) + if (incs.length > 0) { + print(" + ") + print(incs.mkString(" + ")) + } + println(s" = ${num + incs.sum}") + object Test: - def callMain(args: Array[String]): Unit = + def callMainAdd(args: Array[String]): Unit = val clazz = Class.forName("add") val method = clazz.getMethod("main", classOf[Array[String]]) method.invoke(null, args) + def callMainAddAll(args: Array[String]): Unit = + val clazz = Class.forName("addAll") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + def main(args: Array[String]): Unit = - callMain(Array("2", "3")) + callMainAdd(Array("2", "3")) + + callMainAddAll(Array("2", "3")) + callMainAddAll(Array("2")) + callMainAddAll(Array()) + callMainAddAll(Array("1", "2", "3", "4")) end Test From 62b1670a2a5f1e6be662919109695fda0fbe9fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 16:00:41 +0200 Subject: [PATCH 036/121] Add support for default values main-annotation* tests now all pass again This is quite messy, it should most probably be cleaned or done in a cleaner way --- .../dotty/tools/dotc/ast/MainProxies.scala | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index e302645c299b..48fa4ddad0e6 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -7,11 +7,7 @@ import StdNames.{nme, tpnme} import ast.Trees._ import Names.TermName import Comments.Comment - -/** The symbol of an annotated function. */ -private type FunSymbol = Symbol -/** The symbol of a main annotation. */ -private type MainSymbol = Symbol +import NameKinds.DefaultGetterName /** Generate proxy classes for @main functions. * A function like @@ -39,20 +35,35 @@ object MainProxies { def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ - def mainMethods(stats: List[Tree]): List[(FunSymbol, MainSymbol, Option[Comment])] = stats.flatMap { + def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Map[Int, Tree], Option[Comment])] = stats.flatMap { case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => - (stat.symbol, stat.symbol.getAnnotation(defn.MainAnnot).get.symbol, stat.rawComment) :: Nil + val defaultValues: Map[Int, Tree] = + (scope match { + case TypeDef(_, template: Template) => + template.body.flatMap((_: Tree) match { + case dd @ DefDef(name, _, _, _) if name.is(DefaultGetterName) && name.firstPart == stat.symbol.name => + val index: Int = name.toString.split("\\$")(2).toInt - 1 // FIXME please!! + val valueTree = dd.rhs + List(index -> valueTree) + case _ => List() + }).toMap + case _ => Map[Int, Tree]() + }).withDefaultValue(EmptyTree) + + (stat.symbol, defaultValues, stat.rawComment) :: Nil case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => - mainMethods(impl.body) + mainMethods(stat, impl.body) case _ => Nil } - mainMethods(stats).flatMap(mainProxy) + + // Assuming that the top-level object was already generated, all @main methods will have a scope + mainMethods(EmptyTree, stats).flatMap(mainProxy) } import untpd._ - def mainProxy(mainFun: FunSymbol, mainAnnot: MainSymbol, docComment: Option[Comment])(using Context): List[TypeDef] = { - val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span + def mainProxy(mainFun: Symbol, defaultValues: Map[Int, Tree], docComment: Option[Comment])(using Context): List[TypeDef] = { + val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args val cmdName: TermName = Names.termName("cmd") @@ -65,11 +76,16 @@ object MainProxies { Nil } else { - // TODO check & handle default value var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { case ((formal, paramName), n) => val argName = mainArgsName ++ (idx + n).toString - val getterSym = if formal.isRepeatedParam then defn.MainAnnotCommand_argsGetter else defn.MainAnnotCommand_argGetter + val getterSym = + if formal.isRepeatedParam then + defn.MainAnnotCommand_argsGetter + else if defaultValues contains n then + defn.MainAnnotCommand_argGetterDefault + else + defn.MainAnnotCommand_argGetter var argRef: Tree = Apply(Ident(argName), Nil) var formalType = formal @@ -81,13 +97,15 @@ object MainProxies { //returnType = defn.SeqType.appliedTo(formalType) } - val rawArgs = List(paramName.toString, formalType.show, documentation.argDocs(paramName.toString)) // TODO check if better way to print name of formalType val argDef = ValDef( argName, TypeTree(/*defn.FunctionOf(Nil, returnType)*/), Apply( TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), - rawArgs map (arg => Literal(Constant(arg))) + Literal(Constant(paramName.toString)) + :: Literal(Constant(formalType.show)) + :: Literal(Constant(documentation.argDocs(paramName.toString))) + :: defaultValues.get(n).map(List(_)).getOrElse(Nil) ), ) @@ -111,7 +129,7 @@ object MainProxies { cmdName, TypeTree(), // TODO check if good practice Apply( - Select(makeNew(TypeTree(mainAnnot.typeRef)), defn.MainAnnot_command.name), + Select(makeNew(TypeTree(mainAnnot.symbol.typeRef)), defn.MainAnnot_command.name), Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: Literal(Constant(documentation.mainDoc)) :: Nil ) ) @@ -151,7 +169,7 @@ object MainProxies { val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil) val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl) .withFlags(Final | Invisible) - if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil + if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnot.tree.span.toSynthetic) :: Nil } result } From 45490a9e9a01d41a822e04f40e804d5976863cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 16:33:54 +0200 Subject: [PATCH 037/121] Add "(optional)" for args with default values --- library/src/scala/main.scala | 6 +++++- tests/run/main-annotation-help.check | 6 +++--- tests/run/main-annotation-override-usage-1.check | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 7c180c89b86e..5e37aed0de4f 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -48,7 +48,11 @@ class main extends scala.annotation.MainAnnotation: println("Arguments:") for (arg <- args) val argDoc = if arg.doc.isEmpty then "" else s" - ${arg.doc}" - println(s"\t${arg.name}, ${arg.typeName}${argDoc}") + val argDefault = arg match { + case OptionalArgument(_, _, _, _) => s" (optional)" + case _ => "" + } + println(s"\t${arg.name}, ${arg.typeName}$argDefault${argDoc}") } /** Runs the command and handles its return value */ diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index babfbb7a6d55..d44899796a7f 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -3,16 +3,16 @@ Usage: add num inc? Adds two numbers Arguments: num, Int - inc, Int + inc, Int (optional) Usage: add num inc? Adds two numbers Arguments: num, Int - inc, Int + inc, Int (optional) Usage: add num inc? Adds two numbers Arguments: num, Int - inc, Int + inc, Int (optional) diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check index 0d9c7f8ec623..13ed37170f12 100644 --- a/tests/run/main-annotation-override-usage-1.check +++ b/tests/run/main-annotation-override-usage-1.check @@ -3,4 +3,4 @@ My shiny command works like this: add num [inc] Adds two numbers Arguments: num, Int - inc, Int + inc, Int (optional) From 7cf2f7f241b4850744ed53b3065ced5a957ff376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 26 Oct 2021 16:50:36 +0200 Subject: [PATCH 038/121] Make help test more extensive --- tests/run/main-annotation-help.check | 99 +++++++++++++++++-- tests/run/main-annotation-help.scala | 140 +++++++++++++++++++++++++-- 2 files changed, 223 insertions(+), 16 deletions(-) diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index d44899796a7f..3fcdefc653a4 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -1,18 +1,99 @@ -Usage: add num inc? +Usage: doc1 num inc -Adds two numbers +Adds two numbers. Arguments: num, Int - inc, Int (optional) -Usage: add num inc? + inc, Int +Usage: doc1 num inc -Adds two numbers +Adds two numbers. Arguments: num, Int - inc, Int (optional) -Usage: add num inc? + inc, Int +Usage: doc1 num inc -Adds two numbers +Adds two numbers. Arguments: num, Int - inc, Int (optional) + inc, Int +Usage: doc1 num inc + +Adds two numbers. +Arguments: + num, Int + inc, Int +Usage: doc2 num inc + +Adds two numbers. +Arguments: + num, Int + inc, Int +Usage: doc3 num inc + +Adds two numbers. +Arguments: + num, Int + inc, Int +Usage: doc4 num inc + +Adds two numbers. +Arguments: + num, Int - the first number + inc, Int - the second number +Usage: doc5 num inc? + +Adds two numbers. +Arguments: + num, Int - the first number + inc, Int (optional) - the second number +Usage: doc6 num inc + +Adds two numbers. +Arguments: + num, Int - the first number + inc, Int +Usage: doc7 num inc + +Adds two numbers. +Arguments: + num, Int + inc, Int +Usage: doc8 num inc + +Adds two numbers. +Arguments: + num, Int - the first number + inc, Int - the second number +Usage: doc9 num inc + +Adds two numbers. +Arguments: + num, Int - the first number + inc, Int - the second number +Usage: doc10 num inc + +Adds two numbers. Same as doc1. +Arguments: + num, Int - the first number + inc, Int - the second number +Usage: doc11 num inc + +Adds two numbers. +This should be on another line. +And this also. +Arguments: + num, Int - I might have to write this on two lines + inc, Int - I might even have to write this one on three lines +Usage: doc12 num inc + +Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some +point to fit a small terminal screen. +Arguments: + num, Int + inc, Int +Usage: doc13 num inc + +Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. +Arguments: + num, Int + inc, Int diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index 708b3116d401..36c4f25f1ab0 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -1,20 +1,146 @@ // Sample main method object myProgram: - /** Adds two numbers */ - @main def add(num: Int, inc: Int = 1): Unit = + /** + * Adds two numbers. + */ + @main def doc1(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** Adds two numbers. */ + @main def doc2(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + */ + @main def doc3(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * @param num the first number + * @param inc the second number + */ + @main def doc4(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * @param num the first number + * @param inc the second number + * + * This should not be printed! + */ + @main def doc5(num: Int, inc: Int = 1): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * @param num the first number + */ + @main def doc6(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * @param num + * @param inc + */ + @main def doc7(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * @param num the first number + * @param inc the second number + * @return the sum of the two numbers (not really) + */ + @main def doc8(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * @param num the first number + * @param inc the second number + * @return the sum of the two numbers (not really) + */ + @main def doc9(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. Same as {{doc1}}. + * + * @param num the first number + * @param inc the second number + * @return the sum of the two numbers (not really) + * @see {{doc1}} + * + * This should not be printed! + */ + @main def doc10(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. + * + * This should be on another line. + * + * + * + * + * And this also. + * + * + * @param num I might have to write this + * on two lines + * @param inc I might even + * have to write this one + * on three lines + * + * This should not be printed! + */ + @main def doc11(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. + */ + @main def doc12(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + + /** + * Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. + */ + @main def doc13(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") end myProgram object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("add") + def callMain1(args: Array[String]): Unit = + val clazz = Class.forName("doc1") val method = clazz.getMethod("main", classOf[Array[String]]) method.invoke(null, args) + def callAllMains(args: Array[String]): Unit = + val numberOfMains = 13 + for (i <- 1 to numberOfMains) { + val clazz = Class.forName("doc" + i.toString) + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + } + def main(args: Array[String]): Unit = - callMain(Array("--help")) - callMain(Array("Some", "garbage", "before", "--help")) - callMain(Array("--help", "and", "some", "stuff", "after")) + callMain1(Array("--help")) + callMain1(Array("Some", "garbage", "before", "--help")) + callMain1(Array("--help", "and", "some", "stuff", "after")) + + callAllMains(Array("--help")) end Test From 0ab2c434251243ac1e2e76112dae1eb96dbf6736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 27 Oct 2021 10:26:19 +0200 Subject: [PATCH 039/121] Fix help test The "This should not be printed!" lines are, in fact, printed in IDEs --- tests/run/main-annotation-help.check | 24 +++++++------------ tests/run/main-annotation-help.scala | 35 ++++++++-------------------- 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index 3fcdefc653a4..456fdf663a61 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -34,49 +34,43 @@ Adds two numbers. Arguments: num, Int inc, Int -Usage: doc4 num inc - -Adds two numbers. -Arguments: - num, Int - the first number - inc, Int - the second number -Usage: doc5 num inc? +Usage: doc4 num inc? Adds two numbers. Arguments: num, Int - the first number inc, Int (optional) - the second number -Usage: doc6 num inc +Usage: doc5 num inc Adds two numbers. Arguments: num, Int - the first number inc, Int -Usage: doc7 num inc +Usage: doc6 num inc Adds two numbers. Arguments: num, Int inc, Int -Usage: doc8 num inc +Usage: doc7 num inc Adds two numbers. Arguments: num, Int - the first number inc, Int - the second number -Usage: doc9 num inc +Usage: doc8 num inc Adds two numbers. Arguments: num, Int - the first number inc, Int - the second number -Usage: doc10 num inc +Usage: doc9 num inc Adds two numbers. Same as doc1. Arguments: num, Int - the first number inc, Int - the second number -Usage: doc11 num inc +Usage: doc10 num inc Adds two numbers. This should be on another line. @@ -84,14 +78,14 @@ And this also. Arguments: num, Int - I might have to write this on two lines inc, Int - I might even have to write this one on three lines -Usage: doc12 num inc +Usage: doc11 num inc Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. Arguments: num, Int inc, Int -Usage: doc13 num inc +Usage: doc12 num inc Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. Arguments: diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index 36c4f25f1ab0..c362fc43311a 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -23,26 +23,15 @@ object myProgram: * @param num the first number * @param inc the second number */ - @main def doc4(num: Int, inc: Int): Unit = + @main def doc4(num: Int, inc: Int = 1): Unit = println(s"$num + $inc = ${num + inc}") /** * Adds two numbers. * * @param num the first number - * @param inc the second number - * - * This should not be printed! */ - @main def doc5(num: Int, inc: Int = 1): Unit = - println(s"$num + $inc = ${num + inc}") - - /** - * Adds two numbers. - * - * @param num the first number - */ - @main def doc6(num: Int, inc: Int): Unit = + @main def doc5(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** @@ -51,7 +40,7 @@ object myProgram: * @param num * @param inc */ - @main def doc7(num: Int, inc: Int): Unit = + @main def doc6(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** @@ -61,7 +50,7 @@ object myProgram: * @param inc the second number * @return the sum of the two numbers (not really) */ - @main def doc8(num: Int, inc: Int): Unit = + @main def doc7(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** @@ -71,7 +60,7 @@ object myProgram: * @param inc the second number * @return the sum of the two numbers (not really) */ - @main def doc9(num: Int, inc: Int): Unit = + @main def doc8(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** @@ -81,10 +70,8 @@ object myProgram: * @param inc the second number * @return the sum of the two numbers (not really) * @see {{doc1}} - * - * This should not be printed! */ - @main def doc10(num: Int, inc: Int): Unit = + @main def doc9(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** @@ -103,22 +90,20 @@ object myProgram: * @param inc I might even * have to write this one * on three lines - * - * This should not be printed! */ - @main def doc11(num: Int, inc: Int): Unit = + @main def doc10(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** * Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. */ - @main def doc12(num: Int, inc: Int): Unit = + @main def doc11(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** * Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. */ - @main def doc13(num: Int, inc: Int): Unit = + @main def doc12(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") end myProgram @@ -130,7 +115,7 @@ object Test: method.invoke(null, args) def callAllMains(args: Array[String]): Unit = - val numberOfMains = 13 + val numberOfMains = 12 for (i <- 1 to numberOfMains) { val clazz = Class.forName("doc" + i.toString) val method = clazz.getMethod("main", classOf[Array[String]]) From 0657c645d901380e220663d3119a71c36f838614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 27 Oct 2021 10:28:42 +0200 Subject: [PATCH 040/121] Implement doc line wraping --- .../dotty/tools/dotc/ast/MainProxies.scala | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 48fa4ddad0e6..dae1ac27c933 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -190,9 +190,18 @@ object MainProxies { case None => } - private def breakLongLines(s: String): String = - // TODO - s + private def wrapLongLines(s: String): String = + def wrapLongLine(line: String): List[String] = + val lastSpace = line.trim.lastIndexOf(' ', maxLineLength) + if ((line.length <= maxLineLength) || (lastSpace < 0)) + List(line) + else { + val (shortLine, rest) = line.splitAt(lastSpace) + shortLine :: wrapLongLine(rest.trim) + } + + val wrappedLines = s.split('\n').flatMap(wrapLongLine) + wrappedLines.mkString("\n") private def cleanComment(raw: String): String = var lines: Seq[String] = raw.trim.split('\n').toSeq @@ -201,6 +210,7 @@ object MainProxies { case ("", s2) => s2 case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines case (s1, "") => s1 + '\n' + case (s1, s2) if s1.last == '\n' => s1 + s2 case (s1, s2) => s1 + ' ' + s2 } s.trim @@ -212,12 +222,12 @@ object MainProxies { // Parse main comment var mainComment: String = raw.substring(skipLineLead(raw, 0), startTag(raw, tidx)) mainComment = cleanComment(mainComment) - _mainDoc = breakLongLines(mainComment) + _mainDoc = wrapLongLines(mainComment) // Parse arguments comments val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(raw, "@param", tidx) val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(raw, _)) val argsCommentsTexts = argsCommentsTextSpans.mapValues({ case (beg, end) => raw.substring(beg, end) }) - _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).mapValues(breakLongLines(_)).toMap.withDefaultValue("") + _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).mapValues(wrapLongLines(_)).toMap.withDefaultValue("") end Documentation } From ce7471a98a0c18e95a81dd5acf9da624e9d5c2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 27 Oct 2021 11:07:28 +0200 Subject: [PATCH 041/121] Add support for multiline arg doc --- library/src/scala/main.scala | 21 ++++-- tests/run/main-annotation-help.check | 70 +++++++++++-------- tests/run/main-annotation-help.scala | 20 +++++- .../main-annotation-override-usage-1.check | 4 +- .../main-annotation-override-usage-2.check | 4 +- 5 files changed, 76 insertions(+), 43 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 5e37aed0de4f..6d774a5c984d 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -47,12 +47,23 @@ class main extends scala.annotation.MainAnnotation: if (args.nonEmpty) { println("Arguments:") for (arg <- args) - val argDoc = if arg.doc.isEmpty then "" else s" - ${arg.doc}" - val argDefault = arg match { - case OptionalArgument(_, _, _, _) => s" (optional)" - case _ => "" + val argDoc = StringBuilder(s" ${arg.name}, ${arg.typeName}") + + arg match { + case OptionalArgument(_, _, _, _) => argDoc append " (optional)" + case _ => + } + + if (arg.doc.nonEmpty) { + val separator = " - " + // Shift doc's lines to align with the first + // foo, Int - so that this line + // is aligned with this one + val argExpl = arg.doc.split("\n").mkString("\n" + " " * (argDoc.length + separator.length)) + argDoc append separator append argExpl } - println(s"\t${arg.name}, ${arg.typeName}$argDefault${argDoc}") + + println(argDoc) } /** Runs the command and handles its return value */ diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index 456fdf663a61..e5432b2fc1dd 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -2,92 +2,100 @@ Usage: doc1 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc1 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc1 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc1 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc2 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc3 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc4 num inc? Adds two numbers. Arguments: - num, Int - the first number - inc, Int (optional) - the second number + num, Int - the first number + inc, Int (optional) - the second number Usage: doc5 num inc Adds two numbers. Arguments: - num, Int - the first number - inc, Int + num, Int - the first number + inc, Int Usage: doc6 num inc Adds two numbers. Arguments: - num, Int - inc, Int + num, Int + inc, Int Usage: doc7 num inc Adds two numbers. Arguments: - num, Int - the first number - inc, Int - the second number + num, Int - the first number + inc, Int - the second number Usage: doc8 num inc Adds two numbers. Arguments: - num, Int - the first number - inc, Int - the second number + num, Int - the first number + inc, Int - the second number Usage: doc9 num inc Adds two numbers. Same as doc1. Arguments: - num, Int - the first number - inc, Int - the second number + num, Int - the first number + inc, Int - the second number Usage: doc10 num inc Adds two numbers. This should be on another line. And this also. Arguments: - num, Int - I might have to write this on two lines - inc, Int - I might even have to write this one on three lines + num, Int - I might have to write this on two lines + inc, Int - I might even have to write this one on three lines Usage: doc11 num inc +Adds two numbers. +Arguments: + num, Int - the first number + Oh, a new line! + inc, Int - the second number + And another one! +Usage: doc12 num inc + Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. Arguments: - num, Int - inc, Int -Usage: doc12 num inc + num, Int + inc, Int +Usage: doc13 num inc Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. Arguments: - num, Int - inc, Int + num, Int + inc, Int diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index c362fc43311a..4a45ec1c3c56 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -95,17 +95,31 @@ object myProgram: println(s"$num + $inc = ${num + inc}") /** - * Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. + * Adds two numbers. + * + * @param num the first number + * + * Oh, a new line! + * + * @param inc the second number + * + * And another one! */ @main def doc11(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") /** - * Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. + * Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. */ @main def doc12(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") + /** + * Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. + */ + @main def doc13(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + end myProgram object Test: @@ -115,7 +129,7 @@ object Test: method.invoke(null, args) def callAllMains(args: Array[String]): Unit = - val numberOfMains = 12 + val numberOfMains = 13 for (i <- 1 to numberOfMains) { val clazz = Class.forName("doc" + i.toString) val method = clazz.getMethod("main", classOf[Array[String]]) diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check index 13ed37170f12..e74944453500 100644 --- a/tests/run/main-annotation-override-usage-1.check +++ b/tests/run/main-annotation-override-usage-1.check @@ -2,5 +2,5 @@ My shiny command works like this: add num [inc] Adds two numbers Arguments: - num, Int - inc, Int (optional) + num, Int + inc, Int (optional) diff --git a/tests/run/main-annotation-override-usage-2.check b/tests/run/main-annotation-override-usage-2.check index c3159f8a81cf..d7241faed737 100644 --- a/tests/run/main-annotation-override-usage-2.check +++ b/tests/run/main-annotation-override-usage-2.check @@ -2,5 +2,5 @@ My shiny command works like this: add num [inc [inc [...]]] Adds numbers Arguments: - num, Int - inc, Int + num, Int + inc, Int From 8ed3a9f733942f914a610db0667f0779665ef24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 27 Oct 2021 11:23:38 +0200 Subject: [PATCH 042/121] Remove {{ and }} from docstrings --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index dae1ac27c933..1b5404b51136 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -213,7 +213,7 @@ object MainProxies { case (s1, s2) if s1.last == '\n' => s1 + s2 case (s1, s2) => s1 + ' ' + s2 } - s.trim + s.trim.replaceAll(raw"\{\{", "").replaceAll(raw"\}\}", "") private def parseDocComment(raw: String): Unit = // Positions of the sections (@) in the docstring From 8b68474ac9370959b0a0412bf668d93101d4ae95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 29 Oct 2021 11:13:44 +0200 Subject: [PATCH 043/121] Remove wildcards from Argument matching --- library/src/scala/main.scala | 8 ++++---- tests/run/main-annotation-override-usage-1.scala | 6 +++--- tests/run/main-annotation-override-usage-2.scala | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 6d774a5c984d..60912e74a883 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -33,9 +33,9 @@ class main extends scala.annotation.MainAnnotation: def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name, _, _) => name - case OptionalArgument(name, _, _, _) => s"$name?" - case VarArgument(name, _, _) => s"$name*" + case s: SimpleArgument => s.name + case o: OptionalArgument[?] => s"${o.name}?" + case v: VarArgument => s"${v.name}*" } ) println(s"Usage: $commandName ${argInfos.mkString(" ")}") @@ -50,7 +50,7 @@ class main extends scala.annotation.MainAnnotation: val argDoc = StringBuilder(s" ${arg.name}, ${arg.typeName}") arg match { - case OptionalArgument(_, _, _, _) => argDoc append " (optional)" + case o: OptionalArgument[?] => argDoc append " (optional)" case _ => } diff --git a/tests/run/main-annotation-override-usage-1.scala b/tests/run/main-annotation-override-usage-1.scala index f84fc132d2de..71e6d568138c 100644 --- a/tests/run/main-annotation-override-usage-1.scala +++ b/tests/run/main-annotation-override-usage-1.scala @@ -2,9 +2,9 @@ class myMain extends main: override def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name, _, _) => name - case OptionalArgument(name, _, _, _) => s"[$name]" - case VarArgument(name, _, _) => s"[$name [$name [...]]]" + case s: SimpleArgument => s.name + case o: OptionalArgument[?] => s"[${o.name}]" + case v: VarArgument => s"[${v.name} [${v.name} [...]]]" } ) println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala index 44242e045005..8f53b1c1334c 100644 --- a/tests/run/main-annotation-override-usage-2.scala +++ b/tests/run/main-annotation-override-usage-2.scala @@ -2,9 +2,9 @@ class myMain extends main: override def usage(commandName: String, args: Seq[Argument]): Unit = val argInfos = args map ( _ match { - case SimpleArgument(name, _, _) => name - case OptionalArgument(name, _, _, _) => s"[$name]" - case VarArgument(name, _, _) => s"[$name [$name [...]]]" + case s: SimpleArgument => s.name + case o: OptionalArgument[?] => s"[${o.name}]" + case v: VarArgument => s"[${v.name} [${v.name} [...]]]" } ) println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") From 7eb8c0c5ebbf9a3f2a665d68563803d72f290c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 29 Oct 2021 11:20:10 +0200 Subject: [PATCH 044/121] Extract default values getter for clarity --- .../dotty/tools/dotc/ast/MainProxies.scala | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 1b5404b51136..ee8e370aed61 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -35,22 +35,23 @@ object MainProxies { def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ + + def defaultValues(scope: Tree, funSymbol: Symbol): Map[Int, Tree] = + scope match { + case TypeDef(_, template: Template) => + template.body.flatMap((_: Tree) match { + case dd @ DefDef(name, _, _, _) if name.is(DefaultGetterName) && name.firstPart == funSymbol.name => + val index: Int = name.toString.split("\\$").last.toInt - 1 // FIXME please!! + val valueTree = dd.rhs + List(index -> valueTree) + case _ => List() + }).toMap + case _ => Map[Int, Tree]() + } + def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Map[Int, Tree], Option[Comment])] = stats.flatMap { case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => - val defaultValues: Map[Int, Tree] = - (scope match { - case TypeDef(_, template: Template) => - template.body.flatMap((_: Tree) match { - case dd @ DefDef(name, _, _, _) if name.is(DefaultGetterName) && name.firstPart == stat.symbol.name => - val index: Int = name.toString.split("\\$")(2).toInt - 1 // FIXME please!! - val valueTree = dd.rhs - List(index -> valueTree) - case _ => List() - }).toMap - case _ => Map[Int, Tree]() - }).withDefaultValue(EmptyTree) - - (stat.symbol, defaultValues, stat.rawComment) :: Nil + (stat.symbol, defaultValues(scope, stat.symbol), stat.rawComment) :: Nil case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => mainMethods(stat, impl.body) case _ => From 91f5ee4b16dd26bf05fc852a9769d4d92819f5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 29 Oct 2021 17:37:42 +0200 Subject: [PATCH 045/121] Factorize getter code, check duplicate named param - Extract argGetter and argGetterDefault into a helper method - When using multiple times the same arg by name, display an error --- library/src/scala/main.scala | 36 ++++++++++++++------ tests/run/main-annotation-named-params.check | 5 +++ tests/run/main-annotation-named-params.scala | 3 ++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 60912e74a883..e629d6c760e0 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -108,21 +108,37 @@ class main extends scala.annotation.MainAnnotation: private def explain(): Unit = self.explain(this.commandName, this.docComment, argInfos.toSeq) + private def indicesOfArg(argName: String): Seq[Int] = + def allIndicesOf(s: String): Seq[Int] = + def recurse(s: String, from: Int): Seq[Int] = + val i = args.indexOf(s, from) + if i < 0 then Seq() else i +: recurse(s, i + 1) + + recurse(s, 0) + + val indices = allIndicesOf(s"--$argName") + indices.filter(_ >= 0) + + private def getArgGetter[T](argName: String, defaultGetter: => () => T)(using p: ArgumentParser[T]): () => T = + indicesOfArg(argName) match { + case s @ (Seq() | Seq(_)) => + val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) + argOpt match { + case Some(arg) => convert(argName, arg, p) + case None => defaultGetter + } + case s => + val multValues = s.flatMap(idx => argAt(idx + 1)) + error(s"more than one value for $argName: ${multValues.mkString(", ")}") + } + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = argInfos += self.SimpleArgument(argName, argType, argDoc) - val idx = args.indexOf(s"--$argName") - val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() - argOpt match - case Some(arg) => convert(argName, arg, p) - case None => error(s"missing argument for $argName") + getArgGetter(argName, error(s"missing argument for $argName")) override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = argInfos += self.OptionalArgument(argName, argType, argDoc, defaultValue) - val idx = args.indexOf(s"--$argName") - val argOpt = if idx >= 0 then argAt(idx + 1) else nextPositionalArg() - argOpt match - case Some(arg) => convert(argName, arg, p) - case None => () => defaultValue + getArgGetter(argName, () => defaultValue) override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = argInfos += self.VarArgument(argName, argType, argDoc) diff --git a/tests/run/main-annotation-named-params.check b/tests/run/main-annotation-named-params.check index fa94c80607d2..2e6b16739c55 100644 --- a/tests/run/main-annotation-named-params.check +++ b/tests/run/main-annotation-named-params.check @@ -1,2 +1,7 @@ 2 + 3 = 5 2 + 3 = 5 +Error: more than one value for num: 2, 1 +Usage: add num inc +Error: more than one value for num: 2, 1 +Error: more than one value for inc: 1, 3 +Usage: add num inc diff --git a/tests/run/main-annotation-named-params.scala b/tests/run/main-annotation-named-params.scala index dbee28ce8277..b69481d3b1b3 100644 --- a/tests/run/main-annotation-named-params.scala +++ b/tests/run/main-annotation-named-params.scala @@ -16,4 +16,7 @@ object Test: def main(args: Array[String]): Unit = callMain(Array("--num", "2", "--inc", "3")) callMain(Array("--inc", "3", "--num", "2")) + + callMain(Array("--num", "2", "--num", "1", "--inc", "3")) + callMain(Array("--inc", "1", "--num", "2", "--num", "1", "--inc", "3")) end Test \ No newline at end of file From cd5674e533730b5bd836ffabb070c4b305c55718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 29 Oct 2021 18:58:04 +0200 Subject: [PATCH 046/121] Make MainProxy more explicit --- .../dotty/tools/dotc/ast/MainProxies.scala | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index ee8e370aed61..95cace441fee 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -80,34 +80,47 @@ object MainProxies { var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { case ((formal, paramName), n) => val argName = mainArgsName ++ (idx + n).toString - val getterSym = - if formal.isRepeatedParam then - defn.MainAnnotCommand_argsGetter - else if defaultValues contains n then - defn.MainAnnotCommand_argGetterDefault - else - defn.MainAnnotCommand_argGetter - var argRef: Tree = Apply(Ident(argName), Nil) var formalType = formal - //var returnType = formalType + //var returnType = formal - if (formal.isRepeatedParam) { - argRef = repeated(argRef) - formalType = formalType.argTypes.head - //returnType = defn.SeqType.appliedTo(formalType) - } + val (getterSym, getterArgs) = + if formal.isRepeatedParam then + argRef = repeated(argRef) + formalType = formalType.argTypes.head + //returnType = defn.SeqType.appliedTo(returnType) + ( + defn.MainAnnotCommand_argsGetter, + List( + Literal(Constant(paramName.toString)), + Literal(Constant(formalType.show)), + Literal(Constant(documentation.argDocs(paramName.toString))), + ) + ) + else if defaultValues contains n then + ( + defn.MainAnnotCommand_argGetterDefault, + List( + Literal(Constant(paramName.toString)), + Literal(Constant(formalType.show)), + Literal(Constant(documentation.argDocs(paramName.toString))), + defaultValues(n), + ) + ) + else + ( + defn.MainAnnotCommand_argGetter, + List( + Literal(Constant(paramName.toString)), + Literal(Constant(formalType.show)), + Literal(Constant(documentation.argDocs(paramName.toString))), + ) + ) val argDef = ValDef( argName, TypeTree(/*defn.FunctionOf(Nil, returnType)*/), - Apply( - TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), - Literal(Constant(paramName.toString)) - :: Literal(Constant(formalType.show)) - :: Literal(Constant(documentation.argDocs(paramName.toString))) - :: defaultValues.get(n).map(List(_)).getOrElse(Nil) - ), + Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), getterArgs), ) (argRef, argDef) From ccfe578b530e5e898507573aee7663f37051f4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 30 Oct 2021 12:41:39 +0200 Subject: [PATCH 047/121] Change explain formatting --- library/src/scala/main.scala | 14 ++++----- tests/run/main-annotation-help.check | 43 ++++++++++++++++++---------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index e629d6c760e0..1b44f52a0cb6 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -45,9 +45,13 @@ class main extends scala.annotation.MainAnnotation: if (commandDoc.nonEmpty) println(commandDoc) if (args.nonEmpty) { + val argNameShift = 2 + val argDocShift = argNameShift + 2 + println("Arguments:") for (arg <- args) - val argDoc = StringBuilder(s" ${arg.name}, ${arg.typeName}") + val argDoc = StringBuilder(" " * argNameShift) + argDoc append s"${arg.name}, ${arg.typeName}" arg match { case o: OptionalArgument[?] => argDoc append " (optional)" @@ -55,12 +59,8 @@ class main extends scala.annotation.MainAnnotation: } if (arg.doc.nonEmpty) { - val separator = " - " - // Shift doc's lines to align with the first - // foo, Int - so that this line - // is aligned with this one - val argExpl = arg.doc.split("\n").mkString("\n" + " " * (argDoc.length + separator.length)) - argDoc append separator append argExpl + val shiftedDoc = arg.doc.split("\n").map(" " * argDocShift + _).mkString("\n") + argDoc append "\n" append shiftedDoc } println(argDoc) diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index e5432b2fc1dd..f2fdf1a11f51 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -38,13 +38,16 @@ Usage: doc4 num inc? Adds two numbers. Arguments: - num, Int - the first number - inc, Int (optional) - the second number + num, Int + the first number + inc, Int (optional) + the second number Usage: doc5 num inc Adds two numbers. Arguments: - num, Int - the first number + num, Int + the first number inc, Int Usage: doc6 num inc @@ -56,36 +59,46 @@ Usage: doc7 num inc Adds two numbers. Arguments: - num, Int - the first number - inc, Int - the second number + num, Int + the first number + inc, Int + the second number Usage: doc8 num inc Adds two numbers. Arguments: - num, Int - the first number - inc, Int - the second number + num, Int + the first number + inc, Int + the second number Usage: doc9 num inc Adds two numbers. Same as doc1. Arguments: - num, Int - the first number - inc, Int - the second number + num, Int + the first number + inc, Int + the second number Usage: doc10 num inc Adds two numbers. This should be on another line. And this also. Arguments: - num, Int - I might have to write this on two lines - inc, Int - I might even have to write this one on three lines + num, Int + I might have to write this on two lines + inc, Int + I might even have to write this one on three lines Usage: doc11 num inc Adds two numbers. Arguments: - num, Int - the first number - Oh, a new line! - inc, Int - the second number - And another one! + num, Int + the first number + Oh, a new line! + inc, Int + the second number + And another one! Usage: doc12 num inc Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some From 8b42578ba06eeb5caa6eea8adc64d968d7ee48d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 1 Nov 2021 13:20:19 +0100 Subject: [PATCH 048/121] Use f.foo(i) instead of f foo i --- .../src/dotty/tools/dotc/ast/MainProxies.scala | 4 ++-- library/src/scala/main.scala | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 95cace441fee..67e3e4826d5b 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -77,7 +77,7 @@ object MainProxies { Nil } else { - var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex map { + var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => val argName = mainArgsName ++ (idx + n).toString var argRef: Tree = Apply(Ident(argName), Nil) @@ -97,7 +97,7 @@ object MainProxies { Literal(Constant(documentation.argDocs(paramName.toString))), ) ) - else if defaultValues contains n then + else if defaultValues.contains(n) then ( defn.MainAnnotCommand_argGetterDefault, List( diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 1b44f52a0cb6..b85138420940 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -31,13 +31,11 @@ class main extends scala.annotation.MainAnnotation: /** Prints the main function's usage */ def usage(commandName: String, args: Seq[Argument]): Unit = - val argInfos = args map ( - _ match { - case s: SimpleArgument => s.name - case o: OptionalArgument[?] => s"${o.name}?" - case v: VarArgument => s"${v.name}*" - } - ) + val argInfos = args.map { + case s: SimpleArgument => s.name + case o: OptionalArgument[?] => s"${o.name}?" + case v: VarArgument => s"${v.name}*" + } println(s"Usage: $commandName ${argInfos.mkString(" ")}") /** Prints an explanation about the function */ @@ -51,16 +49,16 @@ class main extends scala.annotation.MainAnnotation: println("Arguments:") for (arg <- args) val argDoc = StringBuilder(" " * argNameShift) - argDoc append s"${arg.name}, ${arg.typeName}" + argDoc.append(s"${arg.name}, ${arg.typeName}") arg match { - case o: OptionalArgument[?] => argDoc append " (optional)" + case o: OptionalArgument[?] => argDoc.append(" (optional)") case _ => } if (arg.doc.nonEmpty) { val shiftedDoc = arg.doc.split("\n").map(" " * argDocShift + _).mkString("\n") - argDoc append "\n" append shiftedDoc + argDoc.append("\n").append(shiftedDoc) } println(argDoc) From 91f6d55f8bb224492d60d4e2e56935cb9228f43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 1 Nov 2021 13:22:24 +0100 Subject: [PATCH 049/121] Trim docstring after removing {{ and }} --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 67e3e4826d5b..a3dc97c79d75 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -227,7 +227,7 @@ object MainProxies { case (s1, s2) if s1.last == '\n' => s1 + s2 case (s1, s2) => s1 + ' ' + s2 } - s.trim.replaceAll(raw"\{\{", "").replaceAll(raw"\}\}", "") + s.replaceAll(raw"\{\{", "").replaceAll(raw"\}\}", "").trim private def parseDocComment(raw: String): Unit = // Positions of the sections (@) in the docstring From 87f9c0965af29f313b5d9af5e8e516df1f71d65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 2 Nov 2021 12:49:52 +0100 Subject: [PATCH 050/121] Add test for anon. func. and implicit/using @main --- tests/neg/main-annotation-implicit-given.scala | 11 +++++++++++ tests/neg/main-annotation-nonmethod.scala | 2 ++ 2 files changed, 13 insertions(+) create mode 100644 tests/neg/main-annotation-implicit-given.scala diff --git a/tests/neg/main-annotation-implicit-given.scala b/tests/neg/main-annotation-implicit-given.scala new file mode 100644 index 000000000000..3efd49a8e3cf --- /dev/null +++ b/tests/neg/main-annotation-implicit-given.scala @@ -0,0 +1,11 @@ +object myProgram: + implicit val x: Int = 2 + given Int = 3 + + @main def showImplicit(implicit num: Int): Unit = // error + println(num) + + @main def showUsing(using num: Int): Unit = // error + println(num) + +end myProgram diff --git a/tests/neg/main-annotation-nonmethod.scala b/tests/neg/main-annotation-nonmethod.scala index 9a6cc6cee846..bd713ab39a8a 100644 --- a/tests/neg/main-annotation-nonmethod.scala +++ b/tests/neg/main-annotation-nonmethod.scala @@ -4,4 +4,6 @@ object myProgram: @main class A // error + @main val f = ((s: String) => println(s)) // error + end myProgram From 78fa961dd890d7882bcd0f32c1d4ef64fc8d5c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 2 Nov 2021 14:29:39 +0100 Subject: [PATCH 051/121] Factorize createArgs --- .../dotty/tools/dotc/ast/MainProxies.scala | 67 +++++++++---------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index a3dc97c79d75..da311b0af6be 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -5,7 +5,7 @@ import core._ import Symbols._, Types._, Contexts._, Decorators._, util.Spans._, Flags._, Constants._ import StdNames.{nme, tpnme} import ast.Trees._ -import Names.TermName +import Names.{Name, TermName} import Comments.Comment import NameKinds.DefaultGetterName @@ -71,6 +71,8 @@ object MainProxies { val documentation = new Documentation(docComment) + inline def lit(any: Any): Literal = Literal(Constant(any)) + def createArgs(mt: MethodType, cmdName: TermName, idx: Int): List[(Tree, ValDef)] = if (mt.isImplicitMethod) { report.error(s"@main method cannot have implicit parameters", pos) @@ -80,46 +82,37 @@ object MainProxies { var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => val argName = mainArgsName ++ (idx + n).toString + + val isRepeated = formal.isRepeatedParam + val hasDefaultValue = defaultValues.contains(n) + var argRef: Tree = Apply(Ident(argName), Nil) var formalType = formal - //var returnType = formal - - val (getterSym, getterArgs) = - if formal.isRepeatedParam then - argRef = repeated(argRef) - formalType = formalType.argTypes.head - //returnType = defn.SeqType.appliedTo(returnType) - ( - defn.MainAnnotCommand_argsGetter, - List( - Literal(Constant(paramName.toString)), - Literal(Constant(formalType.show)), - Literal(Constant(documentation.argDocs(paramName.toString))), - ) - ) - else if defaultValues.contains(n) then - ( - defn.MainAnnotCommand_argGetterDefault, - List( - Literal(Constant(paramName.toString)), - Literal(Constant(formalType.show)), - Literal(Constant(documentation.argDocs(paramName.toString))), - defaultValues(n), - ) - ) + if isRepeated then + argRef = repeated(argRef) + formalType = formalType.argTypes.head + + val getterSym = + if isRepeated then + defn.MainAnnotCommand_argsGetter + else if hasDefaultValue then + defn.MainAnnotCommand_argGetterDefault else - ( - defn.MainAnnotCommand_argGetter, - List( - Literal(Constant(paramName.toString)), - Literal(Constant(formalType.show)), - Literal(Constant(documentation.argDocs(paramName.toString))), - ) - ) + defn.MainAnnotCommand_argGetter + + val getterArgs = { + val param = paramName.toString + val optionDefaultValueTree = defaultValues.get(n) + + lit(param) + :: lit(formalType.show) + :: lit(documentation.argDocs(param)) + :: optionDefaultValueTree.map(List(_)).getOrElse(Nil) + } val argDef = ValDef( argName, - TypeTree(/*defn.FunctionOf(Nil, returnType)*/), + TypeTree(), Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), getterArgs), ) @@ -141,10 +134,10 @@ object MainProxies { else { val cmd = ValDef( cmdName, - TypeTree(), // TODO check if good practice + TypeTree(), Apply( Select(makeNew(TypeTree(mainAnnot.symbol.typeRef)), defn.MainAnnot_command.name), - Ident(mainArgsName) :: Literal(Constant(mainFun.showName)) :: Literal(Constant(documentation.mainDoc)) :: Nil + Ident(mainArgsName) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil ) ) var args: List[ValDef] = Nil From a65b76f05f0c4f01b393cd76e86ba993b0332638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 2 Nov 2021 17:24:00 +0100 Subject: [PATCH 052/121] Rework of Arguments, change usage format - Remove "case" keywork for Argument subclasses to avoid unapply pattern matching (future compatibility) - Change output of usage function to be more expressive --- library/src/scala/main.scala | 40 ++++-- tests/run/main-annotation-help.check | 118 +++++++++++------- tests/run/main-annotation-help.scala | 13 +- tests/run/main-annotation-named-params.check | 4 +- .../main-annotation-override-explain.check | 2 +- .../main-annotation-override-usage-1.check | 4 +- .../main-annotation-override-usage-2.check | 4 +- tests/run/main-annotation-wrong-param-1.check | 8 +- .../main-annotation-wrong-param-names.check | 8 +- .../main-annotation-wrong-param-number.check | 8 +- .../main-annotation-wrong-param-type.check | 12 +- 11 files changed, 137 insertions(+), 84 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index b85138420940..667098aca283 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -21,22 +21,42 @@ class main extends scala.annotation.MainAnnotation: def name: String def typeName: String def doc: String + + def usage: String } - protected case class SimpleArgument(name: String, typeName: String, doc: String) extends Argument - protected case class OptionalArgument[T](name: String, typeName: String, doc: String, val defaultValue: T) extends Argument - protected case class VarArgument(name: String, typeName: String, doc: String) extends Argument + protected class SimpleArgument(val name: String, val typeName: String, val doc: String) extends Argument: + override def usage: String = s"[--$name] <$name>" + protected class OptionalArgument[T](val name: String, val typeName: String, val doc: String, val defaultValue: T) + extends Argument: + override def usage: String = s"[[--$name] <$name>]" + protected class VarArgument(val name: String, val typeName: String, val doc: String) extends Argument: + override def usage: String = s"[<$name> [<$name> [...]]]" override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any /** Prints the main function's usage */ def usage(commandName: String, args: Seq[Argument]): Unit = - val argInfos = args.map { - case s: SimpleArgument => s.name - case o: OptionalArgument[?] => s"${o.name}?" - case v: VarArgument => s"${v.name}*" - } - println(s"Usage: $commandName ${argInfos.mkString(" ")}") + def wrappedArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = + def recurse(args: List[String], currentLine: String, acc: Vector[String]): Vector[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg :: t, "") => recurse(t, arg, acc) + case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg :: t, l) => recurse(t, arg, acc :+ l) + } + + recurse(argsUsage, "", Vector()).toList + end wrappedArgumentUsages + + val maxLineLength = 120 // TODO as parameter? As global Dotty parameter? + + val usageBeginning = s"Usage: $commandName " + val argsOffset = usageBeginning.length + val argUsages = wrappedArgumentUsages(args.map(_.usage).toList, maxLineLength - argsOffset) + + println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) /** Prints an explanation about the function */ def explain(commandName: String, commandDoc: String, args: Seq[Argument]): Unit = @@ -49,7 +69,7 @@ class main extends scala.annotation.MainAnnotation: println("Arguments:") for (arg <- args) val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"${arg.name}, ${arg.typeName}") + argDoc.append(s"${arg.name} - ${arg.typeName}") arg match { case o: OptionalArgument[?] => argDoc.append(" (optional)") diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index f2fdf1a11f51..cdb1936fff14 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -1,114 +1,136 @@ -Usage: doc1 num inc +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc1 num inc + num - Int + inc - Int +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc1 num inc + num - Int + inc - Int +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc1 num inc + num - Int + inc - Int +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc2 num inc + num - Int + inc - Int +Usage: doc2 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc3 num inc + num - Int + inc - Int +Usage: doc3 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc4 num inc? + num - Int + inc - Int +Usage: doc4 [--num] [[--inc] ] Adds two numbers. Arguments: - num, Int + num - Int the first number - inc, Int (optional) + inc - Int (optional) the second number -Usage: doc5 num inc +Usage: doc5 [--num] [--inc] Adds two numbers. Arguments: - num, Int + num - Int the first number - inc, Int -Usage: doc6 num inc + inc - Int +Usage: doc6 [--num] [--inc] Adds two numbers. Arguments: - num, Int - inc, Int -Usage: doc7 num inc + num - Int + inc - Int +Usage: doc7 [--num] [--inc] Adds two numbers. Arguments: - num, Int + num - Int the first number - inc, Int + inc - Int the second number -Usage: doc8 num inc +Usage: doc8 [--num] [--inc] Adds two numbers. Arguments: - num, Int + num - Int the first number - inc, Int + inc - Int the second number -Usage: doc9 num inc +Usage: doc9 [--num] [--inc] Adds two numbers. Same as doc1. Arguments: - num, Int + num - Int the first number - inc, Int + inc - Int the second number -Usage: doc10 num inc +Usage: doc10 [--num] [--inc] Adds two numbers. This should be on another line. And this also. Arguments: - num, Int + num - Int I might have to write this on two lines - inc, Int + inc - Int I might even have to write this one on three lines -Usage: doc11 num inc +Usage: doc11 [--num] [--inc] Adds two numbers. Arguments: - num, Int + num - Int the first number Oh, a new line! - inc, Int + inc - Int the second number And another one! -Usage: doc12 num inc +Usage: doc12 [--num] [--inc] Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. Arguments: - num, Int - inc, Int -Usage: doc13 num inc + num - Int + inc - Int +Usage: doc13 [--num] [--inc] Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. Arguments: - num, Int - inc, Int + num - Int + inc - Int +Usage: doc14 [--arg1] [--arg2] [--arg3] [--arg4] [--arg5] [--arg6] + [--arg7] [--arg8] [[--arg9] ] [[--arg10] ] [[--arg11] ] + [[--arg12] ] [[--arg13] ] [[--arg14] ] [[--arg15] ] [ [ [...]]] + +Loudly judges the number of argument you gave to this poor function. +Arguments: + arg1 - String + arg2 - Int + arg3 - String + arg4 - Int + arg5 - String + arg6 - Int + arg7 - String + arg8 - Int + arg9 - String (optional) + arg10 - Int (optional) + arg11 - String (optional) + arg12 - Int (optional) + arg13 - String (optional) + arg14 - Int (optional) + arg15 - String (optional) + arg16 - Int diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index 4a45ec1c3c56..f7e64be771bb 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -120,6 +120,17 @@ object myProgram: @main def doc13(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") + /** + * Loudly judges the number of argument you gave to this poor function. + */ + @main def doc14( + arg1: String, arg2: Int, arg3: String, arg4: Int, + arg5: String, arg6: Int, arg7: String, arg8: Int, + arg9: String = "I", arg10: Int = 42, arg11: String = "used", arg12: Int = 0, + arg13: String = "to", arg14: Int = 34, arg15: String = "wonder", arg16: Int* + ): Unit = + println(s"Wow, now that's a lot of arguments") + end myProgram object Test: @@ -129,7 +140,7 @@ object Test: method.invoke(null, args) def callAllMains(args: Array[String]): Unit = - val numberOfMains = 13 + val numberOfMains = 14 for (i <- 1 to numberOfMains) { val clazz = Class.forName("doc" + i.toString) val method = clazz.getMethod("main", classOf[Array[String]]) diff --git a/tests/run/main-annotation-named-params.check b/tests/run/main-annotation-named-params.check index 2e6b16739c55..045300edb9ec 100644 --- a/tests/run/main-annotation-named-params.check +++ b/tests/run/main-annotation-named-params.check @@ -1,7 +1,7 @@ 2 + 3 = 5 2 + 3 = 5 Error: more than one value for num: 2, 1 -Usage: add num inc +Usage: add [--num] [--inc] Error: more than one value for num: 2, 1 Error: more than one value for inc: 1, 3 -Usage: add num inc +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-override-explain.check b/tests/run/main-annotation-override-explain.check index a71e5dc1ccb1..c0346d6429eb 100644 --- a/tests/run/main-annotation-override-explain.check +++ b/tests/run/main-annotation-override-explain.check @@ -1,4 +1,4 @@ -Usage: add num inc? +Usage: add [--num] [[--inc] ] A d diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check index e74944453500..3b42f200fa61 100644 --- a/tests/run/main-annotation-override-usage-1.check +++ b/tests/run/main-annotation-override-usage-1.check @@ -2,5 +2,5 @@ My shiny command works like this: add num [inc] Adds two numbers Arguments: - num, Int - inc, Int (optional) + num - Int + inc - Int (optional) diff --git a/tests/run/main-annotation-override-usage-2.check b/tests/run/main-annotation-override-usage-2.check index d7241faed737..e1a2261cfe29 100644 --- a/tests/run/main-annotation-override-usage-2.check +++ b/tests/run/main-annotation-override-usage-2.check @@ -2,5 +2,5 @@ My shiny command works like this: add num [inc [inc [...]]] Adds numbers Arguments: - num, Int - inc, Int + num - Int + inc - Int diff --git a/tests/run/main-annotation-wrong-param-1.check b/tests/run/main-annotation-wrong-param-1.check index 3220f4dc201e..a80dd390189f 100644 --- a/tests/run/main-annotation-wrong-param-1.check +++ b/tests/run/main-annotation-wrong-param-1.check @@ -1,13 +1,13 @@ Error: invalid argument for inc: true Error: unused argument: SPAAAAACE -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: add Error: unused argument: 3 -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: true Error: invalid argument for inc: false Error: unused argument: 10 -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: binary Error: unused argument: 01 -Usage: add num inc +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check index 450dec9440bc..0a43156dcc12 100644 --- a/tests/run/main-annotation-wrong-param-names.check +++ b/tests/run/main-annotation-wrong-param-names.check @@ -1,15 +1,15 @@ Error: invalid argument for num: -n Error: unused argument: -i Error: unused argument: 10 -Usage: add num inc +Usage: add [--num] [--inc] Error: missing argument for num Error: missing argument for inc Error: unknown argument name: --n Error: unknown argument name: --i -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: -num Error: unused argument: 1 -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for inc: -inc Error: unused argument: 10 -Usage: add num inc +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-number.check b/tests/run/main-annotation-wrong-param-number.check index 6becb8d4952c..0c354667adff 100644 --- a/tests/run/main-annotation-wrong-param-number.check +++ b/tests/run/main-annotation-wrong-param-number.check @@ -1,10 +1,10 @@ Error: missing argument for num Error: missing argument for inc -Usage: add num inc +Usage: add [--num] [--inc] Error: missing argument for inc -Usage: add num inc +Usage: add [--num] [--inc] Error: unused argument: 3 -Usage: add num inc +Usage: add [--num] [--inc] Error: unused argument: 3 Error: unused argument: 4 Error: unused argument: 5 @@ -13,4 +13,4 @@ Error: unused argument: 7 Error: unused argument: 8 Error: unused argument: 9 Error: unused argument: 10 -Usage: add num inc +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-type.check b/tests/run/main-annotation-wrong-param-type.check index 8b3269a91b2e..a5bb0a80be2d 100644 --- a/tests/run/main-annotation-wrong-param-type.check +++ b/tests/run/main-annotation-wrong-param-type.check @@ -1,14 +1,14 @@ Error: invalid argument for inc: true -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: 2.1 -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for inc: 3.1415921535 -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: 192.168.1.1 -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: false Error: invalid argument for inc: true -Usage: add num inc +Usage: add [--num] [--inc] Error: invalid argument for num: Hello Error: invalid argument for inc: world! -Usage: add num inc +Usage: add [--num] [--inc] From 52ef5dc9ac3d16a4515d559d1110e01de6af9d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 6 Nov 2021 18:11:37 +0100 Subject: [PATCH 053/121] Misc. changes to tests --- .../run/main-annotation-homemade-parser.check | 1 + .../run/main-annotation-homemade-parser.scala | 26 +++++++++++++++++++ tests/run/main-annotation-named-params.check | 2 ++ tests/run/main-annotation-named-params.scala | 3 +++ tests/run/main-annotation-overload.scala | 4 +++ .../main-annotation-override-usage-1.check | 2 +- .../main-annotation-override-usage-1.scala | 4 +-- .../main-annotation-override-usage-2.check | 2 +- .../main-annotation-override-usage-2.scala | 4 +-- tests/run/main-annotation-return-type-1.check | 1 - tests/run/main-annotation-return-type-1.scala | 2 +- tests/run/main-annotation-return-type-2.check | 1 - tests/run/main-annotation-return-type-2.scala | 2 +- .../main-annotation-wrong-param-names.check | 4 +++ .../main-annotation-wrong-param-names.scala | 1 + 15 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 tests/run/main-annotation-homemade-parser.check create mode 100644 tests/run/main-annotation-homemade-parser.scala diff --git a/tests/run/main-annotation-homemade-parser.check b/tests/run/main-annotation-homemade-parser.check new file mode 100644 index 000000000000..56acd5c7a7ab --- /dev/null +++ b/tests/run/main-annotation-homemade-parser.check @@ -0,0 +1 @@ +2 + 3 = 5 diff --git a/tests/run/main-annotation-homemade-parser.scala b/tests/run/main-annotation-homemade-parser.scala new file mode 100644 index 000000000000..5fea7070c870 --- /dev/null +++ b/tests/run/main-annotation-homemade-parser.scala @@ -0,0 +1,26 @@ +import scala.util.CommandLineParser.FromString + +class MyNumber(val value: Int) { + def +(other: MyNumber): MyNumber = MyNumber(value + other.value) +} + +given FromString[MyNumber] with + def fromString(s: String): MyNumber = MyNumber(summon[FromString[Int]].fromString(s)) + +object myProgram: + + @main def add(num: MyNumber, inc: MyNumber): Unit = + println(s"${num.value} + ${inc.value} = ${num.value + inc.value}") + +end myProgram + + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-named-params.check b/tests/run/main-annotation-named-params.check index 045300edb9ec..6cf394ac9f1c 100644 --- a/tests/run/main-annotation-named-params.check +++ b/tests/run/main-annotation-named-params.check @@ -1,5 +1,7 @@ 2 + 3 = 5 2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 Error: more than one value for num: 2, 1 Usage: add [--num] [--inc] Error: more than one value for num: 2, 1 diff --git a/tests/run/main-annotation-named-params.scala b/tests/run/main-annotation-named-params.scala index b69481d3b1b3..f4978bafe480 100644 --- a/tests/run/main-annotation-named-params.scala +++ b/tests/run/main-annotation-named-params.scala @@ -17,6 +17,9 @@ object Test: callMain(Array("--num", "2", "--inc", "3")) callMain(Array("--inc", "3", "--num", "2")) + callMain(Array("2", "--inc", "3")) + callMain(Array("--num", "2", "3")) + callMain(Array("--num", "2", "--num", "1", "--inc", "3")) callMain(Array("--inc", "1", "--num", "2", "--num", "1", "--inc", "3")) end Test \ No newline at end of file diff --git a/tests/run/main-annotation-overload.scala b/tests/run/main-annotation-overload.scala index 5c9f263ef647..5688edc1bfc2 100644 --- a/tests/run/main-annotation-overload.scala +++ b/tests/run/main-annotation-overload.scala @@ -1,6 +1,10 @@ // Sample main method object myProgram: + /** Adds three numbers (malformed, doesn't work) */ + def add(num1: Int, num2: Int, num3: Int): Unit = + ??? + /** Adds two numbers */ @main def add(num: Int, inc: Int): Unit = println(s"$num + $inc = ${num + inc}") diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check index 3b42f200fa61..43919970d676 100644 --- a/tests/run/main-annotation-override-usage-1.check +++ b/tests/run/main-annotation-override-usage-1.check @@ -1,4 +1,4 @@ -My shiny command works like this: add num [inc] +My shiny command works like this: add num inc? Adds two numbers Arguments: diff --git a/tests/run/main-annotation-override-usage-1.scala b/tests/run/main-annotation-override-usage-1.scala index 71e6d568138c..62305ec1729d 100644 --- a/tests/run/main-annotation-override-usage-1.scala +++ b/tests/run/main-annotation-override-usage-1.scala @@ -3,8 +3,8 @@ class myMain extends main: val argInfos = args map ( _ match { case s: SimpleArgument => s.name - case o: OptionalArgument[?] => s"[${o.name}]" - case v: VarArgument => s"[${v.name} [${v.name} [...]]]" + case o: OptionalArgument[?] => s"${o.name}?" + case v: VarArgument => s"${v.name}*" } ) println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") diff --git a/tests/run/main-annotation-override-usage-2.check b/tests/run/main-annotation-override-usage-2.check index e1a2261cfe29..9a27bad4f357 100644 --- a/tests/run/main-annotation-override-usage-2.check +++ b/tests/run/main-annotation-override-usage-2.check @@ -1,4 +1,4 @@ -My shiny command works like this: add num [inc [inc [...]]] +My shiny command works like this: add num inc* Adds numbers Arguments: diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala index 8f53b1c1334c..e9eed8715b2c 100644 --- a/tests/run/main-annotation-override-usage-2.scala +++ b/tests/run/main-annotation-override-usage-2.scala @@ -3,8 +3,8 @@ class myMain extends main: val argInfos = args map ( _ match { case s: SimpleArgument => s.name - case o: OptionalArgument[?] => s"[${o.name}]" - case v: VarArgument => s"[${v.name} [${v.name} [...]]]" + case o: OptionalArgument[?] => s"${o.name}?" + case v: VarArgument => s"${v.name}*" } ) println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") diff --git a/tests/run/main-annotation-return-type-1.check b/tests/run/main-annotation-return-type-1.check index 9dd320bd88a1..0791928884c2 100644 --- a/tests/run/main-annotation-return-type-1.check +++ b/tests/run/main-annotation-return-type-1.check @@ -2,4 +2,3 @@ Direct call 2 + 3 = 5 Main call 2 + 3 = 5 -() diff --git a/tests/run/main-annotation-return-type-1.scala b/tests/run/main-annotation-return-type-1.scala index 8d363ca830b7..834947a3b345 100644 --- a/tests/run/main-annotation-return-type-1.scala +++ b/tests/run/main-annotation-return-type-1.scala @@ -18,5 +18,5 @@ object Test: println("Direct call") assert(myProgram.add(2, 3) == 5) println("Main call") - println(callMain(Array("2", "3"))) + callMain(Array("2", "3")) end Test diff --git a/tests/run/main-annotation-return-type-2.check b/tests/run/main-annotation-return-type-2.check index 9dd320bd88a1..0791928884c2 100644 --- a/tests/run/main-annotation-return-type-2.check +++ b/tests/run/main-annotation-return-type-2.check @@ -2,4 +2,3 @@ Direct call 2 + 3 = 5 Main call 2 + 3 = 5 -() diff --git a/tests/run/main-annotation-return-type-2.scala b/tests/run/main-annotation-return-type-2.scala index 4942d86dbae2..2d6462ff8b9f 100644 --- a/tests/run/main-annotation-return-type-2.scala +++ b/tests/run/main-annotation-return-type-2.scala @@ -21,5 +21,5 @@ object Test: println("Direct call") assert(myProgram.add(2, 3).result == 5) println("Main call") - println(callMain(Array("2", "3"))) + callMain(Array("2", "3")) end Test diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check index 0a43156dcc12..7b161c33ce5d 100644 --- a/tests/run/main-annotation-wrong-param-names.check +++ b/tests/run/main-annotation-wrong-param-names.check @@ -13,3 +13,7 @@ Usage: add [--num] [--inc] Error: invalid argument for inc: -inc Error: unused argument: 10 Usage: add [--num] [--inc] +Error: invalid argument for num: num +Error: unused argument: inc +Error: unused argument: 10 +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-names.scala b/tests/run/main-annotation-wrong-param-names.scala index ce475af0226e..4398ad22a3ef 100644 --- a/tests/run/main-annotation-wrong-param-names.scala +++ b/tests/run/main-annotation-wrong-param-names.scala @@ -18,4 +18,5 @@ object Test: callMain(Array("--n", "1", "--i", "10")) callMain(Array("-num", "1", "--inc", "10")) callMain(Array("--num", "1", "-inc", "10")) + callMain(Array("num", "1", "inc", "10")) end Test From 75e06a1f9637504d315215dae56ab93eacc2af93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 7 Nov 2021 11:05:58 +0100 Subject: [PATCH 054/121] Forbid multiple @main annotations --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 11 +++++++++-- tests/neg/main-annotation-multiple-annot.scala | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index da311b0af6be..ce13cb2b100c 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -50,8 +50,15 @@ object MainProxies { } def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Map[Int, Tree], Option[Comment])] = stats.flatMap { - case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => - (stat.symbol, defaultValues(scope, stat.symbol), stat.rawComment) :: Nil + case stat: DefDef => + val sym = stat.symbol + sym.annotations.filter(_ matches defn.MainAnnot) match { + case Nil => Nil + case _ :: Nil => (sym, defaultValues(scope, sym), stat.rawComment) :: Nil + case mainAnnot :: others => + report.error(s"method cannot have multiple @main annotations", mainAnnot.tree) + Nil + } case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => mainMethods(stat, impl.body) case _ => diff --git a/tests/neg/main-annotation-multiple-annot.scala b/tests/neg/main-annotation-multiple-annot.scala index bd3715c1d98e..f655bc6b8f33 100644 --- a/tests/neg/main-annotation-multiple-annot.scala +++ b/tests/neg/main-annotation-multiple-annot.scala @@ -2,13 +2,13 @@ class myMain extends main object myProgram: - @main @main def add1(num: Int)(inc: Int): Unit = // error + @main @main def add1(num: Int, inc: Int): Unit = // error println(s"$num + $inc = ${num + inc}") - @myMain @main def add2(num: Int)(inc: Int): Unit = // error + @myMain @main def add2(num: Int, inc: Int): Unit = // error println(s"$num + $inc = ${num + inc}") - @myMain @myMain def add3(num: Int)(inc: Int): Unit = // error + @myMain @myMain def add3(num: Int, inc: Int): Unit = // error println(s"$num + $inc = ${num + inc}") end myProgram From 395fd14b2c2ca1f760ebe9bbf407b87fb2866edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 19:21:22 +0100 Subject: [PATCH 055/121] Make defaultValue by-name param Co-authored-by: Nicolas Stucki --- library/src/scala/annotation/MainAnnotation.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index afc7b4f82907..cb7641df15aa 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -30,7 +30,7 @@ trait MainAnnotation extends StaticAnnotation: def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: =>T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ def argsGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => Seq[T] From f3005975a72efd3fdc3fa7c7f4e4005b4505cfa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 19:22:32 +0100 Subject: [PATCH 056/121] Remove redundant val in ExitCode Co-authored-by: Nicolas Stucki --- library/src/scala/main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 667098aca283..93051300d1b5 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -195,5 +195,5 @@ class main extends scala.annotation.MainAnnotation: end main object main: - case class ExitCode(val code: Int) + case class ExitCode(code: Int) end main \ No newline at end of file From 45acb09daf15340f8471d07b7f94706e235e084f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 18:16:12 +0100 Subject: [PATCH 057/121] Fix method call notation --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index ce13cb2b100c..1452cb94abff 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -52,7 +52,7 @@ object MainProxies { def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Map[Int, Tree], Option[Comment])] = stats.flatMap { case stat: DefDef => val sym = stat.symbol - sym.annotations.filter(_ matches defn.MainAnnot) match { + sym.annotations.filter(_.matches(defn.MainAnnot)) match { case Nil => Nil case _ :: Nil => (sym, defaultValues(scope, sym), stat.rawComment) :: Nil case mainAnnot :: others => From 6de547f4bda832bca43ca1d7625c70de530a91c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 18:17:25 +0100 Subject: [PATCH 058/121] Generate code for children of MainAnnotation, not main --- .../src/dotty/tools/dotc/ast/MainProxies.scala | 16 ++++++++-------- .../src/dotty/tools/dotc/core/Definitions.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 8 ++++---- tests/neg/main-annotation-mainannotation.scala | 3 +++ tests/neg/main-annotation-nonmethod.scala | 8 ++++++++ tests/neg/main-annotation-nonstatic.scala | 5 +++++ 6 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 tests/neg/main-annotation-mainannotation.scala create mode 100644 tests/neg/main-annotation-nonstatic.scala diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 1452cb94abff..941a4f4d161d 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -9,7 +9,7 @@ import Names.{Name, TermName} import Comments.Comment import NameKinds.DefaultGetterName -/** Generate proxy classes for @main functions. +/** Generate proxy classes for main functions. * A function like * * /** @@ -56,7 +56,7 @@ object MainProxies { case Nil => Nil case _ :: Nil => (sym, defaultValues(scope, sym), stat.rawComment) :: Nil case mainAnnot :: others => - report.error(s"method cannot have multiple @main annotations", mainAnnot.tree) + report.error(s"method cannot have multiple main annotations", mainAnnot.tree) Nil } case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => @@ -65,7 +65,7 @@ object MainProxies { Nil } - // Assuming that the top-level object was already generated, all @main methods will have a scope + // Assuming that the top-level object was already generated, all main methods will have a scope mainMethods(EmptyTree, stats).flatMap(mainProxy) } @@ -82,7 +82,7 @@ object MainProxies { def createArgs(mt: MethodType, cmdName: TermName, idx: Int): List[(Tree, ValDef)] = if (mt.isImplicitMethod) { - report.error(s"@main method cannot have implicit parameters", pos) + report.error(s"main method cannot have implicit parameters", pos) Nil } else { @@ -128,7 +128,7 @@ object MainProxies { mt.resType match { case restpe: MethodType => if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam) - report.error(s"varargs parameter of @main method must come last", pos) + report.error(s"varargs parameter of main method must come last", pos) valArgs ::: createArgs(restpe, cmdName, idx + valArgs.length) case _ => valArgs @@ -137,7 +137,7 @@ object MainProxies { var result: List[TypeDef] = Nil if (!mainFun.owner.isStaticOwner) - report.error(s"@main method is not statically accessible", pos) + report.error(s"main method is not statically accessible", pos) else { val cmd = ValDef( cmdName, @@ -157,9 +157,9 @@ object MainProxies { args = argVals mainCall = Apply(mainCall, argRefs) case _: PolyType => - report.error(s"@main method cannot have type parameters", pos) + report.error(s"main method cannot have type parameters", pos) case _ => - report.error(s"@main can only annotate a method", pos) + report.error(s"main can only annotate a method", pos) } val run = Apply(Select(Ident(cmdName), defn.MainAnnotCommand_run.name), mainCall) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ca50bb2c7751..275a46123535 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -853,7 +853,7 @@ class Definitions { @tu lazy val XMLTopScopeModule: Symbol = requiredModule("scala.xml.TopScope") - @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.main") + @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.annotation.MainAnnotation") @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") @tu lazy val MainAnnotCommand: ClassSymbol = MainAnnot.requiredClass("Command") @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index be38221ef167..df1a03f991a4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1351,13 +1351,13 @@ trait Checking { /** check that annotation `annot` is applicable to symbol `sym` */ def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean = !ctx.reporter.reportsErrorsFor { - val annotCls = Annotations.annotClass(annot) + val concreteAnnot = Annotations.ConcreteAnnotation(annot) val pos = annot.srcPos - if (annotCls == defn.MainAnnot) { + if (concreteAnnot.matches(defn.MainAnnot)) { if (!sym.isRealMethod) - report.error(em"@main annotation cannot be applied to $sym", pos) + report.error(em"main annotation cannot be applied to $sym", pos) if (!sym.owner.is(Module) || !sym.owner.isStatic) - report.error(em"$sym cannot be a @main method since it cannot be accessed statically", pos) + report.error(em"$sym cannot be a main method since it cannot be accessed statically", pos) } // TODO: Add more checks here } diff --git a/tests/neg/main-annotation-mainannotation.scala b/tests/neg/main-annotation-mainannotation.scala new file mode 100644 index 000000000000..dd35c7f0d639 --- /dev/null +++ b/tests/neg/main-annotation-mainannotation.scala @@ -0,0 +1,3 @@ +import scala.annotation.MainAnnotation + +@MainAnnotation def f(i: Int, n: Int) = () // error \ No newline at end of file diff --git a/tests/neg/main-annotation-nonmethod.scala b/tests/neg/main-annotation-nonmethod.scala index bd713ab39a8a..caf39ef0f06d 100644 --- a/tests/neg/main-annotation-nonmethod.scala +++ b/tests/neg/main-annotation-nonmethod.scala @@ -1,3 +1,5 @@ +class myMain extends main + object myProgram: @main val n = 2 // error @@ -6,4 +8,10 @@ object myProgram: @main val f = ((s: String) => println(s)) // error + @myMain val m = 2 // error + + @myMain class B // error + + @myMain val g = ((s: String) => println(s)) // error + end myProgram diff --git a/tests/neg/main-annotation-nonstatic.scala b/tests/neg/main-annotation-nonstatic.scala new file mode 100644 index 000000000000..77d831be04b8 --- /dev/null +++ b/tests/neg/main-annotation-nonstatic.scala @@ -0,0 +1,5 @@ +class myMain extends main + +class A: + @main def foo(bar: Int) = () // error + @myMain def baz() = () // error \ No newline at end of file From 70ee28a2d60567eed09c53a781fff4c8b85623a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 18:26:08 +0100 Subject: [PATCH 059/121] Forbid curried main methods --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 941a4f4d161d..ff492a60a386 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -80,7 +80,7 @@ object MainProxies { inline def lit(any: Any): Literal = Literal(Constant(any)) - def createArgs(mt: MethodType, cmdName: TermName, idx: Int): List[(Tree, ValDef)] = + def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = if (mt.isImplicitMethod) { report.error(s"main method cannot have implicit parameters", pos) Nil @@ -88,7 +88,7 @@ object MainProxies { else { var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => - val argName = mainArgsName ++ (idx + n).toString + val argName = mainArgsName ++ n.toString val isRepeated = formal.isRepeatedParam val hasDefaultValue = defaultValues.contains(n) @@ -127,9 +127,8 @@ object MainProxies { } mt.resType match { case restpe: MethodType => - if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam) - report.error(s"varargs parameter of main method must come last", pos) - valArgs ::: createArgs(restpe, cmdName, idx + valArgs.length) + report.error(s"main method cannot be curried", pos) + Nil case _ => valArgs } @@ -153,7 +152,7 @@ object MainProxies { mainFun.info match { case _: ExprType => case mt: MethodType => - val (argRefs, argVals) = createArgs(mt, cmdName, 0).unzip + val (argRefs, argVals) = createArgs(mt, cmdName).unzip args = argVals mainCall = Apply(mainCall, argRefs) case _: PolyType => From 50de8400e7885330b06c52309d6fcdcf32acde30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 19:25:27 +0100 Subject: [PATCH 060/121] Add missing arrow in main.argGetterDefault --- library/src/scala/annotation/MainAnnotation.scala | 2 +- library/src/scala/main.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index cb7641df15aa..36ee5a0ca6b6 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -30,7 +30,7 @@ trait MainAnnotation extends StaticAnnotation: def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: =>T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ def argsGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => Seq[T] diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 93051300d1b5..99da289af418 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -154,7 +154,7 @@ class main extends scala.annotation.MainAnnotation: argInfos += self.SimpleArgument(argName, argType, argDoc) getArgGetter(argName, error(s"missing argument for $argName")) - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: T)(using p: ArgumentParser[T]): () => T = + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = argInfos += self.OptionalArgument(argName, argType, argDoc, defaultValue) getArgGetter(argName, () => defaultValue) From 6bc549b830e5c6e5f0f07830cf620101fc731bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 9 Nov 2021 20:04:04 +0100 Subject: [PATCH 061/121] Make Command a trait --- library/src/scala/annotation/MainAnnotation.scala | 2 +- library/src/scala/main.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 36ee5a0ca6b6..db9e87c15894 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -24,7 +24,7 @@ trait MainAnnotation extends StaticAnnotation: def command(args: Array[String], commandName: String, docComment: String): Command /** A class representing a command to run */ - abstract class Command(val commandName: String, val docComment: String): + trait Command: /** The getter for the next argument of type `T` */ def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 99da289af418..e89da92305bb 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -91,7 +91,7 @@ class main extends scala.annotation.MainAnnotation: case _ => override def command(args: Array[String], commandName: String, docComment: String): Command = - new Command(commandName, docComment): + new Command: /** A buffer of demanded arguments */ private var argInfos = new mutable.ListBuffer[self.Argument] @@ -121,10 +121,10 @@ class main extends scala.annotation.MainAnnotation: case None => error(s"invalid argument for $argName: $arg") private def usage(): Unit = - self.usage(this.commandName, argInfos.toSeq) + self.usage(commandName, argInfos.toSeq) private def explain(): Unit = - self.explain(this.commandName, this.docComment, argInfos.toSeq) + self.explain(commandName, docComment, argInfos.toSeq) private def indicesOfArg(argName: String): Seq[Int] = def allIndicesOf(s: String): Seq[Int] = From 1b9c9522ac78ea1fd4e7fcb95c0b7d5ee7612166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 10 Nov 2021 09:28:02 +0100 Subject: [PATCH 062/121] Fix issue where map is not member of Array[String | Null] | Null - This was caused by changes in issue #13729 --- library/src/scala/main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index e89da92305bb..3670d99e0320 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -77,7 +77,7 @@ class main extends scala.annotation.MainAnnotation: } if (arg.doc.nonEmpty) { - val shiftedDoc = arg.doc.split("\n").map(" " * argDocShift + _).mkString("\n") + val shiftedDoc = arg.doc.split("\n").nn.map(" " * argDocShift + _).mkString("\n") argDoc.append("\n").append(shiftedDoc) } From de93b4df1156eb8c3d979069f89a7b11d4e2e4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 10 Nov 2021 09:49:13 +0100 Subject: [PATCH 063/121] Add test for multiple runs --- .../run/main-annotation-override-run-3.check | 3 +++ .../run/main-annotation-override-run-3.scala | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/run/main-annotation-override-run-3.check create mode 100644 tests/run/main-annotation-override-run-3.scala diff --git a/tests/run/main-annotation-override-run-3.check b/tests/run/main-annotation-override-run-3.check new file mode 100644 index 000000000000..4539bbf2d22d --- /dev/null +++ b/tests/run/main-annotation-override-run-3.check @@ -0,0 +1,3 @@ +0 +1 +2 diff --git a/tests/run/main-annotation-override-run-3.scala b/tests/run/main-annotation-override-run-3.scala new file mode 100644 index 000000000000..dec36126c9a4 --- /dev/null +++ b/tests/run/main-annotation-override-run-3.scala @@ -0,0 +1,25 @@ +class myMain extends main: + override def run(f: => MainResultType): Unit = + for (i <- 1 to 3) + println(f) + +object myProgram: + var i = 0 + + /* Get a new value on each call */ + @myMain def inc: Int = + val inc = i + i += 1 + inc + +end myProgram + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("inc") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array()) +end Test From bbcf4ea56e2231941a134e4666403f6d50a6a832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 13 Nov 2021 20:44:43 +0100 Subject: [PATCH 064/121] Add test from doc example --- tests/run/main-annotation-birthday.check | 1 + tests/run/main-annotation-birthday.scala | 30 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/run/main-annotation-birthday.check create mode 100644 tests/run/main-annotation-birthday.scala diff --git a/tests/run/main-annotation-birthday.check b/tests/run/main-annotation-birthday.check new file mode 100644 index 000000000000..71528f2e27a9 --- /dev/null +++ b/tests/run/main-annotation-birthday.check @@ -0,0 +1 @@ +Happy 23rd birthday, Lisa and Peter diff --git a/tests/run/main-annotation-birthday.scala b/tests/run/main-annotation-birthday.scala new file mode 100644 index 000000000000..d38f145e7b1b --- /dev/null +++ b/tests/run/main-annotation-birthday.scala @@ -0,0 +1,30 @@ +/** + * Wishes a happy birthday to lucky people! + * + * @param age the age of the people whose birthday it is + * @param name the name of the luckiest person! + * @param others all the other lucky people + */ +@main def happyBirthday(age: Int, name: String, others: String*) = + val suffix = + age % 100 match + case 11 | 12 | 13 => "th" + case _ => + age % 10 match + case 1 => "st" + case 2 => "nd" + case 3 => "rd" + case _ => "th" + val bldr = new StringBuilder(s"Happy $age$suffix birthday, $name") + for other <- others do bldr.append(" and ").append(other) + println(bldr) + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("happyBirthday") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("23", "Lisa", "Peter")) +end Test From bdddb3f530f81066ccb46c4bd38ce13fa33fffba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 14 Nov 2021 00:33:40 +0100 Subject: [PATCH 065/121] Make main class final --- library/src/scala/main.scala | 2 +- .../neg/main-annotation-multiple-annot.scala | 8 ------ tests/neg/main-annotation-nonmethod.scala | 8 ------ tests/neg/main-annotation-nonstatic.scala | 5 +--- .../main-annotation-override-explain.check | 18 ------------ .../main-annotation-override-explain.scala | 21 -------------- .../run/main-annotation-override-run-1.check | 4 --- .../run/main-annotation-override-run-1.scala | 27 ------------------ .../run/main-annotation-override-run-2.check | 1 - .../run/main-annotation-override-run-2.scala | 21 -------------- .../run/main-annotation-override-run-3.check | 3 -- .../run/main-annotation-override-run-3.scala | 25 ----------------- .../main-annotation-override-usage-1.check | 6 ---- .../main-annotation-override-usage-1.scala | 28 ------------------- .../main-annotation-override-usage-2.check | 6 ---- .../main-annotation-override-usage-2.scala | 28 ------------------- 16 files changed, 2 insertions(+), 209 deletions(-) delete mode 100644 tests/run/main-annotation-override-explain.check delete mode 100644 tests/run/main-annotation-override-explain.scala delete mode 100644 tests/run/main-annotation-override-run-1.check delete mode 100644 tests/run/main-annotation-override-run-1.scala delete mode 100644 tests/run/main-annotation-override-run-2.check delete mode 100644 tests/run/main-annotation-override-run-2.scala delete mode 100644 tests/run/main-annotation-override-run-3.check delete mode 100644 tests/run/main-annotation-override-run-3.scala delete mode 100644 tests/run/main-annotation-override-usage-1.check delete mode 100644 tests/run/main-annotation-override-usage-1.scala delete mode 100644 tests/run/main-annotation-override-usage-2.check delete mode 100644 tests/run/main-annotation-override-usage-2.scala diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 3670d99e0320..15ccfd4530b2 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -13,7 +13,7 @@ import collection.mutable /** An annotation that designates a main function */ -class main extends scala.annotation.MainAnnotation: +final class main extends scala.annotation.MainAnnotation: self => import main._ diff --git a/tests/neg/main-annotation-multiple-annot.scala b/tests/neg/main-annotation-multiple-annot.scala index f655bc6b8f33..0c4dc310a8d6 100644 --- a/tests/neg/main-annotation-multiple-annot.scala +++ b/tests/neg/main-annotation-multiple-annot.scala @@ -1,14 +1,6 @@ -class myMain extends main - object myProgram: @main @main def add1(num: Int, inc: Int): Unit = // error println(s"$num + $inc = ${num + inc}") - @myMain @main def add2(num: Int, inc: Int): Unit = // error - println(s"$num + $inc = ${num + inc}") - - @myMain @myMain def add3(num: Int, inc: Int): Unit = // error - println(s"$num + $inc = ${num + inc}") - end myProgram diff --git a/tests/neg/main-annotation-nonmethod.scala b/tests/neg/main-annotation-nonmethod.scala index caf39ef0f06d..bd713ab39a8a 100644 --- a/tests/neg/main-annotation-nonmethod.scala +++ b/tests/neg/main-annotation-nonmethod.scala @@ -1,5 +1,3 @@ -class myMain extends main - object myProgram: @main val n = 2 // error @@ -8,10 +6,4 @@ object myProgram: @main val f = ((s: String) => println(s)) // error - @myMain val m = 2 // error - - @myMain class B // error - - @myMain val g = ((s: String) => println(s)) // error - end myProgram diff --git a/tests/neg/main-annotation-nonstatic.scala b/tests/neg/main-annotation-nonstatic.scala index 77d831be04b8..fe49646e23ae 100644 --- a/tests/neg/main-annotation-nonstatic.scala +++ b/tests/neg/main-annotation-nonstatic.scala @@ -1,5 +1,2 @@ -class myMain extends main - class A: - @main def foo(bar: Int) = () // error - @myMain def baz() = () // error \ No newline at end of file + @main def foo(bar: Int) = () // error \ No newline at end of file diff --git a/tests/run/main-annotation-override-explain.check b/tests/run/main-annotation-override-explain.check deleted file mode 100644 index c0346d6429eb..000000000000 --- a/tests/run/main-annotation-override-explain.check +++ /dev/null @@ -1,18 +0,0 @@ -Usage: add [--num] [[--inc] ] - -A -d -d -s - -t -w -o - -n -u -m -b -e -r -s diff --git a/tests/run/main-annotation-override-explain.scala b/tests/run/main-annotation-override-explain.scala deleted file mode 100644 index 1fcfcef08c2e..000000000000 --- a/tests/run/main-annotation-override-explain.scala +++ /dev/null @@ -1,21 +0,0 @@ -class myMain extends main: - override def explain(commandName: String, docComment: String, args: Seq[Argument]): Unit = - if docComment.nonEmpty then println(docComment.mkString("\n")) - -object myProgram: - - /** Adds two numbers */ - @myMain def add(num: Int, inc: Int = 1): Unit = - println(s"$num + $inc = ${num + inc}") - -end myProgram - -object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("add") - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, args) - - def main(args: Array[String]): Unit = - callMain(Array("--help")) -end Test diff --git a/tests/run/main-annotation-override-run-1.check b/tests/run/main-annotation-override-run-1.check deleted file mode 100644 index f7fa695217ea..000000000000 --- a/tests/run/main-annotation-override-run-1.check +++ /dev/null @@ -1,4 +0,0 @@ -I'm about to run! -2 + 3 = 5 -Exit with 5 -I'm done! diff --git a/tests/run/main-annotation-override-run-1.scala b/tests/run/main-annotation-override-run-1.scala deleted file mode 100644 index 9fb6e984cbb8..000000000000 --- a/tests/run/main-annotation-override-run-1.scala +++ /dev/null @@ -1,27 +0,0 @@ -class myMain extends main: - override def run(f: => MainResultType): Unit = - println("I'm about to run!") - f match { - case main.ExitCode(n) => println(s"Exit with $n") - case _ => println("I should not have printed this...") - } - println("I'm done!") - -object myProgram: - - /** Adds two numbers */ - @myMain def add(num: Int, inc: Int): main.ExitCode = - println(s"$num + $inc = ${num + inc}") - main.ExitCode(num + inc) - -end myProgram - -object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("add") - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, args) - - def main(args: Array[String]): Unit = - callMain(Array("2", "3")) -end Test diff --git a/tests/run/main-annotation-override-run-2.check b/tests/run/main-annotation-override-run-2.check deleted file mode 100644 index ae59037e583f..000000000000 --- a/tests/run/main-annotation-override-run-2.check +++ /dev/null @@ -1 +0,0 @@ -Yeah, I won't run that... diff --git a/tests/run/main-annotation-override-run-2.scala b/tests/run/main-annotation-override-run-2.scala deleted file mode 100644 index c3187ea382bb..000000000000 --- a/tests/run/main-annotation-override-run-2.scala +++ /dev/null @@ -1,21 +0,0 @@ -class myMain extends main: - override def run(f: => MainResultType): Unit = - println("Yeah, I won't run that...") - -object myProgram: - - /** Halt and catch fire */ - @myMain def hcf(code: Int): Nothing = - throw new Exception("I should not be executed!!") - -end myProgram - -object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("hcf") - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, args) - - def main(args: Array[String]): Unit = - callMain(Array("42")) -end Test diff --git a/tests/run/main-annotation-override-run-3.check b/tests/run/main-annotation-override-run-3.check deleted file mode 100644 index 4539bbf2d22d..000000000000 --- a/tests/run/main-annotation-override-run-3.check +++ /dev/null @@ -1,3 +0,0 @@ -0 -1 -2 diff --git a/tests/run/main-annotation-override-run-3.scala b/tests/run/main-annotation-override-run-3.scala deleted file mode 100644 index dec36126c9a4..000000000000 --- a/tests/run/main-annotation-override-run-3.scala +++ /dev/null @@ -1,25 +0,0 @@ -class myMain extends main: - override def run(f: => MainResultType): Unit = - for (i <- 1 to 3) - println(f) - -object myProgram: - var i = 0 - - /* Get a new value on each call */ - @myMain def inc: Int = - val inc = i - i += 1 - inc - -end myProgram - -object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("inc") - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, args) - - def main(args: Array[String]): Unit = - callMain(Array()) -end Test diff --git a/tests/run/main-annotation-override-usage-1.check b/tests/run/main-annotation-override-usage-1.check deleted file mode 100644 index 43919970d676..000000000000 --- a/tests/run/main-annotation-override-usage-1.check +++ /dev/null @@ -1,6 +0,0 @@ -My shiny command works like this: add num inc? - -Adds two numbers -Arguments: - num - Int - inc - Int (optional) diff --git a/tests/run/main-annotation-override-usage-1.scala b/tests/run/main-annotation-override-usage-1.scala deleted file mode 100644 index 62305ec1729d..000000000000 --- a/tests/run/main-annotation-override-usage-1.scala +++ /dev/null @@ -1,28 +0,0 @@ -class myMain extends main: - override def usage(commandName: String, args: Seq[Argument]): Unit = - val argInfos = args map ( - _ match { - case s: SimpleArgument => s.name - case o: OptionalArgument[?] => s"${o.name}?" - case v: VarArgument => s"${v.name}*" - } - ) - println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") - -object myProgram: - - /** Adds two numbers */ - @myMain def add(num: Int, inc: Int = 1): Unit = - println(s"$num + $inc = ${num + inc}") - -end myProgram - -object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("add") - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, args) - - def main(args: Array[String]): Unit = - callMain(Array("--help")) -end Test diff --git a/tests/run/main-annotation-override-usage-2.check b/tests/run/main-annotation-override-usage-2.check deleted file mode 100644 index 9a27bad4f357..000000000000 --- a/tests/run/main-annotation-override-usage-2.check +++ /dev/null @@ -1,6 +0,0 @@ -My shiny command works like this: add num inc* - -Adds numbers -Arguments: - num - Int - inc - Int diff --git a/tests/run/main-annotation-override-usage-2.scala b/tests/run/main-annotation-override-usage-2.scala deleted file mode 100644 index e9eed8715b2c..000000000000 --- a/tests/run/main-annotation-override-usage-2.scala +++ /dev/null @@ -1,28 +0,0 @@ -class myMain extends main: - override def usage(commandName: String, args: Seq[Argument]): Unit = - val argInfos = args map ( - _ match { - case s: SimpleArgument => s.name - case o: OptionalArgument[?] => s"${o.name}?" - case v: VarArgument => s"${v.name}*" - } - ) - println(s"My shiny command works like this: $commandName ${argInfos.mkString(" ")}") - -object myProgram: - - /** Adds numbers */ - @myMain def add(num: Int, inc: Int*): Unit = - println(s"$num + ${inc.mkString(" + ")} = ${num + inc.sum}") - -end myProgram - -object Test: - def callMain(args: Array[String]): Unit = - val clazz = Class.forName("add") - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, args) - - def main(args: Array[String]): Unit = - callMain(Array("--help")) -end Test From 88a76dfda766c4b93064da86d4feb666a34c3a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 14 Nov 2021 00:35:33 +0100 Subject: [PATCH 066/121] Factorize code of main - Now that main is final, the code can be factorized --- library/src/scala/main.scala | 150 +++++++++++++++++------------------ 1 file changed, 71 insertions(+), 79 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 15ccfd4530b2..7ed1f0efbb4d 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -17,83 +17,19 @@ final class main extends scala.annotation.MainAnnotation: self => import main._ - protected sealed abstract trait Argument { - def name: String - def typeName: String - def doc: String - - def usage: String - } - protected class SimpleArgument(val name: String, val typeName: String, val doc: String) extends Argument: - override def usage: String = s"[--$name] <$name>" - protected class OptionalArgument[T](val name: String, val typeName: String, val doc: String, val defaultValue: T) - extends Argument: - override def usage: String = s"[[--$name] <$name>]" - protected class VarArgument(val name: String, val typeName: String, val doc: String) extends Argument: - override def usage: String = s"[<$name> [<$name> [...]]]" - override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - /** Prints the main function's usage */ - def usage(commandName: String, args: Seq[Argument]): Unit = - def wrappedArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = - def recurse(args: List[String], currentLine: String, acc: Vector[String]): Vector[String] = - (args, currentLine) match { - case (Nil, "") => acc - case (Nil, l) => (acc :+ l) - case (arg :: t, "") => recurse(t, arg, acc) - case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg :: t, l) => recurse(t, arg, acc :+ l) - } - - recurse(argsUsage, "", Vector()).toList - end wrappedArgumentUsages - - val maxLineLength = 120 // TODO as parameter? As global Dotty parameter? - - val usageBeginning = s"Usage: $commandName " - val argsOffset = usageBeginning.length - val argUsages = wrappedArgumentUsages(args.map(_.usage).toList, maxLineLength - argsOffset) - - println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) - - /** Prints an explanation about the function */ - def explain(commandName: String, commandDoc: String, args: Seq[Argument]): Unit = - if (commandDoc.nonEmpty) - println(commandDoc) - if (args.nonEmpty) { - val argNameShift = 2 - val argDocShift = argNameShift + 2 - - println("Arguments:") - for (arg <- args) - val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"${arg.name} - ${arg.typeName}") - - arg match { - case o: OptionalArgument[?] => argDoc.append(" (optional)") - case _ => - } - - if (arg.doc.nonEmpty) { - val shiftedDoc = arg.doc.split("\n").nn.map(" " * argDocShift + _).mkString("\n") - argDoc.append("\n").append(shiftedDoc) - } - - println(argDoc) - } - - /** Runs the command and handles its return value */ - def run(f: => MainResultType): Unit = - f match - case ExitCode(n) => System.exit(n) - case _ => + private enum ArgumentKind { + case SimpleArgument, OptionalArgument, VarArgument + } override def command(args: Array[String], commandName: String, docComment: String): Command = new Command: - /** A buffer of demanded arguments */ - private var argInfos = new mutable.ListBuffer[self.Argument] + private var argNames = new mutable.ListBuffer[String] + private var argTypes = new mutable.ListBuffer[String] + private var argDocs = new mutable.ListBuffer[String] + private var argKinds = new mutable.ListBuffer[ArgumentKind] /** A buffer for all errors */ private var errors = new mutable.ListBuffer[String] @@ -120,11 +56,60 @@ final class main extends scala.annotation.MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") + private def argUsage(pos: Int): String = + val name = argNames(pos) + + argKinds(pos) match { + case ArgumentKind.SimpleArgument => s"[--$name] <$name>" + case ArgumentKind.OptionalArgument => s"[[--$name] <$name>]" + case ArgumentKind.VarArgument => s"[<$name> [<$name> [...]]]" + } + private def usage(): Unit = - self.usage(commandName, argInfos.toSeq) + def wrappedArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = + def recurse(args: List[String], currentLine: String, acc: Vector[String]): Vector[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg :: t, "") => recurse(t, arg, acc) + case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg :: t, l) => recurse(t, arg, acc :+ l) + } + + recurse(argsUsage, "", Vector()).toList + end wrappedArgumentUsages + + val maxLineLength = 120 // TODO as parameter? As global Dotty parameter? + val usageBeginning = s"Usage: $commandName " + val argsOffset = usageBeginning.length + val argUsages = wrappedArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) + + println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) private def explain(): Unit = - self.explain(commandName, docComment, argInfos.toSeq) + if (docComment.nonEmpty) + println(docComment) + if (argNames.nonEmpty) { + val argNameShift = 2 + val argDocShift = argNameShift + 2 + + println("Arguments:") + for (pos <- 0 until argNames.length) + val argDoc = StringBuilder(" " * argNameShift) + argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") + + argKinds(pos) match { + case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") + case _ => + } + + if (argDocs(pos).nonEmpty) { + val shiftedDoc = argDocs(pos).split("\n").nn.map(" " * argDocShift + _).mkString("\n") + argDoc.append("\n").append(shiftedDoc) + } + + println(argDoc) + } private def indicesOfArg(argName: String): Seq[Int] = def allIndicesOf(s: String): Seq[Int] = @@ -150,16 +135,22 @@ final class main extends scala.annotation.MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } + private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = + argNames += argName + argTypes += argType + argDocs += argDoc + argKinds += argKind + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = - argInfos += self.SimpleArgument(argName, argType, argDoc) + registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) getArgGetter(argName, error(s"missing argument for $argName")) override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = - argInfos += self.OptionalArgument(argName, argType, argDoc, defaultValue) + registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) getArgGetter(argName, () => defaultValue) override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = - argInfos += self.VarArgument(argName, argType, argDoc) + registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() case None => Nil @@ -174,7 +165,7 @@ final class main extends scala.annotation.MainAnnotation: case None => for arg <- args - if arg.startsWith("--") && !argInfos.map(_.name).contains(arg.drop(2)) + if arg.startsWith("--") && !argNames.contains(arg.drop(2)) do error(s"unknown argument name: $arg") end flagUnused @@ -188,8 +179,9 @@ final class main extends scala.annotation.MainAnnotation: if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() - else - self.run(f) + else f match + case ExitCode(n) => sys.exit(n) + case _ => end run end command end main From f2bafb43d5c6e1a1755235347d8f4ad97803528e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 15 Nov 2021 15:31:28 +0100 Subject: [PATCH 067/121] Move Command out of MainAnnotation --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 10 +++++----- library/src/scala/annotation/MainAnnotation.scala | 6 ++++-- library/src/scala/main.scala | 6 +++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 275a46123535..cd714e592a1b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -855,11 +855,11 @@ class Definitions { @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.annotation.MainAnnotation") @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") - @tu lazy val MainAnnotCommand: ClassSymbol = MainAnnot.requiredClass("Command") - @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") - @tu lazy val MainAnnotCommand_argGetterDefault: Symbol = MainAnnotCommand.requiredMethod("argGetterDefault") - @tu lazy val MainAnnotCommand_argsGetter: Symbol = MainAnnotCommand.requiredMethod("argsGetter") - @tu lazy val MainAnnotCommand_run: Symbol = MainAnnotCommand.requiredMethod("run") + @tu lazy val MainAnnotCommand: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Command") + @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") + @tu lazy val MainAnnotCommand_argGetterDefault: Symbol = MainAnnotCommand.requiredMethod("argGetterDefault") + @tu lazy val MainAnnotCommand_argsGetter: Symbol = MainAnnotCommand.requiredMethod("argsGetter") + @tu lazy val MainAnnotCommand_run: Symbol = MainAnnotCommand.requiredMethod("run") @tu lazy val TupleTypeRef: TypeRef = requiredClassRef("scala.Tuple") def TupleClass(using Context): ClassSymbol = TupleTypeRef.symbol.asClass diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index db9e87c15894..af0e372b4e1a 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -21,10 +21,12 @@ trait MainAnnotation extends StaticAnnotation: type MainResultType /** A new command with arguments from `args` */ - def command(args: Array[String], commandName: String, docComment: String): Command + def command(args: Array[String], commandName: String, docComment: String): MainAnnotation.Command[ArgumentParser, MainResultType] +end MainAnnotation +object MainAnnotation: /** A class representing a command to run */ - trait Command: + trait Command[ArgumentParser[_], MainResultType]: /** The getter for the next argument of type `T` */ def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 7ed1f0efbb4d..29358d7149a2 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -9,7 +9,7 @@ package scala import collection.mutable - +import annotation.MainAnnotation /** An annotation that designates a main function */ @@ -24,8 +24,8 @@ final class main extends scala.annotation.MainAnnotation: case SimpleArgument, OptionalArgument, VarArgument } - override def command(args: Array[String], commandName: String, docComment: String): Command = - new Command: + override def command(args: Array[String], commandName: String, docComment: String) = + new MainAnnotation.Command[ArgumentParser, MainResultType]: private var argNames = new mutable.ListBuffer[String] private var argTypes = new mutable.ListBuffer[String] private var argDocs = new mutable.ListBuffer[String] From 3cd2a88e456a200d170c3370d24c726ffe1c3fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 15 Nov 2021 15:34:20 +0100 Subject: [PATCH 068/121] Make by-name parameter a function for clarity --- library/src/scala/main.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 29358d7149a2..3e376107e70a 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -122,13 +122,13 @@ final class main extends scala.annotation.MainAnnotation: val indices = allIndicesOf(s"--$argName") indices.filter(_ >= 0) - private def getArgGetter[T](argName: String, defaultGetter: => () => T)(using p: ArgumentParser[T]): () => T = + private def getArgGetter[T](argName: String, getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = indicesOfArg(argName) match { case s @ (Seq() | Seq(_)) => val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) argOpt match { case Some(arg) => convert(argName, arg, p) - case None => defaultGetter + case None => getDefaultGetter() } case s => val multValues = s.flatMap(idx => argAt(idx + 1)) @@ -143,11 +143,11 @@ final class main extends scala.annotation.MainAnnotation: override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) - getArgGetter(argName, error(s"missing argument for $argName")) + getArgGetter(argName, () => error(s"missing argument for $argName")) override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) - getArgGetter(argName, () => defaultValue) + getArgGetter(argName, () => () => defaultValue) override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) From c4a6ff0d645aaf9116bd1fcb5d0e198dc00cb13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 15 Nov 2021 15:49:30 +0100 Subject: [PATCH 069/121] Print returned value when it is not Unit --- library/src/scala/main.scala | 3 ++- tests/run/main-annotation-birthday.scala | 2 +- tests/run/main-annotation-return-type-1.check | 3 +-- tests/run/main-annotation-return-type-1.scala | 1 - tests/run/main-annotation-return-type-2.check | 3 +-- tests/run/main-annotation-return-type-2.scala | 1 - 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 3e376107e70a..100756e5b332 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -181,7 +181,8 @@ final class main extends scala.annotation.MainAnnotation: usage() else f match case ExitCode(n) => sys.exit(n) - case _ => + case () => + case res => println(res) end run end command end main diff --git a/tests/run/main-annotation-birthday.scala b/tests/run/main-annotation-birthday.scala index d38f145e7b1b..c726e91100a8 100644 --- a/tests/run/main-annotation-birthday.scala +++ b/tests/run/main-annotation-birthday.scala @@ -17,7 +17,7 @@ case _ => "th" val bldr = new StringBuilder(s"Happy $age$suffix birthday, $name") for other <- others do bldr.append(" and ").append(other) - println(bldr) + bldr.toString object Test: def callMain(args: Array[String]): Unit = diff --git a/tests/run/main-annotation-return-type-1.check b/tests/run/main-annotation-return-type-1.check index 0791928884c2..c00eba530a32 100644 --- a/tests/run/main-annotation-return-type-1.check +++ b/tests/run/main-annotation-return-type-1.check @@ -1,4 +1,3 @@ Direct call -2 + 3 = 5 Main call -2 + 3 = 5 +5 diff --git a/tests/run/main-annotation-return-type-1.scala b/tests/run/main-annotation-return-type-1.scala index 834947a3b345..4952f4d44516 100644 --- a/tests/run/main-annotation-return-type-1.scala +++ b/tests/run/main-annotation-return-type-1.scala @@ -3,7 +3,6 @@ object myProgram: /** Adds two numbers and returns them */ @main def add(num: Int, inc: Int): Int = - println(f"$num + $inc = ${num + inc}") num + inc end myProgram diff --git a/tests/run/main-annotation-return-type-2.check b/tests/run/main-annotation-return-type-2.check index 0791928884c2..c00eba530a32 100644 --- a/tests/run/main-annotation-return-type-2.check +++ b/tests/run/main-annotation-return-type-2.check @@ -1,4 +1,3 @@ Direct call -2 + 3 = 5 Main call -2 + 3 = 5 +5 diff --git a/tests/run/main-annotation-return-type-2.scala b/tests/run/main-annotation-return-type-2.scala index 2d6462ff8b9f..e1e0a7858070 100644 --- a/tests/run/main-annotation-return-type-2.scala +++ b/tests/run/main-annotation-return-type-2.scala @@ -6,7 +6,6 @@ object myProgram: /** Adds two numbers and returns them */ @main def add(num: Int, inc: Int): MyResult = - println(f"$num + $inc = ${num + inc}") MyResult(num + inc) end myProgram From 7b685c4bca52ba81ab416fdcad0fe054a9156251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 15 Nov 2021 16:14:05 +0100 Subject: [PATCH 070/121] Add more parser checks --- ...=> main-annotation-unknown-parser-1.scala} | 0 .../main-annotation-unknown-parser-2.scala | 26 +++++++++++++++++ ...> main-annotation-homemade-parser-1.check} | 0 ...> main-annotation-homemade-parser-1.scala} | 2 +- .../main-annotation-homemade-parser-2.check | 1 + .../main-annotation-homemade-parser-2.scala | 29 +++++++++++++++++++ .../main-annotation-homemade-parser-3.check | 1 + .../main-annotation-homemade-parser-3.scala | 25 ++++++++++++++++ 8 files changed, 83 insertions(+), 1 deletion(-) rename tests/neg/{main-annotation-unknown-parser.scala => main-annotation-unknown-parser-1.scala} (100%) create mode 100644 tests/neg/main-annotation-unknown-parser-2.scala rename tests/run/{main-annotation-homemade-parser.check => main-annotation-homemade-parser-1.check} (100%) rename tests/run/{main-annotation-homemade-parser.scala => main-annotation-homemade-parser-1.scala} (86%) create mode 100644 tests/run/main-annotation-homemade-parser-2.check create mode 100644 tests/run/main-annotation-homemade-parser-2.scala create mode 100644 tests/run/main-annotation-homemade-parser-3.check create mode 100644 tests/run/main-annotation-homemade-parser-3.scala diff --git a/tests/neg/main-annotation-unknown-parser.scala b/tests/neg/main-annotation-unknown-parser-1.scala similarity index 100% rename from tests/neg/main-annotation-unknown-parser.scala rename to tests/neg/main-annotation-unknown-parser-1.scala diff --git a/tests/neg/main-annotation-unknown-parser-2.scala b/tests/neg/main-annotation-unknown-parser-2.scala new file mode 100644 index 000000000000..e1df3e16ab9a --- /dev/null +++ b/tests/neg/main-annotation-unknown-parser-2.scala @@ -0,0 +1,26 @@ +import scala.util.CommandLineParser.FromString + +object myProgram: + + @main def add(num: Test.MyNumber, inc: Test.MyNumber): Unit = // error + val numV = Test.value(num) + val incV = Test.value(inc) + println(s"$numV + $incV = ${numV + incV}") + +end myProgram + + +object Test: + opaque type MyNumber = Int + + def create(n: Int): MyNumber = n + def value(n: MyNumber): Int = n + + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-homemade-parser.check b/tests/run/main-annotation-homemade-parser-1.check similarity index 100% rename from tests/run/main-annotation-homemade-parser.check rename to tests/run/main-annotation-homemade-parser-1.check diff --git a/tests/run/main-annotation-homemade-parser.scala b/tests/run/main-annotation-homemade-parser-1.scala similarity index 86% rename from tests/run/main-annotation-homemade-parser.scala rename to tests/run/main-annotation-homemade-parser-1.scala index 5fea7070c870..c4d55e879c6d 100644 --- a/tests/run/main-annotation-homemade-parser.scala +++ b/tests/run/main-annotation-homemade-parser-1.scala @@ -5,7 +5,7 @@ class MyNumber(val value: Int) { } given FromString[MyNumber] with - def fromString(s: String): MyNumber = MyNumber(summon[FromString[Int]].fromString(s)) + override def fromString(s: String): MyNumber = MyNumber(summon[FromString[Int]].fromString(s)) object myProgram: diff --git a/tests/run/main-annotation-homemade-parser-2.check b/tests/run/main-annotation-homemade-parser-2.check new file mode 100644 index 000000000000..56acd5c7a7ab --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-2.check @@ -0,0 +1 @@ +2 + 3 = 5 diff --git a/tests/run/main-annotation-homemade-parser-2.scala b/tests/run/main-annotation-homemade-parser-2.scala new file mode 100644 index 000000000000..c79b598bcf86 --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-2.scala @@ -0,0 +1,29 @@ +import scala.util.CommandLineParser.FromString + +given FromString[Test.MyNumber] with + override def fromString(s: String) = Test.create(summon[FromString[Int]].fromString(s)) + +object myProgram: + + @main def add(num: Test.MyNumber, inc: Test.MyNumber): Unit = + val numV = Test.value(num) + val incV = Test.value(inc) + println(s"$numV + $incV = ${numV + incV}") + +end myProgram + + +object Test: + opaque type MyNumber = Int + + def create(n: Int): MyNumber = n + def value(n: MyNumber): Int = n + + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("2", "3")) +end Test diff --git a/tests/run/main-annotation-homemade-parser-3.check b/tests/run/main-annotation-homemade-parser-3.check new file mode 100644 index 000000000000..8a34cb5bfa7a --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-3.check @@ -0,0 +1 @@ +44 + 45 = 89 diff --git a/tests/run/main-annotation-homemade-parser-3.scala b/tests/run/main-annotation-homemade-parser-3.scala new file mode 100644 index 000000000000..d74588c227f5 --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-3.scala @@ -0,0 +1,25 @@ +import scala.util.CommandLineParser.FromString + +given FromString[Int] with + override def fromString(s: String) = s.toInt + 42 + +object myProgram: + + given FromString[Int] with + override def fromString(s: String) = -1 * s.toInt // Should be ignored, because not top-level + + @main def add(num: Int, inc: Int): Unit = + println(s"$num + $inc = ${num + inc}") + +end myProgram + + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("2", "3")) +end Test From a6e9da9c60296b3c31cb1222edabd2f79e1bd62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 16 Nov 2021 12:00:56 +0100 Subject: [PATCH 071/121] Make usage clearer by printing type --- library/src/scala/main.scala | 17 ++++---- tests/run/main-annotation-help.check | 40 +++++++++---------- tests/run/main-annotation-named-params.check | 4 +- tests/run/main-annotation-wrong-param-1.check | 8 ++-- .../main-annotation-wrong-param-names.check | 10 ++--- .../main-annotation-wrong-param-number.check | 8 ++-- .../main-annotation-wrong-param-type.check | 12 +++--- 7 files changed, 50 insertions(+), 49 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 100756e5b332..6230000358f2 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -26,13 +26,13 @@ final class main extends scala.annotation.MainAnnotation: override def command(args: Array[String], commandName: String, docComment: String) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - private var argNames = new mutable.ListBuffer[String] - private var argTypes = new mutable.ListBuffer[String] - private var argDocs = new mutable.ListBuffer[String] - private var argKinds = new mutable.ListBuffer[ArgumentKind] + private var argNames = new mutable.ArrayBuffer[String] + private var argTypes = new mutable.ArrayBuffer[String] + private var argDocs = new mutable.ArrayBuffer[String] + private var argKinds = new mutable.ArrayBuffer[ArgumentKind] /** A buffer for all errors */ - private var errors = new mutable.ListBuffer[String] + private var errors = new mutable.ArrayBuffer[String] /** Issue an error, and return an uncallable getter */ private def error(msg: String): () => Nothing = @@ -60,9 +60,9 @@ final class main extends scala.annotation.MainAnnotation: val name = argNames(pos) argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"[--$name] <$name>" - case ArgumentKind.OptionalArgument => s"[[--$name] <$name>]" - case ArgumentKind.VarArgument => s"[<$name> [<$name> [...]]]" + case ArgumentKind.SimpleArgument => s"[--$name] <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[[--$name] <${argTypes(pos)}>]" + case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" } private def usage(): Unit = @@ -100,6 +100,7 @@ final class main extends scala.annotation.MainAnnotation: argKinds(pos) match { case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") + case ArgumentKind.VarArgument => argDoc.append(" (vararg)") case _ => } diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index cdb1936fff14..4b1b80a3c5f0 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -1,40 +1,40 @@ -Usage: doc1 [--num] [--inc] +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc1 [--num] [--inc] +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc1 [--num] [--inc] +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc1 [--num] [--inc] +Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc2 [--num] [--inc] +Usage: doc2 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc3 [--num] [--inc] +Usage: doc3 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc4 [--num] [[--inc] ] +Usage: doc4 [--num] [[--inc] ] Adds two numbers. Arguments: @@ -42,20 +42,20 @@ Arguments: the first number inc - Int (optional) the second number -Usage: doc5 [--num] [--inc] +Usage: doc5 [--num] [--inc] Adds two numbers. Arguments: num - Int the first number inc - Int -Usage: doc6 [--num] [--inc] +Usage: doc6 [--num] [--inc] Adds two numbers. Arguments: num - Int inc - Int -Usage: doc7 [--num] [--inc] +Usage: doc7 [--num] [--inc] Adds two numbers. Arguments: @@ -63,7 +63,7 @@ Arguments: the first number inc - Int the second number -Usage: doc8 [--num] [--inc] +Usage: doc8 [--num] [--inc] Adds two numbers. Arguments: @@ -71,7 +71,7 @@ Arguments: the first number inc - Int the second number -Usage: doc9 [--num] [--inc] +Usage: doc9 [--num] [--inc] Adds two numbers. Same as doc1. Arguments: @@ -79,7 +79,7 @@ Arguments: the first number inc - Int the second number -Usage: doc10 [--num] [--inc] +Usage: doc10 [--num] [--inc] Adds two numbers. This should be on another line. @@ -89,7 +89,7 @@ Arguments: I might have to write this on two lines inc - Int I might even have to write this one on three lines -Usage: doc11 [--num] [--inc] +Usage: doc11 [--num] [--inc] Adds two numbers. Arguments: @@ -99,22 +99,22 @@ Arguments: inc - Int the second number And another one! -Usage: doc12 [--num] [--inc] +Usage: doc12 [--num] [--inc] Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. Arguments: num - Int inc - Int -Usage: doc13 [--num] [--inc] +Usage: doc13 [--num] [--inc] Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. Arguments: num - Int inc - Int -Usage: doc14 [--arg1] [--arg2] [--arg3] [--arg4] [--arg5] [--arg6] - [--arg7] [--arg8] [[--arg9] ] [[--arg10] ] [[--arg11] ] - [[--arg12] ] [[--arg13] ] [[--arg14] ] [[--arg15] ] [ [ [...]]] +Usage: doc14 [--arg1] [--arg2] [--arg3] [--arg4] [--arg5] [--arg6] + [--arg7] [--arg8] [[--arg9] ] [[--arg10] ] [[--arg11] ] + [[--arg12] ] [[--arg13] ] [[--arg14] ] [[--arg15] ] [ [ [...]]] Loudly judges the number of argument you gave to this poor function. Arguments: @@ -133,4 +133,4 @@ Arguments: arg13 - String (optional) arg14 - Int (optional) arg15 - String (optional) - arg16 - Int + arg16 - Int (vararg) diff --git a/tests/run/main-annotation-named-params.check b/tests/run/main-annotation-named-params.check index 6cf394ac9f1c..743f61642a0c 100644 --- a/tests/run/main-annotation-named-params.check +++ b/tests/run/main-annotation-named-params.check @@ -3,7 +3,7 @@ 2 + 3 = 5 2 + 3 = 5 Error: more than one value for num: 2, 1 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: more than one value for num: 2, 1 Error: more than one value for inc: 1, 3 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-1.check b/tests/run/main-annotation-wrong-param-1.check index a80dd390189f..a51e31141d4f 100644 --- a/tests/run/main-annotation-wrong-param-1.check +++ b/tests/run/main-annotation-wrong-param-1.check @@ -1,13 +1,13 @@ Error: invalid argument for inc: true Error: unused argument: SPAAAAACE -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: add Error: unused argument: 3 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: true Error: invalid argument for inc: false Error: unused argument: 10 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: binary Error: unused argument: 01 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check index 7b161c33ce5d..c8e2330bc99d 100644 --- a/tests/run/main-annotation-wrong-param-names.check +++ b/tests/run/main-annotation-wrong-param-names.check @@ -1,19 +1,19 @@ Error: invalid argument for num: -n Error: unused argument: -i Error: unused argument: 10 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: missing argument for num Error: missing argument for inc Error: unknown argument name: --n Error: unknown argument name: --i -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: -num Error: unused argument: 1 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for inc: -inc Error: unused argument: 10 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: num Error: unused argument: inc Error: unused argument: 10 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-number.check b/tests/run/main-annotation-wrong-param-number.check index 0c354667adff..177b4e0069be 100644 --- a/tests/run/main-annotation-wrong-param-number.check +++ b/tests/run/main-annotation-wrong-param-number.check @@ -1,10 +1,10 @@ Error: missing argument for num Error: missing argument for inc -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: missing argument for inc -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: unused argument: 3 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: unused argument: 3 Error: unused argument: 4 Error: unused argument: 5 @@ -13,4 +13,4 @@ Error: unused argument: 7 Error: unused argument: 8 Error: unused argument: 9 Error: unused argument: 10 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-type.check b/tests/run/main-annotation-wrong-param-type.check index a5bb0a80be2d..db8f007cc823 100644 --- a/tests/run/main-annotation-wrong-param-type.check +++ b/tests/run/main-annotation-wrong-param-type.check @@ -1,14 +1,14 @@ Error: invalid argument for inc: true -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: 2.1 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for inc: 3.1415921535 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: 192.168.1.1 -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: false Error: invalid argument for inc: true -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] Error: invalid argument for num: Hello Error: invalid argument for inc: world! -Usage: add [--num] [--inc] +Usage: add [--num] [--inc] From 5065a097aba420e6ee454411f1b10d3b737f073c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 16 Nov 2021 12:02:10 +0100 Subject: [PATCH 072/121] Add help tests with homemade classes --- tests/run/main-annotation-help.check | 16 +++++++++++ tests/run/main-annotation-help.scala | 41 +++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index 4b1b80a3c5f0..b0f079faca01 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -134,3 +134,19 @@ Arguments: arg14 - Int (optional) arg15 - String (optional) arg16 - Int (vararg) +Usage: doc15 [--myNum] [--myInc] + +Adds two instances of MyNumber. +Arguments: + myNum - MyNumber + my first number to add + myInc - MyNumber + my second number to add +Usage: doc16 [--first] [--second] + +Compares two instances of MyGeneric. +Arguments: + first - MyGeneric[Int] + my first element + second - MyGeneric[Int] + my second element diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index f7e64be771bb..ddae710ec0e8 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -1,4 +1,17 @@ -// Sample main method +import scala.util.CommandLineParser.FromString +import scala.util.Try + +class MyNumber(val value: Int): + def +(other: MyNumber): MyNumber = MyNumber(value + other.value) + +class MyGeneric[T](val value: T) + +given FromString[MyNumber] with + override def fromString(s: String): MyNumber = MyNumber(summon[FromString[Int]].fromString(s)) + +given FromString[MyGeneric[Int]] with + override def fromString(s: String): MyGeneric[Int] = MyGeneric(summon[FromString[Int]].fromString(s)) + object myProgram: /** @@ -131,6 +144,25 @@ object myProgram: ): Unit = println(s"Wow, now that's a lot of arguments") + /** + * Adds two instances of {{MyNumber}}. + * @param myNum my first number to add + * @param myInc my second number to add + */ + @main def doc15(myNum: MyNumber, myInc: MyNumber): Unit = + println(s"$myNum + $myInc = ${myNum + myInc}") + + /** + * Compares two instances of {{MyGeneric}}. + * @param first my first element + * @param second my second element + */ + @main def doc16(first: MyGeneric[Int], second: MyGeneric[Int]): Unit = + if first.value == second.value then + println("Equal!") + else + println("Not equal") + end myProgram object Test: @@ -139,10 +171,11 @@ object Test: val method = clazz.getMethod("main", classOf[Array[String]]) method.invoke(null, args) + val allClazzes: Seq[Class[?]] = + LazyList.from(1).map(i => Try(Class.forName("doc" + i.toString))).takeWhile(_.isSuccess).map(_.get) + def callAllMains(args: Array[String]): Unit = - val numberOfMains = 14 - for (i <- 1 to numberOfMains) { - val clazz = Class.forName("doc" + i.toString) + for (clazz <- allClazzes) { val method = clazz.getMethod("main", classOf[Array[String]]) method.invoke(null, args) } From 63f30f8a5d92eae870f102daad09e092e50fd68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 16 Nov 2021 13:22:13 +0100 Subject: [PATCH 073/121] Simplify main-annotation-help.scala --- tests/run/main-annotation-help.scala | 51 +++++++++------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index ddae710ec0e8..b91d01f5b0d1 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -17,18 +17,15 @@ object myProgram: /** * Adds two numbers. */ - @main def doc1(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc1(num: Int, inc: Int): Unit = () /** Adds two numbers. */ - @main def doc2(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc2(num: Int, inc: Int): Unit = () /** * Adds two numbers. */ - @main def doc3(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc3(num: Int, inc: Int): Unit = () /** * Adds two numbers. @@ -36,16 +33,14 @@ object myProgram: * @param num the first number * @param inc the second number */ - @main def doc4(num: Int, inc: Int = 1): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc4(num: Int, inc: Int = 1): Unit = () /** * Adds two numbers. * * @param num the first number */ - @main def doc5(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc5(num: Int, inc: Int): Unit = () /** * Adds two numbers. @@ -53,8 +48,7 @@ object myProgram: * @param num * @param inc */ - @main def doc6(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc6(num: Int, inc: Int): Unit = () /** * Adds two numbers. @@ -63,8 +57,7 @@ object myProgram: * @param inc the second number * @return the sum of the two numbers (not really) */ - @main def doc7(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc7(num: Int, inc: Int): Unit = () /** * Adds two numbers. @@ -73,8 +66,7 @@ object myProgram: * @param inc the second number * @return the sum of the two numbers (not really) */ - @main def doc8(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc8(num: Int, inc: Int): Unit = () /** * Adds two numbers. Same as {{doc1}}. @@ -84,8 +76,7 @@ object myProgram: * @return the sum of the two numbers (not really) * @see {{doc1}} */ - @main def doc9(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc9(num: Int, inc: Int): Unit = () /** * Adds two numbers. @@ -104,8 +95,7 @@ object myProgram: * have to write this one * on three lines */ - @main def doc10(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc10(num: Int, inc: Int): Unit = () /** * Adds two numbers. @@ -118,20 +108,17 @@ object myProgram: * * And another one! */ - @main def doc11(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc11(num: Int, inc: Int): Unit = () /** * Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. */ - @main def doc12(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc12(num: Int, inc: Int): Unit = () /** * Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. */ - @main def doc13(num: Int, inc: Int): Unit = - println(s"$num + $inc = ${num + inc}") + @main def doc13(num: Int, inc: Int): Unit = () /** * Loudly judges the number of argument you gave to this poor function. @@ -141,27 +128,21 @@ object myProgram: arg5: String, arg6: Int, arg7: String, arg8: Int, arg9: String = "I", arg10: Int = 42, arg11: String = "used", arg12: Int = 0, arg13: String = "to", arg14: Int = 34, arg15: String = "wonder", arg16: Int* - ): Unit = - println(s"Wow, now that's a lot of arguments") + ): Unit = () /** * Adds two instances of {{MyNumber}}. * @param myNum my first number to add * @param myInc my second number to add */ - @main def doc15(myNum: MyNumber, myInc: MyNumber): Unit = - println(s"$myNum + $myInc = ${myNum + myInc}") + @main def doc15(myNum: MyNumber, myInc: MyNumber): Unit = () /** * Compares two instances of {{MyGeneric}}. * @param first my first element * @param second my second element */ - @main def doc16(first: MyGeneric[Int], second: MyGeneric[Int]): Unit = - if first.value == second.value then - println("Equal!") - else - println("Not equal") + @main def doc16(first: MyGeneric[Int], second: MyGeneric[Int]): Unit = () end myProgram From de73f3c8508fb667ff32d2e5af7da6d844d73e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 16 Nov 2021 15:57:34 +0100 Subject: [PATCH 074/121] Update MiMa filters --- project/MiMaFilters.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index f368d2657bba..03cb6fcb1944 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -4,8 +4,10 @@ import com.typesafe.tools.mima.core._ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( ProblemFilters.exclude[MissingTypesProblem]("scala.main"), + ProblemFilters.exclude[FinalClassProblem]("scala.main"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.SimpleArgument"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.OptionalArgument"), From 2cad3dc5d89149f709847f058f10aed8aa84efbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 21 Nov 2021 19:25:20 +0100 Subject: [PATCH 075/121] Add test for docstring starting with // --- tests/run/main-annotation-help.check | 6 ++++++ tests/run/main-annotation-help.scala | 3 +++ 2 files changed, 9 insertions(+) diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index b0f079faca01..7b70ba1fcbe7 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -150,3 +150,9 @@ Arguments: my first element second - MyGeneric[Int] my second element +Usage: doc17 [--a] [--b] [--c] + +Arguments: + a - Int + b - Int + c - String diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index b91d01f5b0d1..91ded60de24d 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -144,6 +144,9 @@ object myProgram: */ @main def doc16(first: MyGeneric[Int], second: MyGeneric[Int]): Unit = () + // This should not be printed in explain! + @main def doc17(a: Int, b: Int, c: String): Unit = () + end myProgram object Test: From d32868ac718def23edb10680ae42f11b0483b6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 21 Nov 2021 20:02:44 +0100 Subject: [PATCH 076/121] Pass doc to command; main parameter for line length - Instead of passing arg docs to the args, pass the whole docstring to the command function and parse it on run - Add parameter in main to control max line width when printing --- .../dotty/tools/dotc/ast/MainProxies.scala | 75 +----- .../src/scala/annotation/MainAnnotation.scala | 6 +- library/src/scala/main.scala | 236 +++++++++++++++--- .../run/main-annotation-max-line-length.check | 40 +++ .../run/main-annotation-max-line-length.scala | 42 ++++ 5 files changed, 295 insertions(+), 104 deletions(-) create mode 100644 tests/run/main-annotation-max-line-length.check create mode 100644 tests/run/main-annotation-max-line-length.scala diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index ff492a60a386..04cd25a79d39 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -19,13 +19,14 @@ import NameKinds.DefaultGetterName * * @param x my param x * * @param ys all my params y * */ - * @main def f(x: S, ys: T*) = ... + * @main(80) def f(x: S, ys: T*) = ... * * would be translated to something like * * final class f { * @static def main(args: Array[String]): Unit = - * val cmd: main#Command = (new scala.main).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") + * val cmd: MainAnnotation#Command[..., ...] = + * (new scala.main(80)).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") * val arg1: () => S = cmd.argGetter[S]("x", "S", "my param x") * val arg2: () => Seq[T] = cmd.argsGetter[T]("ys", "T", "all my params y") * cmd.run(f(arg1(), arg2()*)) @@ -76,8 +77,6 @@ object MainProxies { val mainArgsName: TermName = nme.args val cmdName: TermName = Names.termName("cmd") - val documentation = new Documentation(docComment) - inline def lit(any: Any): Literal = Literal(Constant(any)) def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = @@ -113,7 +112,6 @@ object MainProxies { lit(param) :: lit(formalType.show) - :: lit(documentation.argDocs(param)) :: optionDefaultValueTree.map(List(_)).getOrElse(Nil) } @@ -138,12 +136,18 @@ object MainProxies { if (!mainFun.owner.isStaticOwner) report.error(s"main method is not statically accessible", pos) else { + val mainAnnotArgs = mainAnnot.tree match { + case Apply(_, namedArgs) => namedArgs.filter { + case Select(_, name) => !name.is(DefaultGetterName) // Remove default arguments of main + case _ => true + } + } val cmd = ValDef( cmdName, TypeTree(), Apply( - Select(makeNew(TypeTree(mainAnnot.symbol.typeRef)), defn.MainAnnot_command.name), - Ident(mainArgsName) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil + Select(New(TypeTree(mainAnnot.symbol.typeRef), List(mainAnnotArgs)), defn.MainAnnot_command.name), + Ident(mainArgsName) :: lit(mainFun.showName) :: lit(docComment.map(_.raw).getOrElse("")) :: Nil ) ) var args: List[ValDef] = Nil @@ -186,61 +190,4 @@ object MainProxies { } result } - - private class Documentation(docComment: Option[Comment], val maxLineLength: Int = 120): - import util.CommentParsing._ - - /** The main part of the documentation. */ - lazy val mainDoc: String = _mainDoc - /** The parameters identified by @param. Maps from param name to documentation. */ - lazy val argDocs: Map[String, String] = _argDocs - - private var _mainDoc: String = "" - private var _argDocs: Map[String, String] = Map().withDefaultValue("") - - docComment match { - case Some(comment) => if comment.isDocComment then parseDocComment(comment.raw) else _mainDoc = comment.raw - case None => - } - - private def wrapLongLines(s: String): String = - def wrapLongLine(line: String): List[String] = - val lastSpace = line.trim.lastIndexOf(' ', maxLineLength) - if ((line.length <= maxLineLength) || (lastSpace < 0)) - List(line) - else { - val (shortLine, rest) = line.splitAt(lastSpace) - shortLine :: wrapLongLine(rest.trim) - } - - val wrappedLines = s.split('\n').flatMap(wrapLongLine) - wrappedLines.mkString("\n") - - private def cleanComment(raw: String): String = - var lines: Seq[String] = raw.trim.split('\n').toSeq - lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).trim) - var s = lines.foldLeft("") { - case ("", s2) => s2 - case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines - case (s1, "") => s1 + '\n' - case (s1, s2) if s1.last == '\n' => s1 + s2 - case (s1, s2) => s1 + ' ' + s2 - } - s.replaceAll(raw"\{\{", "").replaceAll(raw"\}\}", "").trim - - private def parseDocComment(raw: String): Unit = - // Positions of the sections (@) in the docstring - val tidx: List[(Int, Int)] = tagIndex(raw) - - // Parse main comment - var mainComment: String = raw.substring(skipLineLead(raw, 0), startTag(raw, tidx)) - mainComment = cleanComment(mainComment) - _mainDoc = wrapLongLines(mainComment) - - // Parse arguments comments - val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(raw, "@param", tidx) - val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(raw, _)) - val argsCommentsTexts = argsCommentsTextSpans.mapValues({ case (beg, end) => raw.substring(beg, end) }) - _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).mapValues(wrapLongLines(_)).toMap.withDefaultValue("") - end Documentation } diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index af0e372b4e1a..170f38cc3d1c 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -29,13 +29,13 @@ object MainAnnotation: trait Command[ArgumentParser[_], MainResultType]: /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T + def argGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, argType: String, defaultValue: => T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => Seq[T] + def argsGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 6230000358f2..19e472c24313 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -13,7 +13,7 @@ import annotation.MainAnnotation /** An annotation that designates a main function */ -final class main extends scala.annotation.MainAnnotation: +final class main(maxLineLength: Int = 120) extends scala.annotation.MainAnnotation: self => import main._ @@ -28,9 +28,10 @@ final class main extends scala.annotation.MainAnnotation: new MainAnnotation.Command[ArgumentParser, MainResultType]: private var argNames = new mutable.ArrayBuffer[String] private var argTypes = new mutable.ArrayBuffer[String] - private var argDocs = new mutable.ArrayBuffer[String] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] + private var documentation = new Documentation(docComment, maxLineLength) + /** A buffer for all errors */ private var errors = new mutable.ArrayBuffer[String] @@ -66,50 +67,40 @@ final class main extends scala.annotation.MainAnnotation: } private def usage(): Unit = - def wrappedArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = - def recurse(args: List[String], currentLine: String, acc: Vector[String]): Vector[String] = - (args, currentLine) match { - case (Nil, "") => acc - case (Nil, l) => (acc :+ l) - case (arg :: t, "") => recurse(t, arg, acc) - case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg :: t, l) => recurse(t, arg, acc :+ l) - } - - recurse(argsUsage, "", Vector()).toList - end wrappedArgumentUsages - - val maxLineLength = 120 // TODO as parameter? As global Dotty parameter? val usageBeginning = s"Usage: $commandName " val argsOffset = usageBeginning.length - val argUsages = wrappedArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) + val argUsages = documentation.wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) private def explain(): Unit = - if (docComment.nonEmpty) - println(docComment) + if (documentation.mainDoc.nonEmpty) + println(documentation.wrapLongLine(documentation.mainDoc, maxLineLength).mkString("\n")) if (argNames.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 println("Arguments:") for (pos <- 0 until argNames.length) - val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") + val argDocBuilder = StringBuilder(" " * argNameShift) + argDocBuilder.append(s"${argNames(pos)} - ${argTypes(pos)}") argKinds(pos) match { - case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") - case ArgumentKind.VarArgument => argDoc.append(" (vararg)") + case ArgumentKind.OptionalArgument => argDocBuilder.append(" (optional)") + case ArgumentKind.VarArgument => argDocBuilder.append(" (vararg)") case _ => } - if (argDocs(pos).nonEmpty) { - val shiftedDoc = argDocs(pos).split("\n").nn.map(" " * argDocShift + _).mkString("\n") - argDoc.append("\n").append(shiftedDoc) + val argDoc = documentation.argDocs(argNames(pos)) + if (argDoc.nonEmpty) { + val formatedDocLines = + argDoc + .split("\n").nn + .map(line => documentation.wrapLongLine(line.nn, maxLineLength - argDocShift).map(" " * argDocShift + _).mkString("\n")) + argDocBuilder.append("\n").append(formatedDocLines.mkString("\n")) } - println(argDoc) + println(argDocBuilder) } private def indicesOfArg(argName: String): Seq[Int] = @@ -136,22 +127,21 @@ final class main extends scala.annotation.MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = + private def registerArg(argName: String, argType: String, argKind: ArgumentKind): Unit = argNames += argName argTypes += argType - argDocs += argDoc argKinds += argKind - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) + override def argGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, ArgumentKind.SimpleArgument) getArgGetter(argName, () => error(s"missing argument for $argName")) - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) + override def argGetterDefault[T](argName: String, argType: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, ArgumentKind.OptionalArgument) getArgGetter(argName, () => () => defaultValue) - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) + override def argsGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(argName, argType, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() case None => Nil @@ -183,11 +173,183 @@ final class main extends scala.annotation.MainAnnotation: else f match case ExitCode(n) => sys.exit(n) case () => - case res => println(res) + case res => + val wrappedLines = res.toString.split("\n").nn.map(line => documentation.wrapLongLine(line.nn, maxLineLength).mkString("\n")) + println(wrappedLines.mkString("\n")) end run end command end main object main: case class ExitCode(code: Int) -end main \ No newline at end of file +end main + +/* + * Most of this class' helper methods come from {{dotty.tools.dotc.util.Chars}} + * and {{dotty.tools.dotc.util.CommentParsing}}. + */ +private class Documentation(docComment: String, maxLineLength: Int): + /** The main part of the documentation. */ + def mainDoc: String = _mainDoc + /** The parameters identified by @param. Maps from param name to documentation. */ + def argDocs: Map[String, String] = _argDocs + + def wrapLongLine(line: String, maxLength: Int): List[String] = { + def recurse(s: String, acc: Vector[String]): Seq[String] = + val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) + if ((s.length <= maxLength) || (lastSpace < 0)) + acc :+ s + else { + val (shortLine, rest) = s.splitAt(lastSpace) + recurse(rest.trim.nn, acc :+ shortLine) + } + + recurse(line, Vector()).toList + } + + def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { + def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg :: t, "") => recurse(t, arg, acc) + case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg :: t, l) => recurse(t, arg, acc :+ l) + } + + recurse(argsUsage, "", Vector()).toList + } + + private var _mainDoc: String = "" + private var _argDocs: Map[String, String] = Map().withDefaultValue("") + + docComment.trim match { + case "" => + case doc => parseDocComment(doc.nn) + } + + private def isWhitespace(c: Char): Boolean = + c == ' ' || c == '\t' || c == '\u000D' + + private def isIdentifierPart(c: Char): Boolean = + (c == '$') || java.lang.Character.isUnicodeIdentifierPart(c) + + private def skipWhitespace(str: String, start: Int): Int = + if (start < str.length && isWhitespace(str charAt start)) skipWhitespace(str, start + 1) + else start + + private def skipIdent(str: String, start: Int): Int = + if (start < str.length && isIdentifierPart(str charAt start)) skipIdent(str, start + 1) + else start + + private def skipTag(str: String, start: Int): Int = + if (start < str.length && (str charAt start) == '@') skipIdent(str, start + 1) + else start + + private def skipLineLead(str: String, start: Int): Int = + if (start == str.length) start + else { + val idx = skipWhitespace(str, start + 1) + if (idx < str.length && (str charAt idx) == '*') skipWhitespace(str, idx + 1) + else if (idx + 2 < str.length && (str charAt idx) == '/' && (str charAt (idx + 1)) == '*' && (str charAt (idx + 2)) == '*') + skipWhitespace(str, idx + 3) + else idx + } + + private def startTag(str: String, sections: List[(Int, Int)]): Int = sections match { + case Nil => str.length - 2 + case (start, _) :: _ => start + } + + private def skipToEol(str: String, start: Int): Int = + if (start + 2 < str.length && (str charAt start) == '/' && (str charAt (start + 1)) == '*' && (str charAt (start + 2)) == '*') start + 3 + else if (start < str.length && (str charAt start) != '\n') skipToEol(str, start + 1) + else start + + private def findNext(str: String, start: Int)(p: Int => Boolean): Int = { + val idx = skipLineLead(str, skipToEol(str, start)) + if (idx < str.length && !p(idx)) findNext(str, idx)(p) + else idx + } + + private def findAll(str: String, start: Int)(p: Int => Boolean): List[Int] = { + val idx = findNext(str, start)(p) + if (idx == str.length) List() + else idx :: findAll(str, idx)(p) + } + + private def tagIndex(str: String, p: Int => Boolean = (idx => true)): List[(Int, Int)] = { + var indices = findAll(str, 0) (idx => str(idx) == '@' && p(idx)) + indices = mergeUsecaseSections(str, indices) + indices = mergeInheritdocSections(str, indices) + + indices match { + case List() => List() + case idxs => idxs zip (idxs.tail ::: List(str.length - 2)) + } + } + + private def mergeUsecaseSections(str: String, idxs: List[Int]): List[Int] = + idxs.indexWhere(str.startsWith("@usecase", _)) match { + case firstUCIndex if firstUCIndex != -1 => + val commentSections = idxs.take(firstUCIndex) + val usecaseSections = idxs.drop(firstUCIndex).filter(str.startsWith("@usecase", _)) + commentSections ::: usecaseSections + case _ => + idxs + } + + private def mergeInheritdocSections(str: String, idxs: List[Int]): List[Int] = + idxs.filterNot(str.startsWith("@inheritdoc", _)) + + private def startsWithTag(str: String, section: (Int, Int), tag: String): Boolean = + startsWithTag(str, section._1, tag) + + private def startsWithTag(str: String, start: Int, tag: String): Boolean = + str.startsWith(tag, start) && !isIdentifierPart(str charAt (start + tag.length)) + + private def paramDocs(str: String, tag: String, sections: List[(Int, Int)]): Map[String, (Int, Int)] = + Map() ++ { + for (section <- sections if startsWithTag(str, section, tag)) yield { + val start = skipWhitespace(str, section._1 + tag.length) + str.substring(start, skipIdent(str, start)).nn -> section + } + } + + private def extractSectionText(str: String, section: (Int, Int)): (Int, Int) = { + val (beg, end) = section + if (str.startsWith("@param", beg) || + str.startsWith("@tparam", beg) || + str.startsWith("@throws", beg)) + (skipWhitespace(str, skipIdent(str, skipWhitespace(str, skipTag(str, beg)))), end) + else + (skipWhitespace(str, skipTag(str, beg)), end) + } + + private def format(raw: String): String = + val remove = List("/**", "*", "/*", "*/") + var lines: Seq[String] = raw.trim.nn.split('\n').toSeq + lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).nn.trim.nn) + var s = lines.foldLeft("") { + case ("", s2) => s2 + case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines + case (s1, "") => s1 + '\n' + case (s1, s2) if s1.last == '\n' => s1 + s2 + case (s1, s2) => s1 + ' ' + s2 + } + s.replaceAll(raw"\{\{", "").nn.replaceAll(raw"\}\}", "").nn.trim.nn + + private def parseDocComment(trimmedDoc: String): Unit = + // Positions of the sections (@) in the docstring + val tidx: List[(Int, Int)] = tagIndex(trimmedDoc) + + // Parse main comment + var mainComment: String = trimmedDoc.substring(skipLineLead(trimmedDoc, 0), startTag(trimmedDoc, tidx)).nn + _mainDoc = format(mainComment) + + // Parse arguments comments + val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(trimmedDoc, "@param", tidx) + val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(trimmedDoc, _)) + val argsCommentsTexts = argsCommentsTextSpans.mapValues { case (beg, end) => trimmedDoc.substring(beg, end) } + _argDocs = argsCommentsTexts.mapValues(s => format(s.nn)).toMap.withDefaultValue("") +end Documentation diff --git a/tests/run/main-annotation-max-line-length.check b/tests/run/main-annotation-max-line-length.check new file mode 100644 index 000000000000..18fbb759c339 --- /dev/null +++ b/tests/run/main-annotation-max-line-length.check @@ -0,0 +1,40 @@ +Usage: doc1 [--a] [--b] [--c] + +Help is printed normally. +Arguments: + a - Int + the first argument + b - Int + the second argument + c - String + the third argument. This one is a String +Usage: doc2 [--a] [--b] + [--c] + +Help is printed in a slightly narrow +column. +Arguments: + a - Int + the first argument + b - Int + the second argument + c - String + the third argument. This one is a + String +Usage: doc3 [--a] + [--b] + [--c] + +Help is printed in a +very narrow column! +Arguments: + a - Int + the first + argument + b - Int + the second + argument + c - String + the third + argument. This + one is a String diff --git a/tests/run/main-annotation-max-line-length.scala b/tests/run/main-annotation-max-line-length.scala new file mode 100644 index 000000000000..7f787edf87d6 --- /dev/null +++ b/tests/run/main-annotation-max-line-length.scala @@ -0,0 +1,42 @@ +import scala.util.CommandLineParser.FromString +import scala.util.Try + +object myProgram: + + /** + * Help is printed normally. + * @param a the first argument + * @param b the second argument + * @param c the third argument. This one is a String + */ + @main() def doc1(a: Int, b: Int, c: String): Unit = () + + /** + * Help is printed in a slightly narrow column. + * @param a the first argument + * @param b the second argument + * @param c the third argument. This one is a String + */ + @main(40) def doc2(a: Int, b: Int, c: String): Unit = () + + /** + * Help is printed in a very narrow column! + * @param a the first argument + * @param b the second argument + * @param c the third argument. This one is a String + */ + @main(maxLineLength = 20) def doc3(a: Int, b: Int, c: String): Unit = () + + +end myProgram + +object Test: + val allClazzes: Seq[Class[?]] = + LazyList.from(1).map(i => Try(Class.forName("doc" + i.toString))).takeWhile(_.isSuccess).map(_.get) + + def main(args: Array[String]): Unit = + for (clazz <- allClazzes) { + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, Array("--help")) + } +end Test From 4d4418e60b081ec72616ee95df7ad2f43ff558a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 22 Nov 2021 11:13:09 +0100 Subject: [PATCH 077/121] Add constructor to main and update MiMa --- library/src/scala/main.scala | 4 +++- project/MiMaFilters.scala | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 19e472c24313..d29f35c22850 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -13,10 +13,12 @@ import annotation.MainAnnotation /** An annotation that designates a main function */ -final class main(maxLineLength: Int = 120) extends scala.annotation.MainAnnotation: +final class main(maxLineLength: Int) extends MainAnnotation: self => import main._ + def this() = this(120) + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 03cb6fcb1944..cce87cdc9509 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -5,7 +5,9 @@ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( ProblemFilters.exclude[MissingTypesProblem]("scala.main"), ProblemFilters.exclude[FinalClassProblem]("scala.main"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.this"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), + ProblemFilters.exclude[MissingClassProblem]("scala.Documentation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), From bded2023b2d42eab5b513a69ac98a42d702415ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 22 Nov 2021 16:45:49 +0100 Subject: [PATCH 078/121] Make Documentation a trait and use anonymous instance --- library/src/scala/main.scala | 355 ++++++++++++++++++----------------- project/MiMaFilters.scala | 1 - 2 files changed, 180 insertions(+), 176 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index d29f35c22850..0ceb832284a2 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -32,7 +32,150 @@ final class main(maxLineLength: Int) extends MainAnnotation: private var argTypes = new mutable.ArrayBuffer[String] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] - private var documentation = new Documentation(docComment, maxLineLength) + private val documentation = new main.Documentation { + /* + * Most of this class' helper methods come from {{dotty.tools.dotc.util.Chars}} + * and {{dotty.tools.dotc.util.CommentParsing}}. + */ + + /** The main part of the documentation. */ + def mainDoc: String = _mainDoc + /** The parameters identified by @param. Maps from param name to documentation. */ + def argDocs: Map[String, String] = _argDocs + + private var _mainDoc: String = "" + private var _argDocs: Map[String, String] = Map().withDefaultValue("") + + docComment.trim match { + case "" => + case doc => parseDocComment(doc.nn) + } + + private def isWhitespace(c: Char): Boolean = + c == ' ' || c == '\t' || c == '\u000D' + + private def isIdentifierPart(c: Char): Boolean = + (c == '$') || java.lang.Character.isUnicodeIdentifierPart(c) + + private def skipWhitespace(str: String, start: Int): Int = + if (start < str.length && isWhitespace(str charAt start)) skipWhitespace(str, start + 1) + else start + + private def skipIdent(str: String, start: Int): Int = + if (start < str.length && isIdentifierPart(str charAt start)) skipIdent(str, start + 1) + else start + + private def skipTag(str: String, start: Int): Int = + if (start < str.length && (str charAt start) == '@') skipIdent(str, start + 1) + else start + + private def skipLineLead(str: String, start: Int): Int = + if (start == str.length) start + else { + val idx = skipWhitespace(str, start + 1) + if (idx < str.length && (str charAt idx) == '*') skipWhitespace(str, idx + 1) + else if (idx + 2 < str.length && (str charAt idx) == '/' && (str charAt (idx + 1)) == '*' && (str charAt (idx + 2)) == '*') + skipWhitespace(str, idx + 3) + else idx + } + + private def startTag(str: String, sections: List[(Int, Int)]): Int = sections match { + case Nil => str.length - 2 + case (start, _) :: _ => start + } + + private def skipToEol(str: String, start: Int): Int = + if (start + 2 < str.length && (str charAt start) == '/' && (str charAt (start + 1)) == '*' && (str charAt (start + 2)) == '*') start + 3 + else if (start < str.length && (str charAt start) != '\n') skipToEol(str, start + 1) + else start + + private def findNext(str: String, start: Int)(p: Int => Boolean): Int = { + val idx = skipLineLead(str, skipToEol(str, start)) + if (idx < str.length && !p(idx)) findNext(str, idx)(p) + else idx + } + + private def findAll(str: String, start: Int)(p: Int => Boolean): List[Int] = { + val idx = findNext(str, start)(p) + if (idx == str.length) List() + else idx :: findAll(str, idx)(p) + } + + private def tagIndex(str: String, p: Int => Boolean = (idx => true)): List[(Int, Int)] = { + var indices = findAll(str, 0) (idx => str(idx) == '@' && p(idx)) + indices = mergeUsecaseSections(str, indices) + indices = mergeInheritdocSections(str, indices) + + indices match { + case List() => List() + case idxs => idxs zip (idxs.tail ::: List(str.length - 2)) + } + } + + private def mergeUsecaseSections(str: String, idxs: List[Int]): List[Int] = + idxs.indexWhere(str.startsWith("@usecase", _)) match { + case firstUCIndex if firstUCIndex != -1 => + val commentSections = idxs.take(firstUCIndex) + val usecaseSections = idxs.drop(firstUCIndex).filter(str.startsWith("@usecase", _)) + commentSections ::: usecaseSections + case _ => + idxs + } + + private def mergeInheritdocSections(str: String, idxs: List[Int]): List[Int] = + idxs.filterNot(str.startsWith("@inheritdoc", _)) + + private def startsWithTag(str: String, section: (Int, Int), tag: String): Boolean = + startsWithTag(str, section._1, tag) + + private def startsWithTag(str: String, start: Int, tag: String): Boolean = + str.startsWith(tag, start) && !isIdentifierPart(str charAt (start + tag.length)) + + private def paramDocs(str: String, tag: String, sections: List[(Int, Int)]): Map[String, (Int, Int)] = + Map() ++ { + for (section <- sections if startsWithTag(str, section, tag)) yield { + val start = skipWhitespace(str, section._1 + tag.length) + str.substring(start, skipIdent(str, start)).nn -> section + } + } + + private def extractSectionText(str: String, section: (Int, Int)): (Int, Int) = { + val (beg, end) = section + if (str.startsWith("@param", beg) || + str.startsWith("@tparam", beg) || + str.startsWith("@throws", beg)) + (skipWhitespace(str, skipIdent(str, skipWhitespace(str, skipTag(str, beg)))), end) + else + (skipWhitespace(str, skipTag(str, beg)), end) + } + + private def format(raw: String): String = + val remove = List("/**", "*", "/*", "*/") + var lines: Seq[String] = raw.trim.nn.split('\n').toSeq + lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).nn.trim.nn) + var s = lines.foldLeft("") { + case ("", s2) => s2 + case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines + case (s1, "") => s1 + '\n' + case (s1, s2) if s1.last == '\n' => s1 + s2 + case (s1, s2) => s1 + ' ' + s2 + } + s.replaceAll(raw"\{\{", "").nn.replaceAll(raw"\}\}", "").nn.trim.nn + + private def parseDocComment(trimmedDoc: String): Unit = + // Positions of the sections (@) in the docstring + val tidx: List[(Int, Int)] = tagIndex(trimmedDoc) + + // Parse main comment + var mainComment: String = trimmedDoc.substring(skipLineLead(trimmedDoc, 0), startTag(trimmedDoc, tidx)).nn + _mainDoc = format(mainComment) + + // Parse arguments comments + val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(trimmedDoc, "@param", tidx) + val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(trimmedDoc, _)) + val argsCommentsTexts = argsCommentsTextSpans.mapValues { case (beg, end) => trimmedDoc.substring(beg, end) } + _argDocs = argsCommentsTexts.mapValues(s => format(s.nn)).toMap.withDefaultValue("") + } /** A buffer for all errors */ private var errors = new mutable.ArrayBuffer[String] @@ -68,16 +211,42 @@ final class main(maxLineLength: Int) extends MainAnnotation: case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" } + def wrapLongLine(line: String, maxLength: Int): List[String] = { + def recurse(s: String, acc: Vector[String]): Seq[String] = + val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) + if ((s.length <= maxLength) || (lastSpace < 0)) + acc :+ s + else { + val (shortLine, rest) = s.splitAt(lastSpace) + recurse(rest.trim.nn, acc :+ shortLine) + } + + recurse(line, Vector()).toList + } + + def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { + def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg :: t, "") => recurse(t, arg, acc) + case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg :: t, l) => recurse(t, arg, acc :+ l) + } + + recurse(argsUsage, "", Vector()).toList + } + private def usage(): Unit = val usageBeginning = s"Usage: $commandName " val argsOffset = usageBeginning.length - val argUsages = documentation.wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) + val argUsages = wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) private def explain(): Unit = if (documentation.mainDoc.nonEmpty) - println(documentation.wrapLongLine(documentation.mainDoc, maxLineLength).mkString("\n")) + println(wrapLongLine(documentation.mainDoc, maxLineLength).mkString("\n")) if (argNames.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 @@ -98,7 +267,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: val formatedDocLines = argDoc .split("\n").nn - .map(line => documentation.wrapLongLine(line.nn, maxLineLength - argDocShift).map(" " * argDocShift + _).mkString("\n")) + .map(line => wrapLongLine(line.nn, maxLineLength - argDocShift).map(" " * argDocShift + _).mkString("\n")) argDocBuilder.append("\n").append(formatedDocLines.mkString("\n")) } @@ -176,7 +345,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: case ExitCode(n) => sys.exit(n) case () => case res => - val wrappedLines = res.toString.split("\n").nn.map(line => documentation.wrapLongLine(line.nn, maxLineLength).mkString("\n")) + val wrappedLines = res.toString.split("\n").nn.map(line => wrapLongLine(line.nn, maxLineLength).mkString("\n")) println(wrappedLines.mkString("\n")) end run end command @@ -184,174 +353,10 @@ end main object main: case class ExitCode(code: Int) -end main - -/* - * Most of this class' helper methods come from {{dotty.tools.dotc.util.Chars}} - * and {{dotty.tools.dotc.util.CommentParsing}}. - */ -private class Documentation(docComment: String, maxLineLength: Int): - /** The main part of the documentation. */ - def mainDoc: String = _mainDoc - /** The parameters identified by @param. Maps from param name to documentation. */ - def argDocs: Map[String, String] = _argDocs - - def wrapLongLine(line: String, maxLength: Int): List[String] = { - def recurse(s: String, acc: Vector[String]): Seq[String] = - val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) - if ((s.length <= maxLength) || (lastSpace < 0)) - acc :+ s - else { - val (shortLine, rest) = s.splitAt(lastSpace) - recurse(rest.trim.nn, acc :+ shortLine) - } - recurse(line, Vector()).toList - } - - def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { - def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = - (args, currentLine) match { - case (Nil, "") => acc - case (Nil, l) => (acc :+ l) - case (arg :: t, "") => recurse(t, arg, acc) - case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg :: t, l) => recurse(t, arg, acc :+ l) - } - - recurse(argsUsage, "", Vector()).toList - } - - private var _mainDoc: String = "" - private var _argDocs: Map[String, String] = Map().withDefaultValue("") - - docComment.trim match { - case "" => - case doc => parseDocComment(doc.nn) - } - - private def isWhitespace(c: Char): Boolean = - c == ' ' || c == '\t' || c == '\u000D' - - private def isIdentifierPart(c: Char): Boolean = - (c == '$') || java.lang.Character.isUnicodeIdentifierPart(c) - - private def skipWhitespace(str: String, start: Int): Int = - if (start < str.length && isWhitespace(str charAt start)) skipWhitespace(str, start + 1) - else start - - private def skipIdent(str: String, start: Int): Int = - if (start < str.length && isIdentifierPart(str charAt start)) skipIdent(str, start + 1) - else start - - private def skipTag(str: String, start: Int): Int = - if (start < str.length && (str charAt start) == '@') skipIdent(str, start + 1) - else start - - private def skipLineLead(str: String, start: Int): Int = - if (start == str.length) start - else { - val idx = skipWhitespace(str, start + 1) - if (idx < str.length && (str charAt idx) == '*') skipWhitespace(str, idx + 1) - else if (idx + 2 < str.length && (str charAt idx) == '/' && (str charAt (idx + 1)) == '*' && (str charAt (idx + 2)) == '*') - skipWhitespace(str, idx + 3) - else idx - } - - private def startTag(str: String, sections: List[(Int, Int)]): Int = sections match { - case Nil => str.length - 2 - case (start, _) :: _ => start - } - - private def skipToEol(str: String, start: Int): Int = - if (start + 2 < str.length && (str charAt start) == '/' && (str charAt (start + 1)) == '*' && (str charAt (start + 2)) == '*') start + 3 - else if (start < str.length && (str charAt start) != '\n') skipToEol(str, start + 1) - else start - - private def findNext(str: String, start: Int)(p: Int => Boolean): Int = { - val idx = skipLineLead(str, skipToEol(str, start)) - if (idx < str.length && !p(idx)) findNext(str, idx)(p) - else idx - } - - private def findAll(str: String, start: Int)(p: Int => Boolean): List[Int] = { - val idx = findNext(str, start)(p) - if (idx == str.length) List() - else idx :: findAll(str, idx)(p) - } - - private def tagIndex(str: String, p: Int => Boolean = (idx => true)): List[(Int, Int)] = { - var indices = findAll(str, 0) (idx => str(idx) == '@' && p(idx)) - indices = mergeUsecaseSections(str, indices) - indices = mergeInheritdocSections(str, indices) - - indices match { - case List() => List() - case idxs => idxs zip (idxs.tail ::: List(str.length - 2)) - } - } - - private def mergeUsecaseSections(str: String, idxs: List[Int]): List[Int] = - idxs.indexWhere(str.startsWith("@usecase", _)) match { - case firstUCIndex if firstUCIndex != -1 => - val commentSections = idxs.take(firstUCIndex) - val usecaseSections = idxs.drop(firstUCIndex).filter(str.startsWith("@usecase", _)) - commentSections ::: usecaseSections - case _ => - idxs - } - - private def mergeInheritdocSections(str: String, idxs: List[Int]): List[Int] = - idxs.filterNot(str.startsWith("@inheritdoc", _)) - - private def startsWithTag(str: String, section: (Int, Int), tag: String): Boolean = - startsWithTag(str, section._1, tag) - - private def startsWithTag(str: String, start: Int, tag: String): Boolean = - str.startsWith(tag, start) && !isIdentifierPart(str charAt (start + tag.length)) - - private def paramDocs(str: String, tag: String, sections: List[(Int, Int)]): Map[String, (Int, Int)] = - Map() ++ { - for (section <- sections if startsWithTag(str, section, tag)) yield { - val start = skipWhitespace(str, section._1 + tag.length) - str.substring(start, skipIdent(str, start)).nn -> section - } - } - - private def extractSectionText(str: String, section: (Int, Int)): (Int, Int) = { - val (beg, end) = section - if (str.startsWith("@param", beg) || - str.startsWith("@tparam", beg) || - str.startsWith("@throws", beg)) - (skipWhitespace(str, skipIdent(str, skipWhitespace(str, skipTag(str, beg)))), end) - else - (skipWhitespace(str, skipTag(str, beg)), end) - } - - private def format(raw: String): String = - val remove = List("/**", "*", "/*", "*/") - var lines: Seq[String] = raw.trim.nn.split('\n').toSeq - lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).nn.trim.nn) - var s = lines.foldLeft("") { - case ("", s2) => s2 - case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines - case (s1, "") => s1 + '\n' - case (s1, s2) if s1.last == '\n' => s1 + s2 - case (s1, s2) => s1 + ' ' + s2 - } - s.replaceAll(raw"\{\{", "").nn.replaceAll(raw"\}\}", "").nn.trim.nn - - private def parseDocComment(trimmedDoc: String): Unit = - // Positions of the sections (@) in the docstring - val tidx: List[(Int, Int)] = tagIndex(trimmedDoc) - - // Parse main comment - var mainComment: String = trimmedDoc.substring(skipLineLead(trimmedDoc, 0), startTag(trimmedDoc, tidx)).nn - _mainDoc = format(mainComment) - - // Parse arguments comments - val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(trimmedDoc, "@param", tidx) - val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(trimmedDoc, _)) - val argsCommentsTexts = argsCommentsTextSpans.mapValues { case (beg, end) => trimmedDoc.substring(beg, end) } - _argDocs = argsCommentsTexts.mapValues(s => format(s.nn)).toMap.withDefaultValue("") -end Documentation + private trait Documentation: + /** The main part of the documentation. */ + def mainDoc: String + /** The parameters identified by @param. Maps from param name to documentation. */ + def argDocs: Map[String, String] +end main diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index cce87cdc9509..8ca1adcec115 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -7,7 +7,6 @@ object MiMaFilters { ProblemFilters.exclude[FinalClassProblem]("scala.main"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.this"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), - ProblemFilters.exclude[MissingClassProblem]("scala.Documentation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), From aff99de708cdd60ba1ef0e470a3ad38fd93fd85b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 22 Nov 2021 17:26:19 +0100 Subject: [PATCH 079/121] Add test for Option[T] and Either[T] --- .../main-annotation-homemade-parser-4.check | 7 +++ .../main-annotation-homemade-parser-4.scala | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/run/main-annotation-homemade-parser-4.check create mode 100644 tests/run/main-annotation-homemade-parser-4.scala diff --git a/tests/run/main-annotation-homemade-parser-4.check b/tests/run/main-annotation-homemade-parser-4.check new file mode 100644 index 000000000000..d1e1a6bdd0cb --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-4.check @@ -0,0 +1,7 @@ +Some(7) +Some(42) +None + +Left(7) +Right(No argument given) +Right(Unable to parse argument abc) diff --git a/tests/run/main-annotation-homemade-parser-4.scala b/tests/run/main-annotation-homemade-parser-4.scala new file mode 100644 index 000000000000..97d08378522f --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-4.scala @@ -0,0 +1,46 @@ +import scala.util.CommandLineParser.FromString + +given [T : FromString]: FromString[Option[T]] with + override def fromString(s: String) = Some(summon[FromString[T]].fromString(s)) + override def fromStringOption(s: String) = + try { + Some(fromString(s)) + } + catch { + case _: IllegalArgumentException => Some(None) + } + +given [T : FromString]: FromString[Either[T, String]] with + override def fromString(s: String) = Left(summon[FromString[T]].fromString(s)) + override def fromStringOption(s: String) = + try { + Some(fromString(s)) + } + catch { + case _: IllegalArgumentException => Some(Right(s"Unable to parse argument $s")) + } + +object myProgram: + + @main def getOption(o: Option[Int] = Some[Int](42)) = o + + @main def getEither(e: Either[Int, String] = Right[Int, String]("No argument given")) = e + +end myProgram + + +object Test: + def call(className: String, args: Array[String]): Unit = + val clazz = Class.forName(className) + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + call("getOption", Array("7")) + call("getOption", Array()) + call("getOption", Array("abc")) + println + call("getEither", Array("7")) + call("getEither", Array()) + call("getEither", Array("abc")) +end Test From 889dbd686b117b6d9f688a4100811e24dd56aef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 28 Nov 2021 12:35:32 +0100 Subject: [PATCH 080/121] Remove ExitCode and result printing in main - After discussing with LAMP members, I was instructed to not print the result of a main method --- library/src/scala/main.scala | 10 ++-------- project/MiMaFilters.scala | 20 +++---------------- .../main-annotation-return-exit-code.scala | 9 --------- tests/run/main-annotation-birthday.scala | 2 +- .../main-annotation-homemade-parser-4.scala | 4 ++-- tests/run/main-annotation-return-type-1.check | 1 + tests/run/main-annotation-return-type-1.scala | 6 +++--- tests/run/main-annotation-return-type-2.check | 1 + tests/run/main-annotation-return-type-2.scala | 6 +++--- 9 files changed, 16 insertions(+), 43 deletions(-) delete mode 100644 tests/pos/main-annotation-return-exit-code.scala diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 0ceb832284a2..39053d5badd3 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -341,19 +341,13 @@ final class main(maxLineLength: Int) extends MainAnnotation: if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() - else f match - case ExitCode(n) => sys.exit(n) - case () => - case res => - val wrappedLines = res.toString.split("\n").nn.map(line => wrapLongLine(line.nn, maxLineLength).mkString("\n")) - println(wrappedLines.mkString("\n")) + else + f end run end command end main object main: - case class ExitCode(code: Int) - private trait Documentation: /** The main part of the documentation. */ def mainDoc: String diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 8ca1adcec115..bbe383dadc09 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -3,29 +3,15 @@ import com.typesafe.tools.mima.core._ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( - ProblemFilters.exclude[MissingTypesProblem]("scala.main"), + // Experimental APIs that can be added in 3.2.0 ProblemFilters.exclude[FinalClassProblem]("scala.main"), + ProblemFilters.exclude[MissingTypesProblem]("scala.main"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.this"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.SimpleArgument"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.OptionalArgument"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.VarArgument"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.usage"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.explain"), - ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.run"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$Argument"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$ExitCode"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$ExitCode$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$OptionalArgument"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$OptionalArgument$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$SimpleArgument"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$SimpleArgument$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$VarArgument"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$VarArgument$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.init"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.last"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"), diff --git a/tests/pos/main-annotation-return-exit-code.scala b/tests/pos/main-annotation-return-exit-code.scala deleted file mode 100644 index fcc1fbda426f..000000000000 --- a/tests/pos/main-annotation-return-exit-code.scala +++ /dev/null @@ -1,9 +0,0 @@ -// Sample main method -object myProgram: - - /** Exits program with error code */ - @main def exit(code: Int): main.ExitCode = - println(f"Exiting with code $code") - main.ExitCode(code) - -end myProgram diff --git a/tests/run/main-annotation-birthday.scala b/tests/run/main-annotation-birthday.scala index c726e91100a8..d38f145e7b1b 100644 --- a/tests/run/main-annotation-birthday.scala +++ b/tests/run/main-annotation-birthday.scala @@ -17,7 +17,7 @@ case _ => "th" val bldr = new StringBuilder(s"Happy $age$suffix birthday, $name") for other <- others do bldr.append(" and ").append(other) - bldr.toString + println(bldr) object Test: def callMain(args: Array[String]): Unit = diff --git a/tests/run/main-annotation-homemade-parser-4.scala b/tests/run/main-annotation-homemade-parser-4.scala index 97d08378522f..f7730ec27a18 100644 --- a/tests/run/main-annotation-homemade-parser-4.scala +++ b/tests/run/main-annotation-homemade-parser-4.scala @@ -22,9 +22,9 @@ given [T : FromString]: FromString[Either[T, String]] with object myProgram: - @main def getOption(o: Option[Int] = Some[Int](42)) = o + @main def getOption(o: Option[Int] = Some[Int](42)) = println(o) - @main def getEither(e: Either[Int, String] = Right[Int, String]("No argument given")) = e + @main def getEither(e: Either[Int, String] = Right[Int, String]("No argument given")) = println(e) end myProgram diff --git a/tests/run/main-annotation-return-type-1.check b/tests/run/main-annotation-return-type-1.check index c00eba530a32..2239817268f0 100644 --- a/tests/run/main-annotation-return-type-1.check +++ b/tests/run/main-annotation-return-type-1.check @@ -1,3 +1,4 @@ Direct call +5 Main call 5 diff --git a/tests/run/main-annotation-return-type-1.scala b/tests/run/main-annotation-return-type-1.scala index 4952f4d44516..8ddc1a07539f 100644 --- a/tests/run/main-annotation-return-type-1.scala +++ b/tests/run/main-annotation-return-type-1.scala @@ -2,8 +2,8 @@ object myProgram: /** Adds two numbers and returns them */ - @main def add(num: Int, inc: Int): Int = - num + inc + @main def add(num: Int, inc: Int) = + println(num + inc) end myProgram @@ -15,7 +15,7 @@ object Test: def main(args: Array[String]): Unit = println("Direct call") - assert(myProgram.add(2, 3) == 5) + myProgram.add(2, 3) println("Main call") callMain(Array("2", "3")) end Test diff --git a/tests/run/main-annotation-return-type-2.check b/tests/run/main-annotation-return-type-2.check index c00eba530a32..2239817268f0 100644 --- a/tests/run/main-annotation-return-type-2.check +++ b/tests/run/main-annotation-return-type-2.check @@ -1,3 +1,4 @@ Direct call +5 Main call 5 diff --git a/tests/run/main-annotation-return-type-2.scala b/tests/run/main-annotation-return-type-2.scala index e1e0a7858070..dbf1503f38a1 100644 --- a/tests/run/main-annotation-return-type-2.scala +++ b/tests/run/main-annotation-return-type-2.scala @@ -5,8 +5,8 @@ class MyResult(val result: Int): object myProgram: /** Adds two numbers and returns them */ - @main def add(num: Int, inc: Int): MyResult = - MyResult(num + inc) + @main def add(num: Int, inc: Int) = + println(MyResult(num + inc)) end myProgram @@ -18,7 +18,7 @@ object Test: def main(args: Array[String]): Unit = println("Direct call") - assert(myProgram.add(2, 3).result == 5) + myProgram.add(2, 3) println("Main call") callMain(Array("2", "3")) end Test From 4d0c14969a718d589b84a9b2818ba6f1274b5276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 29 Nov 2021 08:07:22 +0100 Subject: [PATCH 081/121] Add test for function Parser --- .../main-annotation-homemade-parser-5.check | 2 ++ .../main-annotation-homemade-parser-5.scala | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/run/main-annotation-homemade-parser-5.check create mode 100644 tests/run/main-annotation-homemade-parser-5.scala diff --git a/tests/run/main-annotation-homemade-parser-5.check b/tests/run/main-annotation-homemade-parser-5.check new file mode 100644 index 000000000000..8925cc4f276b --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-5.check @@ -0,0 +1,2 @@ +42 +Hello world! diff --git a/tests/run/main-annotation-homemade-parser-5.scala b/tests/run/main-annotation-homemade-parser-5.scala new file mode 100644 index 000000000000..4621716b537e --- /dev/null +++ b/tests/run/main-annotation-homemade-parser-5.scala @@ -0,0 +1,25 @@ +import scala.util.CommandLineParser.FromString + +given intParser: FromString[Int => Int] with + override def fromString(s: String) = n => summon[FromString[Int]].fromString(s) + n + +given stringParser: FromString[String => String] with + override def fromString(s: String) = s1 => summon[FromString[String]].fromString(s) + s1 + +object myProgram: + + @main def show(getI: Int => Int, getS: String => String) = + println(getI(3)) + println(getS(" world!")) + +end myProgram + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("show") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("39", "Hello")) +end Test From 1be2e7d552a7a3d22b561ada3588518a85aa273c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Mon, 29 Nov 2021 10:18:27 +0100 Subject: [PATCH 082/121] Move Documentation back to MainProxies --- .../dotty/tools/dotc/ast/MainProxies.scala | 48 ++++- .../src/scala/annotation/MainAnnotation.scala | 6 +- library/src/scala/main.scala | 199 +++--------------- 3 files changed, 76 insertions(+), 177 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 04cd25a79d39..0dc835363c87 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -77,6 +77,8 @@ object MainProxies { val mainArgsName: TermName = nme.args val cmdName: TermName = Names.termName("cmd") + val documentation = new Documentation(docComment) + inline def lit(any: Any): Literal = Literal(Constant(any)) def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = @@ -112,6 +114,7 @@ object MainProxies { lit(param) :: lit(formalType.show) + :: lit(documentation.argDocs(param)) :: optionDefaultValueTree.map(List(_)).getOrElse(Nil) } @@ -147,7 +150,7 @@ object MainProxies { TypeTree(), Apply( Select(New(TypeTree(mainAnnot.symbol.typeRef), List(mainAnnotArgs)), defn.MainAnnot_command.name), - Ident(mainArgsName) :: lit(mainFun.showName) :: lit(docComment.map(_.raw).getOrElse("")) :: Nil + Ident(mainArgsName) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil ) ) var args: List[ValDef] = Nil @@ -190,4 +193,47 @@ object MainProxies { } result } + + private class Documentation(docComment: Option[Comment]): + import util.CommentParsing._ + + /** The main part of the documentation. */ + lazy val mainDoc: String = _mainDoc + /** The parameters identified by @param. Maps from param name to documentation. */ + lazy val argDocs: Map[String, String] = _argDocs + + private var _mainDoc: String = "" + private var _argDocs: Map[String, String] = Map().withDefaultValue("") + + docComment match { + case Some(comment) => if comment.isDocComment then parseDocComment(comment.raw) else _mainDoc = comment.raw + case None => + } + + private def cleanComment(raw: String): String = + var lines: Seq[String] = raw.trim.split('\n').toSeq + lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).trim) + var s = lines.foldLeft("") { + case ("", s2) => s2 + case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines + case (s1, "") => s1 + '\n' + case (s1, s2) if s1.last == '\n' => s1 + s2 + case (s1, s2) => s1 + ' ' + s2 + } + s.replaceAll(raw"\{\{", "").replaceAll(raw"\}\}", "").trim + + private def parseDocComment(raw: String): Unit = + // Positions of the sections (@) in the docstring + val tidx: List[(Int, Int)] = tagIndex(raw) + + // Parse main comment + var mainComment: String = raw.substring(skipLineLead(raw, 0), startTag(raw, tidx)) + _mainDoc = cleanComment(mainComment) + + // Parse arguments comments + val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(raw, "@param", tidx) + val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(raw, _)) + val argsCommentsTexts = argsCommentsTextSpans.mapValues({ case (beg, end) => raw.substring(beg, end) }) + _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).toMap.withDefaultValue("") + end Documentation } diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 170f38cc3d1c..af0e372b4e1a 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -29,13 +29,13 @@ object MainAnnotation: trait Command[ArgumentParser[_], MainResultType]: /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => T + def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, argType: String, defaultValue: => T)(using fromString: ArgumentParser[T]): () => T + def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String, argType: String)(using fromString: ArgumentParser[T]): () => Seq[T] + def argsGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 39053d5badd3..1e2bfe12e85a 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -30,153 +30,9 @@ final class main(maxLineLength: Int) extends MainAnnotation: new MainAnnotation.Command[ArgumentParser, MainResultType]: private var argNames = new mutable.ArrayBuffer[String] private var argTypes = new mutable.ArrayBuffer[String] + private var argDocs = new mutable.ArrayBuffer[String] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] - private val documentation = new main.Documentation { - /* - * Most of this class' helper methods come from {{dotty.tools.dotc.util.Chars}} - * and {{dotty.tools.dotc.util.CommentParsing}}. - */ - - /** The main part of the documentation. */ - def mainDoc: String = _mainDoc - /** The parameters identified by @param. Maps from param name to documentation. */ - def argDocs: Map[String, String] = _argDocs - - private var _mainDoc: String = "" - private var _argDocs: Map[String, String] = Map().withDefaultValue("") - - docComment.trim match { - case "" => - case doc => parseDocComment(doc.nn) - } - - private def isWhitespace(c: Char): Boolean = - c == ' ' || c == '\t' || c == '\u000D' - - private def isIdentifierPart(c: Char): Boolean = - (c == '$') || java.lang.Character.isUnicodeIdentifierPart(c) - - private def skipWhitespace(str: String, start: Int): Int = - if (start < str.length && isWhitespace(str charAt start)) skipWhitespace(str, start + 1) - else start - - private def skipIdent(str: String, start: Int): Int = - if (start < str.length && isIdentifierPart(str charAt start)) skipIdent(str, start + 1) - else start - - private def skipTag(str: String, start: Int): Int = - if (start < str.length && (str charAt start) == '@') skipIdent(str, start + 1) - else start - - private def skipLineLead(str: String, start: Int): Int = - if (start == str.length) start - else { - val idx = skipWhitespace(str, start + 1) - if (idx < str.length && (str charAt idx) == '*') skipWhitespace(str, idx + 1) - else if (idx + 2 < str.length && (str charAt idx) == '/' && (str charAt (idx + 1)) == '*' && (str charAt (idx + 2)) == '*') - skipWhitespace(str, idx + 3) - else idx - } - - private def startTag(str: String, sections: List[(Int, Int)]): Int = sections match { - case Nil => str.length - 2 - case (start, _) :: _ => start - } - - private def skipToEol(str: String, start: Int): Int = - if (start + 2 < str.length && (str charAt start) == '/' && (str charAt (start + 1)) == '*' && (str charAt (start + 2)) == '*') start + 3 - else if (start < str.length && (str charAt start) != '\n') skipToEol(str, start + 1) - else start - - private def findNext(str: String, start: Int)(p: Int => Boolean): Int = { - val idx = skipLineLead(str, skipToEol(str, start)) - if (idx < str.length && !p(idx)) findNext(str, idx)(p) - else idx - } - - private def findAll(str: String, start: Int)(p: Int => Boolean): List[Int] = { - val idx = findNext(str, start)(p) - if (idx == str.length) List() - else idx :: findAll(str, idx)(p) - } - - private def tagIndex(str: String, p: Int => Boolean = (idx => true)): List[(Int, Int)] = { - var indices = findAll(str, 0) (idx => str(idx) == '@' && p(idx)) - indices = mergeUsecaseSections(str, indices) - indices = mergeInheritdocSections(str, indices) - - indices match { - case List() => List() - case idxs => idxs zip (idxs.tail ::: List(str.length - 2)) - } - } - - private def mergeUsecaseSections(str: String, idxs: List[Int]): List[Int] = - idxs.indexWhere(str.startsWith("@usecase", _)) match { - case firstUCIndex if firstUCIndex != -1 => - val commentSections = idxs.take(firstUCIndex) - val usecaseSections = idxs.drop(firstUCIndex).filter(str.startsWith("@usecase", _)) - commentSections ::: usecaseSections - case _ => - idxs - } - - private def mergeInheritdocSections(str: String, idxs: List[Int]): List[Int] = - idxs.filterNot(str.startsWith("@inheritdoc", _)) - - private def startsWithTag(str: String, section: (Int, Int), tag: String): Boolean = - startsWithTag(str, section._1, tag) - - private def startsWithTag(str: String, start: Int, tag: String): Boolean = - str.startsWith(tag, start) && !isIdentifierPart(str charAt (start + tag.length)) - - private def paramDocs(str: String, tag: String, sections: List[(Int, Int)]): Map[String, (Int, Int)] = - Map() ++ { - for (section <- sections if startsWithTag(str, section, tag)) yield { - val start = skipWhitespace(str, section._1 + tag.length) - str.substring(start, skipIdent(str, start)).nn -> section - } - } - - private def extractSectionText(str: String, section: (Int, Int)): (Int, Int) = { - val (beg, end) = section - if (str.startsWith("@param", beg) || - str.startsWith("@tparam", beg) || - str.startsWith("@throws", beg)) - (skipWhitespace(str, skipIdent(str, skipWhitespace(str, skipTag(str, beg)))), end) - else - (skipWhitespace(str, skipTag(str, beg)), end) - } - - private def format(raw: String): String = - val remove = List("/**", "*", "/*", "*/") - var lines: Seq[String] = raw.trim.nn.split('\n').toSeq - lines = lines.map(l => l.substring(skipLineLead(l, -1), l.length).nn.trim.nn) - var s = lines.foldLeft("") { - case ("", s2) => s2 - case (s1, "") if s1.last == '\n' => s1 // Multiple newlines are kept as single newlines - case (s1, "") => s1 + '\n' - case (s1, s2) if s1.last == '\n' => s1 + s2 - case (s1, s2) => s1 + ' ' + s2 - } - s.replaceAll(raw"\{\{", "").nn.replaceAll(raw"\}\}", "").nn.trim.nn - - private def parseDocComment(trimmedDoc: String): Unit = - // Positions of the sections (@) in the docstring - val tidx: List[(Int, Int)] = tagIndex(trimmedDoc) - - // Parse main comment - var mainComment: String = trimmedDoc.substring(skipLineLead(trimmedDoc, 0), startTag(trimmedDoc, tidx)).nn - _mainDoc = format(mainComment) - - // Parse arguments comments - val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(trimmedDoc, "@param", tidx) - val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(trimmedDoc, _)) - val argsCommentsTexts = argsCommentsTextSpans.mapValues { case (beg, end) => trimmedDoc.substring(beg, end) } - _argDocs = argsCommentsTexts.mapValues(s => format(s.nn)).toMap.withDefaultValue("") - } - /** A buffer for all errors */ private var errors = new mutable.ArrayBuffer[String] @@ -211,7 +67,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" } - def wrapLongLine(line: String, maxLength: Int): List[String] = { + private def wrapLongLine(line: String, maxLength: Int): List[String] = { def recurse(s: String, acc: Vector[String]): Seq[String] = val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) if ((s.length <= maxLength) || (lastSpace < 0)) @@ -224,7 +80,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: recurse(line, Vector()).toList } - def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { + private def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = (args, currentLine) match { case (Nil, "") => acc @@ -237,6 +93,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: recurse(argsUsage, "", Vector()).toList } + private inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") + private def usage(): Unit = val usageBeginning = s"Usage: $commandName " val argsOffset = usageBeginning.length @@ -245,33 +103,32 @@ final class main(maxLineLength: Int) extends MainAnnotation: println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) private def explain(): Unit = - if (documentation.mainDoc.nonEmpty) - println(wrapLongLine(documentation.mainDoc, maxLineLength).mkString("\n")) + if (docComment.nonEmpty) + println(wrapLongLine(docComment, maxLineLength).mkString("\n")) if (argNames.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 println("Arguments:") for (pos <- 0 until argNames.length) - val argDocBuilder = StringBuilder(" " * argNameShift) - argDocBuilder.append(s"${argNames(pos)} - ${argTypes(pos)}") + val argDoc = StringBuilder(" " * argNameShift) + argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") argKinds(pos) match { - case ArgumentKind.OptionalArgument => argDocBuilder.append(" (optional)") - case ArgumentKind.VarArgument => argDocBuilder.append(" (vararg)") + case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") + case ArgumentKind.VarArgument => argDoc.append(" (vararg)") case _ => } - val argDoc = documentation.argDocs(argNames(pos)) - if (argDoc.nonEmpty) { - val formatedDocLines = - argDoc - .split("\n").nn - .map(line => wrapLongLine(line.nn, maxLineLength - argDocShift).map(" " * argDocShift + _).mkString("\n")) - argDocBuilder.append("\n").append(formatedDocLines.mkString("\n")) + if (argDocs(pos).nonEmpty) { + val shiftedDoc = + argDocs(pos).split("\n").nn + .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) + .mkString("\n") + argDoc.append("\n").append(shiftedDoc) } - println(argDocBuilder) + println(argDoc) } private def indicesOfArg(argName: String): Seq[Int] = @@ -298,21 +155,22 @@ final class main(maxLineLength: Int) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(argName: String, argType: String, argKind: ArgumentKind): Unit = + private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = argNames += argName argTypes += argType + argDocs += argDoc argKinds += argKind - override def argGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, ArgumentKind.SimpleArgument) + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) getArgGetter(argName, () => error(s"missing argument for $argName")) - override def argGetterDefault[T](argName: String, argType: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, ArgumentKind.OptionalArgument) + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) getArgGetter(argName, () => () => defaultValue) - override def argsGetter[T](argName: String, argType: String)(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(argName, argType, ArgumentKind.VarArgument) + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() case None => Nil @@ -348,9 +206,4 @@ final class main(maxLineLength: Int) extends MainAnnotation: end main object main: - private trait Documentation: - /** The main part of the documentation. */ - def mainDoc: String - /** The parameters identified by @param. Maps from param name to documentation. */ - def argDocs: Map[String, String] end main From 816116357ac11c24aac59620bc7a36f7b5ef53d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 4 Dec 2021 10:35:31 +0100 Subject: [PATCH 083/121] Add test with Future[_] as return type --- .../main-annotation-homemade-annot-1.check | 2 + .../main-annotation-homemade-annot-1.scala | 212 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 tests/run/main-annotation-homemade-annot-1.check create mode 100644 tests/run/main-annotation-homemade-annot-1.scala diff --git a/tests/run/main-annotation-homemade-annot-1.check b/tests/run/main-annotation-homemade-annot-1.check new file mode 100644 index 000000000000..daaac9e30302 --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-1.check @@ -0,0 +1,2 @@ +42 +42 diff --git a/tests/run/main-annotation-homemade-annot-1.scala b/tests/run/main-annotation-homemade-annot-1.scala new file mode 100644 index 000000000000..6dca3e883dde --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-1.scala @@ -0,0 +1,212 @@ +import scala.concurrent._ +import scala.annotation.MainAnnotation +import scala.collection.mutable +import ExecutionContext.Implicits.global +import duration._ + +@mainAwait def get(wait: Int): Future[Int] = Future{ + Thread.sleep(1000 * wait) + 42 +} + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("get") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + println(Await.result(get(1), Duration(2, SECONDS))) + callMain(Array("1")) +end Test + +class mainAwait(timeout: Int = 2) extends MainAnnotation: + self => + + private val maxLineLength = 120 + + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] + override type MainResultType = Future[Any] + + private enum ArgumentKind { + case SimpleArgument, OptionalArgument, VarArgument + } + + override def command(args: Array[String], commandName: String, docComment: String) = + new MainAnnotation.Command[ArgumentParser, MainResultType]: + private var argNames = new mutable.ArrayBuffer[String] + private var argTypes = new mutable.ArrayBuffer[String] + private var argDocs = new mutable.ArrayBuffer[String] + private var argKinds = new mutable.ArrayBuffer[ArgumentKind] + + /** A buffer for all errors */ + private var errors = new mutable.ArrayBuffer[String] + + /** Issue an error, and return an uncallable getter */ + private def error(msg: String): () => Nothing = + errors += msg + () => throw new AssertionError("trying to get invalid argument") + + /** The next argument index */ + private var argIdx: Int = 0 + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + private def argUsage(pos: Int): String = + val name = argNames(pos) + + argKinds(pos) match { + case ArgumentKind.SimpleArgument => s"[--$name] <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[[--$name] <${argTypes(pos)}>]" + case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" + } + + private def wrapLongLine(line: String, maxLength: Int): List[String] = { + def recurse(s: String, acc: Vector[String]): Seq[String] = + val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) + if ((s.length <= maxLength) || (lastSpace < 0)) + acc :+ s + else { + val (shortLine, rest) = s.splitAt(lastSpace) + recurse(rest.trim.nn, acc :+ shortLine) + } + + recurse(line, Vector()).toList + } + + private def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { + def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg :: t, "") => recurse(t, arg, acc) + case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg :: t, l) => recurse(t, arg, acc :+ l) + } + + recurse(argsUsage, "", Vector()).toList + } + + private inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") + + private def usage(): Unit = + val usageBeginning = s"Usage: $commandName " + val argsOffset = usageBeginning.length + val argUsages = wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) + + println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) + + private def explain(): Unit = + if (docComment.nonEmpty) + println(wrapLongLine(docComment, maxLineLength).mkString("\n")) + if (argNames.nonEmpty) { + val argNameShift = 2 + val argDocShift = argNameShift + 2 + + println("Arguments:") + for (pos <- 0 until argNames.length) + val argDoc = StringBuilder(" " * argNameShift) + argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") + + argKinds(pos) match { + case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") + case ArgumentKind.VarArgument => argDoc.append(" (vararg)") + case _ => + } + + if (argDocs(pos).nonEmpty) { + val shiftedDoc = + argDocs(pos).split("\n").nn + .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) + .mkString("\n") + argDoc.append("\n").append(shiftedDoc) + } + + println(argDoc) + } + + private def indicesOfArg(argName: String): Seq[Int] = + def allIndicesOf(s: String): Seq[Int] = + def recurse(s: String, from: Int): Seq[Int] = + val i = args.indexOf(s, from) + if i < 0 then Seq() else i +: recurse(s, i + 1) + + recurse(s, 0) + + val indices = allIndicesOf(s"--$argName") + indices.filter(_ >= 0) + + private def getArgGetter[T](argName: String, getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = + indicesOfArg(argName) match { + case s @ (Seq() | Seq(_)) => + val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) + argOpt match { + case Some(arg) => convert(argName, arg, p) + case None => getDefaultGetter() + } + case s => + val multValues = s.flatMap(idx => argAt(idx + 1)) + error(s"more than one value for $argName: ${multValues.mkString(", ")}") + } + + private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = + argNames += argName + argTypes += argType + argDocs += argDoc + argKinds += argKind + + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) + getArgGetter(argName, () => error(s"missing argument for $argName")) + + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) + getArgGetter(argName, () => () => defaultValue) + + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case None => Nil + val getters = remainingArgGetters() + () => getters.map(_()) + + override def run(f: => MainResultType): Unit = + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argNames.contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.contains("--help") then + usage() + println() + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else + println(Await.result(f, Duration(timeout, SECONDS))) + end run + end command +end mainAwait \ No newline at end of file From eafa508b4d09643f8fa619fa0c6056c747274f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 4 Dec 2021 12:50:27 +0100 Subject: [PATCH 084/121] Factorize annotation instanciation code - Tweak code for default values, add support for varargs - Add related tests --- .../dotty/tools/dotc/ast/MainProxies.scala | 27 ++- .../main-annotation-homemade-annot-2.check | 11 + .../main-annotation-homemade-annot-2.scala | 221 ++++++++++++++++++ .../main-annotation-homemade-annot-3.check | 1 + .../main-annotation-homemade-annot-3.scala | 25 ++ .../main-annotation-homemade-annot-4.check | 1 + .../main-annotation-homemade-annot-4.scala | 25 ++ .../main-annotation-homemade-annot-5.check | 2 + .../main-annotation-homemade-annot-5.scala | 27 +++ 9 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 tests/run/main-annotation-homemade-annot-2.check create mode 100644 tests/run/main-annotation-homemade-annot-2.scala create mode 100644 tests/run/main-annotation-homemade-annot-3.check create mode 100644 tests/run/main-annotation-homemade-annot-3.scala create mode 100644 tests/run/main-annotation-homemade-annot-4.check create mode 100644 tests/run/main-annotation-homemade-annot-4.scala create mode 100644 tests/run/main-annotation-homemade-annot-5.check create mode 100644 tests/run/main-annotation-homemade-annot-5.scala diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 0dc835363c87..a27755782ebb 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -8,6 +8,7 @@ import ast.Trees._ import Names.{Name, TermName} import Comments.Comment import NameKinds.DefaultGetterName +import Annotations.Annotation /** Generate proxy classes for main functions. * A function like @@ -135,21 +136,33 @@ object MainProxies { } } + inline def instanciateAnnotation(annot: Annotation, argss: List[List[Tree]]): Tree = New(TypeTree(annot.symbol.typeRef), argss) + + def extractAnnotationArgs(annot: Annotation): List[List[Tree]] = + def recurse(t: Tree, acc: List[List[Tree]]): List[List[Tree]] = t match { + case Apply(t, args: List[Tree]) => recurse(t, extractArgs(args) :: acc) + case _ => acc + } + + def extractArgs(args: List[Tree]): List[Tree] = + args.flatMap { + case Typed(SeqLiteral(varargs, _), _) => varargs + case arg @ Select(_, name) => if name.is(DefaultGetterName) then List() else List(arg) + case arg => List(arg) + } + + recurse(annot.tree, Nil) + var result: List[TypeDef] = Nil if (!mainFun.owner.isStaticOwner) report.error(s"main method is not statically accessible", pos) else { - val mainAnnotArgs = mainAnnot.tree match { - case Apply(_, namedArgs) => namedArgs.filter { - case Select(_, name) => !name.is(DefaultGetterName) // Remove default arguments of main - case _ => true - } - } + val mainAnnotArgss = extractAnnotationArgs(mainAnnot) val cmd = ValDef( cmdName, TypeTree(), Apply( - Select(New(TypeTree(mainAnnot.symbol.typeRef), List(mainAnnotArgs)), defn.MainAnnot_command.name), + Select(instanciateAnnotation(mainAnnot, mainAnnotArgss), defn.MainAnnot_command.name), Ident(mainArgsName) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil ) ) diff --git a/tests/run/main-annotation-homemade-annot-2.check b/tests/run/main-annotation-homemade-annot-2.check new file mode 100644 index 000000000000..f57ec79b8dbd --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-2.check @@ -0,0 +1,11 @@ +I was run! +A +I was run! +A +I was run! +A +Here are some colors: +Purple smart, Blue fast, White fashion, Yellow quiet, Orange honest, Pink loud +This will be printed, but nothing more. +This will be printed, but nothing more. +This will be printed, but nothing more. diff --git a/tests/run/main-annotation-homemade-annot-2.scala b/tests/run/main-annotation-homemade-annot-2.scala new file mode 100644 index 000000000000..e030564d313d --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-2.scala @@ -0,0 +1,221 @@ +import scala.collection.mutable +import scala.annotation.MainAnnotation + +@myMain()("A") +def foo1(): Unit = println("I was run!") + +@myMain(0)("This should not be printed") +def foo2() = throw new Exception("This should not be run") + +@myMain(1)("Purple smart", "Blue fast", "White fashion", "Yellow quiet", "Orange honest", "Pink loud") +def foo3() = println("Here are some colors:") + +@myMain()() +def foo4() = println("This will be printed, but nothing more.") + +object Test: + val allClazzes: Seq[Class[?]] = + LazyList.from(1).map(i => scala.util.Try(Class.forName("foo" + i.toString))).takeWhile(_.isSuccess).map(_.get) + + def callMains(): Unit = + for (clazz <- allClazzes) + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, Array[String]()) + + def main(args: Array[String]) = + callMains() +end Test + +/** Code mostly copied from {{scala.main}}. */ +class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: + self => + + private val maxLineLength = 120 + + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] + override type MainResultType = Any + + private enum ArgumentKind { + case SimpleArgument, OptionalArgument, VarArgument + } + + override def command(args: Array[String], commandName: String, docComment: String) = + new MainAnnotation.Command[ArgumentParser, MainResultType]: + private var argNames = new mutable.ArrayBuffer[String] + private var argTypes = new mutable.ArrayBuffer[String] + private var argDocs = new mutable.ArrayBuffer[String] + private var argKinds = new mutable.ArrayBuffer[ArgumentKind] + + /** A buffer for all errors */ + private var errors = new mutable.ArrayBuffer[String] + + /** Issue an error, and return an uncallable getter */ + private def error(msg: String): () => Nothing = + errors += msg + () => throw new AssertionError("trying to get invalid argument") + + /** The next argument index */ + private var argIdx: Int = 0 + + private def argAt(idx: Int): Option[String] = + if idx < args.length then Some(args(idx)) else None + + private def nextPositionalArg(): Option[String] = + while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + val result = argAt(argIdx) + argIdx += 1 + result + + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = + p.fromStringOption(arg) match + case Some(t) => () => t + case None => error(s"invalid argument for $argName: $arg") + + private def argUsage(pos: Int): String = + val name = argNames(pos) + + argKinds(pos) match { + case ArgumentKind.SimpleArgument => s"[--$name] <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[[--$name] <${argTypes(pos)}>]" + case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" + } + + private def wrapLongLine(line: String, maxLength: Int): List[String] = { + def recurse(s: String, acc: Vector[String]): Seq[String] = + val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) + if ((s.length <= maxLength) || (lastSpace < 0)) + acc :+ s + else { + val (shortLine, rest) = s.splitAt(lastSpace) + recurse(rest.trim.nn, acc :+ shortLine) + } + + recurse(line, Vector()).toList + } + + private def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { + def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg :: t, "") => recurse(t, arg, acc) + case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg :: t, l) => recurse(t, arg, acc :+ l) + } + + recurse(argsUsage, "", Vector()).toList + } + + private inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") + + private def usage(): Unit = + val usageBeginning = s"Usage: $commandName " + val argsOffset = usageBeginning.length + val argUsages = wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) + + println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) + + private def explain(): Unit = + if (docComment.nonEmpty) + println(wrapLongLine(docComment, maxLineLength).mkString("\n")) + if (argNames.nonEmpty) { + val argNameShift = 2 + val argDocShift = argNameShift + 2 + + println("Arguments:") + for (pos <- 0 until argNames.length) + val argDoc = StringBuilder(" " * argNameShift) + argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") + + argKinds(pos) match { + case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") + case ArgumentKind.VarArgument => argDoc.append(" (vararg)") + case _ => + } + + if (argDocs(pos).nonEmpty) { + val shiftedDoc = + argDocs(pos).split("\n").nn + .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) + .mkString("\n") + argDoc.append("\n").append(shiftedDoc) + } + + println(argDoc) + } + + private def indicesOfArg(argName: String): Seq[Int] = + def allIndicesOf(s: String): Seq[Int] = + def recurse(s: String, from: Int): Seq[Int] = + val i = args.indexOf(s, from) + if i < 0 then Seq() else i +: recurse(s, i + 1) + + recurse(s, 0) + + val indices = allIndicesOf(s"--$argName") + indices.filter(_ >= 0) + + private def getArgGetter[T](argName: String, getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = + indicesOfArg(argName) match { + case s @ (Seq() | Seq(_)) => + val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) + argOpt match { + case Some(arg) => convert(argName, arg, p) + case None => getDefaultGetter() + } + case s => + val multValues = s.flatMap(idx => argAt(idx + 1)) + error(s"more than one value for $argName: ${multValues.mkString(", ")}") + } + + private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = + argNames += argName + argTypes += argType + argDocs += argDoc + argKinds += argKind + + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) + getArgGetter(argName, () => error(s"missing argument for $argName")) + + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = + registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) + getArgGetter(argName, () => () => defaultValue) + + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) + def remainingArgGetters(): List[() => T] = nextPositionalArg() match + case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case None => Nil + val getters = remainingArgGetters() + () => getters.map(_()) + + override def run(f: => MainResultType): Unit = + def flagUnused(): Unit = nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + for + arg <- args + if arg.startsWith("--") && !argNames.contains(arg.drop(2)) + do + error(s"unknown argument name: $arg") + end flagUnused + + if args.contains("--help") then + usage() + println() + explain() + else + flagUnused() + if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() + else + for (_ <- 1 to runs) + f + if after.length > 0 then println(after.mkString(", ")) + end run + end command +end myMain \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-3.check b/tests/run/main-annotation-homemade-annot-3.check new file mode 100644 index 000000000000..cd0875583aab --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-3.check @@ -0,0 +1 @@ +Hello world! diff --git a/tests/run/main-annotation-homemade-annot-3.scala b/tests/run/main-annotation-homemade-annot-3.scala new file mode 100644 index 000000000000..88b92745e101 --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-3.scala @@ -0,0 +1,25 @@ +import scala.annotation.MainAnnotation + +@mainNoArgs def foo() = println("Hello world!") + +object Test: + def main(args: Array[String]) = + val clazz = Class.forName("foo") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, Array[String]()) +end Test + +class mainNoArgs extends MainAnnotation: + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] + override type MainResultType = Any + + override def command(args: Array[String], commandName: String, docComment: String) = + new MainAnnotation.Command[ArgumentParser, MainResultType]: + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = ??? + + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = ??? + + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? + + override def run(f: => MainResultType): Unit = f + end command \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-4.check b/tests/run/main-annotation-homemade-annot-4.check new file mode 100644 index 000000000000..cd0875583aab --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-4.check @@ -0,0 +1 @@ +Hello world! diff --git a/tests/run/main-annotation-homemade-annot-4.scala b/tests/run/main-annotation-homemade-annot-4.scala new file mode 100644 index 000000000000..a776e16a1b6e --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-4.scala @@ -0,0 +1,25 @@ +import scala.annotation.MainAnnotation + +@mainManyArgs(1, "B", 3) def foo() = println("Hello world!") + +object Test: + def main(args: Array[String]) = + val clazz = Class.forName("foo") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, Array[String]()) +end Test + +class mainManyArgs(i1: Int, s2: String, i3: Int) extends MainAnnotation: + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] + override type MainResultType = Any + + override def command(args: Array[String], commandName: String, docComment: String) = + new MainAnnotation.Command[ArgumentParser, MainResultType]: + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = ??? + + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = ??? + + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? + + override def run(f: => MainResultType): Unit = f + end command \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-5.check b/tests/run/main-annotation-homemade-annot-5.check new file mode 100644 index 000000000000..7d60d6656c81 --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-5.check @@ -0,0 +1,2 @@ +Hello world! +Hello world! diff --git a/tests/run/main-annotation-homemade-annot-5.scala b/tests/run/main-annotation-homemade-annot-5.scala new file mode 100644 index 000000000000..0f9317ccb35f --- /dev/null +++ b/tests/run/main-annotation-homemade-annot-5.scala @@ -0,0 +1,27 @@ +import scala.annotation.MainAnnotation + +@mainManyArgs(Some[Int](1)) def foo() = println("Hello world!") +@mainManyArgs(None) def bar() = println("Hello world!") + +object Test: + def main(args: Array[String]) = + for (methodName <- List("foo", "bar")) + val clazz = Class.forName(methodName) + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, Array[String]()) +end Test + +class mainManyArgs(o: Option[Int]) extends MainAnnotation: + override type ArgumentParser[T] = util.CommandLineParser.FromString[T] + override type MainResultType = Any + + override def command(args: Array[String], commandName: String, docComment: String) = + new MainAnnotation.Command[ArgumentParser, MainResultType]: + override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = ??? + + override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = ??? + + override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? + + override def run(f: => MainResultType): Unit = f + end command \ No newline at end of file From 99d9c123651de75347476733aaf2ef3fbe06e452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 4 Dec 2021 18:23:46 +0100 Subject: [PATCH 085/121] Pass object to argGetter instead of multiple params - Instead of adding to the list of elements when we add functionalities to the getter functions, we now pass an object containing the values. That way, adding an argument is a matter of expanding the class ParameterInfos and adding a few lines in MainProxies --- .../dotty/tools/dotc/ast/MainProxies.scala | 48 +++++++++++++------ .../dotty/tools/dotc/core/Definitions.scala | 4 +- .../src/scala/annotation/MainAnnotation.scala | 10 ++-- library/src/scala/main.scala | 29 +++++------ project/MiMaFilters.scala | 1 + .../main-annotation-homemade-annot-1.scala | 29 +++++------ .../main-annotation-homemade-annot-2.scala | 29 +++++------ .../main-annotation-homemade-annot-3.scala | 6 +-- .../main-annotation-homemade-annot-4.scala | 6 +-- .../main-annotation-homemade-annot-5.scala | 6 +-- 10 files changed, 93 insertions(+), 75 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index a27755782ebb..5d1e558ab598 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -93,7 +93,7 @@ object MainProxies { val argName = mainArgsName ++ n.toString val isRepeated = formal.isRepeatedParam - val hasDefaultValue = defaultValues.contains(n) + val defaultValue = defaultValues.get(n) var argRef: Tree = Apply(Ident(argName), Nil) var formalType = formal @@ -103,26 +103,45 @@ object MainProxies { val getterSym = if isRepeated then - defn.MainAnnotCommand_argsGetter - else if hasDefaultValue then - defn.MainAnnotCommand_argGetterDefault + defn.MainAnnotCommand_varargGetter else defn.MainAnnotCommand_argGetter - val getterArgs = { + val parameterInfos = { val param = paramName.toString - val optionDefaultValueTree = defaultValues.get(n) - - lit(param) - :: lit(formalType.show) - :: lit(documentation.argDocs(param)) - :: optionDefaultValueTree.map(List(_)).getOrElse(Nil) + val paramInfosName = argName ++ "paramInfos" + val paramInfosIdent = Ident(paramInfosName) + val docTree = documentation.argDocs.get(param) match { + case Some(doc) => Apply(ref(defn.SomeClass.companionModule.termRef), lit(doc)) + case None => ref(defn.NoneModule.termRef) + } + val paramInfosTree = New( + AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), + List(List(lit(param), lit(formalType.show), docTree)) + ) + + var assignations: List[(String, Tree)] = Nil + + if defaultValue.nonEmpty then + assignations = ("defaultValue", defaultValue.get) :: assignations + + val assignationsTrees = assignations.map{ + case (name, value) => + val opt = Apply(ref(defn.SomeClass.companionModule.termRef), value) + Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), opt) + } + + if assignations.isEmpty then + paramInfosTree + else + val paramInfosInstance = ValDef(paramInfosName, TypeTree(), paramInfosTree) + Block(paramInfosInstance :: assignationsTrees, paramInfosIdent) } val argDef = ValDef( argName, TypeTree(), - Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), getterArgs), + Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), parameterInfos), ) (argRef, argDef) @@ -152,6 +171,7 @@ object MainProxies { } recurse(annot.tree, Nil) + end extractAnnotationArgs var result: List[TypeDef] = Nil if (!mainFun.owner.isStaticOwner) @@ -216,7 +236,7 @@ object MainProxies { lazy val argDocs: Map[String, String] = _argDocs private var _mainDoc: String = "" - private var _argDocs: Map[String, String] = Map().withDefaultValue("") + private var _argDocs: Map[String, String] = Map() docComment match { case Some(comment) => if comment.isDocComment then parseDocComment(comment.raw) else _mainDoc = comment.raw @@ -247,6 +267,6 @@ object MainProxies { val argsCommentsSpans: Map[String, (Int, Int)] = paramDocs(raw, "@param", tidx) val argsCommentsTextSpans = argsCommentsSpans.view.mapValues(extractSectionText(raw, _)) val argsCommentsTexts = argsCommentsTextSpans.mapValues({ case (beg, end) => raw.substring(beg, end) }) - _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).toMap.withDefaultValue("") + _argDocs = argsCommentsTexts.mapValues(cleanComment(_)).toMap end Documentation } diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index cd714e592a1b..6973573209e6 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -855,10 +855,10 @@ class Definitions { @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.annotation.MainAnnotation") @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") + @tu lazy val MainAnnotParameterInfos: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterInfos") @tu lazy val MainAnnotCommand: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Command") @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") - @tu lazy val MainAnnotCommand_argGetterDefault: Symbol = MainAnnotCommand.requiredMethod("argGetterDefault") - @tu lazy val MainAnnotCommand_argsGetter: Symbol = MainAnnotCommand.requiredMethod("argsGetter") + @tu lazy val MainAnnotCommand_varargGetter: Symbol = MainAnnotCommand.requiredMethod("varargGetter") @tu lazy val MainAnnotCommand_run: Symbol = MainAnnotCommand.requiredMethod("run") @tu lazy val TupleTypeRef: TypeRef = requiredClassRef("scala.Tuple") diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index af0e372b4e1a..f2ddcb793eac 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -25,17 +25,17 @@ trait MainAnnotation extends StaticAnnotation: end MainAnnotation object MainAnnotation: + class ParameterInfos[T](var name: String, var typeName: String, var doc: Option[String]): + var defaultValue: Option[T] = None + /** A class representing a command to run */ trait Command[ArgumentParser[_], MainResultType]: /** The getter for the next argument of type `T` */ - def argGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => T - - /** The getter for the next argument of type `T` with a default value */ - def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using fromString: ArgumentParser[T]): () => T + def argGetter[T](paramInfos: ParameterInfos[T])(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def argsGetter[T](argName: String, argType: String, argDoc: String)(using fromString: ArgumentParser[T]): () => Seq[T] + def varargGetter[T](paramInfos: ParameterInfos[T])(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 1e2bfe12e85a..04c2446f5068 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -155,24 +155,25 @@ final class main(maxLineLength: Int) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = - argNames += argName - argTypes += argType - argDocs += argDoc + private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = + argNames += paramInfos.name + argTypes += paramInfos.typeName + argDocs += paramInfos.doc.getOrElse("") argKinds += argKind - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) - getArgGetter(argName, () => error(s"missing argument for $argName")) - - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) - getArgGetter(argName, () => () => defaultValue) + override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + val name = paramInfos.name + val (defaultGetter, argumentKind) = paramInfos.defaultValue match { + case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) + case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) + } + registerArg(paramInfos, argumentKind) + getArgGetter(name, defaultGetter) - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) + override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index bbe383dadc09..9cff99a2edad 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -12,6 +12,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterInfos"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.init"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.last"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"), diff --git a/tests/run/main-annotation-homemade-annot-1.scala b/tests/run/main-annotation-homemade-annot-1.scala index 6dca3e883dde..1579b75071d7 100644 --- a/tests/run/main-annotation-homemade-annot-1.scala +++ b/tests/run/main-annotation-homemade-annot-1.scala @@ -161,24 +161,25 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = - argNames += argName - argTypes += argType - argDocs += argDoc + private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = + argNames += paramInfos.name + argTypes += paramInfos.typeName + argDocs += paramInfos.doc.getOrElse("") argKinds += argKind - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) - getArgGetter(argName, () => error(s"missing argument for $argName")) - - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) - getArgGetter(argName, () => () => defaultValue) + override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + val name = paramInfos.name + val (defaultGetter, argumentKind) = paramInfos.defaultValue match { + case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) + case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) + } + registerArg(paramInfos, argumentKind) + getArgGetter(name, defaultGetter) - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) + override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) diff --git a/tests/run/main-annotation-homemade-annot-2.scala b/tests/run/main-annotation-homemade-annot-2.scala index e030564d313d..643a98eaccd5 100644 --- a/tests/run/main-annotation-homemade-annot-2.scala +++ b/tests/run/main-annotation-homemade-annot-2.scala @@ -168,24 +168,25 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(argName: String, argType: String, argDoc: String, argKind: ArgumentKind): Unit = - argNames += argName - argTypes += argType - argDocs += argDoc + private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = + argNames += paramInfos.name + argTypes += paramInfos.typeName + argDocs += paramInfos.doc.getOrElse("") argKinds += argKind - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.SimpleArgument) - getArgGetter(argName, () => error(s"missing argument for $argName")) - - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = - registerArg(argName, argType, argDoc, ArgumentKind.OptionalArgument) - getArgGetter(argName, () => () => defaultValue) + override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + val name = paramInfos.name + val (defaultGetter, argumentKind) = paramInfos.defaultValue match { + case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) + case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) + } + registerArg(paramInfos, argumentKind) + getArgGetter(name, defaultGetter) - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(argName, argType, argDoc, ArgumentKind.VarArgument) + override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(argName, arg, p) :: remainingArgGetters() + case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) diff --git a/tests/run/main-annotation-homemade-annot-3.scala b/tests/run/main-annotation-homemade-annot-3.scala index 88b92745e101..3c32f57415af 100644 --- a/tests/run/main-annotation-homemade-annot-3.scala +++ b/tests/run/main-annotation-homemade-annot-3.scala @@ -15,11 +15,9 @@ class mainNoArgs extends MainAnnotation: override def command(args: Array[String], commandName: String, docComment: String) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = ??? + override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = ??? - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = ??? - - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? + override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = ??? override def run(f: => MainResultType): Unit = f end command \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-4.scala b/tests/run/main-annotation-homemade-annot-4.scala index a776e16a1b6e..baf2fc267861 100644 --- a/tests/run/main-annotation-homemade-annot-4.scala +++ b/tests/run/main-annotation-homemade-annot-4.scala @@ -15,11 +15,9 @@ class mainManyArgs(i1: Int, s2: String, i3: Int) extends MainAnnotation: override def command(args: Array[String], commandName: String, docComment: String) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = ??? + override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = ??? - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = ??? - - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? + override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = ??? override def run(f: => MainResultType): Unit = f end command \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-5.scala b/tests/run/main-annotation-homemade-annot-5.scala index 0f9317ccb35f..843545f43cb9 100644 --- a/tests/run/main-annotation-homemade-annot-5.scala +++ b/tests/run/main-annotation-homemade-annot-5.scala @@ -17,11 +17,9 @@ class mainManyArgs(o: Option[Int]) extends MainAnnotation: override def command(args: Array[String], commandName: String, docComment: String) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - override def argGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => T = ??? + override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = ??? - override def argGetterDefault[T](argName: String, argType: String, argDoc: String, defaultValue: => T)(using p: ArgumentParser[T]): () => T = ??? - - override def argsGetter[T](argName: String, argType: String, argDoc: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? + override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = ??? override def run(f: => MainResultType): Unit = f end command \ No newline at end of file From 282bfc61828307dd20b417665bf46ff38785d32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 4 Dec 2021 18:55:06 +0100 Subject: [PATCH 086/121] Move code around for readability --- .../dotty/tools/dotc/ast/MainProxies.scala | 131 +++++++++--------- 1 file changed, 64 insertions(+), 67 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 5d1e558ab598..1d69ca0cde4d 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -83,77 +83,65 @@ object MainProxies { inline def lit(any: Any): Literal = Literal(Constant(any)) def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = - if (mt.isImplicitMethod) { - report.error(s"main method cannot have implicit parameters", pos) - Nil - } - else { - var valArgs: List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { - case ((formal, paramName), n) => - val argName = mainArgsName ++ n.toString + mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { + case ((formal, paramName), n) => + val argName = mainArgsName ++ n.toString - val isRepeated = formal.isRepeatedParam - val defaultValue = defaultValues.get(n) + val isRepeated = formal.isRepeatedParam + val defaultValue = defaultValues.get(n) - var argRef: Tree = Apply(Ident(argName), Nil) - var formalType = formal + var argRef: Tree = Apply(Ident(argName), Nil) + var formalType = formal + if isRepeated then + argRef = repeated(argRef) + formalType = formalType.argTypes.head + + val getterSym = if isRepeated then - argRef = repeated(argRef) - formalType = formalType.argTypes.head - - val getterSym = - if isRepeated then - defn.MainAnnotCommand_varargGetter - else - defn.MainAnnotCommand_argGetter - - val parameterInfos = { - val param = paramName.toString - val paramInfosName = argName ++ "paramInfos" - val paramInfosIdent = Ident(paramInfosName) - val docTree = documentation.argDocs.get(param) match { - case Some(doc) => Apply(ref(defn.SomeClass.companionModule.termRef), lit(doc)) - case None => ref(defn.NoneModule.termRef) - } - val paramInfosTree = New( - AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), - List(List(lit(param), lit(formalType.show), docTree)) - ) - - var assignations: List[(String, Tree)] = Nil - - if defaultValue.nonEmpty then - assignations = ("defaultValue", defaultValue.get) :: assignations - - val assignationsTrees = assignations.map{ - case (name, value) => - val opt = Apply(ref(defn.SomeClass.companionModule.termRef), value) - Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), opt) - } - - if assignations.isEmpty then - paramInfosTree - else - val paramInfosInstance = ValDef(paramInfosName, TypeTree(), paramInfosTree) - Block(paramInfosInstance :: assignationsTrees, paramInfosIdent) + defn.MainAnnotCommand_varargGetter + else + defn.MainAnnotCommand_argGetter + + val parameterInfos = { + val param = paramName.toString + val paramInfosName = argName ++ "paramInfos" + val paramInfosIdent = Ident(paramInfosName) + val docTree = documentation.argDocs.get(param) match { + case Some(doc) => Apply(ref(defn.SomeClass.companionModule.termRef), lit(doc)) + case None => ref(defn.NoneModule.termRef) } - - val argDef = ValDef( - argName, - TypeTree(), - Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), parameterInfos), + val paramInfosTree = New( + AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), + List(List(lit(param), lit(formalType.show), docTree)) ) - (argRef, argDef) - } - mt.resType match { - case restpe: MethodType => - report.error(s"main method cannot be curried", pos) - Nil - case _ => - valArgs - } + var assignations: List[(String, Tree)] = Nil + + if defaultValue.nonEmpty then + assignations = ("defaultValue", defaultValue.get) :: assignations + + val assignationsTrees = assignations.map{ + case (name, value) => + val opt = Apply(ref(defn.SomeClass.companionModule.termRef), value) + Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), opt) + } + + if assignations.isEmpty then + paramInfosTree + else + val paramInfosInstance = ValDef(paramInfosName, TypeTree(), paramInfosTree) + Block(paramInfosInstance :: assignationsTrees, paramInfosIdent) + } + + val argDef = ValDef( + argName, + TypeTree(), + Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), parameterInfos), + ) + + (argRef, argDef) } + end createArgs inline def instanciateAnnotation(annot: Annotation, argss: List[List[Tree]]): Tree = New(TypeTree(annot.symbol.typeRef), argss) @@ -192,9 +180,18 @@ object MainProxies { mainFun.info match { case _: ExprType => case mt: MethodType => - val (argRefs, argVals) = createArgs(mt, cmdName).unzip - args = argVals - mainCall = Apply(mainCall, argRefs) + if (mt.isImplicitMethod) { + report.error(s"main method cannot have implicit parameters", pos) + } + else mt.resType match { + case restpe: MethodType => + report.error(s"main method cannot be curried", pos) + Nil + case _ => + val (argRefs, argVals) = createArgs(mt, cmdName).unzip + args = argVals + mainCall = Apply(mainCall, argRefs) + } case _: PolyType => report.error(s"main method cannot have type parameters", pos) case _ => From 0d1d1f986c278e020d6a2a169aea1c9ecd835b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 4 Dec 2021 18:57:07 +0100 Subject: [PATCH 087/121] Fix symbol error in Documentation --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 2 +- tests/run/main-annotation-help.scala | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 1d69ca0cde4d..0aa410a3a4ca 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -250,7 +250,7 @@ object MainProxies { case (s1, s2) if s1.last == '\n' => s1 + s2 case (s1, s2) => s1 + ' ' + s2 } - s.replaceAll(raw"\{\{", "").replaceAll(raw"\}\}", "").trim + s.replaceAll(raw"\[\[", "").replaceAll(raw"\]\]", "").trim private def parseDocComment(raw: String): Unit = // Positions of the sections (@) in the docstring diff --git a/tests/run/main-annotation-help.scala b/tests/run/main-annotation-help.scala index 91ded60de24d..cef057789f25 100644 --- a/tests/run/main-annotation-help.scala +++ b/tests/run/main-annotation-help.scala @@ -69,12 +69,12 @@ object myProgram: @main def doc8(num: Int, inc: Int): Unit = () /** - * Adds two numbers. Same as {{doc1}}. + * Adds two numbers. Same as [[doc1]]. * * @param num the first number * @param inc the second number * @return the sum of the two numbers (not really) - * @see {{doc1}} + * @see [[doc1]] */ @main def doc9(num: Int, inc: Int): Unit = () @@ -131,14 +131,14 @@ object myProgram: ): Unit = () /** - * Adds two instances of {{MyNumber}}. + * Adds two instances of [[MyNumber]]. * @param myNum my first number to add * @param myInc my second number to add */ @main def doc15(myNum: MyNumber, myInc: MyNumber): Unit = () /** - * Compares two instances of {{MyGeneric}}. + * Compares two instances of [[MyGeneric]]. * @param first my first element * @param second my second element */ From c8584df19952e00b3dc832ab0467f17f596e2fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 5 Dec 2021 11:54:49 +0100 Subject: [PATCH 088/121] Add main.Arg annotation for argument-related parameters - Add annotation for main method arguments - Add support for alternative names and short names --- .../dotty/tools/dotc/ast/MainProxies.scala | 53 +++++++----- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../src/scala/annotation/MainAnnotation.scala | 4 + library/src/scala/main.scala | 70 ++++++++++++---- project/MiMaFilters.scala | 5 +- ...nnotation-param-annot-invalid-params.check | 2 + ...nnotation-param-annot-invalid-params.scala | 37 +++++++++ tests/run/main-annotation-param-annot.check | 41 ++++++++++ tests/run/main-annotation-param-annot.scala | 80 +++++++++++++++++++ .../main-annotation-wrong-param-names.check | 5 +- 10 files changed, 258 insertions(+), 40 deletions(-) create mode 100644 tests/run/main-annotation-param-annot-invalid-params.check create mode 100644 tests/run/main-annotation-param-annot-invalid-params.scala create mode 100644 tests/run/main-annotation-param-annot.check create mode 100644 tests/run/main-annotation-param-annot.scala diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 0aa410a3a4ca..3b9116f57607 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -51,12 +51,21 @@ object MainProxies { case _ => Map[Int, Tree]() } - def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Map[Int, Tree], Option[Comment])] = stats.flatMap { + def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Vector[Option[Annotation]], Map[Int, Tree], Option[Comment])] = stats.flatMap { case stat: DefDef => val sym = stat.symbol sym.annotations.filter(_.matches(defn.MainAnnot)) match { - case Nil => Nil - case _ :: Nil => (sym, defaultValues(scope, sym), stat.rawComment) :: Nil + case Nil => + Nil + case _ :: Nil => + val paramAnnotations = stat.paramss.flatMap(_.map( + valdef => valdef.symbol.annotations.filter(_.matches(defn.MainAnnotParameterAnnotation)) match { + case Nil => None + case paramAnnot :: Nil => Some(paramAnnot) + case paramAnnot :: others => report.error(s"parameters cannot have multiple annotations", paramAnnot.tree); None + } + )) + (sym, paramAnnotations.toVector, defaultValues(scope, sym), stat.rawComment) :: Nil case mainAnnot :: others => report.error(s"method cannot have multiple main annotations", mainAnnot.tree) Nil @@ -72,7 +81,7 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol, defaultValues: Map[Int, Tree], docComment: Option[Comment])(using Context): List[TypeDef] = { + def mainProxy(mainFun: Symbol, paramAnnotations: Vector[Option[Annotation]], defaultValues: Map[Int, Tree], docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args @@ -120,6 +129,9 @@ object MainProxies { if defaultValue.nonEmpty then assignations = ("defaultValue", defaultValue.get) :: assignations + if paramAnnotations(n).nonEmpty then + assignations = ("annotation", instanciateAnnotation(paramAnnotations(n).get)) :: assignations + val assignationsTrees = assignations.map{ case (name, value) => val opt = Apply(ref(defn.SomeClass.companionModule.termRef), value) @@ -143,34 +155,35 @@ object MainProxies { } end createArgs - inline def instanciateAnnotation(annot: Annotation, argss: List[List[Tree]]): Tree = New(TypeTree(annot.symbol.typeRef), argss) + def instanciateAnnotation(annot: Annotation): Tree = + val argss = { + def recurse(t: Tree, acc: List[List[Tree]]): List[List[Tree]] = t match { + case Apply(t, args: List[Tree]) => recurse(t, extractArgs(args) :: acc) + case _ => acc + } - def extractAnnotationArgs(annot: Annotation): List[List[Tree]] = - def recurse(t: Tree, acc: List[List[Tree]]): List[List[Tree]] = t match { - case Apply(t, args: List[Tree]) => recurse(t, extractArgs(args) :: acc) - case _ => acc - } + def extractArgs(args: List[Tree]): List[Tree] = + args.flatMap { + case Typed(SeqLiteral(varargs, _), _) => varargs + case arg @ Select(_, name) => if name.is(DefaultGetterName) then List() else List(arg) + case arg => List(arg) + } - def extractArgs(args: List[Tree]): List[Tree] = - args.flatMap { - case Typed(SeqLiteral(varargs, _), _) => varargs - case arg @ Select(_, name) => if name.is(DefaultGetterName) then List() else List(arg) - case arg => List(arg) - } + recurse(annot.tree, Nil) + } - recurse(annot.tree, Nil) - end extractAnnotationArgs + New(TypeTree(annot.symbol.typeRef), argss) + end instanciateAnnotation var result: List[TypeDef] = Nil if (!mainFun.owner.isStaticOwner) report.error(s"main method is not statically accessible", pos) else { - val mainAnnotArgss = extractAnnotationArgs(mainAnnot) val cmd = ValDef( cmdName, TypeTree(), Apply( - Select(instanciateAnnotation(mainAnnot, mainAnnotArgss), defn.MainAnnot_command.name), + Select(instanciateAnnotation(mainAnnot), defn.MainAnnot_command.name), Ident(mainArgsName) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil ) ) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 6973573209e6..e31b08db8753 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -856,6 +856,7 @@ class Definitions { @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.annotation.MainAnnotation") @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") @tu lazy val MainAnnotParameterInfos: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterInfos") + @tu lazy val MainAnnotParameterAnnotation: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterAnnotation") @tu lazy val MainAnnotCommand: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Command") @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") @tu lazy val MainAnnotCommand_varargGetter: Symbol = MainAnnotCommand.requiredMethod("varargGetter") diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index f2ddcb793eac..d731b8584083 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -27,6 +27,7 @@ end MainAnnotation object MainAnnotation: class ParameterInfos[T](var name: String, var typeName: String, var doc: Option[String]): var defaultValue: Option[T] = None + var annotation: Option[ParameterAnnotation] = None /** A class representing a command to run */ trait Command[ArgumentParser[_], MainResultType]: @@ -42,4 +43,7 @@ object MainAnnotation: */ def run(program: => MainResultType): Unit end Command + + /** An annotation for the parameters of a MainAnnotated method. */ + trait ParameterAnnotation extends StaticAnnotation end MainAnnotation diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 04c2446f5068..af8cf11e1112 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -9,13 +9,14 @@ package scala import collection.mutable -import annotation.MainAnnotation +import annotation._ /** An annotation that designates a main function */ final class main(maxLineLength: Int) extends MainAnnotation: self => import main._ + import MainAnnotation._ def this() = this(120) @@ -27,8 +28,9 @@ final class main(maxLineLength: Int) extends MainAnnotation: } override def command(args: Array[String], commandName: String, docComment: String) = - new MainAnnotation.Command[ArgumentParser, MainResultType]: + new Command[ArgumentParser, MainResultType]: private var argNames = new mutable.ArrayBuffer[String] + private var argShortNames = new mutable.ArrayBuffer[Option[Char]] private var argTypes = new mutable.ArrayBuffer[String] private var argDocs = new mutable.ArrayBuffer[String] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] @@ -47,12 +49,22 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def argAt(idx: Int): Option[String] = if idx < args.length then Some(args(idx)) else None + private def isArgNameAt(idx: Int): Boolean = + val arg = args(argIdx) + val isFullName = arg.startsWith("--") + val isShortName = arg.startsWith("-") && arg.length == 2 && shortNameIsValid(arg(1)) + + isFullName || isShortName + private def nextPositionalArg(): Option[String] = - while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + while argIdx < args.length && isArgNameAt(argIdx) do argIdx += 2 val result = argAt(argIdx) argIdx += 1 result + private def shortNameIsValid(shortName: Char): Boolean = + shortName == 0 || shortName.isLetter + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match case Some(t) => () => t @@ -60,10 +72,11 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def argUsage(pos: Int): String = val name = argNames(pos) + val namePrint = argShortNames(pos).map(short => s"[-$short | --$name]").getOrElse(s"[--$name]") argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"[--$name] <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[[--$name] <${argTypes(pos)}>]" + case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[$namePrint <${argTypes(pos)}>]" case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" } @@ -131,7 +144,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: println(argDoc) } - private def indicesOfArg(argName: String): Seq[Int] = + private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = def allIndicesOf(s: String): Seq[Int] = def recurse(s: String, from: Int): Seq[Int] = val i = args.indexOf(s, from) @@ -140,10 +153,12 @@ final class main(maxLineLength: Int) extends MainAnnotation: recurse(s, 0) val indices = allIndicesOf(s"--$argName") - indices.filter(_ >= 0) + val indicesShort = shortArgName.map(shortName => allIndicesOf(s"-$shortName")).getOrElse(Seq()) + (indices ++: indicesShort).filter(_ >= 0) - private def getArgGetter[T](argName: String, getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = - indicesOfArg(argName) match { + private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = + val argName = getEffectiveName(paramInfos) + indicesOfArg(argName, getShortName(paramInfos)) match { case s @ (Seq() | Seq(_)) => val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) argOpt match { @@ -155,30 +170,51 @@ final class main(maxLineLength: Int) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = - argNames += paramInfos.name + private def getAnnotationData[T](paramInfos: ParameterInfos[_], extractor: Arg => T): Option[T] = + paramInfos.annotation match { + case Some(annot: Arg) => Some(extractor(annot)) + case _ => None + } + + private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = + getAnnotationData(paramInfos, _.name).filter(_.length > 0).getOrElse(paramInfos.name) + + private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = + getAnnotationData(paramInfos, _.shortName).filterNot(_ == 0) + + private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = + argNames += getEffectiveName(paramInfos) argTypes += paramInfos.typeName argDocs += paramInfos.doc.getOrElse("") argKinds += argKind - override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val name = paramInfos.name + val shortName = getShortName(paramInfos) + if shortName.exists(c => !shortNameIsValid(c)) then throw IllegalArgumentException(s"Invalid short name: -${shortName.get}") + argShortNames += shortName + + override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + val name = getEffectiveName(paramInfos) val (defaultGetter, argumentKind) = paramInfos.defaultValue match { case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) } registerArg(paramInfos, argumentKind) - getArgGetter(name, defaultGetter) + getArgGetter(paramInfos, defaultGetter) - override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() + case Some(arg) => convert(getEffectiveName(paramInfos), arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) override def run(f: => MainResultType): Unit = + def checkShortNamesUnique(): Unit = + val shortNameToIndices = argShortNames.collect{ case Some(short) => short }.zipWithIndex.groupBy(_._1).view.mapValues(_.map(_._2)) + for ((shortName, indices) <- shortNameToIndices if indices.length > 1) + error(s"$shortName is used as short name for multiple parameters: ${indices.map(idx => argNames(idx)).mkString(", ")}") + def flagUnused(): Unit = nextPositionalArg() match case Some(arg) => error(s"unused argument: $arg") @@ -197,6 +233,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: explain() else flagUnused() + checkShortNamesUnique() if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() @@ -207,4 +244,5 @@ final class main(maxLineLength: Int) extends MainAnnotation: end main object main: + final class Arg(val name: String = "", val shortName: Char = 0) extends MainAnnotation.ParameterAnnotation end main diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 9cff99a2edad..ffb317613905 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -6,13 +6,16 @@ object MiMaFilters { // Experimental APIs that can be added in 3.2.0 ProblemFilters.exclude[FinalClassProblem]("scala.main"), ProblemFilters.exclude[MissingTypesProblem]("scala.main"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.this"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$Arg"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$Arg$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterInfos"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$ParameterAnnotation"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.init"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.last"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.append"), diff --git a/tests/run/main-annotation-param-annot-invalid-params.check b/tests/run/main-annotation-param-annot-invalid-params.check new file mode 100644 index 000000000000..2c94e4837100 --- /dev/null +++ b/tests/run/main-annotation-param-annot-invalid-params.check @@ -0,0 +1,2 @@ +OK +OK diff --git a/tests/run/main-annotation-param-annot-invalid-params.scala b/tests/run/main-annotation-param-annot-invalid-params.scala new file mode 100644 index 000000000000..ea3d0b1cd9d2 --- /dev/null +++ b/tests/run/main-annotation-param-annot-invalid-params.scala @@ -0,0 +1,37 @@ +import java.lang.reflect.InvocationTargetException + +object myProgram: + + @main def space( + @main.Arg(shortName = ' ') i: Int, + ): Unit = () + + @main def nonLetter( + @main.Arg(shortName = '1') i: Int, + ): Unit = () + +end myProgram + +object Test: + def hasCauseIllegalArgumentException(e: Throwable): Boolean = + e.getCause match { + case null => false + case _: IllegalArgumentException => true + case e: Throwable => hasCauseIllegalArgumentException(e) + } + + def callMain(className: String, args: Array[String]) = + val clazz = Class.forName(className) + val method = clazz.getMethod("main", classOf[Array[String]]) + try { + method.invoke(null, args) + println(s"Calling $className should result in an IllegalArgumentException being thrown") + } + catch { + case e: InvocationTargetException if hasCauseIllegalArgumentException(e) => println("OK") + } + + def main(args: Array[String]): Unit = + callMain("space", Array("3")) + callMain("nonLetter", Array("3")) +end Test \ No newline at end of file diff --git a/tests/run/main-annotation-param-annot.check b/tests/run/main-annotation-param-annot.check new file mode 100644 index 000000000000..f4063ebda9e3 --- /dev/null +++ b/tests/run/main-annotation-param-annot.check @@ -0,0 +1,41 @@ +2 + 3 = 5 +Error: missing argument for myNum +Error: unknown argument name: --num +Usage: altName1 [--myNum] [--inc] +2 + 3 = 5 +Error: missing argument for myNum +Error: missing argument for myInc +Error: unknown argument name: --num +Error: unknown argument name: --inc +Usage: altName2 [--myNum] [--myInc] +Error: missing argument for myInc +Error: unknown argument name: --inc +Usage: altName2 [--myNum] [--myInc] +Error: missing argument for myNum +Error: unknown argument name: --num +Usage: altName2 [--myNum] [--myInc] +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +Error: missing argument for myNum +Error: missing argument for myInc +Error: unknown argument name: --num +Error: unknown argument name: --inc +Usage: mix [-n | --myNum] [-i | --myInc] +Error: missing argument for myInc +Error: unknown argument name: --inc +Usage: mix [-n | --myNum] [-i | --myInc] +Error: missing argument for myNum +Error: unknown argument name: --num +Usage: mix [-n | --myNum] [-i | --myInc] +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +Error: n is used as short name for multiple parameters: num, inc +Usage: multipleSameShortName [-n | --num] [-n | --inc] diff --git a/tests/run/main-annotation-param-annot.scala b/tests/run/main-annotation-param-annot.scala new file mode 100644 index 000000000000..13a550613d49 --- /dev/null +++ b/tests/run/main-annotation-param-annot.scala @@ -0,0 +1,80 @@ +object myProgram: + @main def noParamAnnotArgs( + @main.Arg() num: Int, + @main.Arg() inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def altName1( + @main.Arg(name = "myNum") num: Int, + inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def altName2( + @main.Arg(name = "myNum") num: Int, + @main.Arg(name = "myInc") inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def shortName1( + @main.Arg(shortName = 'n') num: Int, + inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def shortName2( + @main.Arg(shortName = 'n') num: Int, + @main.Arg(shortName = 'i') inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def mix( + @main.Arg(name = "myNum", shortName = 'n') num: Int, + @main.Arg(shortName = 'i', name = "myInc") inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def multipleSameShortName( + @main.Arg(shortName = 'n') num: Int, + @main.Arg(shortName = 'n') inc: Int + ): Unit = () +end myProgram + + +object Test: + def callMain(className: String, args: Array[String]) = + val clazz = Class.forName(className) + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain("noParamAnnotArgs", Array("--num", "2", "--inc", "3")) + + callMain("altName1", Array("--num", "2", "--inc", "3")) + callMain("altName1", Array("--myNum", "2", "--inc", "3")) + + callMain("altName2", Array("--num", "2", "--inc", "3")) + callMain("altName2", Array("--myNum", "2", "--inc", "3")) + callMain("altName2", Array("--num", "2", "--myInc", "3")) + callMain("altName2", Array("--myNum", "2", "--myInc", "3")) + + callMain("shortName1", Array("--num", "2", "--inc", "3")) + callMain("shortName1", Array("-n", "2", "--inc", "3")) + + callMain("shortName2", Array("--num", "2", "--inc", "3")) + callMain("shortName2", Array("-n", "2", "--inc", "3")) + callMain("shortName2", Array("--num", "2", "-i", "3")) + callMain("shortName2", Array("-n", "2", "-i", "3")) + + callMain("mix", Array("--num", "2", "--inc", "3")) + callMain("mix", Array("-n", "2", "--inc", "3")) + callMain("mix", Array("--num", "2", "-i", "3")) + callMain("mix", Array("-n", "2", "-i", "3")) + callMain("mix", Array("--myNum", "2", "--myInc", "3")) + callMain("mix", Array("-n", "2", "--myInc", "3")) + callMain("mix", Array("--myNum", "2", "-i", "3")) + callMain("mix", Array("-n", "2", "-i", "3")) + + callMain("multipleSameShortName", Array("2", "3")) +end Test \ No newline at end of file diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check index c8e2330bc99d..6b5c3fb2081e 100644 --- a/tests/run/main-annotation-wrong-param-names.check +++ b/tests/run/main-annotation-wrong-param-names.check @@ -1,6 +1,5 @@ -Error: invalid argument for num: -n -Error: unused argument: -i -Error: unused argument: 10 +Error: missing argument for num +Error: missing argument for inc Usage: add [--num] [--inc] Error: missing argument for num Error: missing argument for inc From d1ab4830faf71a8778a6b655fc0cf91fbb04e7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 5 Dec 2021 12:11:42 +0100 Subject: [PATCH 089/121] Move parameter documentation out of constructor --- .../dotty/tools/dotc/ast/MainProxies.scala | 22 ++++++++++--------- .../src/scala/annotation/MainAnnotation.scala | 12 +++++++++- library/src/scala/main.scala | 2 +- .../main-annotation-homemade-annot-1.scala | 2 +- .../main-annotation-homemade-annot-2.scala | 2 +- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 3b9116f57607..21e557a62dc4 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -91,6 +91,8 @@ object MainProxies { inline def lit(any: Any): Literal = Literal(Constant(any)) + inline def some(value: Tree): Tree = Apply(ref(defn.SomeClass.companionModule.termRef), value) + def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => @@ -115,27 +117,27 @@ object MainProxies { val param = paramName.toString val paramInfosName = argName ++ "paramInfos" val paramInfosIdent = Ident(paramInfosName) - val docTree = documentation.argDocs.get(param) match { - case Some(doc) => Apply(ref(defn.SomeClass.companionModule.termRef), lit(doc)) - case None => ref(defn.NoneModule.termRef) - } val paramInfosTree = New( AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), - List(List(lit(param), lit(formalType.show), docTree)) + List(List(lit(param), lit(formalType.show))) ) var assignations: List[(String, Tree)] = Nil if defaultValue.nonEmpty then - assignations = ("defaultValue", defaultValue.get) :: assignations + assignations = ("defaultValue", some(defaultValue.get)) :: assignations if paramAnnotations(n).nonEmpty then - assignations = ("annotation", instanciateAnnotation(paramAnnotations(n).get)) :: assignations + assignations = ("annotation", some(instanciateAnnotation(paramAnnotations(n).get))) :: assignations + + documentation.argDocs.get(param) match { + case Some(doc) => + assignations = ("documentation", some(lit(doc))) :: assignations + case None => + } val assignationsTrees = assignations.map{ - case (name, value) => - val opt = Apply(ref(defn.SomeClass.companionModule.termRef), value) - Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), opt) + case (name, value) => Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), value) } if assignations.isEmpty then diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index d731b8584083..5fbe6c9e21a8 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -25,8 +25,18 @@ trait MainAnnotation extends StaticAnnotation: end MainAnnotation object MainAnnotation: - class ParameterInfos[T](var name: String, var typeName: String, var doc: Option[String]): + /** + * The information related to one of the parameters of the annotated method. + * @param name the name of the parameter + * @param typeName the name of the parameter's type + * @tparam T the type of the parameter + */ + class ParameterInfos[T](var name: String, var typeName: String): + /** The docstring of the parameter. Defaults to None. */ + var documentation: Option[String] = None + /** The default value that the parameter has. Defaults to None. */ var defaultValue: Option[T] = None + /** If there is one, the ParameterAnnotation associated with the parameter. Defaults to None. */ var annotation: Option[ParameterAnnotation] = None /** A class representing a command to run */ diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index af8cf11e1112..6e6139aafa06 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -185,7 +185,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = argNames += getEffectiveName(paramInfos) argTypes += paramInfos.typeName - argDocs += paramInfos.doc.getOrElse("") + argDocs += paramInfos.documentation.getOrElse("") argKinds += argKind val shortName = getShortName(paramInfos) diff --git a/tests/run/main-annotation-homemade-annot-1.scala b/tests/run/main-annotation-homemade-annot-1.scala index 1579b75071d7..a1f65f064f87 100644 --- a/tests/run/main-annotation-homemade-annot-1.scala +++ b/tests/run/main-annotation-homemade-annot-1.scala @@ -164,7 +164,7 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = argNames += paramInfos.name argTypes += paramInfos.typeName - argDocs += paramInfos.doc.getOrElse("") + argDocs += paramInfos.documentation.getOrElse("") argKinds += argKind override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = diff --git a/tests/run/main-annotation-homemade-annot-2.scala b/tests/run/main-annotation-homemade-annot-2.scala index 643a98eaccd5..bfbeb0fbfbb8 100644 --- a/tests/run/main-annotation-homemade-annot-2.scala +++ b/tests/run/main-annotation-homemade-annot-2.scala @@ -171,7 +171,7 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = argNames += paramInfos.name argTypes += paramInfos.typeName - argDocs += paramInfos.doc.getOrElse("") + argDocs += paramInfos.documentation.getOrElse("") argKinds += argKind override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = From f2b5287ee600ac7c76d4e3bf2385280c29145a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 5 Dec 2021 16:25:32 +0100 Subject: [PATCH 090/121] Update example in MainProxies --- .../dotty/tools/dotc/ast/MainProxies.scala | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 21e557a62dc4..0e8594cba020 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -11,28 +11,40 @@ import NameKinds.DefaultGetterName import Annotations.Annotation /** Generate proxy classes for main functions. - * A function like - * - * /** - * * Lorem ipsum dolor sit amet - * * consectetur adipiscing elit. - * * - * * @param x my param x - * * @param ys all my params y - * */ - * @main(80) def f(x: S, ys: T*) = ... - * - * would be translated to something like - * - * final class f { - * @static def main(args: Array[String]): Unit = - * val cmd: MainAnnotation#Command[..., ...] = - * (new scala.main(80)).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") - * val arg1: () => S = cmd.argGetter[S]("x", "S", "my param x") - * val arg2: () => Seq[T] = cmd.argsGetter[T]("ys", "T", "all my params y") - * cmd.run(f(arg1(), arg2()*)) - * } - */ + * A function like + * + * /** + * * Lorem ipsum dolor sit amet + * * consectetur adipiscing elit. + * * + * * @param x my param x + * * @param ys all my params y + * */ + * @main(80) def f(@main.arg(shortName = 'x', name = "myX") x: S, ys: T*) = ... + * + * would be translated to something like + * + * final class f { + * static def main(args: Array[String]): Unit = { + * val cmd = new main(80).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") + * + * val args0: () => S = cmd.argGetter[S]({ + * val args0paramInfos = new scala.annotation.MainAnnotation.ParameterInfos[S]("x", "S") + * args0paramInfos.documentation = Some("my param x") + * args0paramInfos.annotation = Some(new scala.main.arg(name = "myX", shortName = 'x')) + * args0paramInfos + * })(util.CommandLineParser.FromString.given_FromString_Int) + * + * val args1: () => Seq[T] = cmd.varargGetter[T]({ + * val args1paramInfos = new scala.annotation.MainAnnotation.ParameterInfos[T]("ys", "T") + * args1paramInfos.documentation = Some("all my params y") + * args1paramInfos + * })(util.CommandLineParser.FromString.given_FromString_String) + * + * cmd.run(f(args0.apply(), args1.apply()*)) + * } + * } + */ object MainProxies { def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { From 635b79e7bf7bac703ea06ca0dfee548fd54e3a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 5 Dec 2021 16:48:02 +0100 Subject: [PATCH 091/121] Simplify code for assignations --- .../src/dotty/tools/dotc/ast/MainProxies.scala | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 0e8594cba020..fe10c0c73591 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -135,18 +135,12 @@ object MainProxies { ) var assignations: List[(String, Tree)] = Nil - - if defaultValue.nonEmpty then - assignations = ("defaultValue", some(defaultValue.get)) :: assignations - - if paramAnnotations(n).nonEmpty then - assignations = ("annotation", some(instanciateAnnotation(paramAnnotations(n).get))) :: assignations - - documentation.argDocs.get(param) match { - case Some(doc) => - assignations = ("documentation", some(lit(doc))) :: assignations - case None => - } + for (dv <- defaultValue) + assignations = ("defaultValue" -> some(dv)) :: assignations + for (annot <- paramAnnotations(n)) + assignations = ("annotation" -> some(instanciateAnnotation(annot))) :: assignations + for (doc <- documentation.argDocs.get(param)) + assignations = ("documentation" -> some(lit(doc))) :: assignations val assignationsTrees = assignations.map{ case (name, value) => Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), value) From 485d9f8d46b57b855661323ffa0a40c63dd9533e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 5 Dec 2021 19:40:53 +0100 Subject: [PATCH 092/121] Add documentation to MainProxies --- .../dotty/tools/dotc/ast/MainProxies.scala | 24 ++++++++++++++++++- library/src/scala/main.scala | 23 +++++++++--------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index fe10c0c73591..41bc7108aaca 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -50,6 +50,10 @@ object MainProxies { def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ + /** + * Compute the default values of the function. Since they cannot be infered anymore at this point + * of the compilation, they must be explicitely passed by [[mainProxy]]. + */ def defaultValues(scope: Tree, funSymbol: Symbol): Map[Int, Tree] = scope match { case TypeDef(_, template: Template) => @@ -63,6 +67,7 @@ object MainProxies { case _ => Map[Int, Tree]() } + /** Computes the list of main methods present in the code. */ def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Vector[Option[Annotation]], Map[Int, Tree], Option[Comment])] = stats.flatMap { case stat: DefDef => val sym = stat.symbol @@ -105,6 +110,12 @@ object MainProxies { inline def some(value: Tree): Tree = Apply(ref(defn.SomeClass.companionModule.termRef), value) + /** + * Creates a list of references and definitions of arguments, the first referencing the second. + * The goal is to create the + * `val arg0: () => S = ...` + * part of the code. The first element of the tuple is a ref to `arg0`, the second is the whole definition. + */ def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => @@ -125,15 +136,24 @@ object MainProxies { else defn.MainAnnotCommand_argGetter + // The ParameterInfos to be passed to the arg getter val parameterInfos = { val param = paramName.toString val paramInfosName = argName ++ "paramInfos" val paramInfosIdent = Ident(paramInfosName) val paramInfosTree = New( AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), + // Arguments to be passed to ParameterInfos' constructor List(List(lit(param), lit(formalType.show))) ) + /* + * Assignations to be made after the creation of the ParameterInfos. + * For example: + * args0paramInfos.documentation = Some("my param x") + * is represented by the pair + * ("documentation", some(lit("my param x"))) + */ var assignations: List[(String, Tree)] = Nil for (dv <- defaultValue) assignations = ("defaultValue" -> some(dv)) :: assignations @@ -163,6 +183,7 @@ object MainProxies { } end createArgs + /** Turns an annotation (e.g. `@main(40)`) into an instance of the class (e.g. `new scala.main(40)`). */ def instanciateAnnotation(annot: Annotation): Tree = val argss = { def recurse(t: Tree, acc: List[List[Tree]]): List[List[Tree]] = t match { @@ -245,12 +266,13 @@ object MainProxies { result } + /** A class responsible for extracting the docstrings of a method. */ private class Documentation(docComment: Option[Comment]): import util.CommentParsing._ /** The main part of the documentation. */ lazy val mainDoc: String = _mainDoc - /** The parameters identified by @param. Maps from param name to documentation. */ + /** The parameters identified by @param. Maps from parameter name to its documentation. */ lazy val argDocs: Map[String, String] = _argDocs private var _mainDoc: String = "" diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 6e6139aafa06..526bbbd39056 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -11,13 +11,17 @@ package scala import collection.mutable import annotation._ -/** An annotation that designates a main function - */ +/** + * An annotation that designates a main function. + * @param maxLineLength the maximum number of characters to print on a single line when + * displaying the help + */ final class main(maxLineLength: Int) extends MainAnnotation: self => import main._ import MainAnnotation._ + /** An annotation that designates a main function. */ def this() = this(120) override type ArgumentParser[T] = util.CommandLineParser.FromString[T] @@ -145,15 +149,12 @@ final class main(maxLineLength: Int) extends MainAnnotation: } private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = - def allIndicesOf(s: String): Seq[Int] = - def recurse(s: String, from: Int): Seq[Int] = - val i = args.indexOf(s, from) - if i < 0 then Seq() else i +: recurse(s, i + 1) + def allIndicesOf(s: String, from: Int): Seq[Int] = + val i = args.indexOf(s, from) + if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - recurse(s, 0) - - val indices = allIndicesOf(s"--$argName") - val indicesShort = shortArgName.map(shortName => allIndicesOf(s"-$shortName")).getOrElse(Seq()) + val indices = allIndicesOf(s"--$argName", 0) + val indicesShort = shortArgName.map(shortName => allIndicesOf(s"-$shortName", 0)).getOrElse(Seq()) (indices ++: indicesShort).filter(_ >= 0) private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = @@ -189,7 +190,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: argKinds += argKind val shortName = getShortName(paramInfos) - if shortName.exists(c => !shortNameIsValid(c)) then throw IllegalArgumentException(s"Invalid short name: -${shortName.get}") + shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: -$c")) argShortNames += shortName override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = From 1e71cb2380969f42c72bb4dcd0942e8affed09d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 8 Dec 2021 09:40:31 +0100 Subject: [PATCH 093/121] Parametrize -- in main --- library/src/scala/main.scala | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 526bbbd39056..bed62e2eb119 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -33,6 +33,9 @@ final class main(maxLineLength: Int) extends MainAnnotation: override def command(args: Array[String], commandName: String, docComment: String) = new Command[ArgumentParser, MainResultType]: + private val argMarker = "--" + private val shortArgMarker = "-" + private var argNames = new mutable.ArrayBuffer[String] private var argShortNames = new mutable.ArrayBuffer[Option[Char]] private var argTypes = new mutable.ArrayBuffer[String] @@ -55,8 +58,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def isArgNameAt(idx: Int): Boolean = val arg = args(argIdx) - val isFullName = arg.startsWith("--") - val isShortName = arg.startsWith("-") && arg.length == 2 && shortNameIsValid(arg(1)) + val isFullName = arg.startsWith(argMarker) + val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) isFullName || isShortName @@ -76,7 +79,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def argUsage(pos: Int): String = val name = argNames(pos) - val namePrint = argShortNames(pos).map(short => s"[-$short | --$name]").getOrElse(s"[--$name]") + val namePrint = argShortNames(pos).map(short => s"[$shortArgMarker$short | $argMarker$name]").getOrElse(s"[$argMarker$name]") argKinds(pos) match { case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" @@ -153,8 +156,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: val i = args.indexOf(s, from) if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - val indices = allIndicesOf(s"--$argName", 0) - val indicesShort = shortArgName.map(shortName => allIndicesOf(s"-$shortName", 0)).getOrElse(Seq()) + val indices = allIndicesOf(s"$argMarker$argName", 0) + val indicesShort = shortArgName.map(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)).getOrElse(Seq()) (indices ++: indicesShort).filter(_ >= 0) private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = @@ -190,7 +193,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: argKinds += argKind val shortName = getShortName(paramInfos) - shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: -$c")) + shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: $shortArgMarker$c")) argShortNames += shortName override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = @@ -223,12 +226,12 @@ final class main(maxLineLength: Int) extends MainAnnotation: case None => for arg <- args - if arg.startsWith("--") && !argNames.contains(arg.drop(2)) + if arg.startsWith(argMarker) && !argNames.contains(arg.drop(2)) do error(s"unknown argument name: $arg") end flagUnused - if args.contains("--help") then + if args.contains(s"${argMarker}help") then usage() println() explain() From b4dc846af59cf548a60e8cfeb0e0c643b43455d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 8 Dec 2021 13:19:17 +0100 Subject: [PATCH 094/121] Use types in MainProxies for convenience --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 41bc7108aaca..b372d6d4aedd 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -46,6 +46,8 @@ import Annotations.Annotation * } */ object MainProxies { + private type DefaultValues = Map[Int, Tree[_]] + private type ParameterAnnotations = Vector[Option[Annotation]] def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ @@ -54,7 +56,7 @@ object MainProxies { * Compute the default values of the function. Since they cannot be infered anymore at this point * of the compilation, they must be explicitely passed by [[mainProxy]]. */ - def defaultValues(scope: Tree, funSymbol: Symbol): Map[Int, Tree] = + def defaultValues(scope: Tree, funSymbol: Symbol): DefaultValues = scope match { case TypeDef(_, template: Template) => template.body.flatMap((_: Tree) match { @@ -68,7 +70,7 @@ object MainProxies { } /** Computes the list of main methods present in the code. */ - def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, Vector[Option[Annotation]], Map[Int, Tree], Option[Comment])] = stats.flatMap { + def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, ParameterAnnotations, DefaultValues, Option[Comment])] = stats.flatMap { case stat: DefDef => val sym = stat.symbol sym.annotations.filter(_.matches(defn.MainAnnot)) match { @@ -98,7 +100,7 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol, paramAnnotations: Vector[Option[Annotation]], defaultValues: Map[Int, Tree], docComment: Option[Comment])(using Context): List[TypeDef] = { + def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotations, defaultValues: DefaultValues, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args From 760f49c6a46cffb2c80bbb3fd417820f119b8029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 8 Dec 2021 13:58:24 +0100 Subject: [PATCH 095/121] Use default values' symbols instead of trees - This avoids having to somehow retype the value --- .../dotty/tools/dotc/ast/MainProxies.scala | 24 +++++++++---------- .../main-annotation-homemade-parser-4.scala | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index b372d6d4aedd..f442af0fcf57 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -46,31 +46,30 @@ import Annotations.Annotation * } */ object MainProxies { - private type DefaultValues = Map[Int, Tree[_]] + private type DefaultValueSymbols = Map[Int, Symbol] private type ParameterAnnotations = Vector[Option[Annotation]] def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ /** - * Compute the default values of the function. Since they cannot be infered anymore at this point - * of the compilation, they must be explicitely passed by [[mainProxy]]. + * Computes the symbols of the default values of the function. Since they cannot be infered anymore at this + * point of the compilation, they must be explicitely passed by [[mainProxy]]. */ - def defaultValues(scope: Tree, funSymbol: Symbol): DefaultValues = + def defaultValueSymbols(scope: Tree, funSymbol: Symbol): DefaultValueSymbols = scope match { case TypeDef(_, template: Template) => template.body.flatMap((_: Tree) match { case dd @ DefDef(name, _, _, _) if name.is(DefaultGetterName) && name.firstPart == funSymbol.name => val index: Int = name.toString.split("\\$").last.toInt - 1 // FIXME please!! - val valueTree = dd.rhs - List(index -> valueTree) + List(index -> dd.symbol) case _ => List() }).toMap - case _ => Map[Int, Tree]() + case _ => Map.empty } /** Computes the list of main methods present in the code. */ - def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, ParameterAnnotations, DefaultValues, Option[Comment])] = stats.flatMap { + def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, ParameterAnnotations, DefaultValueSymbols, Option[Comment])] = stats.flatMap { case stat: DefDef => val sym = stat.symbol sym.annotations.filter(_.matches(defn.MainAnnot)) match { @@ -84,7 +83,7 @@ object MainProxies { case paramAnnot :: others => report.error(s"parameters cannot have multiple annotations", paramAnnot.tree); None } )) - (sym, paramAnnotations.toVector, defaultValues(scope, sym), stat.rawComment) :: Nil + (sym, paramAnnotations.toVector, defaultValueSymbols(scope, sym), stat.rawComment) :: Nil case mainAnnot :: others => report.error(s"method cannot have multiple main annotations", mainAnnot.tree) Nil @@ -100,7 +99,7 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotations, defaultValues: DefaultValues, docComment: Option[Comment])(using Context): List[TypeDef] = { + def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotations, defaultValueSymbols: DefaultValueSymbols, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args @@ -124,7 +123,6 @@ object MainProxies { val argName = mainArgsName ++ n.toString val isRepeated = formal.isRepeatedParam - val defaultValue = defaultValues.get(n) var argRef: Tree = Apply(Ident(argName), Nil) var formalType = formal @@ -157,8 +155,8 @@ object MainProxies { * ("documentation", some(lit("my param x"))) */ var assignations: List[(String, Tree)] = Nil - for (dv <- defaultValue) - assignations = ("defaultValue" -> some(dv)) :: assignations + for (dvSym <- defaultValueSymbols.get(n)) + assignations = ("defaultValue" -> some(ref(dvSym.termRef))) :: assignations for (annot <- paramAnnotations(n)) assignations = ("annotation" -> some(instanciateAnnotation(annot))) :: assignations for (doc <- documentation.argDocs.get(param)) diff --git a/tests/run/main-annotation-homemade-parser-4.scala b/tests/run/main-annotation-homemade-parser-4.scala index f7730ec27a18..0bc826d0258e 100644 --- a/tests/run/main-annotation-homemade-parser-4.scala +++ b/tests/run/main-annotation-homemade-parser-4.scala @@ -22,9 +22,9 @@ given [T : FromString]: FromString[Either[T, String]] with object myProgram: - @main def getOption(o: Option[Int] = Some[Int](42)) = println(o) + @main def getOption(o: Option[Int] = Some(42)) = println(o) - @main def getEither(e: Either[Int, String] = Right[Int, String]("No argument given")) = println(e) + @main def getEither(e: Either[Int, String] = Right("No argument given")) = println(e) end myProgram From af851b3efcbfa8b49bf493e00fee55475008c593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 8 Dec 2021 14:54:59 +0100 Subject: [PATCH 096/121] Change structure of ParameterInfos - Based on suggestion by Nicolas Stucki, here: https://gist.github.com/nicolasstucki/84ebcd5c2cfc9aa14abba96ae1a0e996 - Add ability to pass multiple ParameterAnnotations --- .../dotty/tools/dotc/ast/MainProxies.scala | 38 ++++----- .../dotty/tools/dotc/core/Definitions.scala | 3 + .../src/scala/annotation/MainAnnotation.scala | 37 +++++--- library/src/scala/main.scala | 14 +-- .../main-annotation-homemade-annot-1.scala | 85 +++++++++++++------ .../main-annotation-homemade-annot-2.scala | 85 +++++++++++++------ 6 files changed, 167 insertions(+), 95 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index f442af0fcf57..981f403991f7 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -47,7 +47,7 @@ import Annotations.Annotation */ object MainProxies { private type DefaultValueSymbols = Map[Int, Symbol] - private type ParameterAnnotations = Vector[Option[Annotation]] + private type ParameterAnnotationss = Seq[Seq[Annotation]] def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ @@ -69,7 +69,7 @@ object MainProxies { } /** Computes the list of main methods present in the code. */ - def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, ParameterAnnotations, DefaultValueSymbols, Option[Comment])] = stats.flatMap { + def mainMethods(scope: Tree, stats: List[Tree]): List[(Symbol, ParameterAnnotationss, DefaultValueSymbols, Option[Comment])] = stats.flatMap { case stat: DefDef => val sym = stat.symbol sym.annotations.filter(_.matches(defn.MainAnnot)) match { @@ -77,11 +77,7 @@ object MainProxies { Nil case _ :: Nil => val paramAnnotations = stat.paramss.flatMap(_.map( - valdef => valdef.symbol.annotations.filter(_.matches(defn.MainAnnotParameterAnnotation)) match { - case Nil => None - case paramAnnot :: Nil => Some(paramAnnot) - case paramAnnot :: others => report.error(s"parameters cannot have multiple annotations", paramAnnot.tree); None - } + valdef => valdef.symbol.annotations.filter(_.matches(defn.MainAnnotParameterAnnotation)) )) (sym, paramAnnotations.toVector, defaultValueSymbols(scope, sym), stat.rawComment) :: Nil case mainAnnot :: others => @@ -99,7 +95,7 @@ object MainProxies { } import untpd._ - def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotations, defaultValueSymbols: DefaultValueSymbols, docComment: Option[Comment])(using Context): List[TypeDef] = { + def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotationss, defaultValueSymbols: DefaultValueSymbols, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos val mainArgsName: TermName = nme.args @@ -111,6 +107,11 @@ object MainProxies { inline def some(value: Tree): Tree = Apply(ref(defn.SomeClass.companionModule.termRef), value) + def unitToValue(value: Tree): Tree = + val anonName = nme.ANON_FUN + val defdef = DefDef(anonName, List(Nil), TypeTree(), value) + Block(defdef, Closure(Nil, Ident(anonName), EmptyTree)) + /** * Creates a list of references and definitions of arguments, the first referencing the second. * The goal is to create the @@ -150,27 +151,24 @@ object MainProxies { /* * Assignations to be made after the creation of the ParameterInfos. * For example: - * args0paramInfos.documentation = Some("my param x") + * args0paramInfos.withDocumentation = Some("my param x") * is represented by the pair - * ("documentation", some(lit("my param x"))) + * (defn.MainAnnotationParameterInfos_withDocumentation, some(lit("my param x"))) */ - var assignations: List[(String, Tree)] = Nil + var assignations: List[(Symbol, List[Tree])] = Nil for (dvSym <- defaultValueSymbols.get(n)) - assignations = ("defaultValue" -> some(ref(dvSym.termRef))) :: assignations - for (annot <- paramAnnotations(n)) - assignations = ("annotation" -> some(instanciateAnnotation(annot))) :: assignations + assignations = (defn.MainAnnotationParameterInfos_withDefaultValue -> List(unitToValue(ref(dvSym.termRef)))) :: assignations for (doc <- documentation.argDocs.get(param)) - assignations = ("documentation" -> some(lit(doc))) :: assignations + assignations = (defn.MainAnnotationParameterInfos_withDocumentation -> List(lit(doc))) :: assignations - val assignationsTrees = assignations.map{ - case (name, value) => Apply(Select(paramInfosIdent, defn.MainAnnotParameterInfos.requiredMethod(name + "_=").name), value) - } + val instanciatedAnnots = paramAnnotations(n).map(instanciateAnnotation).toList + if instanciatedAnnots.nonEmpty then + assignations = (defn.MainAnnotationParameterInfos_withAnnotations -> instanciatedAnnots) :: assignations if assignations.isEmpty then paramInfosTree else - val paramInfosInstance = ValDef(paramInfosName, TypeTree(), paramInfosTree) - Block(paramInfosInstance :: assignationsTrees, paramInfosIdent) + assignations.foldLeft[Tree](paramInfosTree){ case (tree, (setterSym, values)) => Apply(Select(tree, setterSym.name), values) } } val argDef = ValDef( diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e31b08db8753..77d1631144db 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -856,6 +856,9 @@ class Definitions { @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.annotation.MainAnnotation") @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") @tu lazy val MainAnnotParameterInfos: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterInfos") + @tu lazy val MainAnnotationParameterInfos_withDefaultValue: Symbol = MainAnnotParameterInfos.requiredMethod("withDefaultValue") + @tu lazy val MainAnnotationParameterInfos_withDocumentation: Symbol = MainAnnotParameterInfos.requiredMethod("withDocumentation") + @tu lazy val MainAnnotationParameterInfos_withAnnotations: Symbol = MainAnnotParameterInfos.requiredMethod("withAnnotations") @tu lazy val MainAnnotParameterAnnotation: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterAnnotation") @tu lazy val MainAnnotCommand: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.Command") @tu lazy val MainAnnotCommand_argGetter: Symbol = MainAnnotCommand.requiredMethod("argGetter") diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 5fbe6c9e21a8..47c623d891a2 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -25,19 +25,34 @@ trait MainAnnotation extends StaticAnnotation: end MainAnnotation object MainAnnotation: - /** - * The information related to one of the parameters of the annotated method. - * @param name the name of the parameter - * @param typeName the name of the parameter's type - * @tparam T the type of the parameter - */ - class ParameterInfos[T](var name: String, var typeName: String): + // Inspired by https://github.com/scala-js/scala-js/blob/0708917912938714d52be1426364f78a3d1fd269/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala#L23-L218 + final class ParameterInfos[T] private ( + /** The name of the parameter */ + val name: String, + /** The name of the parameter's type */ + val typeName: String, /** The docstring of the parameter. Defaults to None. */ - var documentation: Option[String] = None + val documentation: Option[String], /** The default value that the parameter has. Defaults to None. */ - var defaultValue: Option[T] = None - /** If there is one, the ParameterAnnotation associated with the parameter. Defaults to None. */ - var annotation: Option[ParameterAnnotation] = None + val defaultValueOpt: Option[() => T], + /** The ParameterAnnotations associated with the parameter. Defaults to Seq.empty. */ + val annotations: Seq[ParameterAnnotation], + ) { + // Main public constructor + def this(name: String, typeName: String) = + this(name, typeName, None, None, Seq.empty) + + def withDefaultValue(defaultValueGetter: () => T): ParameterInfos[T] = + new ParameterInfos(name, typeName, documentation, Some(defaultValueGetter), annotations) + + def withDocumentation(doc: String): ParameterInfos[T] = + new ParameterInfos(name, typeName, Some(doc), defaultValueOpt, annotations) + + def withAnnotations(annots: ParameterAnnotation*): ParameterInfos[T] = + new ParameterInfos(name, typeName, documentation, defaultValueOpt, annots) + + override def toString: String = s"$name: $typeName" + } /** A class representing a command to run */ trait Command[ArgumentParser[_], MainResultType]: diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index bed62e2eb119..109f0952b7ad 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -174,17 +174,11 @@ final class main(maxLineLength: Int) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def getAnnotationData[T](paramInfos: ParameterInfos[_], extractor: Arg => T): Option[T] = - paramInfos.annotation match { - case Some(annot: Arg) => Some(extractor(annot)) - case _ => None - } - private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = - getAnnotationData(paramInfos, _.name).filter(_.length > 0).getOrElse(paramInfos.name) + paramInfos.annotations.collectFirst{ case arg: Arg if arg.name.length > 0 => arg.name }.getOrElse(paramInfos.name) private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = - getAnnotationData(paramInfos, _.shortName).filterNot(_ == 0) + paramInfos.annotations.collectFirst{ case arg: Arg if arg.shortName != 0 => arg.shortName } private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = argNames += getEffectiveName(paramInfos) @@ -198,8 +192,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = val name = getEffectiveName(paramInfos) - val (defaultGetter, argumentKind) = paramInfos.defaultValue match { - case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) + val (defaultGetter, argumentKind) = paramInfos.defaultValueOpt match { + case Some(value) => (() => value, ArgumentKind.OptionalArgument) case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) } registerArg(paramInfos, argumentKind) diff --git a/tests/run/main-annotation-homemade-annot-1.scala b/tests/run/main-annotation-homemade-annot-1.scala index a1f65f064f87..3dfea5ebed77 100644 --- a/tests/run/main-annotation-homemade-annot-1.scala +++ b/tests/run/main-annotation-homemade-annot-1.scala @@ -21,7 +21,8 @@ object Test: end Test class mainAwait(timeout: Int = 2) extends MainAnnotation: - self => + import MainAnnotation._ + import main.{Arg} private val maxLineLength = 120 @@ -33,8 +34,12 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: } override def command(args: Array[String], commandName: String, docComment: String) = - new MainAnnotation.Command[ArgumentParser, MainResultType]: + new Command[ArgumentParser, MainResultType]: + private val argMarker = "--" + private val shortArgMarker = "-" + private var argNames = new mutable.ArrayBuffer[String] + private var argShortNames = new mutable.ArrayBuffer[Option[Char]] private var argTypes = new mutable.ArrayBuffer[String] private var argDocs = new mutable.ArrayBuffer[String] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] @@ -53,12 +58,22 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: private def argAt(idx: Int): Option[String] = if idx < args.length then Some(args(idx)) else None + private def isArgNameAt(idx: Int): Boolean = + val arg = args(argIdx) + val isFullName = arg.startsWith(argMarker) + val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) + + isFullName || isShortName + private def nextPositionalArg(): Option[String] = - while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + while argIdx < args.length && isArgNameAt(argIdx) do argIdx += 2 val result = argAt(argIdx) argIdx += 1 result + private def shortNameIsValid(shortName: Char): Boolean = + shortName == 0 || shortName.isLetter + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match case Some(t) => () => t @@ -66,10 +81,11 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: private def argUsage(pos: Int): String = val name = argNames(pos) + val namePrint = argShortNames(pos).map(short => s"[$shortArgMarker$short | $argMarker$name]").getOrElse(s"[$argMarker$name]") argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"[--$name] <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[[--$name] <${argTypes(pos)}>]" + case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[$namePrint <${argTypes(pos)}>]" case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" } @@ -137,19 +153,18 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: println(argDoc) } - private def indicesOfArg(argName: String): Seq[Int] = - def allIndicesOf(s: String): Seq[Int] = - def recurse(s: String, from: Int): Seq[Int] = - val i = args.indexOf(s, from) - if i < 0 then Seq() else i +: recurse(s, i + 1) - - recurse(s, 0) + private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = + def allIndicesOf(s: String, from: Int): Seq[Int] = + val i = args.indexOf(s, from) + if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - val indices = allIndicesOf(s"--$argName") - indices.filter(_ >= 0) + val indices = allIndicesOf(s"$argMarker$argName", 0) + val indicesShort = shortArgName.map(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)).getOrElse(Seq()) + (indices ++: indicesShort).filter(_ >= 0) - private def getArgGetter[T](argName: String, getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = - indicesOfArg(argName) match { + private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = + val argName = getEffectiveName(paramInfos) + indicesOfArg(argName, getShortName(paramInfos)) match { case s @ (Seq() | Seq(_)) => val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) argOpt match { @@ -161,30 +176,45 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = - argNames += paramInfos.name + private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = + paramInfos.annotations.collectFirst{ case arg: Arg if arg.name.length > 0 => arg.name }.getOrElse(paramInfos.name) + + private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = + paramInfos.annotations.collectFirst{ case arg: Arg if arg.shortName != 0 => arg.shortName } + + private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = + argNames += getEffectiveName(paramInfos) argTypes += paramInfos.typeName argDocs += paramInfos.documentation.getOrElse("") argKinds += argKind - override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val name = paramInfos.name - val (defaultGetter, argumentKind) = paramInfos.defaultValue match { - case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) + val shortName = getShortName(paramInfos) + shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: $shortArgMarker$c")) + argShortNames += shortName + + override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + val name = getEffectiveName(paramInfos) + val (defaultGetter, argumentKind) = paramInfos.defaultValueOpt match { + case Some(value) => (() => value, ArgumentKind.OptionalArgument) case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) } registerArg(paramInfos, argumentKind) - getArgGetter(name, defaultGetter) + getArgGetter(paramInfos, defaultGetter) - override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() + case Some(arg) => convert(getEffectiveName(paramInfos), arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) override def run(f: => MainResultType): Unit = + def checkShortNamesUnique(): Unit = + val shortNameToIndices = argShortNames.collect{ case Some(short) => short }.zipWithIndex.groupBy(_._1).view.mapValues(_.map(_._2)) + for ((shortName, indices) <- shortNameToIndices if indices.length > 1) + error(s"$shortName is used as short name for multiple parameters: ${indices.map(idx => argNames(idx)).mkString(", ")}") + def flagUnused(): Unit = nextPositionalArg() match case Some(arg) => error(s"unused argument: $arg") @@ -192,17 +222,18 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: case None => for arg <- args - if arg.startsWith("--") && !argNames.contains(arg.drop(2)) + if arg.startsWith(argMarker) && !argNames.contains(arg.drop(2)) do error(s"unknown argument name: $arg") end flagUnused - if args.contains("--help") then + if args.contains(s"${argMarker}help") then usage() println() explain() else flagUnused() + checkShortNamesUnique() if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() diff --git a/tests/run/main-annotation-homemade-annot-2.scala b/tests/run/main-annotation-homemade-annot-2.scala index bfbeb0fbfbb8..3dcf4e213027 100644 --- a/tests/run/main-annotation-homemade-annot-2.scala +++ b/tests/run/main-annotation-homemade-annot-2.scala @@ -28,7 +28,8 @@ end Test /** Code mostly copied from {{scala.main}}. */ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: - self => + import MainAnnotation._ + import main.{Arg} private val maxLineLength = 120 @@ -40,8 +41,12 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: } override def command(args: Array[String], commandName: String, docComment: String) = - new MainAnnotation.Command[ArgumentParser, MainResultType]: + new Command[ArgumentParser, MainResultType]: + private val argMarker = "--" + private val shortArgMarker = "-" + private var argNames = new mutable.ArrayBuffer[String] + private var argShortNames = new mutable.ArrayBuffer[Option[Char]] private var argTypes = new mutable.ArrayBuffer[String] private var argDocs = new mutable.ArrayBuffer[String] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] @@ -60,12 +65,22 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: private def argAt(idx: Int): Option[String] = if idx < args.length then Some(args(idx)) else None + private def isArgNameAt(idx: Int): Boolean = + val arg = args(argIdx) + val isFullName = arg.startsWith(argMarker) + val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) + + isFullName || isShortName + private def nextPositionalArg(): Option[String] = - while argIdx < args.length && args(argIdx).startsWith("--") do argIdx += 2 + while argIdx < args.length && isArgNameAt(argIdx) do argIdx += 2 val result = argAt(argIdx) argIdx += 1 result + private def shortNameIsValid(shortName: Char): Boolean = + shortName == 0 || shortName.isLetter + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match case Some(t) => () => t @@ -73,10 +88,11 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: private def argUsage(pos: Int): String = val name = argNames(pos) + val namePrint = argShortNames(pos).map(short => s"[$shortArgMarker$short | $argMarker$name]").getOrElse(s"[$argMarker$name]") argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"[--$name] <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[[--$name] <${argTypes(pos)}>]" + case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[$namePrint <${argTypes(pos)}>]" case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" } @@ -144,19 +160,18 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: println(argDoc) } - private def indicesOfArg(argName: String): Seq[Int] = - def allIndicesOf(s: String): Seq[Int] = - def recurse(s: String, from: Int): Seq[Int] = - val i = args.indexOf(s, from) - if i < 0 then Seq() else i +: recurse(s, i + 1) - - recurse(s, 0) + private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = + def allIndicesOf(s: String, from: Int): Seq[Int] = + val i = args.indexOf(s, from) + if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - val indices = allIndicesOf(s"--$argName") - indices.filter(_ >= 0) + val indices = allIndicesOf(s"$argMarker$argName", 0) + val indicesShort = shortArgName.map(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)).getOrElse(Seq()) + (indices ++: indicesShort).filter(_ >= 0) - private def getArgGetter[T](argName: String, getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = - indicesOfArg(argName) match { + private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = + val argName = getEffectiveName(paramInfos) + indicesOfArg(argName, getShortName(paramInfos)) match { case s @ (Seq() | Seq(_)) => val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) argOpt match { @@ -168,30 +183,45 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: error(s"more than one value for $argName: ${multValues.mkString(", ")}") } - private def registerArg(paramInfos: MainAnnotation.ParameterInfos[_], argKind: ArgumentKind): Unit = - argNames += paramInfos.name + private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = + paramInfos.annotations.collectFirst{ case arg: Arg if arg.name.length > 0 => arg.name }.getOrElse(paramInfos.name) + + private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = + paramInfos.annotations.collectFirst{ case arg: Arg if arg.shortName != 0 => arg.shortName } + + private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = + argNames += getEffectiveName(paramInfos) argTypes += paramInfos.typeName argDocs += paramInfos.documentation.getOrElse("") argKinds += argKind - override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val name = paramInfos.name - val (defaultGetter, argumentKind) = paramInfos.defaultValue match { - case Some(value) => (() => () => value, ArgumentKind.OptionalArgument) + val shortName = getShortName(paramInfos) + shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: $shortArgMarker$c")) + argShortNames += shortName + + override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + val name = getEffectiveName(paramInfos) + val (defaultGetter, argumentKind) = paramInfos.defaultValueOpt match { + case Some(value) => (() => value, ArgumentKind.OptionalArgument) case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) } registerArg(paramInfos, argumentKind) - getArgGetter(name, defaultGetter) + getArgGetter(paramInfos, defaultGetter) - override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() + case Some(arg) => convert(getEffectiveName(paramInfos), arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) override def run(f: => MainResultType): Unit = + def checkShortNamesUnique(): Unit = + val shortNameToIndices = argShortNames.collect{ case Some(short) => short }.zipWithIndex.groupBy(_._1).view.mapValues(_.map(_._2)) + for ((shortName, indices) <- shortNameToIndices if indices.length > 1) + error(s"$shortName is used as short name for multiple parameters: ${indices.map(idx => argNames(idx)).mkString(", ")}") + def flagUnused(): Unit = nextPositionalArg() match case Some(arg) => error(s"unused argument: $arg") @@ -199,17 +229,18 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: case None => for arg <- args - if arg.startsWith("--") && !argNames.contains(arg.drop(2)) + if arg.startsWith(argMarker) && !argNames.contains(arg.drop(2)) do error(s"unknown argument name: $arg") end flagUnused - if args.contains("--help") then + if args.contains(s"${argMarker}help") then usage() println() explain() else flagUnused() + checkShortNamesUnique() if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() From 300adcf1b9b6c21e8b6534a8009309457fcf673a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 8 Dec 2021 14:58:54 +0100 Subject: [PATCH 097/121] Add test to check lazyness of default value --- ... => main-annotation-default-value-1.check} | 0 ... => main-annotation-default-value-1.scala} | 0 .../run/main-annotation-default-value-2.check | 2 ++ .../run/main-annotation-default-value-2.scala | 31 +++++++++++++++++++ 4 files changed, 33 insertions(+) rename tests/run/{main-annotation-default-value.check => main-annotation-default-value-1.check} (100%) rename tests/run/{main-annotation-default-value.scala => main-annotation-default-value-1.scala} (100%) create mode 100644 tests/run/main-annotation-default-value-2.check create mode 100644 tests/run/main-annotation-default-value-2.scala diff --git a/tests/run/main-annotation-default-value.check b/tests/run/main-annotation-default-value-1.check similarity index 100% rename from tests/run/main-annotation-default-value.check rename to tests/run/main-annotation-default-value-1.check diff --git a/tests/run/main-annotation-default-value.scala b/tests/run/main-annotation-default-value-1.scala similarity index 100% rename from tests/run/main-annotation-default-value.scala rename to tests/run/main-annotation-default-value-1.scala diff --git a/tests/run/main-annotation-default-value-2.check b/tests/run/main-annotation-default-value-2.check new file mode 100644 index 000000000000..d705e0b20e93 --- /dev/null +++ b/tests/run/main-annotation-default-value-2.check @@ -0,0 +1,2 @@ +42 +OK diff --git a/tests/run/main-annotation-default-value-2.scala b/tests/run/main-annotation-default-value-2.scala new file mode 100644 index 000000000000..33282bdc27fa --- /dev/null +++ b/tests/run/main-annotation-default-value-2.scala @@ -0,0 +1,31 @@ +// Sample main method +object myProgram: + + @main def alwaysPassParam(forbiddenParam: Int = throw new IllegalStateException("This should not be evaluated!")): Unit = + println(forbiddenParam) + +end myProgram + +object Test: + def hasCauseIllegalStateException(e: Throwable): Boolean = + e.getCause match { + case null => false + case _: IllegalStateException => true + case e: Throwable => hasCauseIllegalStateException(e) + } + + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("alwaysPassParam") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("42")) + try { + callMain(Array()) + println("This should not be printed") + } + catch { + case e: Exception if hasCauseIllegalStateException(e) => println("OK") + } +end Test \ No newline at end of file From e818f1392aa47c648c29db2b0a75dc5117e50660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 9 Dec 2021 15:54:43 +0100 Subject: [PATCH 098/121] Simplify homemade annotations tests --- .../main-annotation-homemade-annot-1.scala | 215 +---------------- .../main-annotation-homemade-annot-2.scala | 217 +----------------- 2 files changed, 16 insertions(+), 416 deletions(-) diff --git a/tests/run/main-annotation-homemade-annot-1.scala b/tests/run/main-annotation-homemade-annot-1.scala index 3dfea5ebed77..60308af61100 100644 --- a/tests/run/main-annotation-homemade-annot-1.scala +++ b/tests/run/main-annotation-homemade-annot-1.scala @@ -22,223 +22,22 @@ end Test class mainAwait(timeout: Int = 2) extends MainAnnotation: import MainAnnotation._ - import main.{Arg} - - private val maxLineLength = 120 override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Future[Any] - private enum ArgumentKind { - case SimpleArgument, OptionalArgument, VarArgument - } - + // This is a toy example, it only works with positional args override def command(args: Array[String], commandName: String, docComment: String) = new Command[ArgumentParser, MainResultType]: - private val argMarker = "--" - private val shortArgMarker = "-" - - private var argNames = new mutable.ArrayBuffer[String] - private var argShortNames = new mutable.ArrayBuffer[Option[Char]] - private var argTypes = new mutable.ArrayBuffer[String] - private var argDocs = new mutable.ArrayBuffer[String] - private var argKinds = new mutable.ArrayBuffer[ArgumentKind] - - /** A buffer for all errors */ - private var errors = new mutable.ArrayBuffer[String] - - /** Issue an error, and return an uncallable getter */ - private def error(msg: String): () => Nothing = - errors += msg - () => throw new AssertionError("trying to get invalid argument") - - /** The next argument index */ - private var argIdx: Int = 0 - - private def argAt(idx: Int): Option[String] = - if idx < args.length then Some(args(idx)) else None - - private def isArgNameAt(idx: Int): Boolean = - val arg = args(argIdx) - val isFullName = arg.startsWith(argMarker) - val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) - - isFullName || isShortName - - private def nextPositionalArg(): Option[String] = - while argIdx < args.length && isArgNameAt(argIdx) do argIdx += 2 - val result = argAt(argIdx) - argIdx += 1 - result - - private def shortNameIsValid(shortName: Char): Boolean = - shortName == 0 || shortName.isLetter - - private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = - p.fromStringOption(arg) match - case Some(t) => () => t - case None => error(s"invalid argument for $argName: $arg") - - private def argUsage(pos: Int): String = - val name = argNames(pos) - val namePrint = argShortNames(pos).map(short => s"[$shortArgMarker$short | $argMarker$name]").getOrElse(s"[$argMarker$name]") - - argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[$namePrint <${argTypes(pos)}>]" - case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" - } - - private def wrapLongLine(line: String, maxLength: Int): List[String] = { - def recurse(s: String, acc: Vector[String]): Seq[String] = - val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) - if ((s.length <= maxLength) || (lastSpace < 0)) - acc :+ s - else { - val (shortLine, rest) = s.splitAt(lastSpace) - recurse(rest.trim.nn, acc :+ shortLine) - } - - recurse(line, Vector()).toList - } - - private def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { - def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = - (args, currentLine) match { - case (Nil, "") => acc - case (Nil, l) => (acc :+ l) - case (arg :: t, "") => recurse(t, arg, acc) - case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg :: t, l) => recurse(t, arg, acc :+ l) - } - - recurse(argsUsage, "", Vector()).toList - } - - private inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") - - private def usage(): Unit = - val usageBeginning = s"Usage: $commandName " - val argsOffset = usageBeginning.length - val argUsages = wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) - - println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) - - private def explain(): Unit = - if (docComment.nonEmpty) - println(wrapLongLine(docComment, maxLineLength).mkString("\n")) - if (argNames.nonEmpty) { - val argNameShift = 2 - val argDocShift = argNameShift + 2 - - println("Arguments:") - for (pos <- 0 until argNames.length) - val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") - - argKinds(pos) match { - case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") - case ArgumentKind.VarArgument => argDoc.append(" (vararg)") - case _ => - } - - if (argDocs(pos).nonEmpty) { - val shiftedDoc = - argDocs(pos).split("\n").nn - .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) - .mkString("\n") - argDoc.append("\n").append(shiftedDoc) - } - - println(argDoc) - } - - private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = - def allIndicesOf(s: String, from: Int): Seq[Int] = - val i = args.indexOf(s, from) - if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - - val indices = allIndicesOf(s"$argMarker$argName", 0) - val indicesShort = shortArgName.map(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)).getOrElse(Seq()) - (indices ++: indicesShort).filter(_ >= 0) - - private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = - val argName = getEffectiveName(paramInfos) - indicesOfArg(argName, getShortName(paramInfos)) match { - case s @ (Seq() | Seq(_)) => - val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) - argOpt match { - case Some(arg) => convert(argName, arg, p) - case None => getDefaultGetter() - } - case s => - val multValues = s.flatMap(idx => argAt(idx + 1)) - error(s"more than one value for $argName: ${multValues.mkString(", ")}") - } - - private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = - paramInfos.annotations.collectFirst{ case arg: Arg if arg.name.length > 0 => arg.name }.getOrElse(paramInfos.name) - - private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = - paramInfos.annotations.collectFirst{ case arg: Arg if arg.shortName != 0 => arg.shortName } - - private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = - argNames += getEffectiveName(paramInfos) - argTypes += paramInfos.typeName - argDocs += paramInfos.documentation.getOrElse("") - argKinds += argKind - - val shortName = getShortName(paramInfos) - shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: $shortArgMarker$c")) - argShortNames += shortName + private var idx = 0 override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val name = getEffectiveName(paramInfos) - val (defaultGetter, argumentKind) = paramInfos.defaultValueOpt match { - case Some(value) => (() => value, ArgumentKind.OptionalArgument) - case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) - } - registerArg(paramInfos, argumentKind) - getArgGetter(paramInfos, defaultGetter) + val i = idx + idx += 1 + () => p.fromString(args(i)) override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(paramInfos, ArgumentKind.VarArgument) - def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(getEffectiveName(paramInfos), arg, p) :: remainingArgGetters() - case None => Nil - val getters = remainingArgGetters() - () => getters.map(_()) - - override def run(f: => MainResultType): Unit = - def checkShortNamesUnique(): Unit = - val shortNameToIndices = argShortNames.collect{ case Some(short) => short }.zipWithIndex.groupBy(_._1).view.mapValues(_.map(_._2)) - for ((shortName, indices) <- shortNameToIndices if indices.length > 1) - error(s"$shortName is used as short name for multiple parameters: ${indices.map(idx => argNames(idx)).mkString(", ")}") - - def flagUnused(): Unit = nextPositionalArg() match - case Some(arg) => - error(s"unused argument: $arg") - flagUnused() - case None => - for - arg <- args - if arg.startsWith(argMarker) && !argNames.contains(arg.drop(2)) - do - error(s"unknown argument name: $arg") - end flagUnused + () => for i <- (idx until args.length) yield p.fromString(args(i)) - if args.contains(s"${argMarker}help") then - usage() - println() - explain() - else - flagUnused() - checkShortNamesUnique() - if errors.nonEmpty then - for msg <- errors do println(s"Error: $msg") - usage() - else - println(Await.result(f, Duration(timeout, SECONDS))) - end run - end command + override def run(f: => MainResultType): Unit = println(Await.result(f, Duration(timeout, SECONDS))) end mainAwait \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-2.scala b/tests/run/main-annotation-homemade-annot-2.scala index 3dcf4e213027..de5463565301 100644 --- a/tests/run/main-annotation-homemade-annot-2.scala +++ b/tests/run/main-annotation-homemade-annot-2.scala @@ -26,228 +26,29 @@ object Test: callMains() end Test -/** Code mostly copied from {{scala.main}}. */ +// This is a toy example, it only works with positional args class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: import MainAnnotation._ - import main.{Arg} - - private val maxLineLength = 120 override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - private enum ArgumentKind { - case SimpleArgument, OptionalArgument, VarArgument - } - override def command(args: Array[String], commandName: String, docComment: String) = new Command[ArgumentParser, MainResultType]: - private val argMarker = "--" - private val shortArgMarker = "-" - - private var argNames = new mutable.ArrayBuffer[String] - private var argShortNames = new mutable.ArrayBuffer[Option[Char]] - private var argTypes = new mutable.ArrayBuffer[String] - private var argDocs = new mutable.ArrayBuffer[String] - private var argKinds = new mutable.ArrayBuffer[ArgumentKind] - - /** A buffer for all errors */ - private var errors = new mutable.ArrayBuffer[String] - - /** Issue an error, and return an uncallable getter */ - private def error(msg: String): () => Nothing = - errors += msg - () => throw new AssertionError("trying to get invalid argument") - - /** The next argument index */ - private var argIdx: Int = 0 - - private def argAt(idx: Int): Option[String] = - if idx < args.length then Some(args(idx)) else None - - private def isArgNameAt(idx: Int): Boolean = - val arg = args(argIdx) - val isFullName = arg.startsWith(argMarker) - val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) - - isFullName || isShortName - - private def nextPositionalArg(): Option[String] = - while argIdx < args.length && isArgNameAt(argIdx) do argIdx += 2 - val result = argAt(argIdx) - argIdx += 1 - result - - private def shortNameIsValid(shortName: Char): Boolean = - shortName == 0 || shortName.isLetter - - private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = - p.fromStringOption(arg) match - case Some(t) => () => t - case None => error(s"invalid argument for $argName: $arg") - - private def argUsage(pos: Int): String = - val name = argNames(pos) - val namePrint = argShortNames(pos).map(short => s"[$shortArgMarker$short | $argMarker$name]").getOrElse(s"[$argMarker$name]") - - argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[$namePrint <${argTypes(pos)}>]" - case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" - } - - private def wrapLongLine(line: String, maxLength: Int): List[String] = { - def recurse(s: String, acc: Vector[String]): Seq[String] = - val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) - if ((s.length <= maxLength) || (lastSpace < 0)) - acc :+ s - else { - val (shortLine, rest) = s.splitAt(lastSpace) - recurse(rest.trim.nn, acc :+ shortLine) - } - - recurse(line, Vector()).toList - } - - private def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { - def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = - (args, currentLine) match { - case (Nil, "") => acc - case (Nil, l) => (acc :+ l) - case (arg :: t, "") => recurse(t, arg, acc) - case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg :: t, l) => recurse(t, arg, acc :+ l) - } - - recurse(argsUsage, "", Vector()).toList - } - - private inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") - - private def usage(): Unit = - val usageBeginning = s"Usage: $commandName " - val argsOffset = usageBeginning.length - val argUsages = wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) - - println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) - - private def explain(): Unit = - if (docComment.nonEmpty) - println(wrapLongLine(docComment, maxLineLength).mkString("\n")) - if (argNames.nonEmpty) { - val argNameShift = 2 - val argDocShift = argNameShift + 2 - - println("Arguments:") - for (pos <- 0 until argNames.length) - val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") - - argKinds(pos) match { - case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") - case ArgumentKind.VarArgument => argDoc.append(" (vararg)") - case _ => - } - - if (argDocs(pos).nonEmpty) { - val shiftedDoc = - argDocs(pos).split("\n").nn - .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) - .mkString("\n") - argDoc.append("\n").append(shiftedDoc) - } - - println(argDoc) - } - - private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = - def allIndicesOf(s: String, from: Int): Seq[Int] = - val i = args.indexOf(s, from) - if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - - val indices = allIndicesOf(s"$argMarker$argName", 0) - val indicesShort = shortArgName.map(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)).getOrElse(Seq()) - (indices ++: indicesShort).filter(_ >= 0) - - private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = - val argName = getEffectiveName(paramInfos) - indicesOfArg(argName, getShortName(paramInfos)) match { - case s @ (Seq() | Seq(_)) => - val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) - argOpt match { - case Some(arg) => convert(argName, arg, p) - case None => getDefaultGetter() - } - case s => - val multValues = s.flatMap(idx => argAt(idx + 1)) - error(s"more than one value for $argName: ${multValues.mkString(", ")}") - } - - private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = - paramInfos.annotations.collectFirst{ case arg: Arg if arg.name.length > 0 => arg.name }.getOrElse(paramInfos.name) - - private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = - paramInfos.annotations.collectFirst{ case arg: Arg if arg.shortName != 0 => arg.shortName } - - private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = - argNames += getEffectiveName(paramInfos) - argTypes += paramInfos.typeName - argDocs += paramInfos.documentation.getOrElse("") - argKinds += argKind - - val shortName = getShortName(paramInfos) - shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: $shortArgMarker$c")) - argShortNames += shortName + private var idx = 0 override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val name = getEffectiveName(paramInfos) - val (defaultGetter, argumentKind) = paramInfos.defaultValueOpt match { - case Some(value) => (() => value, ArgumentKind.OptionalArgument) - case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) - } - registerArg(paramInfos, argumentKind) - getArgGetter(paramInfos, defaultGetter) + val i = idx + idx += 1 + () => p.fromString(args(i)) override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(paramInfos, ArgumentKind.VarArgument) - def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(getEffectiveName(paramInfos), arg, p) :: remainingArgGetters() - case None => Nil - val getters = remainingArgGetters() - () => getters.map(_()) + () => for i <- (idx until args.length) yield p.fromString(args(i)) override def run(f: => MainResultType): Unit = - def checkShortNamesUnique(): Unit = - val shortNameToIndices = argShortNames.collect{ case Some(short) => short }.zipWithIndex.groupBy(_._1).view.mapValues(_.map(_._2)) - for ((shortName, indices) <- shortNameToIndices if indices.length > 1) - error(s"$shortName is used as short name for multiple parameters: ${indices.map(idx => argNames(idx)).mkString(", ")}") - - def flagUnused(): Unit = nextPositionalArg() match - case Some(arg) => - error(s"unused argument: $arg") - flagUnused() - case None => - for - arg <- args - if arg.startsWith(argMarker) && !argNames.contains(arg.drop(2)) - do - error(s"unknown argument name: $arg") - end flagUnused - - if args.contains(s"${argMarker}help") then - usage() - println() - explain() - else - flagUnused() - checkShortNamesUnique() - if errors.nonEmpty then - for msg <- errors do println(s"Error: $msg") - usage() - else - for (_ <- 1 to runs) - f - if after.length > 0 then println(after.mkString(", ")) + for (_ <- 1 to runs) + f + if after.length > 0 then println(after.mkString(", ")) end run end command end myMain \ No newline at end of file From 07426c4e11cfde73720dde95fea6d3e980556c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Wed, 8 Dec 2021 15:44:59 +0100 Subject: [PATCH 099/121] Split Arg into ShortName and Name - Allow for multiple parameter annotations - Rework the code of main to reflect changes and simplify treatment - Change output of help and usage to accomodate for multiple names --- library/src/scala/main.scala | 218 +++++++++++------- project/MiMaFilters.scala | 6 +- tests/run/main-annotation-help.check | 110 ++++----- .../run/main-annotation-max-line-length.check | 18 +- tests/run/main-annotation-param-annot-1.check | 20 ++ ...la => main-annotation-param-annot-1.scala} | 31 +-- tests/run/main-annotation-param-annot-2.check | 9 + tests/run/main-annotation-param-annot-2.scala | 57 +++++ ...nnotation-param-annot-invalid-params.scala | 4 +- tests/run/main-annotation-param-annot.check | 41 ---- .../main-annotation-wrong-param-names.check | 15 +- .../main-annotation-wrong-param-names.scala | 5 +- 12 files changed, 307 insertions(+), 227 deletions(-) create mode 100644 tests/run/main-annotation-param-annot-1.check rename tests/run/{main-annotation-param-annot.scala => main-annotation-param-annot-1.scala} (71%) create mode 100644 tests/run/main-annotation-param-annot-2.check create mode 100644 tests/run/main-annotation-param-annot-2.scala delete mode 100644 tests/run/main-annotation-param-annot.check diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 109f0952b7ad..3186a3c93598 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -36,10 +36,11 @@ final class main(maxLineLength: Int) extends MainAnnotation: private val argMarker = "--" private val shortArgMarker = "-" - private var argNames = new mutable.ArrayBuffer[String] - private var argShortNames = new mutable.ArrayBuffer[Option[Char]] + private var argCanonicalNames = new mutable.ArrayBuffer[String] + private var argAlternativeNamess = new mutable.ArrayBuffer[Seq[String]] + private var argShortNamess = new mutable.ArrayBuffer[Seq[Char]] private var argTypes = new mutable.ArrayBuffer[String] - private var argDocs = new mutable.ArrayBuffer[String] + private var argDocOpts = new mutable.ArrayBuffer[Option[String]] private var argKinds = new mutable.ArrayBuffer[ArgumentKind] /** A buffer for all errors */ @@ -60,7 +61,6 @@ final class main(maxLineLength: Int) extends MainAnnotation: val arg = args(argIdx) val isFullName = arg.startsWith(argMarker) val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) - isFullName || isShortName private def nextPositionalArg(): Option[String] = @@ -70,21 +70,30 @@ final class main(maxLineLength: Int) extends MainAnnotation: result private def shortNameIsValid(shortName: Char): Boolean = - shortName == 0 || shortName.isLetter + // If you change this, remember to update the error message when an invalid short name is given + shortName.isLetter private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - private def argUsage(pos: Int): String = - val name = argNames(pos) - val namePrint = argShortNames(pos).map(short => s"[$shortArgMarker$short | $argMarker$name]").getOrElse(s"[$argMarker$name]") - - argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"$namePrint <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[$namePrint <${argTypes(pos)}>]" - case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" + private def allNamesWithMarkers(pos: Int): Seq[String] = + val canonicalName = argMarker + argCanonicalNames(pos) + val alternativeNames = argAlternativeNamess(pos).map(argMarker + _) + val shortNames = argShortNamess(pos).map(shortArgMarker + _) + canonicalName +: shortNames ++: alternativeNames + + private def argsUsage: Seq[String] = + for (pos <- 0 until argCanonicalNames.length) + yield { + val namesPrint = allNamesWithMarkers(pos).mkString("[", " | ", "]") + + argKinds(pos) match { + case ArgumentKind.SimpleArgument => s"$namesPrint <${argTypes(pos)}>" + case ArgumentKind.OptionalArgument => s"[$namesPrint <${argTypes(pos)}>]" + case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" + } } private def wrapLongLine(line: String, maxLength: Int): List[String] = { @@ -100,14 +109,14 @@ final class main(maxLineLength: Int) extends MainAnnotation: recurse(line, Vector()).toList } - private def wrapArgumentUsages(argsUsage: List[String], maxLength: Int): List[String] = { - def recurse(args: List[String], currentLine: String, acc: Vector[String]): Seq[String] = + private def wrapArgumentUsages(argsUsage: Seq[String], maxLength: Int): Seq[String] = { + def recurse(args: Seq[String], currentLine: String, acc: Vector[String]): Seq[String] = (args, currentLine) match { case (Nil, "") => acc case (Nil, l) => (acc :+ l) - case (arg :: t, "") => recurse(t, arg, acc) - case (arg :: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg :: t, l) => recurse(t, arg, acc :+ l) + case (arg +: t, "") => recurse(t, arg, acc) + case (arg +: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg +: t, l) => recurse(t, arg, acc :+ l) } recurse(argsUsage, "", Vector()).toList @@ -118,21 +127,28 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def usage(): Unit = val usageBeginning = s"Usage: $commandName " val argsOffset = usageBeginning.length - val argUsages = wrapArgumentUsages((0 until argNames.length).map(argUsage).toList, maxLineLength - argsOffset) + val usages = wrapArgumentUsages(argsUsage, maxLineLength - argsOffset) - println(usageBeginning + argUsages.mkString("\n" + " " * argsOffset)) + println(usageBeginning + usages.mkString("\n" + " " * argsOffset)) private def explain(): Unit = if (docComment.nonEmpty) println(wrapLongLine(docComment, maxLineLength).mkString("\n")) - if (argNames.nonEmpty) { + if (argCanonicalNames.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 println("Arguments:") - for (pos <- 0 until argNames.length) + for (pos <- 0 until argCanonicalNames.length) + val canonicalName = argMarker + argCanonicalNames(pos) + val shortNames = argShortNamess(pos).map(shortArgMarker + _) + val alternativeNames = argAlternativeNamess(pos).map(argMarker + _) + val otherNames = (shortNames ++: alternativeNames) match { + case Seq() => "" + case names => names.mkString("(", ", ", ") ") + } val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"${argNames(pos)} - ${argTypes(pos)}") + argDoc.append(s"$canonicalName $otherNames- ${argTypes(pos)}") argKinds(pos) match { case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") @@ -140,98 +156,133 @@ final class main(maxLineLength: Int) extends MainAnnotation: case _ => } - if (argDocs(pos).nonEmpty) { - val shiftedDoc = - argDocs(pos).split("\n").nn - .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) - .mkString("\n") - argDoc.append("\n").append(shiftedDoc) - } + argDocOpts(pos).foreach( + doc => if (doc.nonEmpty) { + val shiftedDoc = + doc.split("\n").nn + .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) + .mkString("\n") + argDoc.append("\n").append(shiftedDoc) + } + ) println(argDoc) } - private def indicesOfArg(argName: String, shortArgName: Option[Char]): Seq[Int] = + private def indicesOfArg(argNames: Seq[String], shortArgNames: Seq[Char]): Seq[Int] = def allIndicesOf(s: String, from: Int): Seq[Int] = val i = args.indexOf(s, from) if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - val indices = allIndicesOf(s"$argMarker$argName", 0) - val indicesShort = shortArgName.map(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)).getOrElse(Seq()) + val indices = argNames.flatMap(name => allIndicesOf(s"$argMarker$name", 0)) + val indicesShort = shortArgNames.flatMap(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)) (indices ++: indicesShort).filter(_ >= 0) - private def getArgGetter[T](paramInfos: ParameterInfos[_], getDefaultGetter: () => () => T)(using p: ArgumentParser[T]): () => T = - val argName = getEffectiveName(paramInfos) - indicesOfArg(argName, getShortName(paramInfos)) match { - case s @ (Seq() | Seq(_)) => - val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) - argOpt match { - case Some(arg) => convert(argName, arg, p) - case None => getDefaultGetter() - } - case s => - val multValues = s.flatMap(idx => argAt(idx + 1)) - error(s"more than one value for $argName: ${multValues.mkString(", ")}") - } - - private inline def getEffectiveName(paramInfos: ParameterInfos[_]): String = - paramInfos.annotations.collectFirst{ case arg: Arg if arg.name.length > 0 => arg.name }.getOrElse(paramInfos.name) - - private inline def getShortName(paramInfos: ParameterInfos[_]): Option[Char] = - paramInfos.annotations.collectFirst{ case arg: Arg if arg.shortName != 0 => arg.shortName } + private def getAlternativeNames(paramInfos: ParameterInfos[_]): Seq[String] = + paramInfos.annotations.collect{ case Name(n) => n }.filter(_.length > 0) + + private def getShortNames(paramInfos: ParameterInfos[_]): Seq[Char] = + val (valid, invalid) = + paramInfos.annotations.collect{ case ShortName(c) => c }.partition(shortNameIsValid) + if invalid.nonEmpty then + throw IllegalArgumentException(s"invalid short names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") + valid + + private def checkNamesUnicity(namess: Seq[Seq[Any]]): Unit = + val nameToIndex = namess.zipWithIndex.flatMap{ case (names, idx) => names.map(_ -> idx) } + val nameToIndices = nameToIndex.groupMap(_._1)(_._2) + for + (name, indices) <- nameToIndices if indices.length > 1 + do + val canonicalNamesAtIndices = indices.map(idx => argCanonicalNames(idx)).mkString(", ") + throw AssertionError(s"$name is used for multiple parameters: $canonicalNamesAtIndices") + + private def checkNamesAreUnique(): Unit = + val namess = argCanonicalNames.zip(argAlternativeNamess).map{ case (canon, alts) => canon +: alts }.toList + checkNamesUnicity(namess) + + private def checkShortNamesAreUnique(): Unit = + checkNamesUnicity(argShortNamess.toList) + + private def flagUnused(): Unit = + nextPositionalArg() match + case Some(arg) => + error(s"unused argument: $arg") + flagUnused() + case None => + val longNames = argCanonicalNames ++: argAlternativeNamess.flatten + val shortNames = argShortNamess.flatten + for + arg <- args + do + val isInvalidArg = + arg.length > argMarker.length + && arg.startsWith(argMarker) + && !longNames.contains(arg.drop(argMarker.length)) + val isInvalidShortArg = + arg.length > shortArgMarker.length + && arg.startsWith(shortArgMarker) + && shortNameIsValid(arg(shortArgMarker.length)) + && !shortNames.contains(arg(shortArgMarker.length)) + if isInvalidArg || isInvalidShortArg then error(s"unknown argument name: $arg") private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = - argNames += getEffectiveName(paramInfos) + def checkNamesDuplicates(names: Seq[Any]): Unit = + val occurrences = names.groupMapReduce(identity)(_ => 1)(_ + _) + for (name, numOcc) <- occurrences if numOcc > 1 + do throw AssertionError(s"$name is declared multiple times for ${paramInfos.name}") + + argCanonicalNames += paramInfos.name + argAlternativeNamess += getAlternativeNames(paramInfos) + argShortNamess += getShortNames(paramInfos) argTypes += paramInfos.typeName - argDocs += paramInfos.documentation.getOrElse("") + argDocOpts += paramInfos.documentation argKinds += argKind - val shortName = getShortName(paramInfos) - shortName.foreach(c => if !shortNameIsValid(c) then throw IllegalArgumentException(s"Invalid short name: $shortArgMarker$c")) - argShortNames += shortName + // Check if names are used multiple times by the same param + checkNamesDuplicates(argAlternativeNamess.last) + checkNamesDuplicates(argShortNamess.last) override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val name = getEffectiveName(paramInfos) - val (defaultGetter, argumentKind) = paramInfos.defaultValueOpt match { - case Some(value) => (() => value, ArgumentKind.OptionalArgument) - case None => (() => error(s"missing argument for $name"), ArgumentKind.SimpleArgument) + val dvOpt = paramInfos.defaultValueOpt + registerArg(paramInfos, if dvOpt.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) + + // registerArg placed all infos in the respective buffers + val canonicalName = argCanonicalNames.last + indicesOfArg(canonicalName +: argAlternativeNamess.last, argShortNamess.last) match { + case s @ (Seq() | Seq(_)) => + val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) + (argOpt, dvOpt) match { + case (Some(arg), _) => convert(canonicalName, arg, p) + case (None, Some(defaultValueGetter)) => defaultValueGetter + case (None, None) => error(s"missing argument for ${paramInfos.name}") + } + case s => + val multValues = s.flatMap(idx => argAt(idx + 1)) + error(s"more than one value for $canonicalName: ${multValues.mkString(", ")}") } - registerArg(paramInfos, argumentKind) - getArgGetter(paramInfos, defaultGetter) + end argGetter override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = registerArg(paramInfos, ArgumentKind.VarArgument) def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(getEffectiveName(paramInfos), arg, p) :: remainingArgGetters() + case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() case None => Nil val getters = remainingArgGetters() () => getters.map(_()) override def run(f: => MainResultType): Unit = - def checkShortNamesUnique(): Unit = - val shortNameToIndices = argShortNames.collect{ case Some(short) => short }.zipWithIndex.groupBy(_._1).view.mapValues(_.map(_._2)) - for ((shortName, indices) <- shortNameToIndices if indices.length > 1) - error(s"$shortName is used as short name for multiple parameters: ${indices.map(idx => argNames(idx)).mkString(", ")}") - - def flagUnused(): Unit = nextPositionalArg() match - case Some(arg) => - error(s"unused argument: $arg") - flagUnused() - case None => - for - arg <- args - if arg.startsWith(argMarker) && !argNames.contains(arg.drop(2)) - do - error(s"unknown argument name: $arg") - end flagUnused + def checkAllConstraints(): Unit = + flagUnused() + checkNamesAreUnique() + checkShortNamesAreUnique() if args.contains(s"${argMarker}help") then usage() println() explain() else - flagUnused() - checkShortNamesUnique() + checkAllConstraints() if errors.nonEmpty then for msg <- errors do println(s"Error: $msg") usage() @@ -242,5 +293,6 @@ final class main(maxLineLength: Int) extends MainAnnotation: end main object main: - final class Arg(val name: String = "", val shortName: Char = 0) extends MainAnnotation.ParameterAnnotation + final case class ShortName(val shortName: Char) extends MainAnnotation.ParameterAnnotation + final case class Name(val name: String) extends MainAnnotation.ParameterAnnotation end main diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index ffb317613905..6654975d32a8 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -9,8 +9,10 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.this"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), ProblemFilters.exclude[MissingClassProblem]("scala.main$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$Arg"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$Arg$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$Name"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$Name$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$ShortName"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$ShortName$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index 7b70ba1fcbe7..9f4cf86f1688 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -2,82 +2,82 @@ Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc1 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc2 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc3 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc4 [--num] [[--inc] ] Adds two numbers. Arguments: - num - Int + --num - Int the first number - inc - Int (optional) + --inc - Int (optional) the second number Usage: doc5 [--num] [--inc] Adds two numbers. Arguments: - num - Int + --num - Int the first number - inc - Int + --inc - Int Usage: doc6 [--num] [--inc] Adds two numbers. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc7 [--num] [--inc] Adds two numbers. Arguments: - num - Int + --num - Int the first number - inc - Int + --inc - Int the second number Usage: doc8 [--num] [--inc] Adds two numbers. Arguments: - num - Int + --num - Int the first number - inc - Int + --inc - Int the second number Usage: doc9 [--num] [--inc] Adds two numbers. Same as doc1. Arguments: - num - Int + --num - Int the first number - inc - Int + --inc - Int the second number Usage: doc10 [--num] [--inc] @@ -85,18 +85,18 @@ Adds two numbers. This should be on another line. And this also. Arguments: - num - Int + --num - Int I might have to write this on two lines - inc - Int + --inc - Int I might even have to write this one on three lines Usage: doc11 [--num] [--inc] Adds two numbers. Arguments: - num - Int + --num - Int the first number Oh, a new line! - inc - Int + --inc - Int the second number And another one! Usage: doc12 [--num] [--inc] @@ -104,55 +104,55 @@ Usage: doc12 [--num] [--inc] Adds two numbers. It seems that I have a very long line of documentation and therefore might need to be cut at some point to fit a small terminal screen. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc13 [--num] [--inc] Addstwonumbers.ItseemsthatIhaveaverylonglineofdocumentationandthereforemightneedtobecutatsomepointtofitasmallterminalscreen. Arguments: - num - Int - inc - Int + --num - Int + --inc - Int Usage: doc14 [--arg1] [--arg2] [--arg3] [--arg4] [--arg5] [--arg6] [--arg7] [--arg8] [[--arg9] ] [[--arg10] ] [[--arg11] ] [[--arg12] ] [[--arg13] ] [[--arg14] ] [[--arg15] ] [ [ [...]]] Loudly judges the number of argument you gave to this poor function. Arguments: - arg1 - String - arg2 - Int - arg3 - String - arg4 - Int - arg5 - String - arg6 - Int - arg7 - String - arg8 - Int - arg9 - String (optional) - arg10 - Int (optional) - arg11 - String (optional) - arg12 - Int (optional) - arg13 - String (optional) - arg14 - Int (optional) - arg15 - String (optional) - arg16 - Int (vararg) + --arg1 - String + --arg2 - Int + --arg3 - String + --arg4 - Int + --arg5 - String + --arg6 - Int + --arg7 - String + --arg8 - Int + --arg9 - String (optional) + --arg10 - Int (optional) + --arg11 - String (optional) + --arg12 - Int (optional) + --arg13 - String (optional) + --arg14 - Int (optional) + --arg15 - String (optional) + --arg16 - Int (vararg) Usage: doc15 [--myNum] [--myInc] Adds two instances of MyNumber. Arguments: - myNum - MyNumber + --myNum - MyNumber my first number to add - myInc - MyNumber + --myInc - MyNumber my second number to add Usage: doc16 [--first] [--second] Compares two instances of MyGeneric. Arguments: - first - MyGeneric[Int] + --first - MyGeneric[Int] my first element - second - MyGeneric[Int] + --second - MyGeneric[Int] my second element Usage: doc17 [--a] [--b] [--c] Arguments: - a - Int - b - Int - c - String + --a - Int + --b - Int + --c - String diff --git a/tests/run/main-annotation-max-line-length.check b/tests/run/main-annotation-max-line-length.check index 18fbb759c339..7cf01eadbcbe 100644 --- a/tests/run/main-annotation-max-line-length.check +++ b/tests/run/main-annotation-max-line-length.check @@ -2,11 +2,11 @@ Usage: doc1 [--a] [--b] [--c] Help is printed normally. Arguments: - a - Int + --a - Int the first argument - b - Int + --b - Int the second argument - c - String + --c - String the third argument. This one is a String Usage: doc2 [--a] [--b] [--c] @@ -14,11 +14,11 @@ Usage: doc2 [--a] [--b] Help is printed in a slightly narrow column. Arguments: - a - Int + --a - Int the first argument - b - Int + --b - Int the second argument - c - String + --c - String the third argument. This one is a String Usage: doc3 [--a] @@ -28,13 +28,13 @@ Usage: doc3 [--a] Help is printed in a very narrow column! Arguments: - a - Int + --a - Int the first argument - b - Int + --b - Int the second argument - c - String + --c - String the third argument. This one is a String diff --git a/tests/run/main-annotation-param-annot-1.check b/tests/run/main-annotation-param-annot-1.check new file mode 100644 index 000000000000..71ae2711ee5f --- /dev/null +++ b/tests/run/main-annotation-param-annot-1.check @@ -0,0 +1,20 @@ +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 diff --git a/tests/run/main-annotation-param-annot.scala b/tests/run/main-annotation-param-annot-1.scala similarity index 71% rename from tests/run/main-annotation-param-annot.scala rename to tests/run/main-annotation-param-annot-1.scala index 13a550613d49..e26403bbd865 100644 --- a/tests/run/main-annotation-param-annot.scala +++ b/tests/run/main-annotation-param-annot-1.scala @@ -1,44 +1,33 @@ object myProgram: - @main def noParamAnnotArgs( - @main.Arg() num: Int, - @main.Arg() inc: Int - ): Unit = - println(s"$num + $inc = ${num + inc}") - @main def altName1( - @main.Arg(name = "myNum") num: Int, + @main.Name("myNum") num: Int, inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def altName2( - @main.Arg(name = "myNum") num: Int, - @main.Arg(name = "myInc") inc: Int + @main.Name("myNum") num: Int, + @main.Name("myInc") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def shortName1( - @main.Arg(shortName = 'n') num: Int, + @main.ShortName('n') num: Int, inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def shortName2( - @main.Arg(shortName = 'n') num: Int, - @main.Arg(shortName = 'i') inc: Int + @main.ShortName('n') num: Int, + @main.ShortName('i') inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def mix( - @main.Arg(name = "myNum", shortName = 'n') num: Int, - @main.Arg(shortName = 'i', name = "myInc") inc: Int + @main.Name("myNum") @main.ShortName('n') num: Int, + @main.ShortName('i') @main.Name("myInc") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") - - @main def multipleSameShortName( - @main.Arg(shortName = 'n') num: Int, - @main.Arg(shortName = 'n') inc: Int - ): Unit = () end myProgram @@ -49,8 +38,6 @@ object Test: method.invoke(null, args) def main(args: Array[String]): Unit = - callMain("noParamAnnotArgs", Array("--num", "2", "--inc", "3")) - callMain("altName1", Array("--num", "2", "--inc", "3")) callMain("altName1", Array("--myNum", "2", "--inc", "3")) @@ -75,6 +62,4 @@ object Test: callMain("mix", Array("-n", "2", "--myInc", "3")) callMain("mix", Array("--myNum", "2", "-i", "3")) callMain("mix", Array("-n", "2", "-i", "3")) - - callMain("multipleSameShortName", Array("2", "3")) end Test \ No newline at end of file diff --git a/tests/run/main-annotation-param-annot-2.check b/tests/run/main-annotation-param-annot-2.check new file mode 100644 index 000000000000..6bf25043399c --- /dev/null +++ b/tests/run/main-annotation-param-annot-2.check @@ -0,0 +1,9 @@ +OK +OK +OK +OK +OK +OK +OK +OK +OK diff --git a/tests/run/main-annotation-param-annot-2.scala b/tests/run/main-annotation-param-annot-2.scala new file mode 100644 index 000000000000..2ac910bf7efc --- /dev/null +++ b/tests/run/main-annotation-param-annot-2.scala @@ -0,0 +1,57 @@ +object myProgram: + @main def multipleSameShortNames1( + @main.ShortName('n') num: Int, + @main.ShortName('n') inc: Int + ): Unit = () + + @main def multipleSameShortNames2( + @main.ShortName('n') @main.ShortName('n') num: Int, + inc: Int + ): Unit = () + + @main def multipleSameNames1( + @main.Name("arg") num: Int, + @main.Name("arg") inc: Int + ): Unit = () + + @main def multipleSameNames2( + @main.Name("arg") @main.Name("arg") num: Int, + inc: Int + ): Unit = () + + @main def multipleSameNames3( + num: Int, + @main.Name("num") inc: Int + ): Unit = () +end myProgram + + +object Test: + def hasCauseAssertionError(e: Throwable): Boolean = + e.getCause match { + case null => false + case _: AssertionError => true + case e: Throwable => hasCauseAssertionError(e) + } + + def callMain(className: String, args: Array[String]) = + val clazz = Class.forName(className) + val method = clazz.getMethod("main", classOf[Array[String]]) + + try { method.invoke(null, args) } + catch { + case e: Exception if hasCauseAssertionError(e) => println("OK") + } + + def main(args: Array[String]): Unit = + callMain("multipleSameShortNames1", Array("--num", "2", "--inc", "3")) + callMain("multipleSameShortNames1", Array("-n", "2", "--inc", "3")) + callMain("multipleSameShortNames2", Array("--num", "2", "--inc", "3")) + callMain("multipleSameShortNames2", Array("-n", "2", "--inc", "3")) + + callMain("multipleSameNames1", Array("--num", "2", "--inc", "3")) + callMain("multipleSameNames1", Array("--arg", "2", "--inc", "3")) + callMain("multipleSameNames2", Array("--num", "2", "--inc", "3")) + callMain("multipleSameNames2", Array("--arg", "2", "--inc", "3")) + callMain("multipleSameNames3", Array("--num", "2", "--inc", "3")) +end Test \ No newline at end of file diff --git a/tests/run/main-annotation-param-annot-invalid-params.scala b/tests/run/main-annotation-param-annot-invalid-params.scala index ea3d0b1cd9d2..59f64092a05f 100644 --- a/tests/run/main-annotation-param-annot-invalid-params.scala +++ b/tests/run/main-annotation-param-annot-invalid-params.scala @@ -3,11 +3,11 @@ import java.lang.reflect.InvocationTargetException object myProgram: @main def space( - @main.Arg(shortName = ' ') i: Int, + @main.ShortName(' ') i: Int, ): Unit = () @main def nonLetter( - @main.Arg(shortName = '1') i: Int, + @main.ShortName('1') i: Int, ): Unit = () end myProgram diff --git a/tests/run/main-annotation-param-annot.check b/tests/run/main-annotation-param-annot.check deleted file mode 100644 index f4063ebda9e3..000000000000 --- a/tests/run/main-annotation-param-annot.check +++ /dev/null @@ -1,41 +0,0 @@ -2 + 3 = 5 -Error: missing argument for myNum -Error: unknown argument name: --num -Usage: altName1 [--myNum] [--inc] -2 + 3 = 5 -Error: missing argument for myNum -Error: missing argument for myInc -Error: unknown argument name: --num -Error: unknown argument name: --inc -Usage: altName2 [--myNum] [--myInc] -Error: missing argument for myInc -Error: unknown argument name: --inc -Usage: altName2 [--myNum] [--myInc] -Error: missing argument for myNum -Error: unknown argument name: --num -Usage: altName2 [--myNum] [--myInc] -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -Error: missing argument for myNum -Error: missing argument for myInc -Error: unknown argument name: --num -Error: unknown argument name: --inc -Usage: mix [-n | --myNum] [-i | --myInc] -Error: missing argument for myInc -Error: unknown argument name: --inc -Usage: mix [-n | --myNum] [-i | --myInc] -Error: missing argument for myNum -Error: unknown argument name: --num -Usage: mix [-n | --myNum] [-i | --myInc] -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -2 + 3 = 5 -Error: n is used as short name for multiple parameters: num, inc -Usage: multipleSameShortName [-n | --num] [-n | --inc] diff --git a/tests/run/main-annotation-wrong-param-names.check b/tests/run/main-annotation-wrong-param-names.check index 6b5c3fb2081e..d9bbcc049196 100644 --- a/tests/run/main-annotation-wrong-param-names.check +++ b/tests/run/main-annotation-wrong-param-names.check @@ -1,18 +1,15 @@ Error: missing argument for num Error: missing argument for inc -Usage: add [--num] [--inc] -Error: missing argument for num -Error: missing argument for inc Error: unknown argument name: --n Error: unknown argument name: --i Usage: add [--num] [--inc] -Error: invalid argument for num: -num -Error: unused argument: 1 -Usage: add [--num] [--inc] -Error: invalid argument for inc: -inc -Error: unused argument: 10 -Usage: add [--num] [--inc] Error: invalid argument for num: num Error: unused argument: inc Error: unused argument: 10 Usage: add [--num] [--inc] +Error: missing argument for inc +Error: unknown argument name: --something +Usage: add [--num] [--inc] +Error: missing argument for inc +Error: unknown argument name: --else +Usage: add [--num] [--inc] diff --git a/tests/run/main-annotation-wrong-param-names.scala b/tests/run/main-annotation-wrong-param-names.scala index 4398ad22a3ef..548bfb3521b5 100644 --- a/tests/run/main-annotation-wrong-param-names.scala +++ b/tests/run/main-annotation-wrong-param-names.scala @@ -14,9 +14,8 @@ object Test: method.invoke(null, args) def main(args: Array[String]): Unit = - callMain(Array("-n", "1", "-i", "10")) callMain(Array("--n", "1", "--i", "10")) - callMain(Array("-num", "1", "--inc", "10")) - callMain(Array("--num", "1", "-inc", "10")) callMain(Array("num", "1", "inc", "10")) + callMain(Array("--something", "1", "10")) + callMain(Array("1", "--else", "10")) end Test From cf0c98ce8d20cdd2a878aecf22278ae933d7ffab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 9 Dec 2021 14:15:31 +0100 Subject: [PATCH 100/121] Rename defaultValueOpt for clearer name --- library/src/scala/annotation/MainAnnotation.scala | 6 +++--- library/src/scala/main.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 47c623d891a2..afee5e712573 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -34,7 +34,7 @@ object MainAnnotation: /** The docstring of the parameter. Defaults to None. */ val documentation: Option[String], /** The default value that the parameter has. Defaults to None. */ - val defaultValueOpt: Option[() => T], + val defaultValueGetterOpt: Option[() => T], /** The ParameterAnnotations associated with the parameter. Defaults to Seq.empty. */ val annotations: Seq[ParameterAnnotation], ) { @@ -46,10 +46,10 @@ object MainAnnotation: new ParameterInfos(name, typeName, documentation, Some(defaultValueGetter), annotations) def withDocumentation(doc: String): ParameterInfos[T] = - new ParameterInfos(name, typeName, Some(doc), defaultValueOpt, annotations) + new ParameterInfos(name, typeName, Some(doc), defaultValueGetterOpt, annotations) def withAnnotations(annots: ParameterAnnotation*): ParameterInfos[T] = - new ParameterInfos(name, typeName, documentation, defaultValueOpt, annots) + new ParameterInfos(name, typeName, documentation, defaultValueGetterOpt, annots) override def toString: String = s"$name: $typeName" } diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 3186a3c93598..2ecc72631de0 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -244,7 +244,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: checkNamesDuplicates(argShortNamess.last) override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val dvOpt = paramInfos.defaultValueOpt + val dvOpt = paramInfos.defaultValueGetterOpt registerArg(paramInfos, if dvOpt.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) // registerArg placed all infos in the respective buffers From 99f6ba3403802bd976cf1e5d50e78b1721e1ea6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 9 Dec 2021 22:48:39 +0100 Subject: [PATCH 101/121] Fix issue with explicit type for parameters - Before this fix, trying to remove the [Int] from the option resulted in: cannot infer type; expected type is not fully defined To avoid this, we simply wrap the pre-typed arguments in TypedSplices --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 12 ++++++------ tests/run/main-annotation-homemade-annot-5.scala | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 981f403991f7..c0b045d0a85b 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -184,16 +184,16 @@ object MainProxies { /** Turns an annotation (e.g. `@main(40)`) into an instance of the class (e.g. `new scala.main(40)`). */ def instanciateAnnotation(annot: Annotation): Tree = val argss = { - def recurse(t: Tree, acc: List[List[Tree]]): List[List[Tree]] = t match { - case Apply(t, args: List[Tree]) => recurse(t, extractArgs(args) :: acc) + def recurse(t: tpd.Tree, acc: List[List[Tree]]): List[List[Tree]] = t match { + case Apply(t, args: List[tpd.Tree]) => recurse(t, extractArgs(args) :: acc) case _ => acc } - def extractArgs(args: List[Tree]): List[Tree] = + def extractArgs(args: List[tpd.Tree]): List[Tree] = args.flatMap { - case Typed(SeqLiteral(varargs, _), _) => varargs - case arg @ Select(_, name) => if name.is(DefaultGetterName) then List() else List(arg) - case arg => List(arg) + case Typed(SeqLiteral(varargs, _), _) => varargs.map(arg => TypedSplice(arg)) + case arg @ Select(_, name) => if name.is(DefaultGetterName) then List() else List(TypedSplice(arg)) + case arg => List(TypedSplice(arg)) } recurse(annot.tree, Nil) diff --git a/tests/run/main-annotation-homemade-annot-5.scala b/tests/run/main-annotation-homemade-annot-5.scala index 843545f43cb9..f8c561947770 100644 --- a/tests/run/main-annotation-homemade-annot-5.scala +++ b/tests/run/main-annotation-homemade-annot-5.scala @@ -1,6 +1,6 @@ import scala.annotation.MainAnnotation -@mainManyArgs(Some[Int](1)) def foo() = println("Hello world!") +@mainManyArgs(Some(1)) def foo() = println("Hello world!") @mainManyArgs(None) def bar() = println("Hello world!") object Test: From 2be6820eb96e42276fa1e8672c3401db8fc8da93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 9 Dec 2021 23:31:04 +0100 Subject: [PATCH 102/121] Add crazy test for param annotations --- tests/run/main-annotation-param-annot-1.check | 8 ++++ tests/run/main-annotation-param-annot-1.scala | 43 +++++++++++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/run/main-annotation-param-annot-1.check b/tests/run/main-annotation-param-annot-1.check index 71ae2711ee5f..f44d5fed0879 100644 --- a/tests/run/main-annotation-param-annot-1.check +++ b/tests/run/main-annotation-param-annot-1.check @@ -18,3 +18,11 @@ 2 + 3 = 5 2 + 3 = 5 2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 diff --git a/tests/run/main-annotation-param-annot-1.scala b/tests/run/main-annotation-param-annot-1.scala index e26403bbd865..a3285f358444 100644 --- a/tests/run/main-annotation-param-annot-1.scala +++ b/tests/run/main-annotation-param-annot-1.scala @@ -23,11 +23,28 @@ object myProgram: ): Unit = println(s"$num + $inc = ${num + inc}") - @main def mix( + @main def mix1( @main.Name("myNum") @main.ShortName('n') num: Int, @main.ShortName('i') @main.Name("myInc") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") + + def myNum: String = "myNum" + def myShortNum = { + var short = 'a' + for i <- 0 until 'n' - 'a' + do + short = (short.toInt + 1).toChar + short + } + def myInc = {new Exception("myInc")}.getMessage + def myShortInc = () => 'i' + + @main def mix2( + @main.Name(myNum) @main.ShortName(myShortNum) num: Int, + @main.ShortName(myShortInc()) @main.Name(myInc) inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") end myProgram @@ -54,12 +71,20 @@ object Test: callMain("shortName2", Array("--num", "2", "-i", "3")) callMain("shortName2", Array("-n", "2", "-i", "3")) - callMain("mix", Array("--num", "2", "--inc", "3")) - callMain("mix", Array("-n", "2", "--inc", "3")) - callMain("mix", Array("--num", "2", "-i", "3")) - callMain("mix", Array("-n", "2", "-i", "3")) - callMain("mix", Array("--myNum", "2", "--myInc", "3")) - callMain("mix", Array("-n", "2", "--myInc", "3")) - callMain("mix", Array("--myNum", "2", "-i", "3")) - callMain("mix", Array("-n", "2", "-i", "3")) + callMain("mix1", Array("--num", "2", "--inc", "3")) + callMain("mix1", Array("-n", "2", "--inc", "3")) + callMain("mix1", Array("--num", "2", "-i", "3")) + callMain("mix1", Array("-n", "2", "-i", "3")) + callMain("mix1", Array("--myNum", "2", "--myInc", "3")) + callMain("mix1", Array("-n", "2", "--myInc", "3")) + callMain("mix1", Array("--myNum", "2", "-i", "3")) + callMain("mix1", Array("-n", "2", "-i", "3")) + callMain("mix2", Array("--num", "2", "--inc", "3")) + callMain("mix2", Array("-n", "2", "--inc", "3")) + callMain("mix2", Array("--num", "2", "-i", "3")) + callMain("mix2", Array("-n", "2", "-i", "3")) + callMain("mix2", Array("--myNum", "2", "--myInc", "3")) + callMain("mix2", Array("-n", "2", "--myInc", "3")) + callMain("mix2", Array("--myNum", "2", "-i", "3")) + callMain("mix2", Array("-n", "2", "-i", "3")) end Test \ No newline at end of file From c681a6f5ef447ed9480079d6afa9aae40da34ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 14 Dec 2021 15:05:31 +0100 Subject: [PATCH 103/121] Make Name and ShortName normal classes (remove case) --- library/src/scala/main.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 2ecc72631de0..dbcd5fb41cbb 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -179,11 +179,11 @@ final class main(maxLineLength: Int) extends MainAnnotation: (indices ++: indicesShort).filter(_ >= 0) private def getAlternativeNames(paramInfos: ParameterInfos[_]): Seq[String] = - paramInfos.annotations.collect{ case Name(n) => n }.filter(_.length > 0) + paramInfos.annotations.collect{ case annot: Name => annot.name }.filter(_.length > 0) private def getShortNames(paramInfos: ParameterInfos[_]): Seq[Char] = val (valid, invalid) = - paramInfos.annotations.collect{ case ShortName(c) => c }.partition(shortNameIsValid) + paramInfos.annotations.collect{ case annot: ShortName => annot.shortName }.partition(shortNameIsValid) if invalid.nonEmpty then throw IllegalArgumentException(s"invalid short names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") valid @@ -293,6 +293,6 @@ final class main(maxLineLength: Int) extends MainAnnotation: end main object main: - final case class ShortName(val shortName: Char) extends MainAnnotation.ParameterAnnotation - final case class Name(val name: String) extends MainAnnotation.ParameterAnnotation + final class ShortName(val shortName: Char) extends MainAnnotation.ParameterAnnotation + final class Name(val name: String) extends MainAnnotation.ParameterAnnotation end main From 5b44edff47a35f309a7b42ff65839518af19b431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 21 Jan 2022 12:33:51 +0100 Subject: [PATCH 104/121] Update comments in MainProxies --- .../dotty/tools/dotc/ast/MainProxies.scala | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index c0b045d0a85b..96e0c40c5930 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -20,7 +20,10 @@ import Annotations.Annotation * * @param x my param x * * @param ys all my params y * */ - * @main(80) def f(@main.arg(shortName = 'x', name = "myX") x: S, ys: T*) = ... + * @main(80) def f( + * @main.ShortName('x') @main.Name("myX") x: S, + * ys: T* + * ) = ... * * would be translated to something like * @@ -28,18 +31,16 @@ import Annotations.Annotation * static def main(args: Array[String]): Unit = { * val cmd = new main(80).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") * - * val args0: () => S = cmd.argGetter[S]({ - * val args0paramInfos = new scala.annotation.MainAnnotation.ParameterInfos[S]("x", "S") - * args0paramInfos.documentation = Some("my param x") - * args0paramInfos.annotation = Some(new scala.main.arg(name = "myX", shortName = 'x')) - * args0paramInfos - * })(util.CommandLineParser.FromString.given_FromString_Int) + * val args0: () => S = cmd.argGetter[S]( + * new scala.annotation.MainAnnotation.ParameterInfos[S]("x", "S") + * .withDocumentation("my param x") + * .withAnnotations(new scala.main.ShortName('x'), new scala.main.Name("myX")) + * ) * - * val args1: () => Seq[T] = cmd.varargGetter[T]({ - * val args1paramInfos = new scala.annotation.MainAnnotation.ParameterInfos[T]("ys", "T") - * args1paramInfos.documentation = Some("all my params y") - * args1paramInfos - * })(util.CommandLineParser.FromString.given_FromString_String) + * val args1: () => Seq[T] = cmd.varargGetter[T]( + * new scala.annotation.MainAnnotation.ParameterInfos[T]("ys", "T") + * .withDocumentation("all my params y") + * ) * * cmd.run(f(args0.apply(), args1.apply()*)) * } @@ -103,10 +104,13 @@ object MainProxies { val documentation = new Documentation(docComment) + /** A literal value (Boolean, Int, String, etc.) */ inline def lit(any: Any): Literal = Literal(Constant(any)) + /** Some(value) */ inline def some(value: Tree): Tree = Apply(ref(defn.SomeClass.companionModule.termRef), value) + /** () => value */ def unitToValue(value: Tree): Tree = val anonName = nme.ANON_FUN val defdef = DefDef(anonName, List(Nil), TypeTree(), value) @@ -116,7 +120,7 @@ object MainProxies { * Creates a list of references and definitions of arguments, the first referencing the second. * The goal is to create the * `val arg0: () => S = ...` - * part of the code. The first element of the tuple is a ref to `arg0`, the second is the whole definition. + * part of the code. The first element of a tuple is a ref to `arg0`, the second is the whole definition. */ def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { @@ -151,9 +155,9 @@ object MainProxies { /* * Assignations to be made after the creation of the ParameterInfos. * For example: - * args0paramInfos.withDocumentation = Some("my param x") + * args0paramInfos.withDocumentation("my param x") * is represented by the pair - * (defn.MainAnnotationParameterInfos_withDocumentation, some(lit("my param x"))) + * (defn.MainAnnotationParameterInfos_withDocumentation, List(lit("my param x"))) */ var assignations: List[(Symbol, List[Tree])] = Nil for (dvSym <- defaultValueSymbols.get(n)) From 6f16275821c159ef7b4fe5e05b197c0041c21ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 21 Jan 2022 12:34:24 +0100 Subject: [PATCH 105/121] Quick clean up of MainProxies --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 96e0c40c5930..226497cd9c9b 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -85,7 +85,7 @@ object MainProxies { report.error(s"method cannot have multiple main annotations", mainAnnot.tree) Nil } - case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => + case stat @ TypeDef(_, impl: Template) if stat.symbol.is(Module) => mainMethods(stat, impl.body) case _ => Nil @@ -144,8 +144,6 @@ object MainProxies { // The ParameterInfos to be passed to the arg getter val parameterInfos = { val param = paramName.toString - val paramInfosName = argName ++ "paramInfos" - val paramInfosIdent = Ident(paramInfosName) val paramInfosTree = New( AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), // Arguments to be passed to ParameterInfos' constructor @@ -196,7 +194,7 @@ object MainProxies { def extractArgs(args: List[tpd.Tree]): List[Tree] = args.flatMap { case Typed(SeqLiteral(varargs, _), _) => varargs.map(arg => TypedSplice(arg)) - case arg @ Select(_, name) => if name.is(DefaultGetterName) then List() else List(TypedSplice(arg)) + case arg @ Select(_, name) if name.is(DefaultGetterName) => List() // Ignore default values, they will be added later by the compiler case arg => List(TypedSplice(arg)) } From 86c6bedfa2192db4aa7e65f574c46fec1dafb853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Fri, 21 Jan 2022 14:27:36 +0100 Subject: [PATCH 106/121] Change default value index extraction --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 226497cd9c9b..cff22b3c6454 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -61,8 +61,8 @@ object MainProxies { scope match { case TypeDef(_, template: Template) => template.body.flatMap((_: Tree) match { - case dd @ DefDef(name, _, _, _) if name.is(DefaultGetterName) && name.firstPart == funSymbol.name => - val index: Int = name.toString.split("\\$").last.toInt - 1 // FIXME please!! + case dd: DefDef if dd.name.is(DefaultGetterName) && dd.name.firstPart == funSymbol.name => + val DefaultGetterName.NumberedInfo(index) = dd.name.info List(index -> dd.symbol) case _ => List() }).toMap From fc1fba6a94fcf56c8a44623cc521241e239a9946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 22 Jan 2022 12:45:54 +0100 Subject: [PATCH 107/121] Reformat switch cases --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index cff22b3c6454..38c54363fd37 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -64,7 +64,7 @@ object MainProxies { case dd: DefDef if dd.name.is(DefaultGetterName) && dd.name.firstPart == funSymbol.name => val DefaultGetterName.NumberedInfo(index) = dd.name.info List(index -> dd.symbol) - case _ => List() + case _ => Nil }).toMap case _ => Map.empty } @@ -194,7 +194,7 @@ object MainProxies { def extractArgs(args: List[tpd.Tree]): List[Tree] = args.flatMap { case Typed(SeqLiteral(varargs, _), _) => varargs.map(arg => TypedSplice(arg)) - case arg @ Select(_, name) if name.is(DefaultGetterName) => List() // Ignore default values, they will be added later by the compiler + case arg: Select if arg.name.is(DefaultGetterName) => Nil // Ignore default values, they will be added later by the compiler case arg => List(TypedSplice(arg)) } From f1bc063bc09e10553be61a9bb06df9f086a026df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 22 Jan 2022 12:48:59 +0100 Subject: [PATCH 108/121] Remove unnecessary code --- compiler/src/dotty/tools/dotc/ast/MainProxies.scala | 10 +++------- library/src/scala/main.scala | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 38c54363fd37..b78cf5e7785d 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -99,7 +99,6 @@ object MainProxies { def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotationss, defaultValueSymbols: DefaultValueSymbols, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos - val mainArgsName: TermName = nme.args val cmdName: TermName = Names.termName("cmd") val documentation = new Documentation(docComment) @@ -107,9 +106,6 @@ object MainProxies { /** A literal value (Boolean, Int, String, etc.) */ inline def lit(any: Any): Literal = Literal(Constant(any)) - /** Some(value) */ - inline def some(value: Tree): Tree = Apply(ref(defn.SomeClass.companionModule.termRef), value) - /** () => value */ def unitToValue(value: Tree): Tree = val anonName = nme.ANON_FUN @@ -125,7 +121,7 @@ object MainProxies { def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => - val argName = mainArgsName ++ n.toString + val argName = nme.args ++ n.toString val isRepeated = formal.isRepeatedParam @@ -213,7 +209,7 @@ object MainProxies { TypeTree(), Apply( Select(instanciateAnnotation(mainAnnot), defn.MainAnnot_command.name), - Ident(mainArgsName) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil + Ident(nme.args) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil ) ) var args: List[ValDef] = Nil @@ -242,7 +238,7 @@ object MainProxies { val run = Apply(Select(Ident(cmdName), defn.MainAnnotCommand_run.name), mainCall) val body = Block(cmd :: args, run) - val mainArg = ValDef(mainArgsName, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) + val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) .withFlags(Param) /** Replace typed `Ident`s that have been typed with a TypeSplice with the reference to the symbol. * The annotations will be retype-checked in another scope that may not have the same imports. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index dbcd5fb41cbb..51e34c53b6c2 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -17,7 +17,6 @@ import annotation._ * displaying the help */ final class main(maxLineLength: Int) extends MainAnnotation: - self => import main._ import MainAnnotation._ From 61744dd731e2b8d02187261a3b3923743e23ae6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 22 Jan 2022 12:59:35 +0100 Subject: [PATCH 109/121] Avoid vars in code --- .../src/dotty/tools/dotc/ast/MainProxies.scala | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index b78cf5e7785d..0053d146b49b 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -122,20 +122,14 @@ object MainProxies { mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => val argName = nme.args ++ n.toString - val isRepeated = formal.isRepeatedParam - var argRef: Tree = Apply(Ident(argName), Nil) - var formalType = formal - if isRepeated then - argRef = repeated(argRef) - formalType = formalType.argTypes.head - - val getterSym = - if isRepeated then - defn.MainAnnotCommand_varargGetter - else - defn.MainAnnotCommand_argGetter + val (argRef, formalType, getterSym) = { + val argRef0 = Apply(Ident(argName), Nil) + if formal.isRepeatedParam then + (repeated(argRef0), formal.argTypes.head, defn.MainAnnotCommand_varargGetter) + else (argRef0, formal, defn.MainAnnotCommand_argGetter) + } // The ParameterInfos to be passed to the arg getter val parameterInfos = { From 06514c462fd00fb7885931aacf7b3b03c87f8896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 22 Jan 2022 13:09:13 +0100 Subject: [PATCH 110/121] Keep old version of MainProxies for #14156 --- .../dotty/tools/dotc/ast/MainProxies.scala | 91 ++++++++++++++++++- .../dotty/tools/dotc/core/Definitions.scala | 6 ++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 0053d146b49b..f47c95b71ae3 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -47,6 +47,96 @@ import Annotations.Annotation * } */ object MainProxies { + def mainProxiesOld(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { + import tpd._ + def mainMethods(stats: List[Tree]): List[Symbol] = stats.flatMap { + case stat: DefDef if stat.symbol.hasAnnotation(defn.MainAnnot) => + stat.symbol :: Nil + case stat @ TypeDef(name, impl: Template) if stat.symbol.is(Module) => + mainMethods(impl.body) + case _ => + Nil + } + mainMethods(stats).flatMap(mainProxyOld) + } + + import untpd._ + def mainProxyOld(mainFun: Symbol)(using Context): List[TypeDef] = { + val mainAnnotSpan = mainFun.getAnnotation(defn.MainAnnot).get.tree.span + def pos = mainFun.sourcePos + val argsRef = Ident(nme.args) + + def addArgs(call: untpd.Tree, mt: MethodType, idx: Int): untpd.Tree = + if (mt.isImplicitMethod) { + report.error(s"@main method cannot have implicit parameters", pos) + call + } + else { + val args = mt.paramInfos.zipWithIndex map { + (formal, n) => + val (parserSym, formalElem) = + if (formal.isRepeatedParam) (defn.CLP_parseRemainingArguments, formal.argTypes.head) + else (defn.CLP_parseArgument, formal) + val arg = Apply( + TypeApply(ref(parserSym.termRef), TypeTree(formalElem) :: Nil), + argsRef :: Literal(Constant(idx + n)) :: Nil) + if (formal.isRepeatedParam) repeated(arg) else arg + } + val call1 = Apply(call, args) + mt.resType match { + case restpe: MethodType => + if (mt.paramInfos.lastOption.getOrElse(NoType).isRepeatedParam) + report.error(s"varargs parameter of @main method must come last", pos) + addArgs(call1, restpe, idx + args.length) + case _ => + call1 + } + } + + var result: List[TypeDef] = Nil + if (!mainFun.owner.isStaticOwner) + report.error(s"@main method is not statically accessible", pos) + else { + var call = ref(mainFun.termRef) + mainFun.info match { + case _: ExprType => + case mt: MethodType => + call = addArgs(call, mt, 0) + case _: PolyType => + report.error(s"@main method cannot have type parameters", pos) + case _ => + report.error(s"@main can only annotate a method", pos) + } + val errVar = Ident(nme.error) + val handler = CaseDef( + Typed(errVar, TypeTree(defn.CLP_ParseError.typeRef)), + EmptyTree, + Apply(ref(defn.CLP_showError.termRef), errVar :: Nil)) + val body = Try(call, handler :: Nil, EmptyTree) + val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) + .withFlags(Param) + /** Replace typed `Ident`s that have been typed with a TypeSplice with the reference to the symbol. + * The annotations will be retype-checked in another scope that may not have the same imports. + */ + def insertTypeSplices = new TreeMap { + override def transform(tree: Tree)(using Context): Tree = tree match + case tree: tpd.Ident @unchecked => TypedSplice(tree) + case tree => super.transform(tree) + } + val annots = mainFun.annotations + .filterNot(_.matches(defn.MainAnnot)) + .map(annot => insertTypeSplices.transform(annot.tree)) + val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body) + .withFlags(JavaStatic) + .withAnnotations(annots) + val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil) + val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl) + .withFlags(Final | Invisible) + if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil + } + result + } + private type DefaultValueSymbols = Map[Int, Symbol] private type ParameterAnnotationss = Seq[Seq[Annotation]] @@ -95,7 +185,6 @@ object MainProxies { mainMethods(EmptyTree, stats).flatMap(mainProxy) } - import untpd._ def mainProxy(mainFun: Symbol, paramAnnotations: ParameterAnnotationss, defaultValueSymbols: DefaultValueSymbols, docComment: Option[Comment])(using Context): List[TypeDef] = { val mainAnnot = mainFun.getAnnotation(defn.MainAnnot).get def pos = mainFun.sourcePos diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 77d1631144db..52801129642b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -865,6 +865,12 @@ class Definitions { @tu lazy val MainAnnotCommand_varargGetter: Symbol = MainAnnotCommand.requiredMethod("varargGetter") @tu lazy val MainAnnotCommand_run: Symbol = MainAnnotCommand.requiredMethod("run") + @tu lazy val CommandLineParserModule: Symbol = requiredModule("scala.util.CommandLineParser") + @tu lazy val CLP_ParseError: ClassSymbol = CommandLineParserModule.requiredClass("ParseError").typeRef.symbol.asClass + @tu lazy val CLP_parseArgument: Symbol = CommandLineParserModule.requiredMethod("parseArgument") + @tu lazy val CLP_parseRemainingArguments: Symbol = CommandLineParserModule.requiredMethod("parseRemainingArguments") + @tu lazy val CLP_showError: Symbol = CommandLineParserModule.requiredMethod("showError") + @tu lazy val TupleTypeRef: TypeRef = requiredClassRef("scala.Tuple") def TupleClass(using Context): ClassSymbol = TupleTypeRef.symbol.asClass @tu lazy val Tuple_cons: Symbol = TupleClass.requiredMethod("*:") From 5599dd535762ecbf6c98dafbc3e47081bacf6354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 22 Jan 2022 15:28:01 +0100 Subject: [PATCH 111/121] Add more thorough documentation for main --- library/src/scala/main.scala | 94 ++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 51e34c53b6c2..38afd14e942b 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -12,15 +12,101 @@ import collection.mutable import annotation._ /** - * An annotation that designates a main function. - * @param maxLineLength the maximum number of characters to print on a single line when - * displaying the help + * The annotation that designates a main function. + * Main functions are entry points for Scala programs. They can be called through a command line interface by using + * the `scala` command, followed by their name and, optionally, their parameters. + * + * The parameters of a main function may have any type `T`, as long as there exists a + * `given util.CommandLineParser.FromString[T]` in the scope. It will be used for parsing the string given as input + * into the correct argument type. + * These types already have parsers defined: + * - String, + * - Boolean, + * - Byte, Short, Int, Long, Float, Double. + * + * The parameters of a main function may be passed either by position, or by name. Passing an argument positionaly + * means that you give the arguments in the same order as the function's signature. Passing an argument by name means + * that you give the argument right after giving its name. Considering the function + * `@main def foo(i: Int, s: String)`, we may have arguments passed: + * - by position: `scala foo 1 abc`, + * - by name: `scala foo --i 1 --s abc` or `scala foo --s abc --i 1`. + * + * A mixture of both is also possible: `scala foo --s abc 1` is equivalent to all previous examples. + * + * Note that main function overloading is not currently supported, i.e. you cannot define two main methods that have + * the same name in the same project. + * + * A special argument is used to display help regarding a main function: `--help`. If used as argument, the program + * will display some useful information about the main function. This help directly uses the ScalaDoc comment + * associated with the function, more precisely its description and the description of the parameters documented with + * `@param`. + * + * + * Parameters may be given annotations to add functionalities to the main function: + * - `main.ShortName` adds a short name to a parameter. For example, if a parameter `node` has as short name `n`, it + * may be addressed using either `--node` or `-n`, + * - `main.Name` adds another name to a parameter. For example, if a parameter `node` has as alternative name + * `otherNode`, it may be addressed using either `--node` or `--otherNode`. + * + * Here is an example of a main function with annotated parameters: + * `@main def foo(@main.ShortName('x') number: Int, @main.Name("explanation") s: String)`. The following commands are + * equivalent: + * - `scala foo --number 1 --s abc` + * - `scala foo -x 1 --s abc` + * - `scala foo --number 1 --explanation abc` + * - `scala foo -x 1 --explanation abc` + * + * @param maxLineLength the maximum number of characters to print on a single line when displaying the help */ final class main(maxLineLength: Int) extends MainAnnotation: import main._ import MainAnnotation._ - /** An annotation that designates a main function. */ + /** + * The annotation that designates a main function. + * Main functions are entry points for Scala programs. They can be called through a command line interface by using + * the `scala` command, followed by their name and, optionally, their parameters. + * + * The parameters of a main function may have any type `T`, as long as there exists a + * `given util.CommandLineParser.FromString[T]` in the scope. It will be used for parsing the string given as input + * into the correct argument type. + * These types already have parsers defined: + * - String, + * - Boolean, + * - Byte, Short, Int, Long, Float, Double. + * + * The parameters of a main function may be passed either by position, or by name. Passing an argument positionaly + * means that you give the arguments in the same order as the function's signature. Passing an argument by name means + * that you give the argument right after giving its name. Considering the function + * `@main def foo(i: Int, s: String)`, we may have arguments passed: + * - by position: `scala foo 1 abc`, + * - by name: `scala foo --i 1 --s abc` or `scala foo --s abc --i 1`. + * + * A mixture of both is also possible: `scala foo --s abc 1` is equivalent to all previous examples. + * + * Note that main function overloading is not currently supported, i.e. you cannot define two main methods that have + * the same name in the same project. + * + * A special argument is used to display help regarding a main function: `--help`. If used as argument, the program + * will display some useful information about the main function. This help directly uses the ScalaDoc comment + * associated with the function, more precisely its description and the description of the parameters documented with + * `@param`. + * + * + * Parameters may be given annotations to add functionalities to the main function: + * - `main.ShortName` adds a short name to a parameter. For example, if a parameter `node` has as short name `n`, it + * may be addressed using either `--node` or `-n`, + * - `main.Name` adds another name to a parameter. For example, if a parameter `node` has as alternative name + * `otherNode`, it may be addressed using either `--node` or `--otherNode`. + * + * Here is an example of a main function with annotated parameters: + * `@main def foo(@main.ShortName('x') number: Int, @main.Name("explanation") s: String)`. The following commands are + * equivalent: + * - `scala foo --number 1 --s abc` + * - `scala foo -x 1 --s abc` + * - `scala foo --number 1 --explanation abc` + * - `scala foo -x 1 --explanation abc` + */ def this() = this(120) override type ArgumentParser[T] = util.CommandLineParser.FromString[T] From 823f320e418c0751e6f1d467ca4867782a3e74d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 23 Jan 2022 11:29:17 +0100 Subject: [PATCH 112/121] Allow only standard letters as short names --- library/src/scala/main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 38afd14e942b..210ba19175e6 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -156,7 +156,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def shortNameIsValid(shortName: Char): Boolean = // If you change this, remember to update the error message when an invalid short name is given - shortName.isLetter + ('A' <= shortName && shortName <= 'Z') || ('a' <= shortName && shortName <= 'z') private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match From a8da6b8d20d62d8627adf637bc8dcd007a1a5739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 25 Jan 2022 09:36:26 +0100 Subject: [PATCH 113/121] Rename docComment for clarity --- library/src/scala/annotation/MainAnnotation.scala | 2 +- library/src/scala/main.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index afee5e712573..2c466f612d62 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -21,7 +21,7 @@ trait MainAnnotation extends StaticAnnotation: type MainResultType /** A new command with arguments from `args` */ - def command(args: Array[String], commandName: String, docComment: String): MainAnnotation.Command[ArgumentParser, MainResultType] + def command(args: Array[String], commandName: String, documentation: String): MainAnnotation.Command[ArgumentParser, MainResultType] end MainAnnotation object MainAnnotation: diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 210ba19175e6..47220726e667 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -116,7 +116,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: case SimpleArgument, OptionalArgument, VarArgument } - override def command(args: Array[String], commandName: String, docComment: String) = + override def command(args: Array[String], commandName: String, documentation: String) = new Command[ArgumentParser, MainResultType]: private val argMarker = "--" private val shortArgMarker = "-" @@ -217,8 +217,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: println(usageBeginning + usages.mkString("\n" + " " * argsOffset)) private def explain(): Unit = - if (docComment.nonEmpty) - println(wrapLongLine(docComment, maxLineLength).mkString("\n")) + if (documentation.nonEmpty) + println(wrapLongLine(documentation, maxLineLength).mkString("\n")) if (argCanonicalNames.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 From 36988bb72e70c07c717d135881d9b1f82125404f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Tue, 1 Feb 2022 17:12:33 +0100 Subject: [PATCH 114/121] Move ArgumentKind inside Command --- library/src/scala/main.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 47220726e667..088c7ff8ce89 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -112,12 +112,12 @@ final class main(maxLineLength: Int) extends MainAnnotation: override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - private enum ArgumentKind { - case SimpleArgument, OptionalArgument, VarArgument - } - override def command(args: Array[String], commandName: String, documentation: String) = new Command[ArgumentParser, MainResultType]: + private enum ArgumentKind { + case SimpleArgument, OptionalArgument, VarArgument + } + private val argMarker = "--" private val shortArgMarker = "-" From ddbb6d02a570d04bc4bc286e033d26fcdf3f9e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Thu, 3 Feb 2022 14:22:33 +0100 Subject: [PATCH 115/121] Add function to check name validity --- library/src/scala/main.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 088c7ff8ce89..0782b53532af 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -154,6 +154,9 @@ final class main(maxLineLength: Int) extends MainAnnotation: argIdx += 1 result + private def nameIsValid(name: String): Boolean = + name.length > 0 // TODO add more checks for illegal characters + private def shortNameIsValid(shortName: Char): Boolean = // If you change this, remember to update the error message when an invalid short name is given ('A' <= shortName && shortName <= 'Z') || ('a' <= shortName && shortName <= 'z') @@ -264,7 +267,11 @@ final class main(maxLineLength: Int) extends MainAnnotation: (indices ++: indicesShort).filter(_ >= 0) private def getAlternativeNames(paramInfos: ParameterInfos[_]): Seq[String] = - paramInfos.annotations.collect{ case annot: Name => annot.name }.filter(_.length > 0) + val (valid, invalid) = + paramInfos.annotations.collect{ case annot: Name => annot.name }.partition(nameIsValid) + if invalid.nonEmpty then + throw IllegalArgumentException(s"invalid names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") + valid private def getShortNames(paramInfos: ParameterInfos[_]): Seq[Char] = val (valid, invalid) = From a57ff529c961758ffb4ee2fd08a912336ff8aab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 5 Feb 2022 21:08:08 +0100 Subject: [PATCH 116/121] Pass ParameterInfos in command instead of getters - Pass ParameterInfos for parameters in the command method instead of the getter functions. This way, we know about all of them beforehand, and parsing can be done more efficiently. --- .../dotty/tools/dotc/ast/MainProxies.scala | 155 ++++++----- .../dotty/tools/dotc/core/Definitions.scala | 1 - .../src/scala/annotation/MainAnnotation.scala | 23 +- library/src/scala/main.scala | 243 ++++++++---------- .../main-annotation-homemade-annot-1.scala | 6 +- .../main-annotation-homemade-annot-2.scala | 6 +- .../main-annotation-homemade-annot-3.scala | 6 +- .../main-annotation-homemade-annot-4.scala | 6 +- .../main-annotation-homemade-annot-5.scala | 6 +- 9 files changed, 222 insertions(+), 230 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index f47c95b71ae3..e65eae687f1c 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -10,43 +10,25 @@ import Comments.Comment import NameKinds.DefaultGetterName import Annotations.Annotation -/** Generate proxy classes for main functions. - * A function like - * - * /** - * * Lorem ipsum dolor sit amet - * * consectetur adipiscing elit. - * * - * * @param x my param x - * * @param ys all my params y - * */ - * @main(80) def f( - * @main.ShortName('x') @main.Name("myX") x: S, - * ys: T* - * ) = ... - * - * would be translated to something like - * - * final class f { - * static def main(args: Array[String]): Unit = { - * val cmd = new main(80).command(args, "f", "Lorem ipsum dolor sit amet consectetur adipiscing elit.") - * - * val args0: () => S = cmd.argGetter[S]( - * new scala.annotation.MainAnnotation.ParameterInfos[S]("x", "S") - * .withDocumentation("my param x") - * .withAnnotations(new scala.main.ShortName('x'), new scala.main.Name("myX")) - * ) - * - * val args1: () => Seq[T] = cmd.varargGetter[T]( - * new scala.annotation.MainAnnotation.ParameterInfos[T]("ys", "T") - * .withDocumentation("all my params y") - * ) - * - * cmd.run(f(args0.apply(), args1.apply()*)) - * } - * } - */ object MainProxies { + /** Generate proxy classes for @main functions. + * A function like + * + * @main def f(x: S, ys: T*) = ... + * + * would be translated to something like + * + * import CommandLineParser._ + * class f { + * @static def main(args: Array[String]): Unit = + * try + * f( + * parseArgument[S](args, 0), + * parseRemainingArguments[T](args, 1): _* + * ) + * catch case err: ParseError => showError(err) + * } + */ def mainProxiesOld(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ def mainMethods(stats: List[Tree]): List[Symbol] = stats.flatMap { @@ -140,6 +122,44 @@ object MainProxies { private type DefaultValueSymbols = Map[Int, Symbol] private type ParameterAnnotationss = Seq[Seq[Annotation]] + /** + * Generate proxy classes for main functions. + * A function like + * + * /** + * * Lorem ipsum dolor sit amet + * * consectetur adipiscing elit. + * * + * * @param x my param x + * * @param ys all my params y + * */ + * @main(80) def f( + * @main.ShortName('x') @main.Name("myX") x: S, + * ys: T* + * ) = ... + * + * would be translated to something like + * + * final class f { + * static def main(args: Array[String]): Unit = { + * val cmd = new main(80).command( + * args, + * "f", + * "Lorem ipsum dolor sit amet consectetur adipiscing elit.", + * new scala.annotation.MainAnnotation.ParameterInfos("x", "S") + * .withDocumentation("my param x") + * .withAnnotations(new scala.main.ShortName('x'), new scala.main.Name("myX")), + * new scala.annotation.MainAnnotation.ParameterInfos("ys", "T") + * .withDocumentation("all my params y") + * ) + * + * val args0: () => S = cmd.argGetter[S]("x", None) + * val args1: () => Seq[T] = cmd.varargGetter[T]("ys") + * + * cmd.run(f(args0(), args1()*)) + * } + * } + */ def mainProxies(stats: List[tpd.Tree])(using Context): List[untpd.Tree] = { import tpd._ @@ -195,6 +215,12 @@ object MainProxies { /** A literal value (Boolean, Int, String, etc.) */ inline def lit(any: Any): Literal = Literal(Constant(any)) + /** None */ + inline def none: Tree = ref(defn.NoneModule.termRef) + + /** Some(value) */ + inline def some(value: Tree): Tree = Apply(ref(defn.SomeClass.companionModule.termRef), value) + /** () => value */ def unitToValue(value: Tree): Tree = val anonName = nme.ANON_FUN @@ -204,10 +230,12 @@ object MainProxies { /** * Creates a list of references and definitions of arguments, the first referencing the second. * The goal is to create the - * `val arg0: () => S = ...` - * part of the code. The first element of a tuple is a ref to `arg0`, the second is the whole definition. + * `val args0: () => S = cmd.argGetter[S]("x", None)` + * part of the code. + * For each tuple, the first element is a ref to `args0`, the second is the whole definition, the third + * is the ParameterInfos definition associated to this argument. */ - def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef)] = + def createArgs(mt: MethodType, cmdName: TermName): List[(Tree, ValDef, Tree)] = mt.paramInfos.zip(mt.paramNames).zipWithIndex.map { case ((formal, paramName), n) => val argName = nme.args ++ n.toString @@ -220,11 +248,11 @@ object MainProxies { else (argRef0, formal, defn.MainAnnotCommand_argGetter) } - // The ParameterInfos to be passed to the arg getter + // The ParameterInfos val parameterInfos = { val param = paramName.toString val paramInfosTree = New( - AppliedTypeTree(TypeTree(defn.MainAnnotParameterInfos.typeRef), List(TypeTree(formalType))), + TypeTree(defn.MainAnnotParameterInfos.typeRef), // Arguments to be passed to ParameterInfos' constructor List(List(lit(param), lit(formalType.show))) ) @@ -234,11 +262,9 @@ object MainProxies { * For example: * args0paramInfos.withDocumentation("my param x") * is represented by the pair - * (defn.MainAnnotationParameterInfos_withDocumentation, List(lit("my param x"))) + * defn.MainAnnotationParameterInfos_withDocumentation -> List(lit("my param x")) */ var assignations: List[(Symbol, List[Tree])] = Nil - for (dvSym <- defaultValueSymbols.get(n)) - assignations = (defn.MainAnnotationParameterInfos_withDefaultValue -> List(unitToValue(ref(dvSym.termRef)))) :: assignations for (doc <- documentation.argDocs.get(param)) assignations = (defn.MainAnnotationParameterInfos_withDocumentation -> List(lit(doc))) :: assignations @@ -246,19 +272,28 @@ object MainProxies { if instanciatedAnnots.nonEmpty then assignations = (defn.MainAnnotationParameterInfos_withAnnotations -> instanciatedAnnots) :: assignations - if assignations.isEmpty then - paramInfosTree - else - assignations.foldLeft[Tree](paramInfosTree){ case (tree, (setterSym, values)) => Apply(Select(tree, setterSym.name), values) } + assignations.foldLeft[Tree](paramInfosTree){ case (tree, (setterSym, values)) => Apply(Select(tree, setterSym.name), values) } } + val argParams = + if formal.isRepeatedParam then + List(lit(paramName.toString)) + else + val defaultValueGetterOpt = defaultValueSymbols.get(n) match { + case None => + none + case Some(dvSym) => + some(unitToValue(ref(dvSym.termRef))) + } + List(lit(paramName.toString), defaultValueGetterOpt) + val argDef = ValDef( argName, TypeTree(), - Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), parameterInfos), + Apply(TypeApply(Select(Ident(cmdName), getterSym.name), TypeTree(formalType) :: Nil), argParams), ) - (argRef, argDef) + (argRef, argDef, parameterInfos) } end createArgs @@ -287,16 +322,9 @@ object MainProxies { if (!mainFun.owner.isStaticOwner) report.error(s"main method is not statically accessible", pos) else { - val cmd = ValDef( - cmdName, - TypeTree(), - Apply( - Select(instanciateAnnotation(mainAnnot), defn.MainAnnot_command.name), - Ident(nme.args) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: Nil - ) - ) var args: List[ValDef] = Nil var mainCall: Tree = ref(mainFun.termRef) + var parameterInfoss: List[Tree] = Nil mainFun.info match { case _: ExprType => @@ -309,9 +337,10 @@ object MainProxies { report.error(s"main method cannot be curried", pos) Nil case _ => - val (argRefs, argVals) = createArgs(mt, cmdName).unzip + val (argRefs, argVals, paramInfoss) = createArgs(mt, cmdName).unzip3 args = argVals mainCall = Apply(mainCall, argRefs) + parameterInfoss = paramInfoss } case _: PolyType => report.error(s"main method cannot have type parameters", pos) @@ -319,6 +348,14 @@ object MainProxies { report.error(s"main can only annotate a method", pos) } + val cmd = ValDef( + cmdName, + TypeTree(), + Apply( + Select(instanciateAnnotation(mainAnnot), defn.MainAnnot_command.name), + Ident(nme.args) :: lit(mainFun.showName) :: lit(documentation.mainDoc) :: parameterInfoss + ) + ) val run = Apply(Select(Ident(cmdName), defn.MainAnnotCommand_run.name), mainCall) val body = Block(cmd :: args, run) val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 52801129642b..d60c46e7dfdb 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -856,7 +856,6 @@ class Definitions { @tu lazy val MainAnnot: ClassSymbol = requiredClass("scala.annotation.MainAnnotation") @tu lazy val MainAnnot_command: Symbol = MainAnnot.requiredMethod("command") @tu lazy val MainAnnotParameterInfos: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterInfos") - @tu lazy val MainAnnotationParameterInfos_withDefaultValue: Symbol = MainAnnotParameterInfos.requiredMethod("withDefaultValue") @tu lazy val MainAnnotationParameterInfos_withDocumentation: Symbol = MainAnnotParameterInfos.requiredMethod("withDocumentation") @tu lazy val MainAnnotationParameterInfos_withAnnotations: Symbol = MainAnnotParameterInfos.requiredMethod("withAnnotations") @tu lazy val MainAnnotParameterAnnotation: ClassSymbol = requiredClass("scala.annotation.MainAnnotation.ParameterAnnotation") diff --git a/library/src/scala/annotation/MainAnnotation.scala b/library/src/scala/annotation/MainAnnotation.scala index 2c466f612d62..4dd7d7d11165 100644 --- a/library/src/scala/annotation/MainAnnotation.scala +++ b/library/src/scala/annotation/MainAnnotation.scala @@ -21,35 +21,30 @@ trait MainAnnotation extends StaticAnnotation: type MainResultType /** A new command with arguments from `args` */ - def command(args: Array[String], commandName: String, documentation: String): MainAnnotation.Command[ArgumentParser, MainResultType] + def command(args: Array[String], commandName: String, documentation: String, parameterInfoss: MainAnnotation.ParameterInfos*): MainAnnotation.Command[ArgumentParser, MainResultType] end MainAnnotation object MainAnnotation: // Inspired by https://github.com/scala-js/scala-js/blob/0708917912938714d52be1426364f78a3d1fd269/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala#L23-L218 - final class ParameterInfos[T] private ( + final class ParameterInfos private ( /** The name of the parameter */ val name: String, /** The name of the parameter's type */ val typeName: String, /** The docstring of the parameter. Defaults to None. */ val documentation: Option[String], - /** The default value that the parameter has. Defaults to None. */ - val defaultValueGetterOpt: Option[() => T], /** The ParameterAnnotations associated with the parameter. Defaults to Seq.empty. */ val annotations: Seq[ParameterAnnotation], ) { // Main public constructor def this(name: String, typeName: String) = - this(name, typeName, None, None, Seq.empty) + this(name, typeName, None, Seq.empty) - def withDefaultValue(defaultValueGetter: () => T): ParameterInfos[T] = - new ParameterInfos(name, typeName, documentation, Some(defaultValueGetter), annotations) + def withDocumentation(doc: String): ParameterInfos = + new ParameterInfos(name, typeName, Some(doc), annotations) - def withDocumentation(doc: String): ParameterInfos[T] = - new ParameterInfos(name, typeName, Some(doc), defaultValueGetterOpt, annotations) - - def withAnnotations(annots: ParameterAnnotation*): ParameterInfos[T] = - new ParameterInfos(name, typeName, documentation, defaultValueGetterOpt, annots) + def withAnnotations(annots: ParameterAnnotation*): ParameterInfos = + new ParameterInfos(name, typeName, documentation, annots) override def toString: String = s"$name: $typeName" } @@ -58,10 +53,10 @@ object MainAnnotation: trait Command[ArgumentParser[_], MainResultType]: /** The getter for the next argument of type `T` */ - def argGetter[T](paramInfos: ParameterInfos[T])(using fromString: ArgumentParser[T]): () => T + def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using fromString: ArgumentParser[T]): () => T /** The getter for a final varargs argument of type `T*` */ - def varargGetter[T](paramInfos: ParameterInfos[T])(using fromString: ArgumentParser[T]): () => Seq[T] + def varargGetter[T](name: String)(using fromString: ArgumentParser[T]): () => Seq[T] /** Run `program` if all arguments are valid, * or print usage information and/or error messages. diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 0782b53532af..8a7f3bf8fbb4 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -112,7 +112,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - override def command(args: Array[String], commandName: String, documentation: String) = + override def command(args: Array[String], commandName: String, documentation: String, parameterInfoss: ParameterInfos*) = new Command[ArgumentParser, MainResultType]: private enum ArgumentKind { case SimpleArgument, OptionalArgument, VarArgument @@ -121,44 +121,59 @@ final class main(maxLineLength: Int) extends MainAnnotation: private val argMarker = "--" private val shortArgMarker = "-" - private var argCanonicalNames = new mutable.ArrayBuffer[String] - private var argAlternativeNamess = new mutable.ArrayBuffer[Seq[String]] - private var argShortNamess = new mutable.ArrayBuffer[Seq[Char]] - private var argTypes = new mutable.ArrayBuffer[String] - private var argDocOpts = new mutable.ArrayBuffer[Option[String]] - private var argKinds = new mutable.ArrayBuffer[ArgumentKind] + /** A map from argument canonical name (the name of the parameter in the method definition) to parameter informations */ + private val nameToParameterInfos: Map[String, ParameterInfos] = parameterInfoss.map(infos => infos.name -> infos).toMap + + private val (positionalArgs, byNameArgs, invalidByNameArgs) = { + val namesToCanonicalName: Map[String, String] = parameterInfoss.flatMap(infos => (infos.name +: getAlternativeNames(infos)).map(_ -> infos.name)).toMap + val shortNamesToCanonicalName: Map[Char, String] = parameterInfoss.flatMap(infos => getShortNames(infos).map(_ -> infos.name)).toMap + + def getCanonicalArgName(arg: String): Option[String] = + if arg.startsWith(argMarker) && arg.length > argMarker.length then + namesToCanonicalName.get(arg.drop(argMarker.length)) + else if arg.startsWith(shortArgMarker) && arg.length == shortArgMarker.length + 1 then + shortNamesToCanonicalName.get(arg(shortArgMarker.length)) + else + None + + def recurse(remainingArgs: Seq[String], pa: mutable.Queue[String], bna: Seq[(String, String)], ia: Seq[String]): (mutable.Queue[String], Seq[(String, String)], Seq[String]) = + remainingArgs match { + case Seq() => + (pa, bna, ia) + case argName +: argValue +: rest if isArgName(argName) => + getCanonicalArgName(argName) match { + case Some(canonicalName) => recurse(rest, pa, bna :+ (canonicalName -> argValue), ia) + case None => recurse(rest, pa, bna, ia :+ argName) + } + case arg +: rest => + recurse(rest, pa :+ arg, bna, ia) + } + + val (pa, bna, ia) = recurse(args.toSeq, mutable.Queue.empty, Vector(), Vector()) + val nameToArgValues: Map[String, Seq[String]] = if bna.isEmpty then Map.empty else bna.groupMapReduce(_._1)(p => List(p._2))(_ ++ _) + (pa, nameToArgValues, ia) + } + + /** The kind of the arguments. Used to display help about the main method. */ + private val argKinds = new mutable.ArrayBuffer[ArgumentKind] /** A buffer for all errors */ - private var errors = new mutable.ArrayBuffer[String] + private val errors = new mutable.ArrayBuffer[String] /** Issue an error, and return an uncallable getter */ private def error(msg: String): () => Nothing = errors += msg () => throw new AssertionError("trying to get invalid argument") - /** The next argument index */ - private var argIdx: Int = 0 - - private def argAt(idx: Int): Option[String] = - if idx < args.length then Some(args(idx)) else None - - private def isArgNameAt(idx: Int): Boolean = - val arg = args(argIdx) + private def isArgName(arg: String): Boolean = val isFullName = arg.startsWith(argMarker) val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) isFullName || isShortName - private def nextPositionalArg(): Option[String] = - while argIdx < args.length && isArgNameAt(argIdx) do argIdx += 2 - val result = argAt(argIdx) - argIdx += 1 - result - private def nameIsValid(name: String): Boolean = name.length > 0 // TODO add more checks for illegal characters private def shortNameIsValid(shortName: Char): Boolean = - // If you change this, remember to update the error message when an invalid short name is given ('A' <= shortName && shortName <= 'Z') || ('a' <= shortName && shortName <= 'z') private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = @@ -166,21 +181,18 @@ final class main(maxLineLength: Int) extends MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - private def allNamesWithMarkers(pos: Int): Seq[String] = - val canonicalName = argMarker + argCanonicalNames(pos) - val alternativeNames = argAlternativeNamess(pos).map(argMarker + _) - val shortNames = argShortNamess(pos).map(shortArgMarker + _) - canonicalName +: shortNames ++: alternativeNames - private def argsUsage: Seq[String] = - for (pos <- 0 until argCanonicalNames.length) + for ((infos, kind) <- parameterInfoss.zip(argKinds)) yield { - val namesPrint = allNamesWithMarkers(pos).mkString("[", " | ", "]") - - argKinds(pos) match { - case ArgumentKind.SimpleArgument => s"$namesPrint <${argTypes(pos)}>" - case ArgumentKind.OptionalArgument => s"[$namesPrint <${argTypes(pos)}>]" - case ArgumentKind.VarArgument => s"[<${argTypes(pos)}> [<${argTypes(pos)}> [...]]]" + val canonicalName = argMarker + infos.name + val shortNames = getShortNames(infos).map(shortArgMarker + _) + val alternativeNames = getAlternativeNames(infos).map(argMarker + _) + val namesPrint = (canonicalName +: alternativeNames ++: shortNames).mkString("[", " | ", "]") + + kind match { + case ArgumentKind.SimpleArgument => s"$namesPrint <${infos.typeName}>" + case ArgumentKind.OptionalArgument => s"[$namesPrint <${infos.typeName}>]" + case ArgumentKind.VarArgument => s"[<${infos.typeName}> [<${infos.typeName}> [...]]]" } } @@ -222,29 +234,29 @@ final class main(maxLineLength: Int) extends MainAnnotation: private def explain(): Unit = if (documentation.nonEmpty) println(wrapLongLine(documentation, maxLineLength).mkString("\n")) - if (argCanonicalNames.nonEmpty) { + if (nameToParameterInfos.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 println("Arguments:") - for (pos <- 0 until argCanonicalNames.length) - val canonicalName = argMarker + argCanonicalNames(pos) - val shortNames = argShortNamess(pos).map(shortArgMarker + _) - val alternativeNames = argAlternativeNamess(pos).map(argMarker + _) - val otherNames = (shortNames ++: alternativeNames) match { + for ((infos, kind) <- parameterInfoss.zip(argKinds)) + val canonicalName = argMarker + infos.name + val shortNames = getShortNames(infos).map(shortArgMarker + _) + val alternativeNames = getAlternativeNames(infos).map(argMarker + _) + val otherNames = (alternativeNames ++: shortNames) match { case Seq() => "" case names => names.mkString("(", ", ", ") ") } val argDoc = StringBuilder(" " * argNameShift) - argDoc.append(s"$canonicalName $otherNames- ${argTypes(pos)}") + argDoc.append(s"$canonicalName $otherNames- ${infos.typeName}") - argKinds(pos) match { + kind match { case ArgumentKind.OptionalArgument => argDoc.append(" (optional)") case ArgumentKind.VarArgument => argDoc.append(" (vararg)") case _ => } - argDocOpts(pos).foreach( + infos.documentation.foreach( doc => if (doc.nonEmpty) { val shiftedDoc = doc.split("\n").nn @@ -257,129 +269,78 @@ final class main(maxLineLength: Int) extends MainAnnotation: println(argDoc) } - private def indicesOfArg(argNames: Seq[String], shortArgNames: Seq[Char]): Seq[Int] = - def allIndicesOf(s: String, from: Int): Seq[Int] = - val i = args.indexOf(s, from) - if i < 0 then Seq() else i +: allIndicesOf(s, i + 1) - - val indices = argNames.flatMap(name => allIndicesOf(s"$argMarker$name", 0)) - val indicesShort = shortArgNames.flatMap(shortName => allIndicesOf(s"$shortArgMarker$shortName", 0)) - (indices ++: indicesShort).filter(_ >= 0) - - private def getAlternativeNames(paramInfos: ParameterInfos[_]): Seq[String] = + private def getAlternativeNames(paramInfos: ParameterInfos): Seq[String] = val (valid, invalid) = paramInfos.annotations.collect{ case annot: Name => annot.name }.partition(nameIsValid) if invalid.nonEmpty then throw IllegalArgumentException(s"invalid names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") valid - private def getShortNames(paramInfos: ParameterInfos[_]): Seq[Char] = + private def getShortNames(paramInfos: ParameterInfos): Seq[Char] = val (valid, invalid) = paramInfos.annotations.collect{ case annot: ShortName => annot.shortName }.partition(shortNameIsValid) if invalid.nonEmpty then throw IllegalArgumentException(s"invalid short names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") valid - private def checkNamesUnicity(namess: Seq[Seq[Any]]): Unit = - val nameToIndex = namess.zipWithIndex.flatMap{ case (names, idx) => names.map(_ -> idx) } - val nameToIndices = nameToIndex.groupMap(_._1)(_._2) - for - (name, indices) <- nameToIndices if indices.length > 1 - do - val canonicalNamesAtIndices = indices.map(idx => argCanonicalNames(idx)).mkString(", ") - throw AssertionError(s"$name is used for multiple parameters: $canonicalNamesAtIndices") - - private def checkNamesAreUnique(): Unit = - val namess = argCanonicalNames.zip(argAlternativeNamess).map{ case (canon, alts) => canon +: alts }.toList - checkNamesUnicity(namess) + private def checkNamesUnicity(): Unit = + val nameAndCanonicalName = nameToParameterInfos.toList.flatMap { + case (canonicalName, infos) => (canonicalName +: getAlternativeNames(infos) ++: getShortNames(infos)).map(_ -> canonicalName) + } + val nameToCanonicalNames = nameAndCanonicalName.groupMap(_._1)(_._2) - private def checkShortNamesAreUnique(): Unit = - checkNamesUnicity(argShortNamess.toList) + for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 + do throw AssertionError(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}") private def flagUnused(): Unit = - nextPositionalArg() match - case Some(arg) => - error(s"unused argument: $arg") - flagUnused() + for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg") + for (invalidArg <- invalidByNameArgs) error(s"unknown argument name: $invalidArg") + + override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = + argKinds += (if optDefaultGetter.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) + val parameterInfos = nameToParameterInfos(name) + + byNameArgs.get(name) match { + case Some(Nil) => + throw AssertionError(s"$name present in byNameArgs, but it has no argument value") + case Some(argValues) => + if argValues.length > 1 then + // Do not accept multiple values + // Remove this test to take last given argument + error(s"more than one value for $name: ${argValues.mkString(", ")}") + else + convert(name, argValues.last, p) case None => - val longNames = argCanonicalNames ++: argAlternativeNamess.flatten - val shortNames = argShortNamess.flatten - for - arg <- args - do - val isInvalidArg = - arg.length > argMarker.length - && arg.startsWith(argMarker) - && !longNames.contains(arg.drop(argMarker.length)) - val isInvalidShortArg = - arg.length > shortArgMarker.length - && arg.startsWith(shortArgMarker) - && shortNameIsValid(arg(shortArgMarker.length)) - && !shortNames.contains(arg(shortArgMarker.length)) - if isInvalidArg || isInvalidShortArg then error(s"unknown argument name: $arg") - - private def registerArg(paramInfos: ParameterInfos[_], argKind: ArgumentKind): Unit = - def checkNamesDuplicates(names: Seq[Any]): Unit = - val occurrences = names.groupMapReduce(identity)(_ => 1)(_ + _) - for (name, numOcc) <- occurrences if numOcc > 1 - do throw AssertionError(s"$name is declared multiple times for ${paramInfos.name}") - - argCanonicalNames += paramInfos.name - argAlternativeNamess += getAlternativeNames(paramInfos) - argShortNamess += getShortNames(paramInfos) - argTypes += paramInfos.typeName - argDocOpts += paramInfos.documentation - argKinds += argKind - - // Check if names are used multiple times by the same param - checkNamesDuplicates(argAlternativeNamess.last) - checkNamesDuplicates(argShortNamess.last) - - override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = - val dvOpt = paramInfos.defaultValueGetterOpt - registerArg(paramInfos, if dvOpt.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) - - // registerArg placed all infos in the respective buffers - val canonicalName = argCanonicalNames.last - indicesOfArg(canonicalName +: argAlternativeNamess.last, argShortNamess.last) match { - case s @ (Seq() | Seq(_)) => - val argOpt = s.headOption.map(idx => argAt(idx + 1)).getOrElse(nextPositionalArg()) - (argOpt, dvOpt) match { - case (Some(arg), _) => convert(canonicalName, arg, p) - case (None, Some(defaultValueGetter)) => defaultValueGetter - case (None, None) => error(s"missing argument for ${paramInfos.name}") - } - case s => - val multValues = s.flatMap(idx => argAt(idx + 1)) - error(s"more than one value for $canonicalName: ${multValues.mkString(", ")}") + if positionalArgs.length > 0 then + convert(name, positionalArgs.dequeue, p) + else if optDefaultGetter.nonEmpty then + optDefaultGetter.get + else + error(s"missing argument for $name") } end argGetter - override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = - registerArg(paramInfos, ArgumentKind.VarArgument) - def remainingArgGetters(): List[() => T] = nextPositionalArg() match - case Some(arg) => convert(paramInfos.name, arg, p) :: remainingArgGetters() - case None => Nil - val getters = remainingArgGetters() - () => getters.map(_()) + override def varargGetter[T](name: String)(using p: ArgumentParser[T]): () => Seq[T] = + argKinds += ArgumentKind.VarArgument + + val byNameGetters = byNameArgs.getOrElse(name, Seq()).map(arg => convert(name, arg, p)) + val positionalGetters = positionalArgs.removeAll.map(arg => convert(name, arg, p)) + // First take arguments passed by name, then those passed by position + () => (byNameGetters ++ positionalGetters).map(_()) override def run(f: => MainResultType): Unit = - def checkAllConstraints(): Unit = - flagUnused() - checkNamesAreUnique() - checkShortNamesAreUnique() + flagUnused() + checkNamesUnicity() if args.contains(s"${argMarker}help") then usage() println() explain() + else if errors.nonEmpty then + for msg <- errors do println(s"Error: $msg") + usage() else - checkAllConstraints() - if errors.nonEmpty then - for msg <- errors do println(s"Error: $msg") - usage() - else - f + f end run end command end main diff --git a/tests/run/main-annotation-homemade-annot-1.scala b/tests/run/main-annotation-homemade-annot-1.scala index 60308af61100..60564a9f0cd9 100644 --- a/tests/run/main-annotation-homemade-annot-1.scala +++ b/tests/run/main-annotation-homemade-annot-1.scala @@ -27,16 +27,16 @@ class mainAwait(timeout: Int = 2) extends MainAnnotation: override type MainResultType = Future[Any] // This is a toy example, it only works with positional args - override def command(args: Array[String], commandName: String, docComment: String) = + override def command(args: Array[String], commandName: String, docComment: String, parameterInfos: MainAnnotation.ParameterInfos*) = new Command[ArgumentParser, MainResultType]: private var idx = 0 - override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = val i = idx idx += 1 () => p.fromString(args(i)) - override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + override def varargGetter[T](name: String)(using p: ArgumentParser[T]): () => Seq[T] = () => for i <- (idx until args.length) yield p.fromString(args(i)) override def run(f: => MainResultType): Unit = println(Await.result(f, Duration(timeout, SECONDS))) diff --git a/tests/run/main-annotation-homemade-annot-2.scala b/tests/run/main-annotation-homemade-annot-2.scala index de5463565301..2294271a5c46 100644 --- a/tests/run/main-annotation-homemade-annot-2.scala +++ b/tests/run/main-annotation-homemade-annot-2.scala @@ -33,16 +33,16 @@ class myMain(runs: Int = 3)(after: String*) extends MainAnnotation: override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - override def command(args: Array[String], commandName: String, docComment: String) = + override def command(args: Array[String], commandName: String, docComment: String, parameterInfos: MainAnnotation.ParameterInfos*) = new Command[ArgumentParser, MainResultType]: private var idx = 0 - override def argGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => T = + override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = val i = idx idx += 1 () => p.fromString(args(i)) - override def varargGetter[T](paramInfos: ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = + override def varargGetter[T](name: String)(using p: ArgumentParser[T]): () => Seq[T] = () => for i <- (idx until args.length) yield p.fromString(args(i)) override def run(f: => MainResultType): Unit = diff --git a/tests/run/main-annotation-homemade-annot-3.scala b/tests/run/main-annotation-homemade-annot-3.scala index 3c32f57415af..11e8bf1a6a96 100644 --- a/tests/run/main-annotation-homemade-annot-3.scala +++ b/tests/run/main-annotation-homemade-annot-3.scala @@ -13,11 +13,11 @@ class mainNoArgs extends MainAnnotation: override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - override def command(args: Array[String], commandName: String, docComment: String) = + override def command(args: Array[String], commandName: String, docComment: String, parameterInfos: MainAnnotation.ParameterInfos*) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = ??? + override def argGetter[T](name: String, optDefaultValueGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = ??? - override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = ??? + override def varargGetter[T](name: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? override def run(f: => MainResultType): Unit = f end command \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-4.scala b/tests/run/main-annotation-homemade-annot-4.scala index baf2fc267861..e2d6d315a458 100644 --- a/tests/run/main-annotation-homemade-annot-4.scala +++ b/tests/run/main-annotation-homemade-annot-4.scala @@ -13,11 +13,11 @@ class mainManyArgs(i1: Int, s2: String, i3: Int) extends MainAnnotation: override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - override def command(args: Array[String], commandName: String, docComment: String) = + override def command(args: Array[String], commandName: String, docComment: String, parameterInfos: MainAnnotation.ParameterInfos*) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = ??? + override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = ??? - override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = ??? + override def varargGetter[T](name: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? override def run(f: => MainResultType): Unit = f end command \ No newline at end of file diff --git a/tests/run/main-annotation-homemade-annot-5.scala b/tests/run/main-annotation-homemade-annot-5.scala index f8c561947770..fa7fc1b47057 100644 --- a/tests/run/main-annotation-homemade-annot-5.scala +++ b/tests/run/main-annotation-homemade-annot-5.scala @@ -15,11 +15,11 @@ class mainManyArgs(o: Option[Int]) extends MainAnnotation: override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any - override def command(args: Array[String], commandName: String, docComment: String) = + override def command(args: Array[String], commandName: String, docComment: String, parameterInfos: MainAnnotation.ParameterInfos*) = new MainAnnotation.Command[ArgumentParser, MainResultType]: - override def argGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => T = ??? + override def argGetter[T](name: String, optDefaultValueGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = ??? - override def varargGetter[T](paramInfos: MainAnnotation.ParameterInfos[T])(using p: ArgumentParser[T]): () => Seq[T] = ??? + override def varargGetter[T](name: String)(using p: ArgumentParser[T]): () => Seq[T] = ??? override def run(f: => MainResultType): Unit = f end command \ No newline at end of file From c77f5d3e4aa2a8d95ca3d42249f1a4ff02516163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sat, 5 Feb 2022 21:23:44 +0100 Subject: [PATCH 117/121] Move helper functions inside methods --- library/src/scala/main.scala | 97 ++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 8a7f3bf8fbb4..137bbf8afc89 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -136,6 +136,11 @@ final class main(maxLineLength: Int) extends MainAnnotation: else None + def isArgName(arg: String): Boolean = + val isFullName = arg.startsWith(argMarker) + val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) + isFullName || isShortName + def recurse(remainingArgs: Seq[String], pa: mutable.Queue[String], bna: Seq[(String, String)], ia: Seq[String]): (mutable.Queue[String], Seq[(String, String)], Seq[String]) = remainingArgs match { case Seq() => @@ -165,11 +170,6 @@ final class main(maxLineLength: Int) extends MainAnnotation: errors += msg () => throw new AssertionError("trying to get invalid argument") - private def isArgName(arg: String): Boolean = - val isFullName = arg.startsWith(argMarker) - val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) - isFullName || isShortName - private def nameIsValid(name: String): Boolean = name.length > 0 // TODO add more checks for illegal characters @@ -181,57 +181,58 @@ final class main(maxLineLength: Int) extends MainAnnotation: case Some(t) => () => t case None => error(s"invalid argument for $argName: $arg") - private def argsUsage: Seq[String] = - for ((infos, kind) <- parameterInfoss.zip(argKinds)) - yield { - val canonicalName = argMarker + infos.name - val shortNames = getShortNames(infos).map(shortArgMarker + _) - val alternativeNames = getAlternativeNames(infos).map(argMarker + _) - val namesPrint = (canonicalName +: alternativeNames ++: shortNames).mkString("[", " | ", "]") - - kind match { - case ArgumentKind.SimpleArgument => s"$namesPrint <${infos.typeName}>" - case ArgumentKind.OptionalArgument => s"[$namesPrint <${infos.typeName}>]" - case ArgumentKind.VarArgument => s"[<${infos.typeName}> [<${infos.typeName}> [...]]]" - } - } - - private def wrapLongLine(line: String, maxLength: Int): List[String] = { - def recurse(s: String, acc: Vector[String]): Seq[String] = - val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) - if ((s.length <= maxLength) || (lastSpace < 0)) - acc :+ s - else { - val (shortLine, rest) = s.splitAt(lastSpace) - recurse(rest.trim.nn, acc :+ shortLine) - } - - recurse(line, Vector()).toList - } + private def usage(): Unit = + def argsUsage: Seq[String] = + for ((infos, kind) <- parameterInfoss.zip(argKinds)) + yield { + val canonicalName = argMarker + infos.name + val shortNames = getShortNames(infos).map(shortArgMarker + _) + val alternativeNames = getAlternativeNames(infos).map(argMarker + _) + val namesPrint = (canonicalName +: alternativeNames ++: shortNames).mkString("[", " | ", "]") - private def wrapArgumentUsages(argsUsage: Seq[String], maxLength: Int): Seq[String] = { - def recurse(args: Seq[String], currentLine: String, acc: Vector[String]): Seq[String] = - (args, currentLine) match { - case (Nil, "") => acc - case (Nil, l) => (acc :+ l) - case (arg +: t, "") => recurse(t, arg, acc) - case (arg +: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) - case (arg +: t, l) => recurse(t, arg, acc :+ l) + kind match { + case ArgumentKind.SimpleArgument => s"$namesPrint <${infos.typeName}>" + case ArgumentKind.OptionalArgument => s"[$namesPrint <${infos.typeName}>]" + case ArgumentKind.VarArgument => s"[<${infos.typeName}> [<${infos.typeName}> [...]]]" + } } - recurse(argsUsage, "", Vector()).toList - } + def wrapArgumentUsages(argsUsage: Seq[String], maxLength: Int): Seq[String] = { + def recurse(args: Seq[String], currentLine: String, acc: Vector[String]): Seq[String] = + (args, currentLine) match { + case (Nil, "") => acc + case (Nil, l) => (acc :+ l) + case (arg +: t, "") => recurse(t, arg, acc) + case (arg +: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc) + case (arg +: t, l) => recurse(t, arg, acc :+ l) + } - private inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") + recurse(argsUsage, "", Vector()).toList + } - private def usage(): Unit = val usageBeginning = s"Usage: $commandName " val argsOffset = usageBeginning.length val usages = wrapArgumentUsages(argsUsage, maxLineLength - argsOffset) println(usageBeginning + usages.mkString("\n" + " " * argsOffset)) + end usage private def explain(): Unit = + inline def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n") + + def wrapLongLine(line: String, maxLength: Int): List[String] = { + def recurse(s: String, acc: Vector[String]): Seq[String] = + val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength) + if ((s.length <= maxLength) || (lastSpace < 0)) + acc :+ s + else { + val (shortLine, rest) = s.splitAt(lastSpace) + recurse(rest.trim.nn, acc :+ shortLine) + } + + recurse(line, Vector()).toList + } + if (documentation.nonEmpty) println(wrapLongLine(documentation, maxLineLength).mkString("\n")) if (nameToParameterInfos.nonEmpty) { @@ -268,6 +269,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: println(argDoc) } + end explain private def getAlternativeNames(paramInfos: ParameterInfos): Seq[String] = val (valid, invalid) = @@ -292,10 +294,6 @@ final class main(maxLineLength: Int) extends MainAnnotation: for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 do throw AssertionError(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}") - private def flagUnused(): Unit = - for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg") - for (invalidArg <- invalidByNameArgs) error(s"unknown argument name: $invalidArg") - override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = argKinds += (if optDefaultGetter.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) val parameterInfos = nameToParameterInfos(name) @@ -329,7 +327,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: () => (byNameGetters ++ positionalGetters).map(_()) override def run(f: => MainResultType): Unit = - flagUnused() + for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg") + for (invalidArg <- invalidByNameArgs) error(s"unknown argument name: $invalidArg") checkNamesUnicity() if args.contains(s"${argMarker}help") then From 5558948d247e703acb03f1f8db41eb18b73cbce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 6 Feb 2022 10:37:08 +0100 Subject: [PATCH 118/121] Rework of main.Name and main.ShortName - Merge Name and ShortName into Alias - Make Alias take a variable number of arguments - Distinguish between long and short names by string length --- .../dotty/tools/dotc/ast/MainProxies.scala | 4 +- library/src/scala/main.scala | 96 +++++++++++-------- project/MiMaFilters.scala | 5 +- tests/run/main-annotation-help.check | 8 +- .../run/main-annotation-max-line-length.check | 30 +++--- tests/run/main-annotation-param-annot-1.check | 8 ++ tests/run/main-annotation-param-annot-1.scala | 39 +++++--- tests/run/main-annotation-param-annot-2.scala | 22 ++--- ...nnotation-param-annot-invalid-params.check | 1 + ...nnotation-param-annot-invalid-params.scala | 9 +- tests/run/main-annotation-short-name.check | 7 ++ tests/run/main-annotation-short-name.scala | 20 ++++ 12 files changed, 161 insertions(+), 88 deletions(-) create mode 100644 tests/run/main-annotation-short-name.check create mode 100644 tests/run/main-annotation-short-name.scala diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index e65eae687f1c..37498f0484ea 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -134,7 +134,7 @@ object MainProxies { * * @param ys all my params y * */ * @main(80) def f( - * @main.ShortName('x') @main.Name("myX") x: S, + * @main.Alias("myX") x: S, * ys: T* * ) = ... * @@ -148,7 +148,7 @@ object MainProxies { * "Lorem ipsum dolor sit amet consectetur adipiscing elit.", * new scala.annotation.MainAnnotation.ParameterInfos("x", "S") * .withDocumentation("my param x") - * .withAnnotations(new scala.main.ShortName('x'), new scala.main.Name("myX")), + * .withAnnotations(new scala.main.Alias("myX")), * new scala.annotation.MainAnnotation.ParameterInfos("ys", "T") * .withDocumentation("all my params y") * ) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 137bbf8afc89..363fd4c0369e 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -27,11 +27,11 @@ import annotation._ * The parameters of a main function may be passed either by position, or by name. Passing an argument positionaly * means that you give the arguments in the same order as the function's signature. Passing an argument by name means * that you give the argument right after giving its name. Considering the function - * `@main def foo(i: Int, s: String)`, we may have arguments passed: + * `@main def foo(i: Int, str: String)`, we may have arguments passed: * - by position: `scala foo 1 abc`, - * - by name: `scala foo --i 1 --s abc` or `scala foo --s abc --i 1`. + * - by name: `scala foo -i 1 --str abc` or `scala foo --str abc -i 1`. * - * A mixture of both is also possible: `scala foo --s abc 1` is equivalent to all previous examples. + * A mixture of both is also possible: `scala foo --str abc 1` is equivalent to all previous examples. * * Note that main function overloading is not currently supported, i.e. you cannot define two main methods that have * the same name in the same project. @@ -41,18 +41,15 @@ import annotation._ * associated with the function, more precisely its description and the description of the parameters documented with * `@param`. * - * * Parameters may be given annotations to add functionalities to the main function: - * - `main.ShortName` adds a short name to a parameter. For example, if a parameter `node` has as short name `n`, it - * may be addressed using either `--node` or `-n`, - * - `main.Name` adds another name to a parameter. For example, if a parameter `node` has as alternative name - * `otherNode`, it may be addressed using either `--node` or `--otherNode`. + * - `main.Alias` adds other names to a parameter. For example, if a parameter `node` has as aliases + * `otherNode` and `n`, it may be addressed using `--node`, `--otherNode` or `-n`. * * Here is an example of a main function with annotated parameters: - * `@main def foo(@main.ShortName('x') number: Int, @main.Name("explanation") s: String)`. The following commands are + * `@main def foo(@main.Alias("x") number: Int, @main.Alias("explanation") s: String)`. The following commands are * equivalent: - * - `scala foo --number 1 --s abc` - * - `scala foo -x 1 --s abc` + * - `scala foo --number 1 -s abc` + * - `scala foo -x 1 -s abc` * - `scala foo --number 1 --explanation abc` * - `scala foo -x 1 --explanation abc` * @@ -125,8 +122,20 @@ final class main(maxLineLength: Int) extends MainAnnotation: private val nameToParameterInfos: Map[String, ParameterInfos] = parameterInfoss.map(infos => infos.name -> infos).toMap private val (positionalArgs, byNameArgs, invalidByNameArgs) = { - val namesToCanonicalName: Map[String, String] = parameterInfoss.flatMap(infos => (infos.name +: getAlternativeNames(infos)).map(_ -> infos.name)).toMap - val shortNamesToCanonicalName: Map[Char, String] = parameterInfoss.flatMap(infos => getShortNames(infos).map(_ -> infos.name)).toMap + val namesToCanonicalName: Map[String, String] = parameterInfoss.flatMap( + infos => + var names = getAlternativeNames(infos) + val canonicalName = infos.name + if nameIsValid(canonicalName) then names = canonicalName +: names + names.map(_ -> canonicalName) + ).toMap + val shortNamesToCanonicalName: Map[Char, String] = parameterInfoss.flatMap( + infos => + var names = getShortNames(infos) + val canonicalName = infos.name + if shortNameIsValid(canonicalName) then names = canonicalName(0) +: names + names.map(_ -> canonicalName) + ).toMap def getCanonicalArgName(arg: String): Option[String] = if arg.startsWith(argMarker) && arg.length > argMarker.length then @@ -138,7 +147,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: def isArgName(arg: String): Boolean = val isFullName = arg.startsWith(argMarker) - val isShortName = arg.startsWith(shortArgMarker) && arg.length == 2 && shortNameIsValid(arg(1)) + val isShortName = arg.startsWith(shortArgMarker) && arg.length == shortArgMarker.length + 1 && shortNameIsValid(arg(shortArgMarker.length)) isFullName || isShortName def recurse(remainingArgs: Seq[String], pa: mutable.Queue[String], bna: Seq[(String, String)], ia: Seq[String]): (mutable.Queue[String], Seq[(String, String)], Seq[String]) = @@ -170,12 +179,21 @@ final class main(maxLineLength: Int) extends MainAnnotation: errors += msg () => throw new AssertionError("trying to get invalid argument") - private def nameIsValid(name: String): Boolean = - name.length > 0 // TODO add more checks for illegal characters + private inline def nameIsValid(name: String): Boolean = + name.length > 1 // TODO add more checks for illegal characters + + private inline def shortNameIsValid(name: String): Boolean = + name.length == 1 && shortNameIsValid(name(0)) - private def shortNameIsValid(shortName: Char): Boolean = + private inline def shortNameIsValid(shortName: Char): Boolean = ('A' <= shortName && shortName <= 'Z') || ('a' <= shortName && shortName <= 'z') + private def getNameWithMarker(name: String | Char): String = name match { + case c: Char => shortArgMarker + c + case s: String if shortNameIsValid(s) => shortArgMarker + s + case s => argMarker + s + } + private def convert[T](argName: String, arg: String, p: ArgumentParser[T]): () => T = p.fromStringOption(arg) match case Some(t) => () => t @@ -185,9 +203,9 @@ final class main(maxLineLength: Int) extends MainAnnotation: def argsUsage: Seq[String] = for ((infos, kind) <- parameterInfoss.zip(argKinds)) yield { - val canonicalName = argMarker + infos.name - val shortNames = getShortNames(infos).map(shortArgMarker + _) - val alternativeNames = getAlternativeNames(infos).map(argMarker + _) + val canonicalName = getNameWithMarker(infos.name) + val shortNames = getShortNames(infos).map(getNameWithMarker) + val alternativeNames = getAlternativeNames(infos).map(getNameWithMarker) val namesPrint = (canonicalName +: alternativeNames ++: shortNames).mkString("[", " | ", "]") kind match { @@ -241,9 +259,9 @@ final class main(maxLineLength: Int) extends MainAnnotation: println("Arguments:") for ((infos, kind) <- parameterInfoss.zip(argKinds)) - val canonicalName = argMarker + infos.name - val shortNames = getShortNames(infos).map(shortArgMarker + _) - val alternativeNames = getAlternativeNames(infos).map(argMarker + _) + val canonicalName = getNameWithMarker(infos.name) + val shortNames = getShortNames(infos).map(getNameWithMarker) + val alternativeNames = getAlternativeNames(infos).map(getNameWithMarker) val otherNames = (alternativeNames ++: shortNames) match { case Seq() => "" case names => names.mkString("(", ", ", ") ") @@ -271,28 +289,30 @@ final class main(maxLineLength: Int) extends MainAnnotation: } end explain + private def getAliases(paramInfos: ParameterInfos): Seq[String] = + paramInfos.annotations.collect{ case a: Alias => a }.flatMap(_.aliases) + private def getAlternativeNames(paramInfos: ParameterInfos): Seq[String] = - val (valid, invalid) = - paramInfos.annotations.collect{ case annot: Name => annot.name }.partition(nameIsValid) - if invalid.nonEmpty then - throw IllegalArgumentException(s"invalid names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") - valid + getAliases(paramInfos).filter(nameIsValid(_)) private def getShortNames(paramInfos: ParameterInfos): Seq[Char] = - val (valid, invalid) = - paramInfos.annotations.collect{ case annot: ShortName => annot.shortName }.partition(shortNameIsValid) - if invalid.nonEmpty then - throw IllegalArgumentException(s"invalid short names ${invalid.mkString(", ")} for parameter ${paramInfos.name}") - valid + getAliases(paramInfos).filter(shortNameIsValid(_)).map(_(0)) + + private def getInvalidNames(paramInfos: ParameterInfos): Seq[String | Char] = + getAliases(paramInfos).filter(name => !nameIsValid(name) && !shortNameIsValid(name)) + + private def checkAliasesValidity(): Unit = + val problematicNames = nameToParameterInfos.toList.flatMap((_, infos) => getInvalidNames(infos)) + if problematicNames.length > 0 then throw IllegalArgumentException(s"The following aliases are invalid: ${problematicNames.mkString(", ")}") - private def checkNamesUnicity(): Unit = + private def checkAliasesUnicity(): Unit = val nameAndCanonicalName = nameToParameterInfos.toList.flatMap { case (canonicalName, infos) => (canonicalName +: getAlternativeNames(infos) ++: getShortNames(infos)).map(_ -> canonicalName) } val nameToCanonicalNames = nameAndCanonicalName.groupMap(_._1)(_._2) for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 - do throw AssertionError(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}") + do throw IllegalArgumentException(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}") override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = argKinds += (if optDefaultGetter.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) @@ -327,9 +347,10 @@ final class main(maxLineLength: Int) extends MainAnnotation: () => (byNameGetters ++ positionalGetters).map(_()) override def run(f: => MainResultType): Unit = + checkAliasesUnicity() + checkAliasesValidity() for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg") for (invalidArg <- invalidByNameArgs) error(s"unknown argument name: $invalidArg") - checkNamesUnicity() if args.contains(s"${argMarker}help") then usage() @@ -345,6 +366,5 @@ final class main(maxLineLength: Int) extends MainAnnotation: end main object main: - final class ShortName(val shortName: Char) extends MainAnnotation.ParameterAnnotation - final class Name(val name: String) extends MainAnnotation.ParameterAnnotation + final class Alias(val aliases: String*) extends MainAnnotation.ParameterAnnotation end main diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 6654975d32a8..2758668c6048 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -9,10 +9,7 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.this"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.main.command"), ProblemFilters.exclude[MissingClassProblem]("scala.main$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$Name"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$Name$"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$ShortName"), - ProblemFilters.exclude[MissingClassProblem]("scala.main$ShortName$"), + ProblemFilters.exclude[MissingClassProblem]("scala.main$Alias"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.MainAnnotation$Command"), diff --git a/tests/run/main-annotation-help.check b/tests/run/main-annotation-help.check index 9f4cf86f1688..6410dd50de58 100644 --- a/tests/run/main-annotation-help.check +++ b/tests/run/main-annotation-help.check @@ -150,9 +150,9 @@ Arguments: my first element --second - MyGeneric[Int] my second element -Usage: doc17 [--a] [--b] [--c] +Usage: doc17 [-a] [-b] [-c] Arguments: - --a - Int - --b - Int - --c - String + -a - Int + -b - Int + -c - String diff --git a/tests/run/main-annotation-max-line-length.check b/tests/run/main-annotation-max-line-length.check index 7cf01eadbcbe..6f071b300a2a 100644 --- a/tests/run/main-annotation-max-line-length.check +++ b/tests/run/main-annotation-max-line-length.check @@ -1,40 +1,40 @@ -Usage: doc1 [--a] [--b] [--c] +Usage: doc1 [-a] [-b] [-c] Help is printed normally. Arguments: - --a - Int + -a - Int the first argument - --b - Int + -b - Int the second argument - --c - String + -c - String the third argument. This one is a String -Usage: doc2 [--a] [--b] - [--c] +Usage: doc2 [-a] [-b] + [-c] Help is printed in a slightly narrow column. Arguments: - --a - Int + -a - Int the first argument - --b - Int + -b - Int the second argument - --c - String + -c - String the third argument. This one is a String -Usage: doc3 [--a] - [--b] - [--c] +Usage: doc3 [-a] + [-b] + [-c] Help is printed in a very narrow column! Arguments: - --a - Int + -a - Int the first argument - --b - Int + -b - Int the second argument - --c - String + -c - String the third argument. This one is a String diff --git a/tests/run/main-annotation-param-annot-1.check b/tests/run/main-annotation-param-annot-1.check index f44d5fed0879..13d4bb969d18 100644 --- a/tests/run/main-annotation-param-annot-1.check +++ b/tests/run/main-annotation-param-annot-1.check @@ -26,3 +26,11 @@ 2 + 3 = 5 2 + 3 = 5 2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 +2 + 3 = 5 diff --git a/tests/run/main-annotation-param-annot-1.scala b/tests/run/main-annotation-param-annot-1.scala index a3285f358444..27125d052567 100644 --- a/tests/run/main-annotation-param-annot-1.scala +++ b/tests/run/main-annotation-param-annot-1.scala @@ -1,31 +1,31 @@ object myProgram: @main def altName1( - @main.Name("myNum") num: Int, + @main.Alias("myNum") num: Int, inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def altName2( - @main.Name("myNum") num: Int, - @main.Name("myInc") inc: Int + @main.Alias("myNum") num: Int, + @main.Alias("myInc") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def shortName1( - @main.ShortName('n') num: Int, + @main.Alias("n") num: Int, inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def shortName2( - @main.ShortName('n') num: Int, - @main.ShortName('i') inc: Int + @main.Alias("n") num: Int, + @main.Alias("i") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @main def mix1( - @main.Name("myNum") @main.ShortName('n') num: Int, - @main.ShortName('i') @main.Name("myInc") inc: Int + @main.Alias("myNum") @main.Alias("n") num: Int, + @main.Alias("i") @main.Alias("myInc") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") @@ -35,14 +35,20 @@ object myProgram: for i <- 0 until 'n' - 'a' do short = (short.toInt + 1).toChar - short + short.toString } def myInc = {new Exception("myInc")}.getMessage - def myShortInc = () => 'i' + def myShortInc = () => "i" @main def mix2( - @main.Name(myNum) @main.ShortName(myShortNum) num: Int, - @main.ShortName(myShortInc()) @main.Name(myInc) inc: Int + @main.Alias(myNum) @main.Alias(myShortNum) num: Int, + @main.Alias(myShortInc()) @main.Alias(myInc) inc: Int + ): Unit = + println(s"$num + $inc = ${num + inc}") + + @main def multiple( + @main.Alias("myNum", "n") num: Int, + @main.Alias("i", "myInc") inc: Int ): Unit = println(s"$num + $inc = ${num + inc}") end myProgram @@ -87,4 +93,13 @@ object Test: callMain("mix2", Array("-n", "2", "--myInc", "3")) callMain("mix2", Array("--myNum", "2", "-i", "3")) callMain("mix2", Array("-n", "2", "-i", "3")) + + callMain("multiple", Array("--num", "2", "--inc", "3")) + callMain("multiple", Array("-n", "2", "--inc", "3")) + callMain("multiple", Array("--num", "2", "-i", "3")) + callMain("multiple", Array("-n", "2", "-i", "3")) + callMain("multiple", Array("--myNum", "2", "--myInc", "3")) + callMain("multiple", Array("-n", "2", "--myInc", "3")) + callMain("multiple", Array("--myNum", "2", "-i", "3")) + callMain("multiple", Array("-n", "2", "-i", "3")) end Test \ No newline at end of file diff --git a/tests/run/main-annotation-param-annot-2.scala b/tests/run/main-annotation-param-annot-2.scala index 2ac910bf7efc..1c3699f2ab59 100644 --- a/tests/run/main-annotation-param-annot-2.scala +++ b/tests/run/main-annotation-param-annot-2.scala @@ -1,37 +1,37 @@ object myProgram: @main def multipleSameShortNames1( - @main.ShortName('n') num: Int, - @main.ShortName('n') inc: Int + @main.Alias("n") num: Int, + @main.Alias("n") inc: Int ): Unit = () @main def multipleSameShortNames2( - @main.ShortName('n') @main.ShortName('n') num: Int, + @main.Alias("n") @main.Alias("n") num: Int, inc: Int ): Unit = () @main def multipleSameNames1( - @main.Name("arg") num: Int, - @main.Name("arg") inc: Int + @main.Alias("arg") num: Int, + @main.Alias("arg") inc: Int ): Unit = () @main def multipleSameNames2( - @main.Name("arg") @main.Name("arg") num: Int, + @main.Alias("arg") @main.Alias("arg") num: Int, inc: Int ): Unit = () @main def multipleSameNames3( num: Int, - @main.Name("num") inc: Int + @main.Alias("num") inc: Int ): Unit = () end myProgram object Test: - def hasCauseAssertionError(e: Throwable): Boolean = + def hasCauseIllegalArgumentException(e: Throwable): Boolean = e.getCause match { case null => false - case _: AssertionError => true - case e: Throwable => hasCauseAssertionError(e) + case _: IllegalArgumentException => true + case e: Throwable => hasCauseIllegalArgumentException(e) } def callMain(className: String, args: Array[String]) = @@ -40,7 +40,7 @@ object Test: try { method.invoke(null, args) } catch { - case e: Exception if hasCauseAssertionError(e) => println("OK") + case e: Exception if hasCauseIllegalArgumentException(e) => println("OK") } def main(args: Array[String]): Unit = diff --git a/tests/run/main-annotation-param-annot-invalid-params.check b/tests/run/main-annotation-param-annot-invalid-params.check index 2c94e4837100..0eabe3671300 100644 --- a/tests/run/main-annotation-param-annot-invalid-params.check +++ b/tests/run/main-annotation-param-annot-invalid-params.check @@ -1,2 +1,3 @@ OK OK +OK diff --git a/tests/run/main-annotation-param-annot-invalid-params.scala b/tests/run/main-annotation-param-annot-invalid-params.scala index 59f64092a05f..4cfa15705320 100644 --- a/tests/run/main-annotation-param-annot-invalid-params.scala +++ b/tests/run/main-annotation-param-annot-invalid-params.scala @@ -2,12 +2,16 @@ import java.lang.reflect.InvocationTargetException object myProgram: + @main def empty( + @main.Alias("") i: Int, + ): Unit = () + @main def space( - @main.ShortName(' ') i: Int, + @main.Alias(" ") i: Int, ): Unit = () @main def nonLetter( - @main.ShortName('1') i: Int, + @main.Alias("1") i: Int, ): Unit = () end myProgram @@ -32,6 +36,7 @@ object Test: } def main(args: Array[String]): Unit = + callMain("empty", Array("3")) callMain("space", Array("3")) callMain("nonLetter", Array("3")) end Test \ No newline at end of file diff --git a/tests/run/main-annotation-short-name.check b/tests/run/main-annotation-short-name.check new file mode 100644 index 000000000000..930ac62b97ee --- /dev/null +++ b/tests/run/main-annotation-short-name.check @@ -0,0 +1,7 @@ +2 + 3 = 5 +2 + 3 = 5 +Error: missing argument for n +Error: missing argument for i +Error: unknown argument name: --n +Error: unknown argument name: --i +Usage: add [-n] [-i] diff --git a/tests/run/main-annotation-short-name.scala b/tests/run/main-annotation-short-name.scala new file mode 100644 index 000000000000..8efa51c55354 --- /dev/null +++ b/tests/run/main-annotation-short-name.scala @@ -0,0 +1,20 @@ +object myProgram: + + /** Adds two numbers */ + @main def add(n: Int, i: Int): Unit = + println(s"$n + $i = ${n + i}") + +end myProgram + +object Test: + def callMain(args: Array[String]): Unit = + val clazz = Class.forName("add") + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + + def main(args: Array[String]): Unit = + callMain(Array("-n", "2", "-i", "3")) + callMain(Array("-i", "3", "-n", "2")) + + callMain(Array("--n", "2", "--i", "3")) +end Test From 4e99de083da44390e6c55680ef5f4c83287aee46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 6 Feb 2022 10:55:29 +0100 Subject: [PATCH 119/121] Remove maxLineLength from main --- library/src/scala/main.scala | 59 ++----------------- .../run/main-annotation-max-line-length.check | 40 ------------- .../run/main-annotation-max-line-length.scala | 42 ------------- 3 files changed, 6 insertions(+), 135 deletions(-) delete mode 100644 tests/run/main-annotation-max-line-length.check delete mode 100644 tests/run/main-annotation-max-line-length.scala diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index 363fd4c0369e..f94bfe1e3344 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -52,60 +52,11 @@ import annotation._ * - `scala foo -x 1 -s abc` * - `scala foo --number 1 --explanation abc` * - `scala foo -x 1 --explanation abc` - * - * @param maxLineLength the maximum number of characters to print on a single line when displaying the help */ -final class main(maxLineLength: Int) extends MainAnnotation: +final class main extends MainAnnotation: import main._ import MainAnnotation._ - /** - * The annotation that designates a main function. - * Main functions are entry points for Scala programs. They can be called through a command line interface by using - * the `scala` command, followed by their name and, optionally, their parameters. - * - * The parameters of a main function may have any type `T`, as long as there exists a - * `given util.CommandLineParser.FromString[T]` in the scope. It will be used for parsing the string given as input - * into the correct argument type. - * These types already have parsers defined: - * - String, - * - Boolean, - * - Byte, Short, Int, Long, Float, Double. - * - * The parameters of a main function may be passed either by position, or by name. Passing an argument positionaly - * means that you give the arguments in the same order as the function's signature. Passing an argument by name means - * that you give the argument right after giving its name. Considering the function - * `@main def foo(i: Int, s: String)`, we may have arguments passed: - * - by position: `scala foo 1 abc`, - * - by name: `scala foo --i 1 --s abc` or `scala foo --s abc --i 1`. - * - * A mixture of both is also possible: `scala foo --s abc 1` is equivalent to all previous examples. - * - * Note that main function overloading is not currently supported, i.e. you cannot define two main methods that have - * the same name in the same project. - * - * A special argument is used to display help regarding a main function: `--help`. If used as argument, the program - * will display some useful information about the main function. This help directly uses the ScalaDoc comment - * associated with the function, more precisely its description and the description of the parameters documented with - * `@param`. - * - * - * Parameters may be given annotations to add functionalities to the main function: - * - `main.ShortName` adds a short name to a parameter. For example, if a parameter `node` has as short name `n`, it - * may be addressed using either `--node` or `-n`, - * - `main.Name` adds another name to a parameter. For example, if a parameter `node` has as alternative name - * `otherNode`, it may be addressed using either `--node` or `--otherNode`. - * - * Here is an example of a main function with annotated parameters: - * `@main def foo(@main.ShortName('x') number: Int, @main.Name("explanation") s: String)`. The following commands are - * equivalent: - * - `scala foo --number 1 --s abc` - * - `scala foo -x 1 --s abc` - * - `scala foo --number 1 --explanation abc` - * - `scala foo -x 1 --explanation abc` - */ - def this() = this(120) - override type ArgumentParser[T] = util.CommandLineParser.FromString[T] override type MainResultType = Any @@ -118,6 +69,8 @@ final class main(maxLineLength: Int) extends MainAnnotation: private val argMarker = "--" private val shortArgMarker = "-" + private val maxUsageLineLength = 120 + /** A map from argument canonical name (the name of the parameter in the method definition) to parameter informations */ private val nameToParameterInfos: Map[String, ParameterInfos] = parameterInfoss.map(infos => infos.name -> infos).toMap @@ -230,7 +183,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: val usageBeginning = s"Usage: $commandName " val argsOffset = usageBeginning.length - val usages = wrapArgumentUsages(argsUsage, maxLineLength - argsOffset) + val usages = wrapArgumentUsages(argsUsage, maxUsageLineLength - argsOffset) println(usageBeginning + usages.mkString("\n" + " " * argsOffset)) end usage @@ -252,7 +205,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: } if (documentation.nonEmpty) - println(wrapLongLine(documentation, maxLineLength).mkString("\n")) + println(wrapLongLine(documentation, maxUsageLineLength).mkString("\n")) if (nameToParameterInfos.nonEmpty) { val argNameShift = 2 val argDocShift = argNameShift + 2 @@ -279,7 +232,7 @@ final class main(maxLineLength: Int) extends MainAnnotation: doc => if (doc.nonEmpty) { val shiftedDoc = doc.split("\n").nn - .map(line => shiftLines(wrapLongLine(line.nn, maxLineLength - argDocShift), argDocShift)) + .map(line => shiftLines(wrapLongLine(line.nn, maxUsageLineLength - argDocShift), argDocShift)) .mkString("\n") argDoc.append("\n").append(shiftedDoc) } diff --git a/tests/run/main-annotation-max-line-length.check b/tests/run/main-annotation-max-line-length.check deleted file mode 100644 index 6f071b300a2a..000000000000 --- a/tests/run/main-annotation-max-line-length.check +++ /dev/null @@ -1,40 +0,0 @@ -Usage: doc1 [-a] [-b] [-c] - -Help is printed normally. -Arguments: - -a - Int - the first argument - -b - Int - the second argument - -c - String - the third argument. This one is a String -Usage: doc2 [-a] [-b] - [-c] - -Help is printed in a slightly narrow -column. -Arguments: - -a - Int - the first argument - -b - Int - the second argument - -c - String - the third argument. This one is a - String -Usage: doc3 [-a] - [-b] - [-c] - -Help is printed in a -very narrow column! -Arguments: - -a - Int - the first - argument - -b - Int - the second - argument - -c - String - the third - argument. This - one is a String diff --git a/tests/run/main-annotation-max-line-length.scala b/tests/run/main-annotation-max-line-length.scala deleted file mode 100644 index 7f787edf87d6..000000000000 --- a/tests/run/main-annotation-max-line-length.scala +++ /dev/null @@ -1,42 +0,0 @@ -import scala.util.CommandLineParser.FromString -import scala.util.Try - -object myProgram: - - /** - * Help is printed normally. - * @param a the first argument - * @param b the second argument - * @param c the third argument. This one is a String - */ - @main() def doc1(a: Int, b: Int, c: String): Unit = () - - /** - * Help is printed in a slightly narrow column. - * @param a the first argument - * @param b the second argument - * @param c the third argument. This one is a String - */ - @main(40) def doc2(a: Int, b: Int, c: String): Unit = () - - /** - * Help is printed in a very narrow column! - * @param a the first argument - * @param b the second argument - * @param c the third argument. This one is a String - */ - @main(maxLineLength = 20) def doc3(a: Int, b: Int, c: String): Unit = () - - -end myProgram - -object Test: - val allClazzes: Seq[Class[?]] = - LazyList.from(1).map(i => Try(Class.forName("doc" + i.toString))).takeWhile(_.isSuccess).map(_.get) - - def main(args: Array[String]): Unit = - for (clazz <- allClazzes) { - val method = clazz.getMethod("main", classOf[Array[String]]) - method.invoke(null, Array("--help")) - } -end Test From b35946586baec0d28288d535de3ee1a7be0076b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 6 Feb 2022 11:54:13 +0100 Subject: [PATCH 120/121] Add -h to display help - --help and -h display usage and help - if an argument or an alias is the same as either help or h, disable help printing for that argument --- library/src/scala/main.scala | 29 +++++++++- tests/run/main-annotation-help-override.check | 56 +++++++++++++++++++ tests/run/main-annotation-help-override.scala | 47 ++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/run/main-annotation-help-override.check create mode 100644 tests/run/main-annotation-help-override.scala diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index f94bfe1e3344..fc48e33d19ec 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -36,10 +36,13 @@ import annotation._ * Note that main function overloading is not currently supported, i.e. you cannot define two main methods that have * the same name in the same project. * - * A special argument is used to display help regarding a main function: `--help`. If used as argument, the program + * Special arguments are used to display help regarding a main function: `--help` and `-h`. If used as argument, the program * will display some useful information about the main function. This help directly uses the ScalaDoc comment * associated with the function, more precisely its description and the description of the parameters documented with - * `@param`. + * `@param`. Note that if a parameter is named `help` or `h`, or if one of the parameters has as alias one of those names, + * the help displaying will be disabled for that argument. + * For example, for `@main def foo(help: Boolean)`, `scala foo -h` will display the help, but `scala foo --help` will fail, + * as it will expect a Boolean value after `--help`. * * Parameters may be given annotations to add functionalities to the main function: * - `main.Alias` adds other names to a parameter. For example, if a parameter `node` has as aliases @@ -69,6 +72,20 @@ final class main extends MainAnnotation: private val argMarker = "--" private val shortArgMarker = "-" + /** + * The name of the special argument to display the method's help. + * If one of the method's parameters is called the same, will be ignored. + */ + private val helpArg = "help" + private var helpIsOverridden = false + + /** + * The short name of the special argument to display the method's help. + * If one of the method's parameters uses the same short name, will be ignored. + */ + private val shortHelpArg = 'h' + private var shortHelpIsOverridden = false + private val maxUsageLineLength = 120 /** A map from argument canonical name (the name of the parameter in the method definition) to parameter informations */ @@ -90,6 +107,9 @@ final class main extends MainAnnotation: names.map(_ -> canonicalName) ).toMap + helpIsOverridden = namesToCanonicalName.exists((name, _) => name == helpArg) + shortHelpIsOverridden = shortNamesToCanonicalName.exists((name, _) => name == shortHelpArg) + def getCanonicalArgName(arg: String): Option[String] = if arg.startsWith(argMarker) && arg.length > argMarker.length then namesToCanonicalName.get(arg.drop(argMarker.length)) @@ -305,7 +325,10 @@ final class main extends MainAnnotation: for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg") for (invalidArg <- invalidByNameArgs) error(s"unknown argument name: $invalidArg") - if args.contains(s"${argMarker}help") then + val displayHelp = + (!helpIsOverridden && args.contains(getNameWithMarker(helpArg))) || (!shortHelpIsOverridden && args.contains(getNameWithMarker(shortHelpArg))) + + if displayHelp then usage() println() explain() diff --git a/tests/run/main-annotation-help-override.check b/tests/run/main-annotation-help-override.check new file mode 100644 index 000000000000..921f6a185fcf --- /dev/null +++ b/tests/run/main-annotation-help-override.check @@ -0,0 +1,56 @@ +##### --help +Usage: helpOverride1 [--notHelp] + +A method that should let --help and -h display help. +Arguments: + --notHelp - Int +Error: invalid argument for help: --help +Usage: helpOverride2 [--help] +Usage: helpOverride3 [-h] + +A method that should let --help display help, but not -h. +Arguments: + -h - Int +Error: invalid argument for help: --help +Error: missing argument for h +Usage: helpOverride4 [--help] [-h] +Error: invalid argument for notHelp: --help +Usage: helpOverride5 [--notHelp | --help] +Usage: helpOverride6 [--notHelp | -h] + +A method that should let --help display help, but not -h. +Arguments: + --notHelp (-h) - Int +Error: invalid argument for notHelp: --help +Error: missing argument for notH +Usage: helpOverride7 [--notHelp | --help] [--notH | -h] +Error: invalid argument for notHelp: --help +Usage: helpOverride8 [--notHelp | --help | -h] +##### -h +Usage: helpOverride1 [--notHelp] + +A method that should let --help and -h display help. +Arguments: + --notHelp - Int +Usage: helpOverride2 [--help] + +A method that should let -h display help, but not --help. +Arguments: + --help - Int +Error: invalid argument for h: -h +Usage: helpOverride3 [-h] +Error: invalid argument for help: -h +Error: missing argument for h +Usage: helpOverride4 [--help] [-h] +Usage: helpOverride5 [--notHelp | --help] + +A method that should let -h display help, but not --help. +Arguments: + --notHelp (--help) - Int +Error: invalid argument for notHelp: -h +Usage: helpOverride6 [--notHelp | -h] +Error: invalid argument for notHelp: -h +Error: missing argument for notH +Usage: helpOverride7 [--notHelp | --help] [--notH | -h] +Error: invalid argument for notHelp: -h +Usage: helpOverride8 [--notHelp | --help | -h] diff --git a/tests/run/main-annotation-help-override.scala b/tests/run/main-annotation-help-override.scala new file mode 100644 index 000000000000..18e3f038178f --- /dev/null +++ b/tests/run/main-annotation-help-override.scala @@ -0,0 +1,47 @@ +import scala.util.Try + +object myProgram: + + /** A method that should let --help and -h display help. */ + @main def helpOverride1(notHelp: Int) = ??? + + /** A method that should let -h display help, but not --help. */ + @main def helpOverride2(help: Int) = ??? + + /** A method that should let --help display help, but not -h. */ + @main def helpOverride3(h: Int) = ??? + + /** A method that should not let --help and -h display help. */ + @main def helpOverride4(help: Int, h: Int) = ??? + + + /** A method that should let -h display help, but not --help. */ + @main def helpOverride5(@main.Alias("help") notHelp: Int) = ??? + + /** A method that should let --help display help, but not -h. */ + @main def helpOverride6(@main.Alias("h") notHelp: Int) = ??? + + /** A method that should not let --help and -h display help. */ + @main def helpOverride7(@main.Alias("help") notHelp: Int, @main.Alias("h") notH: Int) = ??? + + /** A method that should not let --help and -h display help. */ + @main def helpOverride8(@main.Alias("help", "h") notHelp: Int) = ??? + +end myProgram + +object Test: + val allClazzes: Seq[Class[?]] = + LazyList.from(1).map(i => Try(Class.forName("helpOverride" + i.toString))).takeWhile(_.isSuccess).map(_.get) + + def callAllMains(args: Array[String]): Unit = + for (clazz <- allClazzes) { + val method = clazz.getMethod("main", classOf[Array[String]]) + method.invoke(null, args) + } + + def main(args: Array[String]): Unit = + println("##### --help") + callAllMains(Array("--help")) + println("##### -h") + callAllMains(Array("-h")) +end Test \ No newline at end of file From 4ce8911124d95cb3356b27b2533661c5d06d7b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Loyck=20Andres?= Date: Sun, 6 Feb 2022 18:13:47 +0100 Subject: [PATCH 121/121] Move helper functions' code inside run --- library/src/scala/main.scala | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/library/src/scala/main.scala b/library/src/scala/main.scala index fc48e33d19ec..0556a4ddec0f 100644 --- a/library/src/scala/main.scala +++ b/library/src/scala/main.scala @@ -274,19 +274,6 @@ final class main extends MainAnnotation: private def getInvalidNames(paramInfos: ParameterInfos): Seq[String | Char] = getAliases(paramInfos).filter(name => !nameIsValid(name) && !shortNameIsValid(name)) - private def checkAliasesValidity(): Unit = - val problematicNames = nameToParameterInfos.toList.flatMap((_, infos) => getInvalidNames(infos)) - if problematicNames.length > 0 then throw IllegalArgumentException(s"The following aliases are invalid: ${problematicNames.mkString(", ")}") - - private def checkAliasesUnicity(): Unit = - val nameAndCanonicalName = nameToParameterInfos.toList.flatMap { - case (canonicalName, infos) => (canonicalName +: getAlternativeNames(infos) ++: getShortNames(infos)).map(_ -> canonicalName) - } - val nameToCanonicalNames = nameAndCanonicalName.groupMap(_._1)(_._2) - - for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 - do throw IllegalArgumentException(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}") - override def argGetter[T](name: String, optDefaultGetter: Option[() => T])(using p: ArgumentParser[T]): () => T = argKinds += (if optDefaultGetter.nonEmpty then ArgumentKind.OptionalArgument else ArgumentKind.SimpleArgument) val parameterInfos = nameToParameterInfos(name) @@ -320,8 +307,20 @@ final class main extends MainAnnotation: () => (byNameGetters ++ positionalGetters).map(_()) override def run(f: => MainResultType): Unit = - checkAliasesUnicity() - checkAliasesValidity() + // Check aliases unicity + val nameAndCanonicalName = nameToParameterInfos.toList.flatMap { + case (canonicalName, infos) => (canonicalName +: getAlternativeNames(infos) ++: getShortNames(infos)).map(_ -> canonicalName) + } + val nameToCanonicalNames = nameAndCanonicalName.groupMap(_._1)(_._2) + + for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 + do throw IllegalArgumentException(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}") + + // Check aliases validity + val problematicNames = nameToParameterInfos.toList.flatMap((_, infos) => getInvalidNames(infos)) + if problematicNames.length > 0 then throw IllegalArgumentException(s"The following aliases are invalid: ${problematicNames.mkString(", ")}") + + // Handle unused and invalid args for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg") for (invalidArg <- invalidByNameArgs) error(s"unknown argument name: $invalidArg")