Skip to content

Commit

Permalink
Start threading the source position ...
Browse files Browse the repository at this point in the history
... from macros to compiler-plugin via hints. Only for interfaces and
operations for now
  • Loading branch information
Baccata committed May 10, 2024
1 parent 4cd1500 commit 29e57aa
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
package smithy4s.deriving.compiler

import dotty.tools.backend.jvm.GenBCode
import dotty.tools.dotc.CompilationUnit
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.report
import dotty.tools.dotc.plugins.PluginPhase
import dotty.tools.dotc.plugins.StandardPlugin
import dotty.tools.backend.jvm.GenBCode
import scala.jdk.CollectionConverters._
import dotty.tools.dotc.report
import dotty.tools.dotc.util.NoSourcePosition
import dotty.tools.dotc.util.Spans
import io.github.classgraph.ClassGraph
import io.github.classgraph.ClassRefTypeSignature
import smithy4s.Document
import smithy4s.deriving.internals.SourcePosition
import smithy4s.dynamic.DynamicSchemaIndex
import smithy4s.dynamic.NodeToDocument
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ModelSerializer
import software.amazon.smithy.model.shapes.ShapeId as SmithyShapeId
import software.amazon.smithy.model.validation.Severity
import software.amazon.smithy.model.validation.ValidationEvent

import java.net.URLClassLoader
import java.util.Optional
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.*
import scala.util.control.NonFatal
import dotty.tools.dotc.CompilationUnit
import io.github.classgraph.ClassRefTypeSignature
import software.amazon.smithy.model.validation.ValidationEvent
import software.amazon.smithy.model.validation.Severity

class Smithy4sDerivingCompiler extends StandardPlugin {
val name: String = "smithy4s-deriving-compiler"
Expand Down Expand Up @@ -91,21 +100,21 @@ class Smithy4sDerivingCompilerPhase() extends PluginPhase {

val unvalidatedModel = builder.build().toSmithyModel
val node = ModelSerializer.builder().build().serialize(unvalidatedModel)
val events = Model
val assemblyResult = Model
.assembler(this.getClass().getClassLoader())
.discoverModels(this.getClass().getClassLoader())
.addDocumentNode(node)
.assemble()
.getValidationEvents()
.asScala
events.foreach(reportEvent)

val events = assemblyResult.getValidationEvents().asScala
events.foreach(reportEvent(unvalidatedModel))
} finally {
scanResult.close()
}
result
}

private def reportEvent(event: ValidationEvent)(using Context): Unit = {
private def reportEvent(model: Model)(event: ValidationEvent)(using context: Context): Unit = {
var message = event.getMessage()

val reason = event.getSuppressionReason().orElse(null)
Expand All @@ -118,13 +127,33 @@ class Smithy4sDerivingCompilerPhase() extends PluginPhase {
event.getShapeId().map(_.toString).orElse("-"),
message,
event.getId()
);
)

val SourcePositionId = SmithyShapeId.fromParts(SourcePosition.id.namespace, SourcePosition.id.name)
val sourcePositionDecoder = Document.Decoder.fromSchema(SourcePosition.schema)

val maybeSourcePos = event
.getShapeId()
.flatMap(model.getShape)
.flatMap(sourcePos => Optional.ofNullable(sourcePos.getAllTraits().get(SourcePositionId)))
.map(_.toNode())
.map(NodeToDocument(_))
.flatMap(sourcePositionDecoder.decode(_).toOption.toJava)
.toScala

val scalaPosition = maybeSourcePos match {
case None => NoSourcePosition
case Some(pos) =>
val sourceFile = context.getSource(pos.path)
dotty.tools.dotc.util.SourcePosition(sourceFile, Spans.Span(pos.start, pos.end))
}

event.getSeverity() match
case Severity.SUPPRESSED => report.inform(formatted)
case Severity.NOTE => report.inform(formatted)
case Severity.WARNING => report.warning(formatted)
case Severity.DANGER => report.error(formatted)
case Severity.ERROR => report.error(formatted)
case Severity.SUPPRESSED => report.inform(formatted, scalaPosition)
case Severity.NOTE => report.inform(formatted, scalaPosition)
case Severity.WARNING => report.warning(formatted, scalaPosition)
case Severity.DANGER => report.error(formatted, scalaPosition)
case Severity.ERROR => report.error(formatted, scalaPosition)
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
$version: "2"

namespace smithy4s.deriving.internals

@trait()
structure SourcePosition {
@required
path: String
@required
start: Integer
@required
startLine: Integer
@required
startColumn: Integer
@required
end: Integer
@required
endLine: Integer
@required
endColumn: Integer
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
internals.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package smithy4s.deriving.internals

import smithy4s.deriving.{*, given}
import smithy4s.schema.Schema
import smithy4s.ShapeTag
import smithy4s.ShapeId

case class SourcePosition(
path: String,
start: Int,
startLine: Int,
startColumn: Int,
end: Int,
endLine: Int,
endColumn: Int
) derives Schema

object SourcePosition extends ShapeTag.Companion[SourcePosition] {
val id: ShapeId = ShapeId("smithy4s.deriving.internals", "SourcePosition")
def schema: Schema[SourcePosition] = derived$Schema
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,13 @@ def derivedAPIImpl[T: Type, F[_]: Type](
val tpe = TypeRepr.of[T]
val cls = tpe.classSymbol
val serviceDocs: Option[String] = cls.flatMap(_.docstring).map(Docs.parse).map(_.main)
val serviceHints = maybeAddDocs(hintsForType[T], serviceDocs)
val serviceHints = maybeAddDocs(hintsForType[T], serviceDocs).maybeAddPos(cls)
val methodDocs = cls.toList.flatMap(_.declarations.flatMap {
sym => sym.docstring.map(docs => sym.name -> Docs.parse(docs))
}).toMap
val methodSymbols = cls.toList.flatMap(_.declarations.map {
sym => sym.name -> sym
}).toMap

mirror match {
case '{
Expand All @@ -151,7 +154,7 @@ def derivedAPIImpl[T: Type, F[_]: Type](
} =>
val serviceNamespace = stringFromSingleton[ns]
val serviceName = stringFromSingleton[label]
val opSchemas = operationSchemasExpression[operations, operationLabels, F](serviceNamespace, serviceName, methodDocs)
val opSchemas = operationSchemasExpression[operations, operationLabels, F](serviceNamespace, serviceName, methodDocs, methodSymbols)
'{
new DynamicAPI[T] {
type Effect[I, E, O, SI, SO] = F[O]
Expand Down Expand Up @@ -349,6 +352,27 @@ private def maybeAddDocs(expr: Expr[Hints], docs: Option[String], member: Boolea
}
}

extension(expr: Expr[Hints]){
private[internals] def maybeAddPos(using Quotes)(symbol: Option[quotes.reflect.Symbol]) : Expr[Hints] = {
symbol.flatMap(_.pos) match {
case None => expr
case Some(pos) =>
val sourceLoc = '{
SourcePosition(
path = ${Expr(pos.sourceFile.path)},
start = ${Expr(pos.start)},
startLine = ${Expr(pos.startLine)},
startColumn = ${Expr(pos.startColumn)},
end = ${Expr(pos.end)},
endLine = ${Expr(pos.endLine)},
endColumn = ${Expr(pos.endColumn)}
) : Hints.Binding
}
'{$expr.add($sourceLoc)}
}
}
}

private def fieldHintsMap[T: Type](docs: Map[String, String])(using
Quotes
): Map[String, Expr[Hints]] = {
Expand All @@ -375,11 +399,12 @@ private def fieldHintsMap[T: Type](docs: Map[String, String])(using
.toMap
}

private def operationSchemasExpression[Ts: Type, OpLabels: Type, F[_]: Type](
private def operationSchemasExpression[Ts: Type, OpLabels: Type, F[_]: Type](using Quotes)(
serviceNamespace: String,
serviceName: String,
methodDocs: Map[String, Docs]
)(using Quotes): Expr[List[OperationSchema[?, ?, ?, ?, ?]]] = {
methodDocs: Map[String, Docs],
methodSymbols: Map[String, quotes.reflect.Symbol]
): Expr[List[OperationSchema[?, ?, ?, ?, ?]]] = {
val expressionList = typesFromTuple[Ts]
.zip(stringsFromTupleOfSingletons[OpLabels])
.map { case ('[op], opName) =>
Expand All @@ -397,7 +422,8 @@ private def operationSchemasExpression[Ts: Type, OpLabels: Type, F[_]: Type](
val opAnnotations = extractAnnotationFromType[annotations]
val opDocs = methodDocs.get(opName).map(_.main)
val outputDocs = Expr(methodDocs.get(opName).flatMap(_.output))
val opHints = maybeAddDocs(operationHints(opAnnotations), opDocs)
val methodSymbol = methodSymbols.get(opName)
val opHints = maybeAddDocs(operationHints(opAnnotations), opDocs).maybeAddPos(methodSymbol)
val paramDocs = methodDocs.get(opName).map(_.params).getOrElse(Map.empty)
val paramAnnotations = extractAnnotationsFromTuple[inputAnnotations]
val fieldHints = paramHintsMap(paramAnnotations, labels, paramDocs)
Expand Down
2 changes: 1 addition & 1 deletion modules/examples/shared/src/main/scala/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ case class LocationNotRecognised(errorMessage: String) extends Throwable derives
class HelloWorldService() derives API {

@errors[LocationNotRecognised]
@hints(Http(method = "GET", uri = "/hello/{name}"))
@hints(Http(method = "GET", uri = "/hello/{namea}"))
def hello(
@hints(HttpLabel()) name: String,
@hints(HttpQuery("from")) from: Option[String]
Expand Down
23 changes: 9 additions & 14 deletions modules/tests/jvm/src/test/scala/utils.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ trait APISuite extends FunSuite {
val api = API[A]
val unvalidated = DynamicSchemaIndex.builder.addAll(Service[api.Free]).build().toSmithyModel
val node = ModelSerializer.builder().build().serialize(unvalidated)
val validated = Model.assembler().addDocumentNode(node).assemble().unwrap()
val validated = Model.assembler().discoverModels().addDocumentNode(node).assemble().unwrap()
val filtered = ModelTransformer
.create()
.filterShapes(validated, _.getId().getNamespace() != "smithy4s.deriving.internals")
val expectedAssembler = Model.assembler()
modelStrings.zipWithIndex.foreach { case (string, index) =>
expectedAssembler.addUnparsedModel(s"expected$index.smithy", string)
}
val expected = expectedAssembler.assemble().unwrap()
assertEquals(ModelWrapper(validated), ModelWrapper(expected))
assertEquals(ModelWrapper(filtered), ModelWrapper(expected))
}
}

Expand All @@ -77,8 +80,8 @@ class ModelWrapper(val model: Model) {

override def equals(obj: Any): Boolean = obj match {
case wrapper: ModelWrapper =>
val one = reorderMetadata(reorderFields(model))
val two = reorderMetadata(reorderFields(wrapper.model))
val one = reorderMetadata(model)
val two = reorderMetadata(wrapper.model)
val diff = ModelDiff
.builder()
.oldModel(one)
Expand Down Expand Up @@ -147,15 +150,6 @@ class ModelWrapper(val model: Model) {
builder.build()
}

private val reorderFields: Model => Model = m => {
val structures = m.getStructureShapes().asScala.map { structShape =>
val sortedMembers =
structShape.members().asScala.toList.sortBy(_.getMemberName())
structShape.toBuilder().members(sortedMembers.asJava).build()
}
m.toBuilder().addShapes(structures.asJava).build()
}

private def update(model: Model): Model = {
val filterSuppressions: Model => Model = m =>
new FilterSuppressions().transform(
Expand All @@ -167,12 +161,13 @@ class ModelWrapper(val model: Model) {
)
.build()
)
(filterSuppressions andThen reorderFields)(model)
(filterSuppressions)(model)
}

override def toString() =
SmithyIdlModelSerializer
.builder()
.metadataFilter(_ => false)
.build()
.serialize(update(model))
.asScala
Expand Down

0 comments on commit 29e57aa

Please sign in to comment.