Skip to content

Commit

Permalink
Adds the basis for a compiler-plugin ...
Browse files Browse the repository at this point in the history
... that validates smithy models during Scala compilation.
  • Loading branch information
Baccata committed May 8, 2024
1 parent 375f4cb commit d060e5b
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 1 deletion.
21 changes: 20 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ lazy val core = crossProject(JVMPlatform, JSPlatform)
)
)

lazy val compiler = project
.in(file("modules/compiler"))
.dependsOn(core.jvm)
.settings(
name := "smithy4s-deriving-compiler",
libraryDependencies ++= Seq(
"org.scala-lang" %% "scala3-compiler" % scalaVersion.value,
"io.github.classgraph" % "classgraph" % "4.8.172",
"com.disneystreaming.smithy4s" %% "smithy4s-dynamic" % smithy4sVersion,
"com.disneystreaming.alloy" % "alloy-core" % alloyVersion
)
)

lazy val tests = crossProject(JVMPlatform)
.in(file("modules/tests"))
.enablePlugins(NoPublishPlugin)
Expand Down Expand Up @@ -63,5 +76,11 @@ lazy val examples = crossProject(JVMPlatform, JSPlatform)
.jvmSettings(
libraryDependencies ++= Seq(
"software.amazon.smithy" % "smithy-model" % smithyVersion
)
),
autoCompilerPlugins := true,
Compile / fork := true,
Compile / scalacOptions += {
val pluginClasspath = (compiler / Compile / fullClasspathAsJars).value.map(_.data.getAbsolutePath()).mkString(":")
s"""-Xplugin:$pluginClasspath"""
}
)
1 change: 1 addition & 0 deletions modules/compiler/src/main/resources/plugin.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pluginClass=smithy4s.deriving.compiler.Smithy4sDerivingCompiler
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package smithy4s.deriving.compiler

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 io.github.classgraph.ClassGraph
import smithy4s.dynamic.DynamicSchemaIndex
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ModelSerializer
import java.net.URLClassLoader
import scala.util.control.NonFatal

class Smithy4sDerivingCompiler extends StandardPlugin {
val name: String = "smithy4s-deriving-compiler"
override val description: String = "Runs smithy linting on derived constructs"
override def init(options: List[String]): List[PluginPhase] =
List(Smithy4sDerivingCompilerPhase())
}

class Smithy4sDerivingCompilerPhase() extends PluginPhase {
override def phaseName: String = Smithy4sDerivingCompilerPhase.name
override val runsAfter = Set(GenBCode.name)
override def run(using context: Context): Unit = {
val compileClasspath = context.settings.classpath.value
val output = context.settings.outputDir.value.jpath
val urls = compileClasspath.split(":").map(new java.io.File(_).toURI().toURL())
val allUrls = urls.appended(output.toUri().toURL())
val classLoader = new URLClassLoader(allUrls, this.getClass().getClassLoader())

val scanResult = new ClassGraph()
.addClassLoader(classLoader)
.enableAllInfo()
.scan()

try {
val builder = scanResult
.getClassesImplementing("smithy4s.deriving.API")
.filter(info => !info.isAbstract())
.asMap()
.asScala
.foldLeft(DynamicSchemaIndex.builder) { case (builder, (name, info)) =>
try {
val cls = info.loadClass(true)
val clsLocation = cls.getProtectionDomain().getCodeSource().getLocation().toURI()
// checking that the class comes from the current compilation unit
if (clsLocation == output.toUri()) {
// Getting the outer class, with the assumption that it'll be the companion object
// of the class for which an API is derived
// TODO : add some more protections
val outer = info.getOuterClasses().get(0)
val givenAPIMethodInfo = outer
.getMethodInfo()
.asScala
.find { methodInfo =>
val sig = methodInfo.getTypeSignature()
methodInfo.getParameterInfo().isEmpty &&
sig != null &&
sig.getResultType().toString().startsWith("smithy4s.deriving.API")
}

val companionConstructor = outer.getConstructorInfo().get(0).loadClassAndGetConstructor()
companionConstructor.setAccessible(true)
val companion = companionConstructor.newInstance()
val givenAPIMethod = givenAPIMethodInfo.get.loadClassAndGetMethod()
val api = givenAPIMethod.invoke(companion).asInstanceOf[smithy4s.deriving.API[?]]
builder.addService[api.Free]
} else {
builder
}
} catch {
case NonFatal(e) =>
report.error(s"Error when loading ${info.getName()} ${e.getMessage()}")
e.printStackTrace()
builder
}
}

val unvalidatedModel = builder.build().toSmithyModel
val node = ModelSerializer.builder().build().serialize(unvalidatedModel)
val events = Model
.assembler(this.getClass().getClassLoader())
.discoverModels(this.getClass().getClassLoader())
.addDocumentNode(node)
.assemble()
.getValidationEvents()
.asScala
events.foreach { validationEvent =>
report.warning(validationEvent.toString())
}
} finally {
scanResult.close()
}
}
}

object Smithy4sDerivingCompilerPhase {
val name = "smithy4s-deriving-compiler-phase"
}

0 comments on commit d060e5b

Please sign in to comment.