Skip to content

Commit 2bee48c

Browse files
authored
Migrate from old using_directives to Scala-rewritten directives-parser module (#4192)
* Migrate from old `using_directives` to Scala-rewritten `directives-parser` module * Refactor tests * Handle edge cases * Include `directives-parser` module in `AGENTS.md`
1 parent 2de46c7 commit 2bee48c

27 files changed

Lines changed: 1908 additions & 317 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Modules live under `modules/`. The dependency graph flows roughly as:
5151

5252
```
5353
specification-level → config → core → options → directives → build-module → cli
54+
55+
directives-parser
5456
```
5557

5658
### Module overview
@@ -64,6 +66,7 @@ The list below may not be exhaustive — check `modules/` and `build.mill` for t
6466
| `build-macros` | Compile-time macros (e.g. `EitherCps`). |
6567
| `core` | Core types: `Inputs`, `Sources`, build constants, Bloop integration, JVM/JS/Native tooling. |
6668
| `options` | `BuildOptions`, `SharedOptions`, and all option types. |
69+
| `directives-parser` | Pure Scala 3 parser for `//> using` directive syntax: comment extraction, lexing, and parsing into AST nodes. |
6770
| `directives` | Using directive handlers — the bridge between `//> using` directives and `BuildOptions`. |
6871
| `build-module` (aliased from `build` in mill) | The main build pipeline: preprocessing, compilation, post-processing. Most business logic lives here. |
6972
| `cli` | Command definitions, argument parsing (CaseApp), the `ScalaCli` entry point. Packaged as the native image. |
@@ -106,9 +109,9 @@ Using directives are in-source configuration comments:
106109
//> using test.dep org.scalameta::munit::1.1.1
107110
```
108111

109-
Directives are parsed by `using_directives`, then `ExtractedDirectives``DirectivesPreprocessor``BuildOptions`/
110-
`BuildRequirements`. **CLI options override directive values.** To add a new directive,
111-
see [agentskills/adding-directives/](agentskills/adding-directives/SKILL.md).
112+
Directives are parsed by the `directives-parser` module (`CommentExtractor``Lexer``Parser`), then
113+
`ExtractedDirectives``DirectivesPreprocessor``BuildOptions`/`BuildRequirements`. **CLI options override directive
114+
values.** To add a new directive, see [agentskills/adding-directives/](agentskills/adding-directives/SKILL.md).
112115

113116
## Testing
114117

build.mill

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ object `specification-level` extends Cross[SpecificationLevel](Scala.scala3MainV
9898
with CrossScalaDefaultToInternal
9999
object `build-macros` extends Cross[BuildMacros](Scala.scala3MainVersions)
100100
with CrossScalaDefaultToInternal
101+
object `directives-parser` extends Cross[DirectivesParserModule](Scala.scala3MainVersions)
102+
with CrossScalaDefaultToInternal
101103
object config extends Cross[Config](Scala.scala3MainVersions)
102104
with CrossScalaDefaultToInternal
103105
object options extends Cross[Options](Scala.scala3MainVersions)
@@ -616,7 +618,8 @@ trait Directives extends ScalaCliCrossSbtModule
616618
options(crossScalaVersion),
617619
core(crossScalaVersion),
618620
`build-macros`(crossScalaVersion),
619-
`specification-level`(crossScalaVersion)
621+
`specification-level`(crossScalaVersion),
622+
`directives-parser`(crossScalaVersion)
620623
)
621624
override def scalacOptions: T[Seq[String]] = Task {
622625
super.scalacOptions() ++ asyncScalacOptions(crossScalaVersion)
@@ -630,8 +633,7 @@ trait Directives extends ScalaCliCrossSbtModule
630633
// Deps.asm,
631634
Deps.bloopConfig,
632635
Deps.jsoniterCore,
633-
Deps.pprint,
634-
Deps.usingDirectives
636+
Deps.pprint
635637
)
636638

637639
override def repositoriesTask: Task[Seq[Repository]] =
@@ -806,6 +808,16 @@ trait Build extends ScalaCliCrossSbtModule
806808
}
807809
}
808810

811+
trait DirectivesParserModule extends ScalaCliCrossSbtModule
812+
with ScalaCliPublishModule
813+
with HasTests
814+
with ScalaCliScalafixModule
815+
with LocatedInModules {
816+
override def crossScalaVersion: String = crossValue
817+
818+
object test extends ScalaCliTests with ScalaCliScalafixModule
819+
}
820+
809821
trait SpecificationLevel extends ScalaCliCrossSbtModule
810822
with ScalaCliPublishModule
811823
with LocatedInModules {
@@ -1024,6 +1036,9 @@ trait CliIntegration extends SbtModule
10241036
)
10251037

10261038
trait IntegrationScalaTests extends super.ScalaCliTests with ScalaCliScalafixModule {
1039+
override def moduleDeps: Seq[JavaModule] = super.moduleDeps ++ Seq(
1040+
`directives-parser`(sv)
1041+
)
10271042
override def mvnDeps: T[Seq[Dep]] = super.mvnDeps() ++ Seq(
10281043
Deps.bsp4j,
10291044
Deps.coursier
@@ -1032,8 +1047,7 @@ trait CliIntegration extends SbtModule
10321047
Deps.jsoniterCore,
10331048
Deps.libsodiumjni,
10341049
Deps.pprint,
1035-
Deps.slf4jNop,
1036-
Deps.usingDirectives
1050+
Deps.slf4jNop
10371051
)
10381052
override def compileMvnDeps: T[Seq[Dep]] = super.compileMvnDeps() ++ Seq(
10391053
Deps.jsoniterMacros

modules/build/src/main/scala/scala/build/preprocessing/CustomDirectivesReporter.scala

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package scala.build.preprocessing
22

3-
import com.virtuslab.using_directives.UsingDirectivesProcessor
4-
import com.virtuslab.using_directives.custom.model.{BooleanValue, EmptyValue, StringValue, Value}
5-
import com.virtuslab.using_directives.custom.utils.ast.*
6-
73
import scala.annotation.targetName
84
import scala.build.errors.*
95
import scala.build.options.SuppressWarningOptions
10-
import scala.build.preprocessing.UsingDirectivesOps.*
116
import scala.build.preprocessing.directives.StrictDirective
127
import scala.build.{Logger, Position}
8+
import scala.cli.parse.{DiagnosticSeverity, DirectiveValue, UsingDirectivesParser}
139
import scala.collection.mutable
14-
import scala.jdk.CollectionConverters.*
1510

1611
case class ExtractedDirectives(
1712
directives: Seq[StrictDirective],
@@ -33,65 +28,68 @@ object ExtractedDirectives {
3328
logger: Logger,
3429
maybeRecoverOnError: BuildException => Option[BuildException]
3530
): Either[BuildException, ExtractedDirectives] = {
36-
val errors = new mutable.ListBuffer[Diagnostic]
37-
val reporter = CustomDirectivesReporter
38-
.create(path) {
39-
case diag
40-
if diag.severity == Severity.Warning &&
41-
diag.message.toLowerCase.contains("deprecated") &&
42-
suppressWarningOptions.suppressDeprecatedFeatureWarning.getOrElse(false) =>
43-
() // skip deprecated feature warnings if suppressed
44-
case diag if diag.severity == Severity.Warning =>
45-
logger.log(Seq(diag))
46-
case diag => errors += diag
47-
}
48-
val processor = new UsingDirectivesProcessor(reporter)
49-
val allDirectives = processor.extract(contentChars).asScala
31+
val result = UsingDirectivesParser.parse(contentChars)
32+
val diagnosticErrors = mutable.ListBuffer.empty[Diagnostic]
33+
34+
for diag <- result.diagnostics do
35+
val positions = diag.position.map { p =>
36+
Position.File(path, (p.line, p.column), (p.line, p.column))
37+
}.toSeq
38+
39+
if diag.severity == DiagnosticSeverity.Warning then
40+
if diag.message.toLowerCase.contains("deprecated") &&
41+
suppressWarningOptions.suppressDeprecatedFeatureWarning.getOrElse(false)
42+
then () // skip
43+
else logger.log(Seq(Diagnostic(diag.message, Severity.Warning, positions)))
44+
else
45+
diagnosticErrors += Diagnostic(diag.message, Severity.Error, positions)
46+
5047
val malformedDirectiveErrors =
51-
errors.map(diag => new MalformedDirectiveError(diag.message, diag.positions)).toSeq
48+
diagnosticErrors
49+
.map(diag => new MalformedDirectiveError(diag.message, diag.positions))
50+
.toSeq
51+
5252
val maybeCompositeMalformedDirectiveError =
53-
if (malformedDirectiveErrors.nonEmpty)
53+
if malformedDirectiveErrors.nonEmpty then
5454
maybeRecoverOnError(CompositeBuildException(malformedDirectiveErrors))
5555
else None
56-
if (malformedDirectiveErrors.isEmpty || maybeCompositeMalformedDirectiveError.isEmpty) {
5756

58-
val directivesOpt = allDirectives.headOption
59-
val directivesPositionOpt = directivesOpt match {
60-
case Some(directives)
61-
if directives.containsTargetDirectives ||
62-
directives.isEmpty => None
63-
case Some(directives) => Some(directives.getPosition(path))
64-
case None => None
65-
}
57+
if malformedDirectiveErrors.isEmpty || maybeCompositeMalformedDirectiveError.isEmpty then
58+
val directives = result.directives
59+
60+
val containsTargetDirectives = directives.exists(_.key.startsWith("target."))
6661

67-
val strictDirectives = directivesOpt.toSeq.flatMap { directives =>
68-
def toStrictValue(value: UsingValue): Seq[Value[?]] = value match {
69-
case uvs: UsingValues => uvs.values.asScala.toSeq.flatMap(toStrictValue)
70-
case el: EmptyLiteral => Seq(EmptyValue(el))
71-
case sl: StringLiteral => Seq(StringValue(sl.getValue(), sl))
72-
case bl: BooleanLiteral => Seq(BooleanValue(bl.getValue(), bl))
73-
}
74-
def toStrictDirective(ud: UsingDef) =
75-
StrictDirective(
76-
ud.getKey(),
77-
toStrictValue(ud.getValue()),
78-
ud.getPosition().getColumn(),
79-
ud.getPosition().getLine()
80-
)
62+
val directivesPositionOpt =
63+
if containsTargetDirectives || directives.isEmpty then None
64+
else
65+
val lastDirective = directives.last
66+
val (endLine, endCol) = lastDirective.values.lastOption match
67+
case Some(sv: DirectiveValue.StringVal) if sv.isQuoted =>
68+
(sv.pos.line, sv.pos.column + sv.value.length + 2)
69+
case Some(sv: DirectiveValue.StringVal) =>
70+
(sv.pos.line, sv.pos.column + sv.value.length)
71+
case Some(bv: DirectiveValue.BoolVal) =>
72+
(bv.pos.line, bv.pos.column + bv.value.toString.length)
73+
case Some(ev: DirectiveValue.EmptyVal) =>
74+
(ev.pos.line, ev.pos.column)
75+
case None =>
76+
val kp = lastDirective.keyPosition
77+
(kp.line, kp.column + lastDirective.key.length)
78+
Some(Position.File(path, (0, 0), (endLine, endCol), result.codeOffset))
8179

82-
directives.getAst match
83-
case uds: UsingDefs => uds.getUsingDefs.asScala.toSeq.map(toStrictDirective)
84-
case ud: UsingDef => Seq(toStrictDirective(ud))
85-
case _ => Nil // There should be nothing else here other than UsingDefs or UsingDef
80+
val strictDirectives = directives.map { ud =>
81+
StrictDirective(
82+
ud.key,
83+
ud.values,
84+
ud.keyPosition.column,
85+
ud.keyPosition.line
86+
)
8687
}
8788

8889
Right(ExtractedDirectives(strictDirectives.reverse, directivesPositionOpt))
89-
}
9090
else
91-
maybeCompositeMalformedDirectiveError match {
91+
maybeCompositeMalformedDirectiveError match
9292
case Some(e) => Left(e)
9393
case None => Right(ExtractedDirectives.empty)
94-
}
9594
}
96-
9795
}

modules/build/src/main/scala/scala/build/preprocessing/UsingDirectivesOps.scala

Lines changed: 0 additions & 55 deletions
This file was deleted.

modules/core/src/main/scala/scala/build/errors/NoValueProvidedError.scala

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)