Scala Support - #12695
Conversation
* micronaut-aws = "5.0.0-RC1"
* micronaut-build-plugins="8.0.0-RC1"
* micronaut-sql = "7.0.0-RC1"
* micronaut-test = "5.0.0-RC1"
* micronaut-validation = "5.0.0-RC2"
* micronaut-rxjava3 = "4.0.0-RC1"
* micronaut-sourcegen = "2.0.0-RC4"
* micronaut-reactor = "5.0.0-RC1"
* micronaut-kotlin = "5.0.0-RC1"
* register Kotlin/KSP plugin versions once
> Changed build.gradle:1 to register Kotlin/KSP plugin versions once at the root with apply false. That removes the “Kotlin Gradle plugin was loaded multiple times” warning for the failing task.
* Updated the affected Kotest 6 API usages
- test-suite-kotlin/src/test/kotlin/io/micronaut/docs/aop/around_reactive/TxSpec.kt:18 now uses kotlinx.coroutines.runBlocking.
- test-suite-kotlin/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt:4 and test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt:3 use Kotest’s new
io.kotest.assertions.nondeterministic.eventually package and current overload.
* remove unused import
* remove deprecated constructor
> Task :micronaut-router:checkstyleMain FAILED
[ant:checkstyle] [ERROR] /Users/sdelamo/github/micronaut-projects/micronaut-core/router/src/main/java/io/micronaut/web/router/version/RouteVersionFilter.java:67: Javadoc comment at column 48 has parse error. Details: mismatched input '(' expecting JAVADOC_INLINE_TAG_END while parsing INLINE_TAG [UnusedImports]
* Using the '.*' form of import should be avoided
* remove unused constructor
* del Deprecated const in DefaultHttpClientConfiguration
* del Deprecated const in ServiceHttpClientConfiguration
* remove unused constructor
* Adapt RouteVersionFilter tests to new constructor
| @@ -0,0 +1,488 @@ | |||
| # Scala 3 Support for Micronaut Core | |||
There was a problem hiding this comment.
One things that's not in the plan is the long term maintance plan - we cannot expect Micronaut team to know Scala, and especially about compiler plugin knowledge, so if it's merged there should be somebody responsible for this part.
As a compiler team member, and release officer I can help with opening PRs for upgrade to new minor, but I have no domain knowledge about this ecosystem, so I'd rather limit my involvement to version upgrades and critical compiler fixes. However, I'm involved in multiple different OSS areas, so my time might be really limited
There was a problem hiding this comment.
hopefully this can be low overhead, but it is great if you can be a contact if we have a blocking change we can't work out ourselves
| @@ -0,0 +1,488 @@ | |||
| # Scala 3 Support for Micronaut Core | |||
|
|
|||
There was a problem hiding this comment.
Other part is publishing and maintance of the plugin - compiler plugins have neither binary or source binary compatibility. The sources typically dont't change much, but compiler plugin is strongly coupled with compiler itself - we're not operating on abstraction over compiler, we're touching it directly, so any change compiler, but introduce source incompatibility for maintianers, or when using 2 different versions, binary issues for users.
Every compiler plugin should be published per supported Scala version using full-cross version, so we need a plan for this part as well:
- all Scala versions since 3.3 LTS?
- only latest versions in series LTS and Next series
- only LTS versions?
- a single version?
Personally I'd lean toward: latest 3.3 LTS (supported for 1 more year) and latest 3.9 LTS (first release in 6-9 weeks)
https://docs.scala-lang.org/overviews/plugins/index.html#binary-compatibility-of-compiler-plugins
Example of such an issue (an links to other known to me historical problems in the ecosystem): scalus3/scalus#225
There was a problem hiding this comment.
some good points here, it might make sense to have an external micronaut-scala project where this lives so it can have a different versioning lifecycle
| private def skipMethod(symbol: Symbol)(using Context): Boolean = | ||
| symbol == Symbols.NoSymbol || | ||
| symbol.denot.isConstructor || | ||
| hasFlag(symbol, Flags.Synthetic) || | ||
| hasFlag(symbol, Flags.Artifact) || | ||
| hasFlag(symbol, Flags.Accessor) |
There was a problem hiding this comment.
hasFlag takes a FlagSet, yet you're invoking it multiple times
| private def skipMethod(symbol: Symbol)(using Context): Boolean = | |
| symbol == Symbols.NoSymbol || | |
| symbol.denot.isConstructor || | |
| hasFlag(symbol, Flags.Synthetic) || | |
| hasFlag(symbol, Flags.Artifact) || | |
| hasFlag(symbol, Flags.Accessor) | |
| private def skipMethod(symbol: Symbol)(using Context): Boolean = | |
| symbol.denot.isConstructor || | |
| hasFlag(symbol, Flags.Synthetic | Flags.Artifact | Flags.Accessor) |
I think NoSymbol.denot returns NoDenotation which is safe to use directly to cehck for isConstructor of flags directly. It might only throw when trying to access owner
| override def initialize(options: List[String])(using Context): List[PluginPhase] = | ||
| val delegate = MicronautScalaCompilerPlugin.delegate(getClass) | ||
| val initializeMethod = delegate.getClass.getMethod("initialize", classOf[List[?]], classOf[Context]) | ||
| initializeMethod.invoke(delegate, options, summon[Context]).asInstanceOf[List[PluginPhase]] |
There was a problem hiding this comment.
I don't quite understand why reflective invocation is required to start the compiler plugin, and why we can't do it directly. It should be documented in there.
There was a problem hiding this comment.
Micronaut itself supports compiler extensions to the main processor, so I guess this is trying to allow dynamic load of these extensions, but will review if it is necessary
| arrayLiteralValues(tree) match | ||
| case Some(values) => | ||
| values | ||
| case None => | ||
| classLiteralValue(tree) match | ||
| case Some(value) => | ||
| value | ||
| case None => | ||
| nestedAnnotationValue(tree) match | ||
| case Some(value) => | ||
| value | ||
| case None => | ||
| typedConstantValue(tree) match | ||
| case Some(value) => | ||
| value | ||
| case None => | ||
| tree match | ||
| case literal: tpd.Literal => | ||
| annotationConstantValue(literal.const.value).orNull | ||
| case select: tpd.Select if isEnumConstant(select.symbol) => | ||
| select.name.toString | ||
| case ident: tpd.Ident if ident.name.toString == "_" => | ||
| null | ||
| case ident: tpd.Ident if isEnumConstant(ident.symbol) => | ||
| ident.name.toString | ||
| case _ => | ||
| renderedClassLiteralValue(tree.show).map(name => classValueData(name)).getOrElse(tree.show) |
There was a problem hiding this comment.
Using LLMs is fine for contributing, but let's keep some redability standards...
We're represeting the Scala community here against multiple JVM languages
| arrayLiteralValues(tree) match | |
| case Some(values) => | |
| values | |
| case None => | |
| classLiteralValue(tree) match | |
| case Some(value) => | |
| value | |
| case None => | |
| nestedAnnotationValue(tree) match | |
| case Some(value) => | |
| value | |
| case None => | |
| typedConstantValue(tree) match | |
| case Some(value) => | |
| value | |
| case None => | |
| tree match | |
| case literal: tpd.Literal => | |
| annotationConstantValue(literal.const.value).orNull | |
| case select: tpd.Select if isEnumConstant(select.symbol) => | |
| select.name.toString | |
| case ident: tpd.Ident if ident.name.toString == "_" => | |
| null | |
| case ident: tpd.Ident if isEnumConstant(ident.symbol) => | |
| ident.name.toString | |
| case _ => | |
| renderedClassLiteralValue(tree.show).map(name => classValueData(name)).getOrElse(tree.show) | |
| arrayLiteralValues(tree) | |
| .orElse(classLiteralValue(tree)) | |
| .orElse(nestedAnnotationValue(tree)) | |
| ... |
There was a problem hiding this comment.
that a comment that im happy to read :)
its important to keep readability something that acts as a characteristic of that framework 👍
| case constant: ConstantType => | ||
| annotationConstantValue(constant.value.value) |
There was a problem hiding this comment.
we can easily drop most the the helper method
| case constant: ConstantType => | |
| annotationConstantValue(constant.value.value) | |
| case ConstantType(constant) => | |
| constant.value match | |
| case _: Unit => None | |
| case value: AnyVal => Some(value) | |
| case value: String => ??? | |
| case _ => null |
| literal.const.value match | ||
| case value: String => value | ||
| case value: java.lang.Boolean => value | ||
| case value: java.lang.Byte => value | ||
| case value: java.lang.Short => value | ||
| case value: java.lang.Integer => value | ||
| case value: java.lang.Long => value | ||
| case value: java.lang.Float => value | ||
| case value: java.lang.Double => value | ||
| case value: java.lang.Character => value | ||
| case _ => null |
There was a problem hiding this comment.
Wouldn't that be easier to flip the check? Literal can be AnyVal | Class | String | Nul, AnyVal includes Unit
Constant has isAnyVa check so only remaining check guarding against Unit.
| private val ScalaPrimitiveNames = Map( | ||
| "scala.Boolean" -> "boolean", | ||
| "scala.Byte" -> "byte", | ||
| "scala.Char" -> "char", | ||
| "scala.Double" -> "double", | ||
| "scala.Float" -> "float", | ||
| "scala.Int" -> "int", | ||
| "scala.Long" -> "long", | ||
| "scala.Short" -> "short", | ||
| "scala.Unit" -> "void" | ||
| ) |
There was a problem hiding this comment.
generaly you should match on symbol or type, not the string representation
Summary
Adds first-class Scala 3 support to Micronaut by integrating Scala compilation with Micronaut's compile-time visitor and bean definition infrastructure.
This PR introduces the Scala compiler integration, the Scala element model used by Micronaut visitors, collection and annotation metadata support, documentation for users, and broad parity coverage for framework features exercised from Scala source.
Implementation details
Scala 3 compiler plugin integration
The implementation uses the Scala 3 compiler plugin API to observe Scala sources during compilation and feed Micronaut's compile-time processing model with Scala-native symbols, types, annotations, members, constructors, fields, methods, properties, companion structures, enum constants, and generic signatures.
The compiler plugin entry point lives under
inject-scala/src/main/scala/io/micronaut/scala/processing/MicronautScalaCompilerPlugin.scala. It bridges Scala compiler phases into Micronaut processing so existingTypeElementVisitorimplementations can operate over Scala classes in the same style as Java, Kotlin, and Groovy sources.Scala element model
The
inject-scalamodule adds a Scala-specific visitor model underio.micronaut.scala.processing.visitor, including:ScalaProcessingEngineto coordinate processing.ScalaVisitorContextfor visitor state, generated elements, diagnostics, and classpath access.ScalaClassElement,ScalaMethodElement,ScalaConstructorElement,ScalaFieldElement,ScalaParameterElement,ScalaPropertyElement,ScalaPackageElement, and enum/generic/wildcard variants.ScalaAnnotationMetadataBuilderand related data types to convert Scala compiler annotation data into Micronaut annotation metadata.This keeps the Scala implementation aligned with Micronaut's existing compile-time abstractions instead of creating a separate runtime path.
Annotation metadata and type handling
The implementation resolves Scala annotation constructor arguments, defaults, aliases, annotation mappers/remappers/transformers, class-valued members, constants, placeholders, wildcard bounds, explicit-null unions, inherited metadata, type variables, nested generics, arrays, and enum constants.
It also adds support for visitor-generated beans, visitor ordering, annotation mutations, classpath annotation lookup, service-loader expressions, package elements, member equality, and diagnostic handling at Scala compiler phase boundaries.
Bean definition, injection, AOP, and configuration support
Scala sources can now participate in core Micronaut compile-time features, including:
Provider,BeanRegistration, and collection/map injection.Optionhandling, including mutable and immutable collection binding/injection paths.Documentation and examples
The user guide now includes Scala language support documentation and Scala snippets across the docs where parity examples are available. The PR adds a Scala test suite with examples covering HTTP clients and servers, filters, routing, binding, uploads, SSE/streaming, configuration, factories, validation, events, propagation, resources, scopes, qualifiers, replacements, AOP, introspection, and other documented framework features.
The Scala docs examples are backed by tests in
test-suite-scala, and the disabled-test tracking files document remaining parity gaps and follow-up areas.Testing
The branch adds dedicated Scala compiler and visitor tests in
inject-scala-test, plus documentation parity coverage intest-suite-scala.Key coverage areas include:
Notes for reviewers
This is intentionally opened as a draft because the change set is broad and spans compiler integration, framework compile-time model support, docs, and parity tests. Review is probably easiest by starting with the Scala compiler plugin and
inject-scalavisitor model, then moving through the focused parity specs and finally the documentation/test-suite additions.