Skip to content

Handle help, version and @file parameters in scalac and scaladoc #11476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions compiler/src/dotty/tools/dotc/Driver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,24 @@ class Driver {

protected def sourcesRequired: Boolean = true

def setup(args: Array[String], rootCtx: Context): (List[AbstractFile], Context) = {
protected def command: CompilerCommand = ScalacCommand

def setup(args: Array[String], rootCtx: Context): (Option[List[AbstractFile]], Context) = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An Option of a List is weird, the documentation of this method should explain clearly how None differs from Some(Nil) here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update documentation

val ictx = rootCtx.fresh
val summary = CompilerCommand.distill(args, config.ScalaSettings(), ictx.settingsState)
val settings = config.ScalaSettings()
val summary = command.distill(args, settings, settings.defaultState)
ictx.setSettings(summary.sstate)
MacroClassLoader.init(ictx)
Positioned.init(using ictx)

inContext(ictx) {
if !ctx.settings.YdropComments.value || ctx.mode.is(Mode.ReadComments) then
ictx.setProperty(ContextDoc, new ContextDocstrings)
val fileNames = CompilerCommand.checkUsage(summary, sourcesRequired)(using ctx.settings)(using ctx.settingsState)
val files = fileNames.map(ctx.getFile)
(files, fromTastySetup(files))
val fileNamesOrNone = command.checkUsage(summary, sourcesRequired)(using ctx.settings)(using ctx.settingsState)
fileNamesOrNone.fold((None, ictx)) { fileNames =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not very important but we rarely use fold on Option in the compiler, instead we prefer to match on Some(...) and None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fill fix

val files = fileNames.map(ctx.getFile)
(Some(files), fromTastySetup(files))
}
}
}

Expand Down Expand Up @@ -183,7 +188,9 @@ class Driver {
*/
def process(args: Array[String], rootCtx: Context): Reporter = {
val (files, compileCtx) = setup(args, rootCtx)
doCompile(newCompiler(using compileCtx), files)(using compileCtx)
files.fold(compileCtx.reporter) {
doCompile(newCompiler(using compileCtx), _)(using compileCtx)
}
}

def main(args: Array[String]): Unit = {
Expand Down
25 changes: 15 additions & 10 deletions compiler/src/dotty/tools/dotc/Resident.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,21 @@ class Resident extends Driver {

final override def process(args: Array[String], rootCtx: Context): Reporter = {
@tailrec def loop(args: Array[String], prevCtx: Context): Reporter = {
var (files, ctx) = setup(args, prevCtx)
inContext(ctx) { doCompile(residentCompiler, files) }
var nextCtx = ctx
var line = getLine()
while (line == reset) {
nextCtx = rootCtx
line = getLine()
}
if (line.startsWith(quit)) ctx.reporter
else loop(line split "\\s+", nextCtx)
var (possibleFiles, ctx) = setup(args, prevCtx)
if possibleFiles.isDefined then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would pattern match on possibleFiles here instead of using isDefined/get

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix

inContext(ctx) {
doCompile(residentCompiler, possibleFiles.get) // using more complex constructs like fold or map instead of get will make @tailrec complain
}
var nextCtx = ctx
var line = getLine()
while (line == reset) {
nextCtx = rootCtx
line = getLine()
}
if (line.startsWith(quit)) ctx.reporter
else loop(line split "\\s+", nextCtx)
else
ctx.reporter
}
loop(args, rootCtx)
}
Expand Down
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/ScalacCommand.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dotty.tools.dotc

import config.Properties._
import config.CompilerCommand

object ScalacCommand extends CompilerCommand:
override def cmdName: String = "scalac"
override def versionMsg: String = s"Scala compiler $versionString -- $copyrightString"
override def ifErrorsMsg: String = " scalac -help gives more information"
66 changes: 36 additions & 30 deletions compiler/src/dotty/tools/dotc/config/CliCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ trait CliCommand:

type ConcreteSettings <: CommonScalaSettings with Settings.SettingGroup

def versionMsg: String

def ifErrorsMsg: String

/** The name of the command */
def cmdName: String

def isHelpFlag(using settings: ConcreteSettings)(using SettingsState): Boolean

def helpMsg(using settings: ConcreteSettings)(using SettingsState, Context): String

private def explainAdvanced = """
|-- Notes on option parsing --
|Boolean settings are always false unless set.
Expand All @@ -31,14 +39,6 @@ trait CliCommand:
| already are in phase X + 1.
"""

def shortUsage: String = s"Usage: $cmdName <options> <source files>"

def versionMsg: String = s"Scala $versionString -- $copyrightString"

def ifErrorsMsg: String = " -help gives more information"

def shouldStopWithInfo(using settings: ConcreteSettings)(using SettingsState): Boolean

/** Distill arguments into summary detailing settings, errors and files to main */
def distill(args: Array[String], sg: Settings.SettingGroup, ss: SettingsState): ArgsSummary =
/**
Expand All @@ -64,11 +64,8 @@ trait CliCommand:

sg.processArguments(expandedArguments, ss, processAll = true)


def infoMessage(using settings: ConcreteSettings)(using SettingsState)(using Context): String

/** Creates a help message for a subset of options based on cond */
def availableOptionsMsg(cond: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
protected def availableOptionsMsg(cond: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
val ss = (settings.allSettings filter cond).toList sortBy (_.name)
val width = (ss map (_.name.length)).max
def format(s: String) = ("%-" + width + "s") format s
Expand All @@ -90,8 +87,9 @@ trait CliCommand:

ss.map(helpStr).mkString("", "\n", s"\n${format("@<file>")} A text file containing compiler arguments (options and source files).\n")

protected def shortUsage: String = s"Usage: $cmdName <options> <source files>"

def createUsageMsg(label: String, shouldExplain: Boolean, cond: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
protected def createUsageMsg(label: String, shouldExplain: Boolean, cond: Setting[?] => Boolean)(using settings: ConcreteSettings)(using SettingsState): String =
val prefix = List(
Some(shortUsage),
Some(explainAdvanced) filter (_ => shouldExplain),
Expand All @@ -100,42 +98,50 @@ trait CliCommand:

prefix + "\n" + availableOptionsMsg(cond)

def isStandard(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean = !isAdvanced(s) && !isPrivate(s)
def isAdvanced(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean = s.name.startsWith("-X") && s.name != "-X"
def isPrivate(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean = s.name.startsWith("-Y") && s.name != "-Y"
protected def isStandard(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
!isAdvanced(s) && !isPrivate(s)
protected def isAdvanced(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-X") && s.name != "-X"
protected def isPrivate(s: Setting[?])(using settings: ConcreteSettings)(using SettingsState): Boolean =
s.name.startsWith("-Y") && s.name != "-Y"

/** Messages explaining usage and options */
def usageMessage(using settings: ConcreteSettings)(using SettingsState) = createUsageMsg("where possible standard", shouldExplain = false, isStandard)
def xusageMessage(using settings: ConcreteSettings)(using SettingsState) = createUsageMsg("Possible advanced", shouldExplain = true, isAdvanced)
def yusageMessage(using settings: ConcreteSettings)(using SettingsState) = createUsageMsg("Possible private", shouldExplain = true, isPrivate)

def phasesMessage: String =
protected def usageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("where possible standard", shouldExplain = false, isStandard)
protected def xusageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("Possible advanced", shouldExplain = true, isAdvanced)
protected def yusageMessage(using settings: ConcreteSettings)(using SettingsState) =
createUsageMsg("Possible private", shouldExplain = true, isPrivate)

protected def phasesMessage: String =
(new Compiler()).phases.map {
case List(single) => single.phaseName
case more => more.map(_.phaseName).mkString("{", ", ", "}")
}.mkString("\n")

/** Provide usage feedback on argument summary, assuming that all settings
* are already applied in context.
* @return The list of files passed as arguments.
* @return Either Some list of files passed as arguments or None if further processing should be interrupted.
*/
def checkUsage(summary: ArgsSummary, sourcesRequired: Boolean)(using settings: ConcreteSettings)(using SettingsState)(using Context): List[String] =
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(_))

if summary.errors.nonEmpty then
summary.errors foreach (report.error(_))
report.echo(ifErrorsMsg)
Nil
None
else if settings.version.value then
report.echo(versionMsg)
Nil
else if shouldStopWithInfo then
report.echo(infoMessage)
Nil
None
else if isHelpFlag then
report.echo(helpMsg)
None
else if (sourcesRequired && summary.arguments.isEmpty)
report.echo(usageMessage)
None
else
if (sourcesRequired && summary.arguments.isEmpty) report.echo(usageMessage)
summary.arguments
Some(summary.arguments)

extension [T](setting: Setting[T])
protected def value(using ss: SettingsState): T = setting.valueIn(ss)
11 changes: 4 additions & 7 deletions compiler/src/dotty/tools/dotc/config/CompilerCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,16 @@ import Properties._

import scala.collection.JavaConverters._

object CompilerCommand extends CliCommand:
type ConcreteSettings = ScalaSettings
override def cmdName: String = "scalac"
override def versionMsg: String = s"Scala compiler $versionString -- $copyrightString"
override def ifErrorsMsg: String = " scalac -help gives more information"
abstract class CompilerCommand extends CliCommand:
final type ConcreteSettings = ScalaSettings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think final on a type alias does anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove


def infoMessage(using settings: ScalaSettings)(using SettingsState)(using Context): String =
final def helpMsg(using settings: ScalaSettings)(using SettingsState, Context): String =
if (settings.help.value) usageMessage
else if (settings.Xhelp.value) xusageMessage
else if (settings.Yhelp.value) yusageMessage
else if (settings.showPlugins.value) ctx.base.pluginDescriptions
else if (settings.XshowPhases.value) phasesMessage
else ""

def shouldStopWithInfo(using settings: ScalaSettings)(using SettingsState): Boolean =
final def isHelpFlag(using settings: ScalaSettings)(using SettingsState): Boolean =
Set(settings.help, settings.Xhelp, settings.Yhelp, settings.showPlugins, settings.XshowPhases) exists (_.value)
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/decompiler/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object Main extends dotc.Driver {
new TASTYDecompiler
}

override def setup(args0: Array[String], rootCtx: Context): (List[AbstractFile], Context) = {
override def setup(args0: Array[String], rootCtx: Context): (Option[List[AbstractFile]], Context) = {
var args = args0.filter(a => a != "-decompile")
if (!args.contains("-from-tasty")) args = "-from-tasty" +: args
if (args.contains("-d")) args = "-color:never" +: args
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/repl/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package dotty.tools.repl
/** Main entry point to the REPL */
object Main {
def main(args: Array[String]): Unit =
new ReplDriver(args).runUntilQuit()
new ReplDriver(args).tryRunning
}
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/repl/ReplCommand.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dotty.tools.repl

import dotty.tools.dotc.config.Properties._
import dotty.tools.dotc.config.CompilerCommand

object ReplCommand extends CompilerCommand:
override def cmdName: String = "scala"
override def versionMsg: String = s"Scala code runner $versionString -- $copyrightString"
override def ifErrorsMsg: String = " scala -help gives more information"
15 changes: 14 additions & 1 deletion compiler/src/dotty/tools/repl/ReplDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import dotty.tools.dotc.reporting.{Message, Diagnostic}
import dotty.tools.dotc.util.Spans.Span
import dotty.tools.dotc.util.{SourceFile, SourcePosition}
import dotty.tools.dotc.{CompilationUnit, Driver}
import dotty.tools.dotc.config.CompilerCommand
import dotty.tools.io._
import org.jline.reader._

Expand Down Expand Up @@ -67,7 +68,8 @@ class ReplDriver(settings: Array[String],
private def initialCtx = {
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions | Mode.Interactive | Mode.ReadComments)
rootCtx.setSetting(rootCtx.settings.YcookComments, true)
val ictx = setup(settings, rootCtx)._2
val (files, ictx) = setup(settings, rootCtx)
shouldStart = files.isDefined
ictx.base.initialize()(using ictx)
ictx
}
Expand All @@ -91,13 +93,24 @@ class ReplDriver(settings: Array[String],
}

private var rootCtx: Context = _
private var shouldStart: Boolean = _
private var compiler: ReplCompiler = _
private var rendering: Rendering = _

// initialize the REPL session as part of the constructor so that once `run`
// is called, we're in business
resetToInitial()

override protected def command: CompilerCommand = ReplCommand

/** Try to run REPL if there is nothing that prevents us doing so.
*
* Possible reason for unsuccessful run are raised flags in CLI like --help or --version
*/
final def tryRunning = if shouldStart then
println("Starting scala3 REPL...")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this was here before, but I would just delete that line, the repl already prints enough stuff when it starts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove

runUntilQuit()

/** Run REPL with `state` until `:quit` command found
*
* This method is the main entry point into the REPL. Its effects are not
Expand Down
46 changes: 24 additions & 22 deletions compiler/src/dotty/tools/scripting/ScriptingDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,30 @@ import sys.process._
class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
def compileAndRun(pack:(Path, String, String) => Boolean = null): Unit =
val outDir = Files.createTempDirectory("scala3-scripting")
val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh)
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
new PlainDirectory(Directory(outDir)))

if doCompile(newCompiler, toCompile).hasErrors then
throw ScriptingException("Errors encountered during compilation")

try
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile)
val invokeMain: Boolean =
Option(pack) match
case Some(func) =>
func(outDir, ctx.settings.classpath.value, mainClass)
case None =>
true
end match
if invokeMain then mainMethod.invoke(null, scriptArgs)
catch
case e: java.lang.reflect.InvocationTargetException =>
throw e.getCause
finally
deleteFile(outDir.toFile)
val (toCompileOrNone, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh)
toCompileOrNone.map { toCompile =>
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
new PlainDirectory(Directory(outDir)))

if doCompile(newCompiler, toCompile).hasErrors then
throw ScriptingException("Errors encountered during compilation")

try
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile)
val invokeMain: Boolean =
Option(pack) match
case Some(func) =>
func(outDir, ctx.settings.classpath.value, mainClass)
case None =>
true
end match
if invokeMain then mainMethod.invoke(null, scriptArgs)
catch
case e: java.lang.reflect.InvocationTargetException =>
throw e.getCause
finally
deleteFile(outDir.toFile)
}
end compileAndRun

private def deleteFile(target: File): Unit =
Expand Down
39 changes: 39 additions & 0 deletions compiler/test/dotty/tools/dotc/ScalaCommandTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dotty.tools.dotc

import org.junit.Test
import org.junit.Assert._
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import dotty.tools.dotc.config.Settings._

class ScalaCommandTest:

private val _temporaryFolder = new TemporaryFolder

@Rule
def temporaryFolder = _temporaryFolder

@Test def `Simple one parameter`: Unit =
val settings = config.ScalaSettings()
val args = "-cp path/to/classes1:other/path/to/classes2 files".split(" ")
val summary = ScalacCommand.distill(args, settings, settings.defaultState)
given SettingsState = summary.sstate
assertEquals("path/to/classes1:other/path/to/classes2", settings.classpath.value)
assertEquals("files" :: Nil, summary.arguments)

@Test def `Unfold @file`: Unit =
val settings = config.ScalaSettings()
val file = temporaryFolder.newFile("config")
val writer = java.io.FileWriter(file);
writer.write("-sourceroot myNewRoot someMoreFiles");
writer.close();
val args = s"-cp path/to/classes1:other/path/to/classes2 @$file someFiles".split(" ")
val summary = ScalacCommand.distill(args, settings, settings.defaultState)

given SettingsState = summary.sstate
assertEquals("path/to/classes1:other/path/to/classes2", settings.classpath.value)
assertEquals("myNewRoot", settings.sourceroot.value)
assertEquals("someMoreFiles" :: "someFiles" :: Nil, summary.arguments)

extension [T](setting: Setting[T])
private def value(using ss: SettingsState): T = setting.valueIn(ss)
Loading