Skip to content

Commit e4b99cb

Browse files
committed
Improve settings state erroring
1 parent dab2777 commit e4b99cb

File tree

4 files changed

+101
-85
lines changed

4 files changed

+101
-85
lines changed

compiler/src/dotty/tools/dotc/config/PathResolver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ object PathResolver {
152152
}
153153
else inContext(ContextBase().initialCtx) {
154154
val ArgsSummary(sstate, rest, errors, warnings) =
155-
ctx.settings.processArguments(args.toList, true, ctx.settingsState)
155+
ctx.settings.processArguments(args.toList, processAll = true, ctx.settingsState)
156156
errors.foreach(println)
157157
val pr = inContext(ctx.fresh.setSettings(sstate)) {
158158
new PathResolver()

compiler/src/dotty/tools/dotc/config/Settings.scala

Lines changed: 84 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import dotty.tools.io.{AbstractFile, Directory, JarArchive, PlainDirectory}
77

88
import annotation.tailrec
99
import annotation.internal.unshared
10-
import collection.mutable.ArrayBuffer
11-
import collection.mutable
10+
import collection.mutable, mutable.ArrayBuffer
1211
import reflect.ClassTag
1312
import scala.util.{Success, Failure}
14-
import dotty.tools.dotc.config.Settings.Setting.ChoiceWithHelp
15-
import dotty.tools.dotc.util.chaining.*
13+
14+
import config.Settings.Setting.ChoiceWithHelp
15+
import util.chaining.*
1616

1717
object Settings:
1818

@@ -55,27 +55,37 @@ object Settings:
5555
case class ArgsSummary(sstate: SettingsState, arguments: List[String], errors: List[String], warnings: List[String])
5656

5757
extension (summary: ArgsSummary)
58-
/** Add an error and drop an argument. If erroring on bad `-o junk`, the next arg is collected by processArgs. */
58+
/** Error and update arguments */
59+
def fail(msg: String, arguments: List[String]): ArgsSummary =
60+
summary.copy(arguments = arguments, errors = summary.errors :+ msg)
61+
/** Error without updating arguments */
5962
def fail(msg: String): ArgsSummary =
60-
summary.copy(arguments = summary.arguments.drop(1), errors = summary.errors :+ msg)
61-
/** Warn without skipping. */
63+
summary.copy(errors = summary.errors :+ msg)
64+
/** Warn and update arguments */
65+
def warn(msg: String, arguments: List[String]): ArgsSummary =
66+
summary.copy(arguments = arguments, warnings = summary.warnings :+ msg)
67+
/** Warn without updating arguments */
6268
def warn(msg: String): ArgsSummary =
6369
summary.copy(warnings = summary.warnings :+ msg)
64-
/** Warn and skip. */
65-
def skip(msg: String): ArgsSummary =
66-
summary.copy(arguments = summary.arguments.drop(1), warnings = summary.warnings :+ msg)
67-
/** Skip without warning. */
68-
def skip(): ArgsSummary = summary.copy(arguments = summary.arguments.drop(1))
69-
def updated(sstate: SettingsState): ArgsSummary = summary.copy(sstate = sstate)
70+
/** Update state and shift arguments */
71+
def updated(sstate: SettingsState, arguments: List[String]): ArgsSummary =
72+
summary.copy(sstate = sstate, arguments = arguments)
73+
/** Only shift arguments */
74+
def shifted(arguments: List[String]): ArgsSummary =
75+
summary.copy(arguments = arguments)
7076
/** Warn and skip, prepending the alternative arguments as substitution. */
71-
def deprecated(msg: String, altArgs: List[String] = Nil): ArgsSummary =
72-
summary.copy(arguments = altArgs ++ summary.arguments.drop(1), warnings = summary.warnings :+ msg)
77+
def deprecated(msg: String, altArgs: List[String], args: List[String]): ArgsSummary =
78+
summary.copy(arguments = altArgs ++ args, warnings = summary.warnings :+ msg)
7379

7480
@unshared
7581
val settingCharacters = "[a-zA-Z0-9_\\-]*".r
7682
def validateSettingString(name: String): Unit =
7783
assert(settingCharacters.matches(name), s"Setting string $name contains invalid characters")
7884

85+
val validTags = List(BooleanTag, IntTag, StringTag, ListTag, VersionTag, OptionTag, OutputTag)
86+
def validateSettingTag(ct: ClassTag[?]): Unit =
87+
assert(validTags.contains(ct), s"Unsupported option value $ct")
88+
7989
/** List of setting-value pairs that are required for another setting to be valid.
8090
* For example, `s = Setting(..., depends = List(YprofileEnabled -> true))`
8191
* means that `s` requires `YprofileEnabled` to be set to `true`.
@@ -105,6 +115,8 @@ object Settings:
105115
aliases.foreach(validateSettingString)
106116
assert(name.startsWith(s"-${category.prefixLetter}"), s"Setting $name does not start with category -$category")
107117
assert(legacyArgs || !choices.exists(_.contains("")), s"Empty string is not supported as a choice for setting $name")
118+
validateSettingTag(ct)
119+
108120
// Without the following assertion, it would be easy to mistakenly try to pass a file to a setting that ignores invalid args.
109121
// Example: -opt Main.scala would be interpreted as -opt:Main.scala, and the source file would be ignored.
110122
assert(!(ct == ListTag && ignoreInvalidArgs), s"Ignoring invalid args is not supported for multivalue settings: $name")
@@ -132,7 +144,7 @@ object Settings:
132144

133145
// Updates the state from the next arg if this setting is applicable.
134146
def tryToSet(state: ArgsSummary): ArgsSummary =
135-
val ArgsSummary(sstate, arg :: args, errors, warnings) = state: @unchecked
147+
val ArgsSummary(sstate, arg :: args, _, _) = state: @unchecked
136148
def changed = sstate.wasChanged(idx)
137149

138150
/** Updates the value in state.
@@ -149,42 +161,32 @@ object Settings:
149161
val altArg1 =
150162
if altArg.isEmpty then List(replacedBy)
151163
else List(s"$replacedBy:$altArg")
152-
state.deprecated(deprecatedMsg, altArg1)
164+
state.deprecated(deprecatedMsg, altArg1, args) // retry with reconstructed arg
153165
case Some(Deprecation(msg, _)) =>
154-
state.deprecated(s"Option $name is deprecated: $msg")
166+
state.updated(updateIn(sstate, value), args) // allow but warn
167+
.warn(s"Option $name is deprecated: $msg")
155168
case None =>
156-
ArgsSummary(updateIn(sstate, value), args, errors, warnings)
169+
state.updated(updateIn(sstate, value), args)
157170
end update
158171

159-
def ignoreValue(args: List[String]): ArgsSummary =
160-
ArgsSummary(sstate, args, errors, warnings)
161-
162-
def missingArg =
163-
val msg = s"missing argument for option $name"
164-
if ignoreInvalidArgs then state.warn(msg + ", the tag was ignored") else state.fail(msg)
165-
166-
def invalidChoices(invalid: List[String]) =
167-
val msg = s"invalid choice(s) for $name: ${invalid.mkString(",")}"
168-
if ignoreInvalidArgs then state.warn(msg + ", the tag was ignored") else state.fail(msg)
169-
170172
def setBoolean(argValue: String, args: List[String]) =
171173
def checkAndSet(v: Boolean) =
172174
val dubious = changed && v != valueIn(sstate).asInstanceOf[Boolean]
173-
def updated = update(v, argValue, args)
174175
if dubious then
175176
if preferPrevious then
176-
state.warn(s"Conflicting value for Boolean flag $name")
177+
state.warn(s"Ignoring conflicting value for Boolean flag $name", args)
177178
else
178-
updated.warn(s"Conflicting value for Boolean flag $name") // error instead?
179-
else updated
179+
update(v, argValue, args).warn(s"Conflicting value for Boolean flag $name")
180+
else
181+
update(v, argValue, args)
180182
if argValue.isEmpty || argValue.equalsIgnoreCase("true") then checkAndSet(true)
181183
else if argValue.equalsIgnoreCase("false") then checkAndSet(false)
182-
else state.fail(s"$argValue is not a valid choice for Boolean flag $name")
184+
else state.fail(s"$argValue is not a valid choice for Boolean flag $name", args)
183185

184186
def setString(argValue: String, args: List[String]) =
185187
choices match
186188
case Some(choices) if !choices.contains(argValue) =>
187-
state.fail(s"$argValue is not a valid choice for $name")
189+
state.fail(s"$argValue is not a valid choice for $name", args)
188190
case _ =>
189191
if changed && argValue != valueIn(sstate).asInstanceOf[String] then
190192
update(argValue, argValue, args).warn(s"Option $name was updated")
@@ -195,32 +197,35 @@ object Settings:
195197
argValue.toIntOption.map: intValue =>
196198
choices match
197199
case Some(r: Range) if intValue < r.head || r.last < intValue =>
198-
state.fail(s"$argValue is out of legal range ${r.head}..${r.last} for $name")
200+
state.fail(s"$argValue is out of legal range ${r.head}..${r.last} for $name", args)
199201
case Some(choices) if !choices.contains(intValue) =>
200-
state.fail(s"$argValue is not a valid choice for $name")
202+
state.fail(s"$argValue is not a valid choice for $name", args)
201203
case _ =>
202204
val dubious = changed && intValue != valueIn(sstate).asInstanceOf[Int]
203205
val updated = update(intValue, argValue, args)
204206
if dubious then updated.warn(s"Option $name was updated") else updated
205207
.getOrElse:
206-
state.fail(s"$argValue is not an integer argument for $name")
208+
state.fail(s"$argValue is not an integer argument for $name", args)
207209

208210
def setOutput(arg: String, args: List[String]) =
209211
val path = Directory(arg)
210212
val isJar = path.ext.isJar
211213
if (!isJar && !path.isDirectory) then
212-
state.fail(s"'$arg' does not exist or is not a directory or .jar file")
214+
state.fail(s"'$arg' does not exist or is not a directory or .jar file", args)
213215
else
214216
/* Side effect, do not change this method to evaluate eagerly */
215217
def output = if (isJar) JarArchive.create(path) else new PlainDirectory(path)
216-
if changed && output != valueIn(sstate).asInstanceOf[AbstractFile] then
217-
update(output, arg, args).skip(s"Option $name was updated")
218-
else
219-
update(output, arg, args)
218+
val dubious = changed && output != valueIn(sstate).asInstanceOf[AbstractFile]
219+
val updated = update(output, arg, args)
220+
if dubious then updated.warn(s"Option $name was updated") else updated
220221

221222
// argRest is the remainder of -foo:bar if any. This setting will receive a value from argRest or args.head.
222223
// useArg means use argRest even if empty.
223224
def doSet(argRest: String, useArg: Boolean): ArgsSummary =
225+
def missingArg =
226+
val msg = s"missing argument for option $name"
227+
if ignoreInvalidArgs then state.warn(s"$msg, the tag was ignored", args) else state.fail(msg, args)
228+
224229
if ct == BooleanTag then setBoolean(argRest, args)
225230
else if ct == OptionTag then update(Some(propertyClass.get.getConstructor().newInstance()), "", args)
226231
else
@@ -233,24 +238,20 @@ object Settings:
233238
else return missingArg
234239
def doSet(arg: String, args: List[String]) =
235240
ct match
236-
case _ if preferPrevious && changed => state.skip(s"Ignoring update of option $name")
241+
case _ if preferPrevious && changed => state.warn(s"Ignoring update of option $name", args)
237242
case ListTag => setMultivalue(arg, args)
238243
case StringTag => setString(arg, args)
239-
case OutputTag =>
240-
if changed && preferPrevious then
241-
state.skip() // do not risk side effects e.g. overwriting a jar
242-
else
243-
setOutput(arg, args)
244+
case OutputTag => setOutput(arg, args)
244245
case IntTag => setInt(arg, args)
245246
case VersionTag => setVersion(arg, args)
246-
case _ => missingArg
247+
case _ => state.fail(s"unknown $ct", args)
247248
doSet(arg1, args1)
248249
end doSet
249250

250251
def setVersion(arg: String, args: List[String]) =
251252
ScalaVersion.parse(arg) match
252253
case Success(v) => update(v, arg, args)
253-
case Failure(e) => state.fail(e.getMessage)
254+
case Failure(e) => state.fail(e.getMessage, args)
254255

255256
def setMultivalue(arg: String, args: List[String]) =
256257
val split = arg.split(",").toList
@@ -262,6 +263,10 @@ object Settings:
262263
else
263264
actual
264265
update(updated, arg, args)
266+
def invalidChoices(invalid: List[String]) =
267+
val msg = s"invalid choice(s) for $name: ${invalid.mkString(",")}"
268+
if ignoreInvalidArgs then state.warn(s"$msg, the tag was ignored", args) else state.fail(msg, args)
269+
265270
choices match
266271
case Some(choices) =>
267272
split.partition(choices.contains) match
@@ -274,6 +279,7 @@ object Settings:
274279
case invalid => invalidChoices(invalid)
275280
case none => invalidChoices(invalid)
276281
case none => setOrUpdate(split)
282+
end setMultivalue
277283

278284
def matches: Boolean =
279285
val name = arg.takeWhile(_ != ':')
@@ -282,7 +288,7 @@ object Settings:
282288
if matches then
283289
deprecation match
284290
case Some(Deprecation(msg, _)) if ignoreInvalidArgs => // a special case for Xlint
285-
state.deprecated(s"Option $name is deprecated: $msg")
291+
state.warn(s"Option $name is deprecated: $msg", args)
286292
case _ =>
287293
prefix match
288294
case Some(prefix) =>
@@ -299,25 +305,6 @@ object Settings:
299305
end tryToSet
300306
end Setting
301307

302-
/**
303-
* Class used for deprecating purposes.
304-
* It contains all necessary information to deprecate given option.
305-
* Scala Settings are considered deprecated when this object is present at their creation site.
306-
*
307-
* @param msg deprecation message that will be displayed in following format: s"Option $name is deprecated: $msg"
308-
* @param replacedBy option that is substituting current option
309-
*/
310-
case class Deprecation(
311-
msg: String,
312-
replacedBy: Option[String] = None,
313-
)
314-
315-
object Deprecation:
316-
def renamed(replacement: String) = Some(Deprecation(s"Use $replacement instead.", Some(replacement)))
317-
def removed(removedVersion: Option[String] = None) =
318-
val msg = removedVersion.map(" in " + _).getOrElse(".")
319-
Some(Deprecation(s"Scheduled for removal$msg", None))
320-
321308
object Setting:
322309
extension [T](setting: Setting[T])
323310
def value(using Context): T = setting.valueIn(ctx.settingsState)
@@ -341,6 +328,25 @@ object Settings:
341328
Setting(RootSetting, name, "internal", default = value)(-1)
342329
end Setting
343330

331+
/**
332+
* Class used for deprecating purposes.
333+
* It contains all necessary information to deprecate given option.
334+
* Scala Settings are considered deprecated when this object is present at their creation site.
335+
*
336+
* @param msg deprecation message that will be displayed in following format: s"Option $name is deprecated: $msg"
337+
* @param replacedBy option that is substituting current option
338+
*/
339+
case class Deprecation(
340+
msg: String,
341+
replacedBy: Option[String] = None,
342+
)
343+
344+
object Deprecation:
345+
def renamed(replacement: String) = Some(Deprecation(s"Use $replacement instead.", Some(replacement)))
346+
def removed(removedVersion: Option[String] = None) =
347+
val msg = removedVersion.map(" in " + _).getOrElse(".")
348+
Some(Deprecation(s"Scheduled for removal$msg", None))
349+
344350
class SettingGroup:
345351

346352
@unshared
@@ -361,7 +367,7 @@ object Settings:
361367
private def checkDependenciesOfSetting(state: ArgsSummary, setting: Setting[?]) =
362368
setting.depends.foldLeft(state): (s, dep) =>
363369
val (depSetting, reqValue) = dep
364-
if (depSetting.valueIn(state.sstate) == reqValue) s
370+
if (depSetting.valueIn(s.sstate) == reqValue) s
365371
else s.fail(s"incomplete option ${setting.name} (requires ${depSetting.name})")
366372

367373
/** Iterates over the arguments applying them to settings where applicable.
@@ -381,26 +387,24 @@ object Settings:
381387
*/
382388
@tailrec
383389
final def processArguments(state: ArgsSummary, processAll: Boolean, skipped: List[String]): ArgsSummary =
384-
def stateWithArgs(args: List[String]) = state.copy(arguments = args)
385390
state.arguments match
386391
case Nil =>
387-
checkDependencies(stateWithArgs(skipped))
392+
checkDependencies(state.shifted(skipped))
388393
case "--" :: args =>
389-
checkDependencies(stateWithArgs(skipped ++ args))
390-
case arg :: _ if arg.startsWith("-") =>
394+
checkDependencies(state.shifted(skipped ++ args))
395+
case arg :: args if arg.startsWith("-") =>
391396
// find a setting to consume the next arg
392397
@tailrec def loop(settings: List[Setting[?]]): ArgsSummary = settings match
393398
case setting :: settings =>
394399
val state1 = setting.tryToSet(state)
395400
if state1 ne state then state1
396401
else loop(settings)
397402
case Nil =>
398-
state.skip(s"bad option '$arg' was ignored")
403+
state.warn(s"bad option '$arg' was ignored", args)
399404
processArguments(loop(allSettings.toList), processAll, skipped)
400405
case arg :: args =>
401-
if processAll then processArguments(stateWithArgs(args), processAll, skipped :+ arg)
406+
if processAll then processArguments(state.shifted(args), processAll, skipped :+ arg)
402407
else state
403-
end processArguments
404408

405409
def processArguments(arguments: List[String], processAll: Boolean, settingsState: SettingsState = defaultState): ArgsSummary =
406410
processArguments(ArgsSummary(settingsState, arguments, errors = Nil, warnings = Nil), processAll, skipped = Nil)

compiler/test/dotty/tools/dotc/SettingsTests.scala

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,30 @@ class SettingsTests:
3030
assertEquals(1, summary.errors.size)
3131
assertEquals("'not_here' does not exist or is not a directory or .jar file", summary.errors.head)
3232

33-
@Test def `skip enuf args`: Unit =
33+
// if -d rejects its arg, ignore it instead of keeping it as a residual arg
34+
@Test def `skip failed args`: Unit =
3435
val args = List("-d", "not_here", "-Vprint")
3536
val summary = ScalaSettings.processArguments(args, processAll = true)
3637
assertEquals(2, summary.errors.size)
38+
assertTrue(summary.arguments.isEmpty)
3739
assertEquals("'not_here' does not exist or is not a directory or .jar file", summary.errors.head)
40+
assertEquals("missing argument for option -Vprint", summary.errors.last)
3841

39-
@Test def jarOutput: Unit =
42+
// same as previous but for colon arg
43+
@Test def `skip failed colon args`: Unit =
44+
val args = List("-d:not_here", "-Vprint")
45+
val summary = ScalaSettings.processArguments(args, processAll = true)
46+
assertEquals(2, summary.errors.size)
47+
assertTrue(summary.arguments.isEmpty)
48+
assertEquals("'not_here' does not exist or is not a directory or .jar file", summary.errors.head)
49+
assertEquals("missing argument for option -Vprint", summary.errors.last)
50+
51+
@Test def `output jar file is created eagerly`: Unit =
52+
import TestConfiguration.basicClasspath
4053
val source = "tests/pos/Foo.scala"
4154
val out = Paths.get("out/jarredFoo.jar").normalize
4255
if Files.exists(out) then Files.delete(out)
43-
val args = List("-Xmain-class", "Jarred", "-classpath", TestConfiguration.basicClasspath, "-d", out.toString, source)
56+
val args = List("-Xmain-class", "Jarred", "-classpath", basicClasspath, "-d", out.toString, source)
4457
val summary = ScalaSettings.processArguments(args, processAll = true)
4558
assertEquals(0, summary.errors.size)
4659
assertTrue(Files.exists(out))

tests/neg-custom-args/captures/cc-fresh-levels.check

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
Flag -source set repeatedly
21
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-fresh-levels.scala:14:10 ------------------------------
32
14 | r.put(x) // error
43
| ^

0 commit comments

Comments
 (0)