diff --git a/compiler/src/dotty/tools/MainGenericCompiler.scala b/compiler/src/dotty/tools/MainGenericCompiler.scala index 2c3f6f97e79e..206d4cc69d5e 100644 --- a/compiler/src/dotty/tools/MainGenericCompiler.scala +++ b/compiler/src/dotty/tools/MainGenericCompiler.scala @@ -7,6 +7,7 @@ import java.io.File import java.lang.Thread import scala.annotation.internal.sharable import dotty.tools.dotc.util.ClasspathFromClassloader +import dotty.tools.dotc.util.chaining.* import dotty.tools.runner.ObjectRunner import dotty.tools.dotc.config.Properties.envOrNone import dotty.tools.io.Jar @@ -82,66 +83,82 @@ case class CompileSettings( def withNoColors: CompileSettings = this.copy(colors = false) } +object CompileSettings: + @sharable val javaOption = raw"""-J(.*)""".r + @sharable val javaPropOption = raw"""-D(.+?)=(.?)""".r + + def from(args: List[String]): CompileSettings = + @tailrec def process(args: List[String], settings: CompileSettings): CompileSettings = args match + case Nil => + settings + case "--" :: tail => + process(Nil, settings.withResidualArgs(tail.toList*)) + case ("-v" | "-verbose" | "--verbose") :: tail => + process(tail, settings.withScalaArgs("-verbose")) + case ("-q" | "-quiet") :: tail => + process(tail, settings.withQuiet) + case "-repl" :: tail => + process(tail, settings.withCompileMode(CompileMode.Repl)) + case "-script" :: targetScript :: tail => + process(Nil, settings + .withCompileMode(CompileMode.Script) + .withJavaProps("script.path" -> targetScript) + .withTargetScript(targetScript) + .withScriptArgs(tail*)) + case "-compile" :: tail => + process(tail, settings.withCompileMode(CompileMode.Compile)) + case "-decompile" :: tail => + process(tail, settings.withCompileMode(CompileMode.Decompile)) + case "-print-tasty" :: tail => + process(tail, settings.withCompileMode(CompileMode.PrintTasty)) + case "-run" :: tail => + process(tail, settings.withCompileMode(CompileMode.Run)) + case "-colors" :: tail => + process(tail, settings.withColors) + case "-no-colors" :: tail => + process(tail, settings.withNoColors) + case "-with-compiler" :: tail => + process(tail, settings.withCompiler) + case ("-cp" | "-classpath" | "--class-path") :: cp :: tail => + if cp.startsWith("-") then + process(cp :: tail, settings) + else + MainGenericRunner.processClasspath(cp, tail) match + case (tail, newEntries) => + process(tail, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty))) + case "-Oshort" :: tail => + // Nothing is to be done here. Request that the user adds the relevant flags manually. + // i.e this has no effect when MainGenericRunner is invoked programatically. + val addTC="-XX:+TieredCompilation" + val tStopAtLvl="-XX:TieredStopAtLevel=1" + println(s"ignoring deprecated -Oshort flag, please add `-J$addTC` and `-J$tStopAtLvl` flags manually") + process(tail, settings) + case javaOption(stripped) :: tail => + process(tail, settings.withJavaArgs(stripped)) + case javaPropOption(opt, value) :: tail => + process(tail, settings.withJavaProps(opt -> value)) + case arg :: tail => + process(tail, settings.withResidualArgs(arg)) + end process + process(args, new CompileSettings) + end from +end CompileSettings object MainGenericCompiler { val classpathSeparator = File.pathSeparator - @sharable val javaOption = raw"""-J(.*)""".r - @sharable val javaPropOption = raw"""-D(.+?)=(.?)""".r - @tailrec - def process(args: List[String], settings: CompileSettings): CompileSettings = args match - case Nil => - settings - case "--" :: tail => - process(Nil, settings.withResidualArgs(tail.toList*)) - case ("-v" | "-verbose" | "--verbose") :: tail => - process(tail, settings.withScalaArgs("-verbose")) - case ("-q" | "-quiet") :: tail => - process(tail, settings.withQuiet) - case "-repl" :: tail => - process(tail, settings.withCompileMode(CompileMode.Repl)) - case "-script" :: targetScript :: tail => - process(Nil, settings - .withCompileMode(CompileMode.Script) - .withJavaProps("script.path" -> targetScript) - .withTargetScript(targetScript) - .withScriptArgs(tail*)) - case "-compile" :: tail => - process(tail, settings.withCompileMode(CompileMode.Compile)) - case "-decompile" :: tail => - process(tail, settings.withCompileMode(CompileMode.Decompile)) - case "-print-tasty" :: tail => - process(tail, settings.withCompileMode(CompileMode.PrintTasty)) - case "-run" :: tail => - process(tail, settings.withCompileMode(CompileMode.Run)) - case "-colors" :: tail => - process(tail, settings.withColors) - case "-no-colors" :: tail => - process(tail, settings.withNoColors) - case "-with-compiler" :: tail => - process(tail, settings.withCompiler) - case ("-cp" | "-classpath" | "--class-path") :: cp :: tail => - val (tailargs, newEntries) = MainGenericRunner.processClasspath(cp, tail) - process(tailargs, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty))) - case "-Oshort" :: tail => - // Nothing is to be done here. Request that the user adds the relevant flags manually. - // i.e this has no effect when MainGenericRunner is invoked programatically. - val addTC="-XX:+TieredCompilation" - val tStopAtLvl="-XX:TieredStopAtLevel=1" - println(s"ignoring deprecated -Oshort flag, please add `-J$addTC` and `-J$tStopAtLvl` flags manually") - process(tail, settings) - case javaOption(stripped) :: tail => - process(tail, settings.withJavaArgs(stripped)) - case javaPropOption(opt, value) :: tail => - process(tail, settings.withJavaProps(opt -> value)) - case arg :: tail => - process(tail, settings.withResidualArgs(arg)) - end process - def main(args: Array[String]): Unit = - val settings = process(args.toList, CompileSettings()) - if settings.exitCode != 0 then System.exit(settings.exitCode) + import CompileMode.* + import dotc.Main.main as compiler + import dotc.decompiler.Main.main as decompiler + import dotc.core.tasty.TastyPrinter.main as tastyPrinter + import scripting.Main.main as scripting + import repl.Main.main as repl + + val settings = CompileSettings.from(args.toList) + .tap: settings => + if settings.exitCode != 0 then System.exit(settings.exitCode) def classpathSetting = if settings.classPath.isEmpty then List() @@ -153,34 +170,32 @@ object MainGenericCompiler { def addJavaProps(): Unit = settings.javaProps.foreach { (k, v) => sys.props(k) = v } - def run(settings: CompileSettings): Unit = settings.compileMode match - case CompileMode.Compile => - addJavaProps() - val properArgs = reconstructedArgs() - dotty.tools.dotc.Main.main(properArgs.toArray) - case CompileMode.Decompile => - addJavaProps() - val properArgs = reconstructedArgs() - dotty.tools.dotc.decompiler.Main.main(properArgs.toArray) - case CompileMode.PrintTasty => - addJavaProps() - val properArgs = reconstructedArgs() - dotty.tools.dotc.core.tasty.TastyPrinter.main(properArgs.toArray) - case CompileMode.Script => // Naive copy from scalac bash script - addJavaProps() - val properArgs = - reconstructedArgs() - ++ List("-script", settings.targetScript) - ++ settings.scriptArgs - scripting.Main.main(properArgs.toArray) - case CompileMode.Repl | CompileMode.Run => - addJavaProps() - val properArgs = reconstructedArgs() - repl.Main.main(properArgs.toArray) - case CompileMode.Guess => - run(settings.withCompileMode(CompileMode.Compile)) - end run - - run(settings) + settings.compileMode match + case Compile => + addJavaProps() + val properArgs = reconstructedArgs() + compiler(properArgs.toArray) + case Decompile => + addJavaProps() + val properArgs = reconstructedArgs() + decompiler(properArgs.toArray) + case PrintTasty => + addJavaProps() + val properArgs = reconstructedArgs() + tastyPrinter(properArgs.toArray) + case Script => // Naive copy from scalac bash script + addJavaProps() + val properArgs = + reconstructedArgs() + ++ List("-script", settings.targetScript) + ++ settings.scriptArgs + scripting(properArgs.toArray) + case Repl | Run => + addJavaProps() + val properArgs = reconstructedArgs() + repl(properArgs.toArray) + case Guess => + addJavaProps() + compiler(reconstructedArgs().toArray) end main } diff --git a/compiler/src/dotty/tools/MainGenericRunner.scala b/compiler/src/dotty/tools/MainGenericRunner.scala index b32630a5d63b..0ede1faf3e85 100644 --- a/compiler/src/dotty/tools/MainGenericRunner.scala +++ b/compiler/src/dotty/tools/MainGenericRunner.scala @@ -98,6 +98,7 @@ object MainGenericRunner { val classpathSeparator = File.pathSeparator + // returns (tail, cp.split) modulo globbing def processClasspath(cp: String, tail: List[String]): (List[String], List[String]) = val cpEntries = cp.split(classpathSeparator).toList val singleEntryClasspath: Boolean = cpEntries.take(2).size == 1 @@ -122,8 +123,12 @@ object MainGenericRunner { case "-run" :: fqName :: tail => processArgs(tail, settings.withExecuteMode(ExecuteMode.Run).withTargetToRun(fqName)) case ("-cp" | "-classpath" | "--class-path") :: cp :: tail => - val (tailargs, newEntries) = processClasspath(cp, tail) - processArgs(tailargs, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty))) + if cp.startsWith("-") then + processArgs(cp :: tail, settings) + else + processClasspath(cp, tail) match + case (tail, newEntries) => + processArgs(tail, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty))) case ("-version" | "--version") :: _ => settings.copy( executeMode = ExecuteMode.Repl, @@ -166,7 +171,9 @@ object MainGenericRunner { .withTargetScript(arg) .withScriptArgs(tail*) else - val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun + val newSettings = + if arg.startsWith("-") then settings + else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun processArgs(tail, newSettings.withResidualArgs(arg)) end processArgs @@ -293,5 +300,4 @@ object MainGenericRunner { def main(args: Array[String]): Unit = if (!process(args)) System.exit(1) - } diff --git a/compiler/src/dotty/tools/dotc/config/CliCommand.scala b/compiler/src/dotty/tools/dotc/config/CliCommand.scala index 9c1b0871c144..15a6d6256941 100644 --- a/compiler/src/dotty/tools/dotc/config/CliCommand.scala +++ b/compiler/src/dotty/tools/dotc/config/CliCommand.scala @@ -4,6 +4,7 @@ package config import Settings.* import core.Contexts.* import printing.Highlighting +import reporting.NoExplanation import dotty.tools.dotc.util.chaining.* import scala.PartialFunction.cond @@ -125,7 +126,8 @@ trait CliCommand: */ def checkUsage(summary: ArgsSummary, sourcesRequired: Boolean)(using settings: ConcreteSettings)(using SettingsState, Context): Option[List[String]] = // Print all warnings encountered during arguments parsing - summary.warnings.foreach(report.warning(_)) + for warning <- summary.warnings; message = NoExplanation(warning) do + report.configurationWarning(message) if summary.errors.nonEmpty then summary.errors foreach (report.error(_)) diff --git a/compiler/src/dotty/tools/dotc/config/Settings.scala b/compiler/src/dotty/tools/dotc/config/Settings.scala index 7842113b5e48..145f011e58d2 100644 --- a/compiler/src/dotty/tools/dotc/config/Settings.scala +++ b/compiler/src/dotty/tools/dotc/config/Settings.scala @@ -12,6 +12,7 @@ import collection.mutable import reflect.ClassTag import scala.util.{Success, Failure} import dotty.tools.dotc.config.Settings.Setting.ChoiceWithHelp +import dotty.tools.dotc.util.chaining.* object Settings: @@ -51,20 +52,24 @@ object Settings: end SettingsState - case class ArgsSummary( - sstate: SettingsState, - arguments: List[String], - errors: List[String], - warnings: List[String]): - - def fail(msg: String): Settings.ArgsSummary = - ArgsSummary(sstate, arguments.tail, errors :+ msg, warnings) - - def warn(msg: String): Settings.ArgsSummary = - ArgsSummary(sstate, arguments.tail, errors, warnings :+ msg) - - def deprecated(msg: String, extraArgs: List[String] = Nil): Settings.ArgsSummary = - ArgsSummary(sstate, extraArgs ++ arguments.tail, errors, warnings :+ msg) + case class ArgsSummary(sstate: SettingsState, arguments: List[String], errors: List[String], warnings: List[String]) + + extension (summary: ArgsSummary) + /** Add an error and drop an argument. If erroring on bad `-o junk`, the next arg is collected by processArgs. */ + def fail(msg: String): ArgsSummary = + summary.copy(arguments = summary.arguments.drop(1), errors = summary.errors :+ msg) + /** Warn without skipping. */ + def warn(msg: String): ArgsSummary = + summary.copy(warnings = summary.warnings :+ msg) + /** Warn and skip. */ + def skip(msg: String): ArgsSummary = + summary.copy(arguments = summary.arguments.drop(1), warnings = summary.warnings :+ msg) + /** Skip without warning. */ + def skip(): ArgsSummary = summary.copy(arguments = summary.arguments.drop(1)) + def updated(sstate: SettingsState): ArgsSummary = summary.copy(sstate = sstate) + /** Warn and skip, prepending the alternative arguments as substitution. */ + def deprecated(msg: String, altArgs: List[String] = Nil): ArgsSummary = + summary.copy(arguments = altArgs ++ summary.arguments.drop(1), warnings = summary.warnings :+ msg) @unshared val settingCharacters = "[a-zA-Z0-9_\\-]*".r @@ -77,7 +82,7 @@ object Settings: */ type SettingDependencies = List[(Setting[?], Any)] - case class Setting[T: ClassTag] private[Settings] ( + case class Setting[T] private[Settings] ( category: SettingCategory, name: String, description: String, @@ -94,7 +99,7 @@ object Settings: // kept only for -Xkind-projector option compatibility legacyArgs: Boolean = false, // accept legacy choices (for example, valid in Scala 2 but no longer supported) - legacyChoices: Option[Seq[?]] = None)(private[Settings] val idx: Int): + legacyChoices: Option[Seq[?]] = None)(private[Settings] val idx: Int)(using ct: ClassTag[T]): validateSettingString(prefix.getOrElse(name)) aliases.foreach(validateSettingString) @@ -102,7 +107,7 @@ object Settings: assert(legacyArgs || !choices.exists(_.contains("")), s"Empty string is not supported as a choice for setting $name") // Without the following assertion, it would be easy to mistakenly try to pass a file to a setting that ignores invalid args. // Example: -opt Main.scala would be interpreted as -opt:Main.scala, and the source file would be ignored. - assert(!(summon[ClassTag[T]] == ListTag && ignoreInvalidArgs), s"Ignoring invalid args is not supported for multivalue settings: $name") + assert(!(ct == ListTag && ignoreInvalidArgs), s"Ignoring invalid args is not supported for multivalue settings: $name") val allFullNames: List[String] = s"$name" :: s"-$name" :: aliases @@ -110,13 +115,13 @@ object Settings: def updateIn(state: SettingsState, x: Any): SettingsState = x match case _: T => state.update(idx, x) - case _ => throw IllegalArgumentException(s"found: $x of type ${x.getClass.getName}, required: ${summon[ClassTag[T]]}") + case _ => throw IllegalArgumentException(s"found: $x of type ${x.getClass.getName}, required: $ct") def isDefaultIn(state: SettingsState): Boolean = valueIn(state) == default - def isMultivalue: Boolean = summon[ClassTag[T]] == ListTag + def isMultivalue: Boolean = ct == ListTag - def acceptsNoArg: Boolean = summon[ClassTag[T]] == BooleanTag || summon[ClassTag[T]] == OptionTag || choices.exists(_.contains("")) + def acceptsNoArg: Boolean = ct == BooleanTag || ct == OptionTag || choices.exists(_.contains("")) def legalChoices: String = choices match @@ -125,42 +130,30 @@ object Settings: case Some(xs) => xs.mkString(", ") case None => "" + // Updates the state from the next arg if this setting is applicable. def tryToSet(state: ArgsSummary): ArgsSummary = val ArgsSummary(sstate, arg :: args, errors, warnings) = state: @unchecked - - /** - * Updates the value in state - * - * @param getValue it is crucial that this argument is passed by name, as [setOutput] have side effects. - * @param argStringValue string value of currently proccessed argument that will be used to set deprecation replacement - * @param args remaining arguments to process - * @return new argumment state - */ - def update(getValue: => Any, argStringValue: String, args: List[String]): ArgsSummary = + def changed = sstate.wasChanged(idx) + + /** Updates the value in state. + * + * @param value will be evaluated at most once for side effects + * @param altArg alt string to apply with alt setting if this setting is deprecated + * @param args remaining arguments to process + * @return updated argument state + */ + def update(value: => Any, altArg: String, args: List[String]): ArgsSummary = deprecation match - case Some(Deprecation(msg, Some(replacedBy))) => - val deprecatedMsg = s"Option $name is deprecated: $msg" - if argStringValue.isEmpty then state.deprecated(deprecatedMsg, List(replacedBy)) - else state.deprecated(deprecatedMsg, List(s"$replacedBy:$argStringValue")) - - case Some(Deprecation(msg, _)) => - state.deprecated(s"Option $name is deprecated: $msg") - - case None => - val value = getValue - var dangers = warnings - val valueNew = - if sstate.wasChanged(idx) && isMultivalue then - val valueList = value.asInstanceOf[List[String]] - val current = valueIn(sstate).asInstanceOf[List[String]] - valueList.filter(current.contains).foreach(s => dangers :+= s"Setting $name set to $s redundantly") - current ++ valueList - else - if sstate.wasChanged(idx) then - assert(!preferPrevious, "should have shortcutted with ignoreValue, side-effect may be present!") - dangers :+= s"Flag $name set repeatedly" - value - ArgsSummary(updateIn(sstate, valueNew), args, errors, dangers) + case Some(Deprecation(msg, Some(replacedBy))) => + val deprecatedMsg = s"Option $name is deprecated: $msg" + val altArg1 = + if altArg.isEmpty then List(replacedBy) + else List(s"$replacedBy:$altArg") + state.deprecated(deprecatedMsg, altArg1) + case Some(Deprecation(msg, _)) => + state.deprecated(s"Option $name is deprecated: $msg") + case None => + ArgsSummary(updateIn(sstate, value), args, errors, warnings) end update def ignoreValue(args: List[String]): ArgsSummary = @@ -175,101 +168,131 @@ object Settings: if ignoreInvalidArgs then state.warn(msg + ", the tag was ignored") else state.fail(msg) def setBoolean(argValue: String, args: List[String]) = - if argValue.equalsIgnoreCase("true") || argValue.isEmpty then update(true, argValue, args) - else if argValue.equalsIgnoreCase("false") then update(false, argValue, args) - else state.fail(s"$argValue is not a valid choice for boolean setting $name") + def checkAndSet(v: Boolean) = + val dubious = changed && v != valueIn(sstate).asInstanceOf[Boolean] + def updated = update(v, argValue, args) + if dubious then + if preferPrevious then + state.warn(s"Conflicting value for Boolean flag $name") + else + updated.warn(s"Conflicting value for Boolean flag $name") // error instead? + else updated + if argValue.isEmpty || argValue.equalsIgnoreCase("true") then checkAndSet(true) + else if argValue.equalsIgnoreCase("false") then checkAndSet(false) + else state.fail(s"$argValue is not a valid choice for Boolean flag $name") def setString(argValue: String, args: List[String]) = choices match - case Some(xs) if !xs.contains(argValue) => - state.fail(s"$argValue is not a valid choice for $name") - case _ => + case Some(choices) if !choices.contains(argValue) => + state.fail(s"$argValue is not a valid choice for $name") + case _ => + if changed && argValue != valueIn(sstate).asInstanceOf[String] then + update(argValue, argValue, args).warn(s"Option $name was updated") + else update(argValue, argValue, args) def setInt(argValue: String, args: List[String]) = argValue.toIntOption.map: intValue => choices match - case Some(r: Range) if intValue < r.head || r.last < intValue => - state.fail(s"$argValue is out of legal range ${r.head}..${r.last} for $name") - case Some(xs) if !xs.contains(intValue) => - state.fail(s"$argValue is not a valid choice for $name") - case _ => - update(intValue, argValue, args) + case Some(r: Range) if intValue < r.head || r.last < intValue => + state.fail(s"$argValue is out of legal range ${r.head}..${r.last} for $name") + case Some(choices) if !choices.contains(intValue) => + state.fail(s"$argValue is not a valid choice for $name") + case _ => + val dubious = changed && intValue != valueIn(sstate).asInstanceOf[Int] + val updated = update(intValue, argValue, args) + if dubious then updated.warn(s"Option $name was updated") else updated .getOrElse: state.fail(s"$argValue is not an integer argument for $name") - def setOutput(argValue: String, args: List[String]) = - val path = Directory(argValue) + def setOutput(arg: String, args: List[String]) = + val path = Directory(arg) val isJar = path.ext.isJar if (!isJar && !path.isDirectory) then - state.fail(s"'$argValue' does not exist or is not a directory or .jar file") + state.fail(s"'$arg' does not exist or is not a directory or .jar file") else /* Side effect, do not change this method to evaluate eagerly */ def output = if (isJar) JarArchive.create(path) else new PlainDirectory(path) - update(output, argValue, args) - - def setVersion(argValue: String, args: List[String]) = - ScalaVersion.parse(argValue) match - case Success(v) => update(v, argValue, args) - case Failure(ex) => state.fail(ex.getMessage) - - def appendList(strings: List[String], argValue: String, args: List[String]) = + if changed && output != valueIn(sstate).asInstanceOf[AbstractFile] then + update(output, arg, args).skip(s"Option $name was updated") + else + update(output, arg, args) + + // argRest is the remainder of -foo:bar if any. This setting will receive a value from argRest or args.head. + // useArg means use argRest even if empty. + def doSet(argRest: String, useArg: Boolean): ArgsSummary = + if ct == BooleanTag then setBoolean(argRest, args) + else if ct == OptionTag then update(Some(propertyClass.get.getConstructor().newInstance()), "", args) + else + // `-option:v` or `-option v` + val (arg1, args1) = + val argInArgRest = useArg || !argRest.isEmpty || legacyArgs + val useNextArg = !argInArgRest && args.nonEmpty && (ct == IntTag || !args.head.startsWith("-")) + if argInArgRest then (argRest, args) + else if useNextArg then (args.head, args.tail) + else return missingArg + def doSet(arg: String, args: List[String]) = + ct match + case _ if preferPrevious && changed => state.skip(s"Ignoring update of option $name") + case ListTag => setMultivalue(arg, args) + case StringTag => setString(arg, args) + case OutputTag => + if changed && preferPrevious then + state.skip() // do not risk side effects e.g. overwriting a jar + else + setOutput(arg, args) + case IntTag => setInt(arg, args) + case VersionTag => setVersion(arg, args) + case _ => missingArg + doSet(arg1, args1) + end doSet + + def setVersion(arg: String, args: List[String]) = + ScalaVersion.parse(arg) match + case Success(v) => update(v, arg, args) + case Failure(e) => state.fail(e.getMessage) + + def setMultivalue(arg: String, args: List[String]) = + val split = arg.split(",").toList + // check whether a value was previously set + def checkRedundant(actual: List[String]) = + if changed then + val current = valueIn(sstate).asInstanceOf[List[String]] + update(current ++ actual, arg, args) + else + update(actual, arg, args) choices match - case Some(valid) => strings.partition(valid.contains) match - case (_, Nil) => update(strings, argValue, args) - case (validStrs, invalidStrs) => legacyChoices match - case Some(validBefore) => - invalidStrs.filterNot(validBefore.contains) match - case Nil => update(validStrs, argValue, args) - case realInvalidStrs => invalidChoices(realInvalidStrs) - case _ => invalidChoices(invalidStrs) - case _ => update(strings, argValue, args) - - def doSet(argRest: String) = - ((summon[ClassTag[T]], args): @unchecked) match - case (BooleanTag, _) => - if sstate.wasChanged(idx) && preferPrevious then ignoreValue(args) - else setBoolean(argRest, args) - case (OptionTag, _) => - update(Some(propertyClass.get.getConstructor().newInstance()), "", args) - case (ct, args) => - val argInArgRest = !argRest.isEmpty || legacyArgs - val argAfterParam = !argInArgRest && args.nonEmpty && (ct == IntTag || !args.head.startsWith("-")) - if argInArgRest then - doSetArg(argRest, args) - else if argAfterParam then - doSetArg(args.head, args.tail) - else missingArg - - def doSetArg(arg: String, argsLeft: List[String]) = summon[ClassTag[T]] match - case ListTag => - val strings = arg.split(",").toList - appendList(strings, arg, argsLeft) - case StringTag => - setString(arg, argsLeft) - case OutputTag => - if sstate.wasChanged(idx) && preferPrevious then - ignoreValue(argsLeft) // do not risk side effects e.g. overwriting a jar - else - setOutput(arg, argsLeft) - case IntTag => - setInt(arg, argsLeft) - case VersionTag => - setVersion(arg, argsLeft) - case _ => - missingArg - - def matches(argName: String): Boolean = - (allFullNames).exists(_ == argName.takeWhile(_ != ':')) || prefix.exists(arg.startsWith) - - def argValRest: String = - if(prefix.isEmpty) arg.dropWhile(_ != ':').drop(1) else arg.drop(prefix.get.length) - - if matches(arg) then + case Some(choices) => + split.partition(choices.contains) match + case (_, Nil) => checkRedundant(split) + case (valid, invalid) => + legacyChoices match + case Some(legacyChoices) => + invalid.filterNot(legacyChoices.contains) match + case Nil => checkRedundant(valid) // silently ignore legacy choices + case invalid => invalidChoices(invalid) + case none => invalidChoices(invalid) + case none => checkRedundant(split) + + def matches: Boolean = + val name = arg.takeWhile(_ != ':') + allFullNames.exists(_ == name) || prefix.exists(arg.startsWith) + + if matches then deprecation match - case Some(Deprecation(msg, _)) if ignoreInvalidArgs => // a special case for Xlint - state.deprecated(s"Option $name is deprecated: $msg") - case _ => doSet(argValRest) + case Some(Deprecation(msg, _)) if ignoreInvalidArgs => // a special case for Xlint + state.deprecated(s"Option $name is deprecated: $msg") + case _ => + prefix match + case Some(prefix) => + // todo an error if empty suffix + doSet(arg.drop(prefix.length), useArg = true) + case none => + val split = arg.split(":", 2) + if split.length == 1 then + doSet("", useArg = false) + else + doSet(split(1), useArg = true) else state end tryToSet @@ -307,10 +330,14 @@ object Settings: */ case class ChoiceWithHelp[T](name: T, description: String): override def equals(x: Any): Boolean = x match - case s:String => s == name.toString() + case s: String => s == name.toString() case _ => false override def toString(): String = s"\n- $name${if description.isEmpty() then "" else s" :\n\t${description.replace("\n","\n\t")}"}" + + import ScalaSettingCategories.RootSetting + def internal[T: ClassTag](name: String, value: T): Setting[T] = + Setting(RootSetting, name, "internal", default = value)(-1) end Setting class SettingGroup: @@ -353,20 +380,21 @@ object Settings: */ @tailrec final def processArguments(state: ArgsSummary, processAll: Boolean, skipped: List[String]): ArgsSummary = - def stateWithArgs(args: List[String]) = ArgsSummary(state.sstate, args, state.errors, state.warnings) + def stateWithArgs(args: List[String]) = state.copy(arguments = args) state.arguments match case Nil => checkDependencies(stateWithArgs(skipped)) case "--" :: args => checkDependencies(stateWithArgs(skipped ++ args)) - case x :: _ if x startsWith "-" => + case arg :: _ if arg startsWith "-" => + // find a setting to consume the next arg @tailrec def loop(settings: List[Setting[?]]): ArgsSummary = settings match - case setting :: settings1 => + case setting :: settings => val state1 = setting.tryToSet(state) if state1 ne state then state1 - else loop(settings1) + else loop(settings) case Nil => - state.warn(s"bad option '$x' was ignored") + state.skip(s"bad option '$arg' was ignored") processArguments(loop(allSettings.toList), processAll, skipped) case arg :: args => if processAll then processArguments(stateWithArgs(args), processAll, skipped :+ arg) @@ -374,7 +402,7 @@ object Settings: end processArguments def processArguments(arguments: List[String], processAll: Boolean, settingsState: SettingsState = defaultState): ArgsSummary = - processArguments(ArgsSummary(settingsState, arguments, Nil, Nil), processAll, Nil) + processArguments(ArgsSummary(settingsState, arguments, errors = Nil, warnings = Nil), processAll, skipped = Nil) def publish[T](settingf: Int => Setting[T]): Setting[T] = val setting = settingf(_allSettings.length) diff --git a/compiler/src/dotty/tools/dotc/report.scala b/compiler/src/dotty/tools/dotc/report.scala index 61bcc9c8d780..7d7d5963c57a 100644 --- a/compiler/src/dotty/tools/dotc/report.scala +++ b/compiler/src/dotty/tools/dotc/report.scala @@ -20,6 +20,9 @@ object report: private def issueWarning(warning: Warning)(using Context): Unit = ctx.reporter.report(warning) + def configurationWarning(msg: Message, pos: SrcPos = NoSourcePosition)(using Context): Unit = + issueWarning(ConfigurationWarning(msg, pos.sourcePos)) + def deprecationWarning(msg: Message, pos: SrcPos, origin: String = "")(using Context): Unit = issueWarning(DeprecationWarning(msg, addInlineds(pos), origin)) diff --git a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala index 30a9b8f60cf8..7d8ad304a438 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala @@ -87,6 +87,10 @@ object Diagnostic: extends ConditionalWarning(msg, pos), OriginWarning(origin): def enablingOption(using Context): Setting[Boolean] = ctx.settings.deprecation + class ConfigurationWarning(msg: Message, pos: SourcePosition) extends ConditionalWarning(msg, pos): + def enablingOption(using Context): Setting[Boolean] = Setting.internal("-configuration", value = true) + override def isSummarizedConditional(using Context): Boolean = false + class MigrationWarning( msg: Message, pos: SourcePosition diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index aadac68b37e1..c744b2232d7f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -13,7 +13,7 @@ import dotty.tools.dotc.util.NoSourcePosition import java.io.{BufferedReader, PrintWriter} import scala.annotation.internal.sharable -import scala.collection.mutable +import scala.collection.mutable.ListBuffer import core.Decorators.{em, toMessage} import core.handleRecursive @@ -228,14 +228,13 @@ abstract class Reporter extends interfaces.ReporterResult { report(new Error("No warnings can be incurred under -Werror (or -Xfatal-warnings)", NoSourcePosition)) /** Summary of warnings and errors */ - def summary: String = { - val b = new mutable.ListBuffer[String] + def summary: String = + val b = ListBuffer.empty[String] if (warningCount > 0) b += countString(warningCount, "warning") + " found" if (errorCount > 0) b += countString(errorCount, "error") + " found" b.mkString("\n") - } def summarizeUnreportedWarnings()(using Context): Unit = for (settingName, count) <- unreportedWarnings do @@ -246,7 +245,7 @@ abstract class Reporter extends interfaces.ReporterResult { /** Print the summary of warnings and errors */ def printSummary()(using Context): Unit = val s = summary - if (s != "") doReport(Warning(s.toMessage, NoSourcePosition)) + if !s.isEmpty then doReport(Warning(s.toMessage, NoSourcePosition)) /** Returns a string meaning "n elements". */ protected def countString(n: Int, elements: String): String = n match { diff --git a/compiler/src/dotty/tools/dotc/reporting/WConf.scala b/compiler/src/dotty/tools/dotc/reporting/WConf.scala index cff15aa6dc38..711d3c417d1d 100644 --- a/compiler/src/dotty/tools/dotc/reporting/WConf.scala +++ b/compiler/src/dotty/tools/dotc/reporting/WConf.scala @@ -19,12 +19,13 @@ enum MessageFilter: import Diagnostic.* this match case Any => true + case Configuration => message.isInstanceOf[ConfigurationWarning] case Deprecated => message.isInstanceOf[DeprecationWarning] case Feature => message.isInstanceOf[FeatureWarning] case Unchecked => message.isInstanceOf[UncheckedWarning] case MessageID(errorId) => message.msg.errorId == errorId case MessagePattern(pattern) => - val noHighlight = message.msg.message.replaceAll("\\e\\[[\\d;]*[^\\d;]","") + val noHighlight = message.msg.message.replaceAll("\\e\\[[\\d;]*[^\\d;]", "") pattern.findFirstIn(noHighlight).nonEmpty case SourcePattern(pattern) => val source = message.position.orElse(NoSourcePosition).source() @@ -38,7 +39,7 @@ enum MessageFilter: case _ => false case None => false - case Any, Deprecated, Feature, Unchecked, None + case Any, Configuration, Deprecated, Feature, Unchecked, None case MessagePattern(pattern: Regex) case MessageID(errorId: ErrorMessageID) case SourcePattern(pattern: Regex) @@ -97,6 +98,7 @@ object WConf: catch case _: IllegalArgumentException => Left(s"unknown error message name: $conf") case "cat" => conf match + case "configuration" => Right(Configuration) case "deprecation" => Right(Deprecated) case "feature" => Right(Feature) case "unchecked" => Right(Unchecked) diff --git a/compiler/test/dotty/tools/dotc/SettingsTests.scala b/compiler/test/dotty/tools/dotc/SettingsTests.scala index 24549ade4d23..ebcc45693a41 100644 --- a/compiler/test/dotty/tools/dotc/SettingsTests.scala +++ b/compiler/test/dotty/tools/dotc/SettingsTests.scala @@ -5,45 +5,54 @@ import reporting.StoreReporter import vulpix.TestConfiguration import core.Contexts.{Context, ContextBase} +import dotty.tools.Useables.given import dotty.tools.dotc.config.Settings.* import dotty.tools.dotc.config.Settings.Setting.ChoiceWithHelp import dotty.tools.dotc.config.ScalaSettingCategories.* +import dotty.tools.dotc.config.ScalaSettings import dotty.tools.vulpix.TestConfiguration.mkClasspath import dotty.tools.io.PlainDirectory import dotty.tools.io.Directory import dotty.tools.dotc.config.ScalaVersion -import java.nio.file._ +import java.nio.file.*, Files.* import org.junit.Test -import org.junit.Assert._ +import org.junit.Assert.{assertEquals, assertFalse, assertNotEquals, assertTrue} + import scala.util.Using -class SettingsTests { +class SettingsTests: @Test def missingOutputDir: Unit = - val options = Array("-d", "not_here") - val reporter = Main.process(options, reporter = StoreReporter()) - assertEquals(1, reporter.errorCount) - assertEquals("'not_here' does not exist or is not a directory or .jar file", reporter.allErrors.head.message) + val args = List("-d", "not_here") + val summary = ScalaSettings.processArguments(args, processAll = true) + assertEquals(1, summary.errors.size) + assertEquals("'not_here' does not exist or is not a directory or .jar file", summary.errors.head) + + @Test def `skip enuf args`: Unit = + val args = List("-d", "not_here", "-Vprint") + val summary = ScalaSettings.processArguments(args, processAll = true) + assertEquals(2, summary.errors.size) + assertEquals("'not_here' does not exist or is not a directory or .jar file", summary.errors.head) @Test def jarOutput: Unit = val source = "tests/pos/Foo.scala" - val out = Paths.get("out/jaredFoo.jar").normalize - if (Files.exists(out)) Files.delete(out) - val options = Array("-classpath", TestConfiguration.basicClasspath, "-d", out.toString, source) - val reporter = Main.process(options) - assertEquals(0, reporter.errorCount) + val out = Paths.get("out/jarredFoo.jar").normalize + if Files.exists(out) then Files.delete(out) + val args = List("-Xmain-class", "Jarred", "-classpath", TestConfiguration.basicClasspath, "-d", out.toString, source) + val summary = ScalaSettings.processArguments(args, processAll = true) + assertEquals(0, summary.errors.size) assertTrue(Files.exists(out)) @Test def `t8124 Don't crash on missing argument`: Unit = - val source = Paths.get("tests/pos/Foo.scala").normalize - val outputDir = Paths.get("out/testSettings").normalize - if Files.notExists(outputDir) then Files.createDirectory(outputDir) - // -encoding takes an arg! - val options = Array("-encoding", "-d", outputDir.toString, source.toString) - val reporter = Main.process(options, reporter = StoreReporter()) - assertEquals(1, reporter.errorCount) + //val outputDir = Paths.get("out/testSettings").normalize + Using.resource(Files.createTempDirectory("testDir")): dir => + //if Files.notExists(outputDir) then Files.createDirectory(outputDir) + val source = Paths.get("tests/pos/Foo.scala").normalize + val args = List("-encoding", "-d", dir.toString, source.toString) // -encoding takes an arg! + val summary = ScalaSettings.processArguments(args, processAll = true) + assertEquals(1, summary.errors.size) @Test def acceptUnconstrained: Unit = object Settings extends SettingGroup: @@ -51,7 +60,7 @@ class SettingsTests { val bar = IntSetting(RootSetting, "bar", "Bar", 0) val args = List("-foo", "b", "-bar", "1") - val summary = Settings.processArguments(args, true) + val summary = Settings.processArguments(args, processAll = true) assertTrue(summary.errors.isEmpty) withProcessedArgs(summary) { assertEquals("b", Settings.foo.value) @@ -76,17 +85,17 @@ class SettingsTests { @Test def `dont crash on many options`: Unit = object Settings extends SettingGroup: - val option = BooleanSetting(RootSetting, "option", "Some option") + val option = StringSetting(RootSetting, "option", "opt", "Some option", "zero") val limit = 6000 - val args = List.fill(limit)("-option") + val args = List.tabulate(limit)(i => if i % 2 == 0 then "-option" else i.toString) val summary = Settings.processArguments(args, processAll = true) assertTrue(summary.errors.isEmpty) - assertEquals(limit-1, summary.warnings.size) - assertTrue(summary.warnings.head.contains("repeatedly")) + assertEquals(limit/2 - 1, summary.warnings.size) // should warn on all but first + assertTrue(summary.warnings.head.contains("was updated")) assertEquals(0, summary.arguments.size) withProcessedArgs(summary) { - assertTrue(Settings.option.value) + assertEquals("5999", Settings.option.value.toString) } @Test def `bad option warning consumes an arg`: Unit = @@ -98,6 +107,7 @@ class SettingsTests { assertTrue(summary.errors.isEmpty) assertFalse(summary.warnings.isEmpty) assertEquals(2, summary.arguments.size) + assertEquals(List("dogs", "cats"), summary.arguments) @Test def `bad option settings throws`: Unit = object Settings extends SettingGroup: @@ -110,7 +120,7 @@ class SettingsTests { false val default = Settings.defaultState - dotty.tools.assertThrows[IllegalArgumentException](checkMessage("found: not an option of type java.lang.String, required: Boolean")) { + assertThrows[IllegalArgumentException](checkMessage("found: not an option of type java.lang.String, required: Boolean")) { Settings.option.updateIn(default, "not an option") } @@ -170,37 +180,112 @@ class SettingsTests { ) assertEquals(expectedErrors, summary.errors) } + end validateChoices @Test def `Allow IntSetting's to be set with a colon`: Unit = object Settings extends SettingGroup: val foo = IntSetting(RootSetting, "foo", "foo", 80) import Settings._ - val args = List("-foo:100") - val summary = processArguments(args, processAll = true) - assertTrue(s"Setting args errors:\n ${summary.errors.take(5).mkString("\n ")}", summary.errors.isEmpty) - withProcessedArgs(summary) { - assertEquals(100, foo.value) + def check(args: List[String]) = { + val summary = processArguments(args, processAll = true) + assertTrue(s"Setting args errors:\n ${summary.errors.take(5).mkString("\n ")}", summary.errors.isEmpty) + withProcessedArgs(summary) { + assertEquals(100, foo.value) + } + } + check(List("-foo:100")) + check(List("-foo", "100")) + assertThrows[AssertionError](_.getMessage.contains("missing argument for option -foo"))(check(List("-foo"))) + + @Test def `option fundamentals`: Unit = + object Settings extends SettingGroup: + val option = BooleanSetting(RootSetting, "option", "Some option") + val args = List("-option", "-option") + val summary = Settings.processArguments(args, processAll = true) + assertTrue("Multiple options is not an error", summary.errors.isEmpty) + assertTrue("Multiple options is not a warning if consistent", summary.warnings.isEmpty) + + @Test def `boolean option fundamentals`: Unit = + object Settings extends SettingGroup: + val option = BooleanSetting(RootSetting, "option", "Some option") + val args = List("-option", "-option:false") + val summary = Settings.processArguments(args, processAll = true) + assertTrue("Multiple options is not an error", summary.errors.isEmpty) + assertFalse("Multiple conflicting options is a warning", summary.warnings.isEmpty) + assertTrue(summary.warnings.forall(_.contains("Conflicting"))) + + @Test def `string option may be consistent`: Unit = + object Settings extends SettingGroup: + val option = StringSetting(RootSetting, "option", "opt", "Some option", "none") + val args = List("-option:something", "-option:something") + val summary = Settings.processArguments(args, processAll = true) + assertTrue("Multiple options is not an error", summary.errors.isEmpty) + assertTrue("Multiple consistent options is not a warning", summary.warnings.isEmpty) + + @Test def `string option must be consistent`: Unit = + object Settings extends SettingGroup: + val option = StringSetting(RootSetting, "option", "opt", "Some option", "none") + val args = List("-option:something", "-option:nothing") + val summary = Settings.processArguments(args, processAll = true) + assertTrue("Multiple options is not an error", summary.errors.isEmpty) + assertFalse("Multiple conflicting options is a warning", summary.warnings.isEmpty) + assertTrue(summary.warnings.forall(_.contains("updated"))) + + @Test def `int option also warns`: Unit = + object Settings extends SettingGroup: + val option = IntSetting(RootSetting, "option", "Some option", 42) + val args = List("-option:17", "-option:27") + val summary = Settings.processArguments(args, processAll = true) + assertTrue("Multiple options is not an error", summary.errors.isEmpty) + assertFalse("Multiple conflicting options is a warning", summary.warnings.isEmpty) + assertTrue(summary.warnings.forall(_.contains("updated"))) + + @Test def `dir option also warns`: Unit = + import java.nio.file.Paths + import io.PlainFile, PlainFile.* + val abc: PlainFile = Paths.get("a", "b", "c").toPlainFile + object Settings extends SettingGroup: + val option = OutputSetting(RootSetting, "option", "out", "A file", Paths.get("a", "b", "c").toPlainFile) + Using.resource(createTempDirectory("i13887")) { dir => + val target = createDirectory(dir.resolve("x")) + val mistake = createDirectory(dir.resolve("y")) + val args = List("-option", target.toString, "-option", mistake.toString) + val summary = Settings.processArguments(args, processAll = true) + assertTrue("Multiple options is not an error", summary.errors.isEmpty) + assertFalse("Multiple conflicting options is a warning", summary.warnings.isEmpty) + assertTrue(summary.warnings.forall(_.contains("updated"))) } @Test def `Set BooleanSettings correctly`: Unit = object Settings extends SettingGroup: - val foo = BooleanSetting(RootSetting, "foo", "foo", false) - val bar = BooleanSetting(RootSetting, "bar", "bar", true) - val baz = BooleanSetting(RootSetting, "baz", "baz", false) - val qux = BooleanSetting(RootSetting, "qux", "qux", false) + val foo = BooleanSetting(RootSetting, "foo", "foo", initialValue = false) + val bar = BooleanSetting(RootSetting, "bar", "bar", initialValue = true) + val baz = BooleanSetting(RootSetting, "baz", "baz", initialValue = false) + val qux = BooleanSetting(RootSetting, "qux", "qux", initialValue = false) import Settings._ val args = List("-foo:true", "-bar:false", "-baz", "-qux:true", "-qux:false") val summary = processArguments(args, processAll = true) assertTrue(s"Setting args errors:\n ${summary.errors.take(5).mkString("\n ")}", summary.errors.isEmpty) - withProcessedArgs(summary) { + withProcessedArgs(summary): assertEquals(true, foo.value) assertEquals(false, bar.value) assertEquals(true, baz.value) assertEquals(false, qux.value) - assertEquals(List("Flag -qux set repeatedly"), summary.warnings) - } + assertEquals(List("Conflicting value for Boolean flag -qux"), summary.warnings) + + @Test def `flag can't be set with separate arg`: Unit = + object Settings extends SettingGroup: + val foo = BooleanSetting(RootSetting, "foo", "foo", initialValue = false) + import Settings._ + + val args = List("-foo", "false") + val summary = processArguments(args, processAll = true) + withProcessedArgs(summary): + assertTrue("Nothing to see here", summary.errors.isEmpty) + assertTrue("Nothing to see here", summary.warnings.isEmpty) + assertEquals(true, foo.value) @Test def `Output setting is overriding existing jar`: Unit = val result = Using.resource(Files.createTempFile("myfile", ".jar")){ file => @@ -220,7 +305,7 @@ class SettingsTests { }(Files.deleteIfExists(_)) - @Test def `Output setting is respecting previous setting`: Unit = + @Test def `Output setting respects previous setting`: Unit = val result = Using.resources( Files.createTempFile("myfile", ".jar"), Files.createTempFile("myfile2", ".jar") ){ (file1, file2) => @@ -265,17 +350,18 @@ class SettingsTests { }(Files.deleteIfExists(_)) - @Test def `Arguments of flags are correctly parsed with both ":" and " " separating`: Unit = + @Test def `Arguments of options are correctly parsed with either ":" or " " separators`: Unit = + val Help = "" // i.e. help = Help object Settings extends SettingGroup: val booleanSetting = BooleanSetting(RootSetting, "booleanSetting", "booleanSetting", false) - val stringSetting = StringSetting(RootSetting, "stringSetting", "stringSetting", "", "test") - val choiceSetting = ChoiceSetting(RootSetting, "choiceSetting", "choiceSetting", "", List("a", "b"), "a") - val multiChoiceSetting= MultiChoiceSetting(RootSetting, "multiChoiceSetting", "multiChoiceSetting", "", List("a", "b"), List(), legacyChoices = List("c")) - val multiChoiceHelpSetting= MultiChoiceHelpSetting(RootSetting, "multiChoiceHelpSetting", "multiChoiceHelpSetting", "", List(ChoiceWithHelp("a", "a"), ChoiceWithHelp("b", "b")), List(), legacyChoices = List("c")) + val stringSetting = StringSetting(RootSetting, "stringSetting", "stringSetting", Help, "test") + val choiceSetting = ChoiceSetting(RootSetting, "choiceSetting", "choiceSetting", Help, List("a", "b"), "a") + val multiChoiceSetting = MultiChoiceSetting(RootSetting, "multiChoiceSetting", "multiChoiceSetting", Help, choices = List("a", "b"), legacyChoices = List("c")) + val multiChoiceHelpSetting= MultiChoiceHelpSetting(RootSetting, "multiChoiceHelpSetting", "multiChoiceHelpSetting", Help, List(ChoiceWithHelp("a", "a"), ChoiceWithHelp("b", "b")), List(), legacyChoices = List("c")) val intSetting = IntSetting(RootSetting, "intSetting", "intSetting", 0) val intChoiceSetting = IntChoiceSetting(RootSetting, "intChoiceSetting", "intChoiceSetting", List(1,2,3), 1) - val multiStringSetting = MultiStringSetting(RootSetting, "multiStringSetting", "multiStringSetting", "", List("a", "b"), List()) - val outputSetting = OutputSetting(RootSetting, "outputSetting", "outputSetting", "", new PlainDirectory(Directory("."))) + val multiStringSetting = MultiStringSetting(RootSetting, "multiStringSetting", "multiStringSetting", Help, default = List("a", "b")) + val outputSetting = OutputSetting(RootSetting, "outputSetting", "outputSetting", Help, new PlainDirectory(Directory("."))) val pathSetting = PathSetting(RootSetting, "pathSetting", "pathSetting", ".") val phasesSetting = PhasesSetting(RootSetting, "phasesSetting", "phasesSetting", "all") val versionSetting= VersionSetting(RootSetting, "versionSetting", "versionSetting") @@ -284,7 +370,7 @@ class SettingsTests { Using.resource(Files.createTempDirectory("testDir")) { dir => val args = List( - List("-booleanSetting", "true"), + List("-booleanSetting", "true"), // `-b false` does not mean `-b:false` List("-stringSetting", "newTest"), List("-choiceSetting", "b"), List("-multiChoiceSetting", "a,b,c"), @@ -299,7 +385,7 @@ class SettingsTests { ) def testValues(summary: ArgsSummary) = - withProcessedArgs(summary) { + withProcessedArgs(summary): assertEquals(true, booleanSetting.value) assertEquals("newTest", stringSetting.value) assertEquals("b", choiceSetting.value) @@ -312,7 +398,6 @@ class SettingsTests { assertEquals(dir.toString, pathSetting.value) assertEquals(List("parser", "typer"), phasesSetting.value) assertEquals(ScalaVersion.parse("1.0.0").get, versionSetting.value) - } val summaryColon = processArguments(args.map(_.mkString(":")), processAll = true) val summaryWhitespace = processArguments(args.flatten, processAll = true) @@ -321,8 +406,9 @@ class SettingsTests { }(Files.deleteIfExists(_)) + // use the supplied summary for evaluating settings private def withProcessedArgs(summary: ArgsSummary)(f: SettingsState ?=> Unit) = f(using summary.sstate) + // evaluate a setting using only a SettingsState (instead of a full-blown Context) extension [T](setting: Setting[T]) private def value(using ss: SettingsState): T = setting.valueIn(ss) -} diff --git a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala index c74be4901137..dfe79f5f8f5b 100644 --- a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala +++ b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala @@ -58,7 +58,7 @@ class ScalaSettingsTests: assertTrue("Has the feature", set.contains("implicitConversions")) assertTrue("Has the feature", set.contains("dynamics")) - @Test def `Warn if multistring element is supplied multiply`: Unit = + @Test def `Don't warn if multistring element is supplied multiply`: Unit = class SUT extends SettingGroup: val language: Setting[List[String]] = MultiStringSetting(RootSetting, "language", "feature", "Enable one or more language features.") val sut = SUT() @@ -67,7 +67,7 @@ class ScalaSettingsTests: val res = sut.processArguments(sumy, processAll = true, skipped = Nil) val set = sut.language.valueIn(res.sstate) assertEquals(3, args.length) - assertEquals("Must warn", 1, res.warnings.length) + assertTrue("Mustn't warn", res.warnings.isEmpty) assertTrue("No errors!", res.errors.isEmpty) assertTrue("Has the feature", set.contains("implicitConversions")) assertTrue("Has the feature", set.contains("dynamics")) diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala index 14981e001d38..2c252d37c47f 100644 --- a/compiler/test/dotty/tools/utils.scala +++ b/compiler/test/dotty/tools/utils.scala @@ -11,7 +11,7 @@ import java.nio.file.{Files, Path => JPath} import scala.io.Source import scala.jdk.StreamConverters._ import scala.reflect.ClassTag -import scala.util.Using.resource +import scala.util.Using.{Releasable, resource} import scala.util.chaining.given import scala.util.control.{ControlThrowable, NonFatal} @@ -44,6 +44,24 @@ def withFile[T](file: File)(action: Source => T): T = resource(Source.fromFile(f def readLines(f: File): List[String] = withFile(f)(_.getLines().toList) def readFile(f: File): String = withFile(f)(_.mkString) +// Make common types useable with Using. +object Useables: + import java.io.IOException + import java.nio.file.{FileVisitResult, SimpleFileVisitor}, FileVisitResult.CONTINUE as Continue + import java.nio.file.attribute.* + import scala.util.Properties + + // delete the result of createTempDirectory + given Releasable[JPath] with + override def release(released: JPath) = if !Properties.isWin then remove(released) + private def remove(path: JPath): Unit = if Files.isDirectory(path) then removeRecursively(path) else Files.delete(path) + private def removeRecursively(path: JPath): Unit = Files.walkFileTree(path, ZappingFileVisitor()) + private class ZappingFileVisitor extends SimpleFileVisitor[JPath]: + private def zap(path: JPath) = { Files.delete(path) ; Continue } + override def postVisitDirectory(path: JPath, e: IOException): FileVisitResult = if e != null then throw e else zap(path) + override def visitFile(path: JPath, attrs: BasicFileAttributes): FileVisitResult = zap(path) +end Useables + private object Unthrown extends ControlThrowable def assertThrows[T <: Throwable: ClassTag](p: T => Boolean)(body: => Any): Unit = diff --git a/tests/neg/i22906.check b/tests/neg/i22906.check index 118f9f4fa069..a5fa90be9337 100644 --- a/tests/neg/i22906.check +++ b/tests/neg/i22906.check @@ -1,4 +1,3 @@ -Flag -indent set repeatedly -- Error: tests/neg/i22906.scala:6:15 ---------------------------------------------------------------------------------- 6 | {`1`: Int => 5} // error | ^