Skip to content

Commit

Permalink
Add exclude directive and command line options to exclude sources
Browse files Browse the repository at this point in the history
  • Loading branch information
lwronski committed Apr 28, 2023
1 parent 2c57665 commit 5d7c38e
Show file tree
Hide file tree
Showing 23 changed files with 472 additions and 13 deletions.
3 changes: 2 additions & 1 deletion modules/build/src/main/scala/scala/build/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ object Build {
() => options.javaHome().value.javaCommand
),
logger,
options.suppressWarningOptions
options.suppressWarningOptions,
options.internal.exclude
)
}
val sharedOptions = crossSources.sharedOptions(options)
Expand Down
90 changes: 86 additions & 4 deletions modules/build/src/main/scala/scala/build/CrossSources.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package scala.build

import java.io.File

import scala.build.EitherCps.{either, value}
import scala.build.Ops.*
import scala.build.Positioned
import scala.build.errors.{BuildException, CompositeBuildException, MalformedDirectiveError}
import scala.build.errors.{
BuildException,
CompositeBuildException,
ExcludeDefinitionError,
MalformedDirectiveError
}
import scala.build.input.ElementsUtils.*
import scala.build.input.*
import scala.build.internal.Constants
import scala.build.internal.util.RegexUtils
import scala.build.options.{
BuildOptions,
BuildRequirements,
Expand All @@ -16,6 +24,9 @@ import scala.build.options.{
WithBuildRequirements
}
import scala.build.preprocessing.*
import scala.build.testrunner.DynamicTestRunner.globPattern
import scala.util.Try
import scala.util.chaining.*

final case class CrossSources(
paths: Seq[WithBuildRequirements[(os.Path, os.RelPath)]],
Expand Down Expand Up @@ -128,6 +139,7 @@ object CrossSources {
preprocessors: Seq[Preprocessor],
logger: Logger,
suppressWarningOptions: SuppressWarningOptions,
exclude: Seq[Positioned[String]] = Nil,
maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e)
): Either[BuildException, (CrossSources, Inputs)] = either {

Expand Down Expand Up @@ -155,8 +167,21 @@ object CrossSources {
.left.map(CompositeBuildException(_))
.map(_.flatten)

val flattenedInputs = inputs.flattened()
val allExclude = { // supports only one exclude directive in one source file, which should be the project file.
val projectScalaFileOpt = flattenedInputs.collectFirst {
case f: ProjectScalaFile => f
}
val excludeFromProjectFile =
value(preprocessSources(projectScalaFileOpt.toSeq))
.flatMap(_.options).flatMap(_.internal.exclude)
exclude ++ excludeFromProjectFile
}

val preprocessedInputFromArgs: Seq[PreprocessedSource] =
value(preprocessSources(inputs.flattened()))
value(
preprocessSources(value(excludeSources(flattenedInputs, inputs.workspace, allExclude)))
)

val sourcesFromDirectives =
preprocessedInputFromArgs
Expand All @@ -166,11 +191,16 @@ object CrossSources {
val inputsElemFromDirectives: Seq[SingleFile] =
value(resolveInputsFromSources(sourcesFromDirectives, inputs.enableMarkdown))
val preprocessedSourcesFromDirectives: Seq[PreprocessedSource] =
value(preprocessSources(inputsElemFromDirectives))
val allInputs = inputs.add(inputsElemFromDirectives)
value(preprocessSources(inputsElemFromDirectives.pipe(elements =>
value(excludeSources(elements, inputs.workspace, allExclude))
)))
val allInputs = inputs.add(inputsElemFromDirectives).filter(elems =>
value(excludeSources(elems, inputs.workspace, allExclude))
)

val preprocessedSources =
(preprocessedInputFromArgs ++ preprocessedSourcesFromDirectives).distinct
.pipe(sources => value(validateExcludeDirectives(sources, allInputs.workspace)))

val scopedRequirements = preprocessedSources.flatMap(_.scopedRequirements)
val scopedRequirementsByRoot = scopedRequirements.groupBy(_.path.root)
Expand Down Expand Up @@ -305,4 +335,56 @@ object CrossSources {
.left.map(CompositeBuildException(_))
.map(_.flatten)

/** Filters out the sources from the input sequence based on the provided 'exclude' patterns. The
* exclude patterns can be absolute paths, relative paths, or glob patterns.
*
* @throws BuildException
* If multiple 'exclude' patterns are defined across the input sources.
*/
private def excludeSources[E <: Element](
elements: Seq[E],
workspaceDir: os.Path,
exclude: Seq[Positioned[String]]
): Either[BuildException, Seq[E]] = either {
val excludePatterns = exclude.map(_.value).flatMap { p =>
val maybeRelPath = Try(os.RelPath(p)).toOption
maybeRelPath match {
case Some(relPath) if os.isDir(workspaceDir / relPath) =>
// exclude relative directory paths, add * to exclude all files in the directory
Seq(p, (workspaceDir / relPath / "*").toString)
case Some(relPath) =>
Seq(p, (workspaceDir / relPath).toString) // exclude relative paths
case None => Seq(p)
}
}

def isSourceIncluded(path: String, excludePatterns: Seq[String]): Boolean =
excludePatterns
.forall(pattern => !RegexUtils.globPattern(pattern).matcher(path).matches())

elements.filter {
case e: OnDisk => isSourceIncluded(e.path.toString, excludePatterns)
case _ => true
}
}

/** Validates that exclude directives are defined only in the one source.
*/
def validateExcludeDirectives(
sources: Seq[PreprocessedSource],
workspaceDir: os.Path
): Either[BuildException, Seq[PreprocessedSource]] = {
val excludeDirectives = sources.flatMap(_.options).map(_.internal.exclude).toList.flatten

excludeDirectives match {
case Nil | Seq(_) =>
Right(sources)
case _ =>
val expectedProjectFilePath = workspaceDir / Constants.projectFileName
Left(new ExcludeDefinitionError(
excludeDirectives.flatMap(_.positions),
expectedProjectFilePath
))
}
}
}
1 change: 1 addition & 0 deletions modules/build/src/main/scala/scala/build/bsp/BspImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ final class BspImpl(
),
logger = persistentLogger,
suppressWarningOptions = buildOptions.suppressWarningOptions,
exclude = buildOptions.internal.exclude,
maybeRecoverOnError = maybeRecoverOnError(Scope.Main)
).left.map((_, Scope.Main))
}
Expand Down
2 changes: 2 additions & 0 deletions modules/build/src/main/scala/scala/build/input/Inputs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ final case class Inputs(

def add(extraElements: Seq[Element]): Inputs =
if elements.isEmpty then this else copy(elements = (elements ++ extraElements).distinct)
def filter(filterElements: Seq[Element] => Seq[Element]): Inputs =
copy(elements = filterElements(elements))

def generatedSrcRoot(scope: Scope): os.Path =
workspace / Constants.workspaceDirName / projectName / "src_generated" / scope.name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package scala.build.internal.util

import java.util.regex.Pattern

object RegexUtils {

/** Based on junit-interface [GlobFilter.
* compileGlobPattern](https://github.com/sbt/junit-interface/blob/f8c6372ed01ce86f15393b890323d96afbe6d594/src/main/java/com/novocode/junit/GlobFilter.java#L37)
*
* @return
* Pattern allows to regex input which contains only *, for example `*foo*` match to
* `MyTests.foo`
*/
def globPattern(expr: String): Pattern = {
val a = expr.split("\\*", -1)
val b = new StringBuilder()
for (i <- 0 until a.length) {
if (i != 0) b.append(".*")
if (a(i).nonEmpty) b.append(Pattern.quote(a(i).replaceAll("\n", "\\n")))
}
Pattern.compile(b.toString)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ case object ScalaPreprocessor extends Preprocessor {

val usingDirectiveHandlers: Seq[DirectiveHandler[BuildOptions]] =
Seq[DirectiveHandler[_ <: HasBuildOptions]](
directives.Exclude.handler,
directives.JavaHome.handler,
directives.Jvm.handler,
directives.MainClass.handler,
Expand Down
183 changes: 183 additions & 0 deletions modules/build/src/test/scala/scala/build/tests/ExcludeTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package scala.build.tests

import com.eed3si9n.expecty.Expecty.expect
import coursier.cache.{ArchiveCache, Cache}
import coursier.util.{Artifact, Task}

import java.io.File
import scala.build.Ops.*
import scala.build.Sources
import scala.build.internal.CustomCodeWrapper
import scala.build.CrossSources
import scala.build.errors.ExcludeDefinitionError
import scala.build.options.{BuildOptions, Scope, SuppressWarningOptions}

class ExcludeTests extends munit.FunSuite {

val preprocessors = Sources.defaultPreprocessors(
CustomCodeWrapper,
ArchiveCache().withCache(
new Cache[Task] {
def fetch = _ => sys.error("shouldn't be used")

def file(artifact: Artifact) = sys.error("shouldn't be used")

def ec = sys.error("shouldn't be used")
}
),
None,
() => sys.error("shouldn't be used")
)

test("throw error when exclude found in multiple file") {
val testInputs = TestInputs(
os.rel / "Hello.scala" ->
"""//> using exclude "*.sc"
|""".stripMargin,
os.rel / "Main.scala" ->
"""//> using exclude "*/test/*"
|""".stripMargin
)
testInputs.withInputs { (_, inputs) =>
val crossSources =
CrossSources.forInputs(
inputs,
preprocessors,
TestLogger(),
SuppressWarningOptions()
)
crossSources match {
case Left(_: ExcludeDefinitionError) =>
case o => fail("Exception expected", clues(o))
}
}
}

test("throw error when exclude found in non top-level project.scala and file") {
val testInputs = TestInputs(
os.rel / "Main.scala" ->
"""//> using exclude "*/test/*"
|""".stripMargin,
os.rel / "src" / "project.scala" ->
s"""//> using exclude "*.sc" """
)
testInputs.withInputs { (_, inputs) =>
val crossSources =
CrossSources.forInputs(
inputs,
preprocessors,
TestLogger(),
SuppressWarningOptions()
)
crossSources match {
case Left(_: ExcludeDefinitionError) =>
case o => fail("Exception expected", clues(o))
}
}
}

test("exclude relative paths") {
val testInputs = TestInputs(
os.rel / "Hello.scala" -> "object Hello",
os.rel / "Main.scala" ->
"""object Main {
|}""".stripMargin,
os.rel / "project.scala" ->
s"""//> using exclude "Main.scala" """
)
testInputs.withInputs { (_, inputs) =>
val (crossSources, _) =
CrossSources.forInputs(
inputs,
preprocessors,
TestLogger(),
SuppressWarningOptions()
).orThrow
val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow
val sources = scopedSources.sources(Scope.Main, crossSources.sharedOptions(BuildOptions()))

expect(sources.paths.nonEmpty)
expect(sources.paths.length == 2)
expect(sources.paths.map(_._2) == Seq(os.rel / "Hello.scala", os.rel / "project.scala"))
}
}

test("exclude absolute file paths") {
val testInputs = TestInputs(
os.rel / "Hello.scala" -> "object Hello",
os.rel / "Main.scala" ->
"""object Main {
|}""".stripMargin,
os.rel / "project.scala" ->
s"""//> using exclude "$${.}${File.separator}Main.scala" """
)
testInputs.withInputs { (_, inputs) =>
val (crossSources, _) =
CrossSources.forInputs(
inputs,
preprocessors,
TestLogger(),
SuppressWarningOptions()
).orThrow
val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow
val sources = scopedSources.sources(Scope.Main, crossSources.sharedOptions(BuildOptions()))

expect(sources.paths.nonEmpty)
expect(sources.paths.length == 2)
expect(sources.paths.map(_._2) == Seq(os.rel / "Hello.scala", os.rel / "project.scala"))
}
}

test("exclude relative directory paths") {
val testInputs = TestInputs(
os.rel / "Hello.scala" -> "object Hello",
os.rel / "src" / "scala" / "Main.scala" ->
"""object Main {
|}""".stripMargin,
os.rel / "project.scala" ->
"""//> using exclude "src/*.scala" """
)
testInputs.withInputs { (_, inputs) =>
val (crossSources, _) =
CrossSources.forInputs(
inputs,
preprocessors,
TestLogger(),
SuppressWarningOptions()
).orThrow
val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow
val sources = scopedSources.sources(Scope.Main, crossSources.sharedOptions(BuildOptions()))

expect(sources.paths.nonEmpty)
expect(sources.paths.length == 2)
expect(sources.paths.map(_._2) == Seq(os.rel / "Hello.scala", os.rel / "project.scala"))
}
}

test("exclude relative directory paths with glob pattern") {
val testInputs = TestInputs(
os.rel / "Hello.scala" -> "object Hello",
os.rel / "src" / "scala" / "Main.scala" ->
"""object Main {
|}""".stripMargin,
os.rel / "project.scala" ->
"""//> using exclude "src/*.scala" """
)
testInputs.withInputs { (_, inputs) =>
val (crossSources, _) =
CrossSources.forInputs(
inputs,
preprocessors,
TestLogger(),
SuppressWarningOptions()
).orThrow
val scopedSources = crossSources.scopedSources(BuildOptions()).orThrow
val sources = scopedSources.sources(Scope.Main, crossSources.sharedOptions(BuildOptions()))

expect(sources.paths.nonEmpty)
expect(sources.paths.length == 2)
expect(sources.paths.map(_._2) == Seq(os.rel / "Hello.scala", os.rel / "project.scala"))
}
}

}
3 changes: 2 additions & 1 deletion modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ object Bsp extends ScalaCommand[BspOptions] {
() => buildOptions0.javaHome().value.javaCommand
),
persistentLogger,
buildOptions0.suppressWarningOptions
buildOptions0.suppressWarningOptions,
buildOptions0.internal.exclude
).map(_._2).getOrElse(initialInputs)

Build.updateInputs(allInputs, buildOptions(sharedOptions))
Expand Down
Loading

0 comments on commit 5d7c38e

Please sign in to comment.