-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds the basis for a compiler-plugin ...
... that validates smithy models during Scala compilation.
- Loading branch information
Showing
3 changed files
with
122 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pluginClass=smithy4s.deriving.compiler.Smithy4sDerivingCompiler |
101 changes: 101 additions & 0 deletions
101
modules/compiler/src/main/scala/smithy4s/deriving/compiler/Smithy4sDerivingCompiler.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |