Skip to content
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

Add support for using directive in java files #639

Merged
merged 6 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package scala.build.errors

final class DirectiveErrors(errors: ::[String]) extends BuildException(
"Directives errors: " + errors.mkString(", ")
import scala.build.Position

final class DirectiveErrors(errors: ::[String], positions: Seq[Position]) extends BuildException(
"Directives errors: " + errors.mkString(", "),
positions = positions
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package scala.build.preprocessing

import com.virtuslab.using_directives.config.Settings
import com.virtuslab.using_directives.custom.model.{
UsingDirectiveKind,
UsingDirectiveSyntax,
UsingDirectives
}
import com.virtuslab.using_directives.custom.utils.ast.{UsingDef, UsingDefs}
import com.virtuslab.using_directives.{Context, UsingDirectivesProcessor}

import scala.build.errors._
import scala.build.preprocessing.directives.{DirectiveUtil, StrictDirective}
import scala.build.{Logger, Position}
import scala.collection.mutable
import scala.jdk.CollectionConverters._

case class ExtractedDirectives(
offset: Int,
directives: Seq[StrictDirective]
)

object ExtractedDirectives {

val changeToSpecialCommentMsg =
"Using directive using plain comments are deprecated. Please use a special comment syntax: '//> ...' or '/*> ... */'"

def from(
contentChars: Array[Char],
path: Either[String, os.Path],
logger: Logger,
supportedDirectives: Array[UsingDirectiveKind],
cwd: ScopePath
): Either[BuildException, ExtractedDirectives] = {
val errors = new mutable.ListBuffer[Diagnostic]
val reporter = CustomDirectivesReporter.create(path) { diag =>
if (diag.severity == Severity.Warning)
logger.log(Seq(diag))
else
errors += diag
}
val processor = {
val settings = new Settings
settings.setAllowStartWithoutAt(true)
settings.setAllowRequire(false)
val context = new Context(reporter, settings)
new UsingDirectivesProcessor(context)
}
val all = processor.extract(contentChars, true, true).asScala
if (errors.isEmpty) {

def byKind(kind: UsingDirectiveKind) = all.find(_.getKind == kind).get

def getDirectives(directives: UsingDirectives) =
directives.getAst() match {
case ud: UsingDefs =>
ud.getUsingDefs().asScala
case _ =>
Nil
}

val codeDirectives = byKind(UsingDirectiveKind.Code)
val specialCommentDirectives = byKind(UsingDirectiveKind.SpecialComment)
val plainCommentDirectives = byKind(UsingDirectiveKind.PlainComment)

def reportWarning(msg: String, values: Seq[UsingDef], before: Boolean = true): Unit =
values.foreach { v =>
val astPos = v.getPosition()
val (start, end) =
if (before) (0, astPos.getColumn())
else (astPos.getColumn(), astPos.getColumn() + v.getSyntax.getKeyword.size)
val position = Position.File(path, (astPos.getLine(), start), (astPos.getLine(), end))
logger.diagnostic(msg, positions = Seq(position))
}

val usedDirectives =
if (!codeDirectives.getFlattenedMap().isEmpty()) {
val msg =
"This using directive is ignored. File contains directives outside comments and those have higher precedence."
reportWarning(
msg,
getDirectives(plainCommentDirectives) ++ getDirectives(specialCommentDirectives)
)
codeDirectives
}
else if (!specialCommentDirectives.getFlattenedMap().isEmpty()) {
val msg =
s"This using directive is ignored. $changeToSpecialCommentMsg"
reportWarning(msg, getDirectives(plainCommentDirectives))
specialCommentDirectives
}
else {
reportWarning(changeToSpecialCommentMsg, getDirectives(plainCommentDirectives))
plainCommentDirectives
}

// All using directives should use just `using` keyword, no @using or require
reportWarning(
"Deprecated using directive syntax, please use keyword `using`.",
getDirectives(usedDirectives).filter(_.getSyntax() != UsingDirectiveSyntax.Using),
before = false
)

val flattened = usedDirectives.getFlattenedMap.asScala.toSeq
val strictDirectives =
flattened.map {
case (k, l) =>
StrictDirective(k.getPath.asScala.mkString("."), l.asScala.toSeq)
}

val offset =
if (usedDirectives.getKind() != UsingDirectiveKind.Code) 0
else usedDirectives.getCodeOffset()
if (supportedDirectives.contains(usedDirectives.getKind()))
Right(ExtractedDirectives(offset, strictDirectives))
else {
val directiveVales =
usedDirectives.getFlattenedMap.values().asScala.toList.flatMap(_.asScala)
val values = DirectiveUtil.stringValues(directiveVales, path, cwd) ++
DirectiveUtil.numericValues(directiveVales, path, cwd)
Left(new DirectiveErrors(
::(s"Directive '${usedDirectives.getKind}' is not supported in the given context'", Nil),
values.flatMap(_._1.positions)
))
}
}
else {
val errors0 = errors.map(diag => new MalformedDirectiveError(diag.message, diag.positions))
Left(CompositeBuildException(errors0.toSeq))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package scala.build.preprocessing

import java.nio.charset.StandardCharsets
import com.virtuslab.using_directives.custom.model.UsingDirectiveKind

import java.nio.charset.StandardCharsets
import scala.build.EitherCps.{either, value}
import scala.build.errors.BuildException
import scala.build.options.BuildRequirements
import scala.build.preprocessing.ExtractedDirectives.from
import scala.build.preprocessing.ScalaPreprocessor._
import scala.build.{Inputs, Logger}

case object JavaPreprocessor extends Preprocessor {
Expand All @@ -11,9 +16,32 @@ case object JavaPreprocessor extends Preprocessor {
logger: Logger
): Option[Either[BuildException, Seq[PreprocessedSource]]] =
input match {
case j: Inputs.JavaFile =>
Some(Right(Seq(PreprocessedSource.OnDisk(j.path, None, None, Nil, None))))

case j: Inputs.JavaFile => Some(either {
val content = value(PreprocessingUtil.maybeRead(j.path))
val scopePath = ScopePath.fromPath(j.path)
val ExtractedDirectives(_, directives0) =
value(from(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be nice if we could enrich the DirectiveError here with the information that it happened in the java context but the DirectiveError is an opaque class.

content.toCharArray,
Right(j.path),
logger,
Array(UsingDirectiveKind.PlainComment, UsingDirectiveKind.SpecialComment),
scopePath
))
val updatedOptions = value(DirectivesProcessor.process(
directives0,
usingDirectiveHandlers,
Right(j.path),
scopePath,
logger
))
Seq(PreprocessedSource.OnDisk(
j.path,
Some(updatedOptions.global),
Some(BuildRequirements()),
Nil,
None
))
})
case v: Inputs.VirtualJavaFile =>
val content = new String(v.content, StandardCharsets.UTF_8)
val s = PreprocessedSource.InMemory(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
package scala.build.preprocessing

import com.virtuslab.using_directives.config.Settings
import com.virtuslab.using_directives.custom.model.{
UsingDirectiveKind,
UsingDirectiveSyntax,
UsingDirectives
}
import com.virtuslab.using_directives.custom.utils.ast.{UsingDef, UsingDefs}
import com.virtuslab.using_directives.{Context, UsingDirectivesProcessor}
import com.virtuslab.using_directives.custom.model.UsingDirectiveKind
import dependency.AnyDependency
import dependency.parser.DependencyParser

import java.nio.charset.StandardCharsets

import scala.build.EitherCps.{either, value}
import scala.build.Ops._
import scala.build.errors._
import scala.build.internal.{AmmUtil, Util}
import scala.build.options.{BuildOptions, BuildRequirements, ClassPathOptions, ShadowingSeq}
import scala.build.preprocessing.directives._
import scala.build.{Inputs, Logger, Position, Positioned}
import scala.collection.mutable
import scala.jdk.CollectionConverters._

case object ScalaPreprocessor extends Preprocessor {

Expand Down Expand Up @@ -189,6 +179,7 @@ case object ScalaPreprocessor extends Preprocessor {
): Either[BuildException, Option[SpecialImportsProcessingOutput]] = either {

import fastparse._

import scala.build.internal.ScalaParse._

val res = parse(content, Header(_))
Expand Down Expand Up @@ -274,10 +265,9 @@ case object ScalaPreprocessor extends Preprocessor {
cwd: ScopePath,
logger: Logger
): Either[BuildException, StrictDirectivesProcessingOutput] = either {

val contentChars = content.toCharArray
val ExtractedDirectives(codeOffset, directives0) =
value(extractUsingDirectives(contentChars, path, logger))
value(ExtractedDirectives.from(contentChars, path, logger, UsingDirectiveKind.values(), cwd))

val updatedOptions = value {
DirectivesProcessor.process(
Expand Down Expand Up @@ -347,103 +337,9 @@ case object ScalaPreprocessor extends Preprocessor {
new UnusedDirectiveError(directive.key, values.map(_._1.value), values.flatMap(_._1.positions))
}

case class ExtractedDirectives(offset: Int, directives: Seq[StrictDirective])

val changeToSpecialCommentMsg =
"Using directive using plain comments are deprecated. Please use a special comment syntax: '//> ...' or '/*> ... */'"

def extractUsingDirectives(
contentChars: Array[Char],
path: Either[String, os.Path],
logger: Logger
): Either[BuildException, ExtractedDirectives] = {
val errors = new mutable.ListBuffer[Diagnostic]
val reporter = CustomDirectivesReporter.create(path) { diag =>
if (diag.severity == Severity.Warning)
logger.log(Seq(diag))
else
errors += diag
}
val processor = {
val settings = new Settings
settings.setAllowStartWithoutAt(true)
settings.setAllowRequire(false)
val context = new Context(reporter, settings)
new UsingDirectivesProcessor(context)
}
val all = processor.extract(contentChars, true, true).asScala
if (errors.isEmpty) {

def byKind(kind: UsingDirectiveKind) = all.find(_.getKind == kind).get

def getDirectives(directives: UsingDirectives) =
directives.getAst() match {
case ud: UsingDefs =>
ud.getUsingDefs().asScala.toSeq
case _ =>
Nil
}

val codeDirectives = byKind(UsingDirectiveKind.Code)
val specialCommentDirectives = byKind(UsingDirectiveKind.SpecialComment)
val plainCommentDirectives = byKind(UsingDirectiveKind.PlainComment)

def reportWarning(msg: String, values: Seq[UsingDef], before: Boolean = true): Unit =
values.foreach { v =>
val astPos = v.getPosition()
val (start, end) =
if (before) (0, astPos.getColumn())
else (astPos.getColumn(), astPos.getColumn() + v.getSyntax.getKeyword.size)
val position = Position.File(path, (astPos.getLine(), start), (astPos.getLine(), end))
logger.diagnostic(msg, positions = Seq(position))
}

val usedDirectives =
if (!codeDirectives.getFlattenedMap().isEmpty()) {
val msg =
"This using directive is ignored. File contains directives outside comments and those have higher precedence."
reportWarning(
msg,
getDirectives(plainCommentDirectives) ++ getDirectives(specialCommentDirectives)
)
codeDirectives
}
else if (!specialCommentDirectives.getFlattenedMap().isEmpty()) {
val msg =
s"This using directive is ignored. $changeToSpecialCommentMsg"
reportWarning(msg, getDirectives(plainCommentDirectives))
specialCommentDirectives
}
else {
reportWarning(changeToSpecialCommentMsg, getDirectives(plainCommentDirectives))
plainCommentDirectives
}

// All using directives should use just `using` keyword, no @using or require
reportWarning(
"Deprecated using directive syntax, please use keyword `using`.",
getDirectives(usedDirectives).filter(_.getSyntax() != UsingDirectiveSyntax.Using),
before = false
)

val flattened = usedDirectives.getFlattenedMap.asScala.toSeq
val strictDirectives =
flattened.map {
case (k, l) =>
StrictDirective(k.getPath.asScala.mkString("."), l.asScala.toSeq)
}

val offset =
if (usedDirectives.getKind() != UsingDirectiveKind.Code) 0
else usedDirectives.getCodeOffset()
Right(ExtractedDirectives(offset, strictDirectives))
}
else {
val errors0 = errors.map(diag => new MalformedDirectiveError(diag.message, diag.positions))
Left(CompositeBuildException(errors0.toSeq))
}
}

private def parseDependency(str: String, pos: Position): Either[BuildException, AnyDependency] =
DependencyParser.parse(str) match {
case Left(msg) => Left(new DependencyFormatError(str, msg, positionOpt = Some(pos)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ case object RequireScalaVersionDirectiveHandler extends RequireDirectiveHandler
Right(Some(req))
case _ =>
// TODO: Handle errors and conflicts
Left(new DirectiveErrors(::("Match error in ScalaVersionDirectiveHandler", Nil)))
Left(new DirectiveErrors(::("Match error in ScalaVersionDirectiveHandler", Nil), Seq.empty))
}

def handleValues(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ case object RequireScopeDirectiveHandler extends RequireDirectiveHandler {
scope = Some(BuildRequirements.ScopeRequirement(scope))
)
Right(Some(req))
case _ => Left(new DirectiveErrors(::("No such scope", Nil)))
case _ => Left(new DirectiveErrors(::("No such scope", Nil), Seq.empty))
}

val scoped = values
Expand All @@ -54,7 +54,7 @@ case object RequireScopeDirectiveHandler extends RequireDirectiveHandler {
)
)
Right(req)
case (_, Some(_)) => Left(new DirectiveErrors(::("No such scope", Nil)))
case (_, Some(_)) => Left(new DirectiveErrors(::("No such scope", Nil), Seq.empty))
}
.toSeq
.sequence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package scala.build
package tests

import com.eed3si9n.expecty.Expecty.{assert => expect}
import com.virtuslab.using_directives.custom.model.UsingDirectiveKind

import scala.build.Logger
import scala.build.errors.Diagnostic
import scala.build.preprocessing.ScalaPreprocessor
import scala.build.preprocessing.{ExtractedDirectives, ScopePath}

class ScalaPreprocessorTests extends munit.FunSuite {

Expand Down Expand Up @@ -40,8 +40,7 @@ class ScalaPreprocessorTests extends munit.FunSuite {
private def testWarnings(lines: String*)(expectedWarnings: Check*): Unit = {
val persistentLogger = new PersistentDiagnosticLogger(Logger.nop)
val code = lines.mkString("\n").toCharArray()
val res =
ScalaPreprocessor.extractUsingDirectives(code, Right(path), persistentLogger)
val res = ExtractedDirectives.from(code, Right(path), persistentLogger, UsingDirectiveKind.values(), ScopePath.fromPath(path))
expect(res.isRight)

val diags = persistentLogger.diagnostics
Expand Down
Loading