diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.sourcegen-testsuite.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.sourcegen-testsuite.gradle index 1b6b294d..18d26574 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.sourcegen-testsuite.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.sourcegen-testsuite.gradle @@ -6,3 +6,7 @@ plugins { tasks.withType(Test).configureEach { useJUnitPlatform() } + +tasks.withType(Checkstyle).configureEach { + enabled = false +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e813b4db..7c74a9e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,28 +1,11 @@ -# -# This file is used to declare the list of libraries -# which are used as dependencies in the project. -# See https://docs.gradle.org/7.4.2/userguide/platforms.html#sub:central-declaration-of-dependencies -# -# For Micronaut, we have 3 kinds of dependencies: -# - managed dependencies, which are exposed to consumers via a BOM (or version catalog) -# - managed BOMs, which are imported into the BOM that we generate -# - all other dependencies, which are implementation details -# -# If a library needs to appear in the BOM of the project, then it must be -# declared with the "managed-" prefix. -# If a BOM needs to be imported in the BOM of the project, then it must be -# declared with the "boms-" prefix. -# Both managed dependencies and BOMs need to have their version declared via -# a managed version (a version which alias starts with "managed-" - [versions] -micronaut = "4.0.3-SNAPSHOT" +micronaut = "4.2.0-SNAPSHOT" micronaut-docs = "2.0.0" micronaut-test = "3.9.1" groovy = "4.0.12" spock = "2.3-groovy-4.0" -javapoet = "1.13.0" -kotlinpoet = "1.14.2" +managed-javapoet = "1.13.0" +managed-kotlinpoet = "1.14.2" # Managed versions appear in the BOM # managed-somelib = "1.0" @@ -31,24 +14,9 @@ kotlinpoet = "1.14.2" [libraries] # Core micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' } -javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" } -kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } -kotlinpoet-javapoet = { module = "com.squareup:kotlinpoet-javapoet", version.ref = "kotlinpoet" } - -# -# Managed dependencies appear in the BOM -# -# managed-somelib = { module = "group:artifact", version.ref = "managed-somelib" } - -# -# Imported BOMs, also appearing in the generated BOM -# -# boms-somebom = { module = "com.foo:somebom", version.ref = "managed-somebom" } - -# Other libraries used by the project but non managed - -# micronaut-bom = { module = "io.micronaut:micronaut-bom", version.ref = "micronaut" } -# jdoctor = { module = "me.champeau.jdoctor:jdoctor-core", version.ref="jdoctor" } +managed-javapoet = { module = "com.squareup:javapoet", version.ref = "managed-javapoet" } +managed-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "managed-kotlinpoet" } +managed-kotlinpoet-javapoet = { module = "com.squareup:kotlinpoet-javapoet", version.ref = "managed-kotlinpoet" } [bundles] diff --git a/settings.gradle.kts b/settings.gradle.kts index 238ec1d2..5a776675 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,9 +12,14 @@ plugins { rootProject.name = "sourcegen-parent" include("sourcegen-annotations") -include("sourcegen-core") +include("sourcegen-model") +include("sourcegen-generator") +include("sourcegen-generator-java") +include("sourcegen-generator-kotlin") include("sourcegen-bom") + include("test-suite-java") +//include("test-suite-groovy") TODO include("test-suite-kotlin") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/Builder.java b/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java similarity index 77% rename from sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/Builder.java rename to sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java index 23fb842d..625bbe7d 100644 --- a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/Builder.java +++ b/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/Builder.java @@ -1,11 +1,11 @@ /* - * Copyright 2003-2021 the original author or authors. + * Copyright 2017-2021 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.sourcegen.ann; +package io.micronaut.sourcegen.annotations; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -22,6 +22,12 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; +/** + * The builder annotation on a bean should create a builder. + * + * @author Denis Stepanov + * @since 1.0 + */ @Documented @Retention(RUNTIME) @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) diff --git a/sourcegen-core/build.gradle.kts b/sourcegen-core/build.gradle.kts index c799a7c2..0b62f41e 100644 --- a/sourcegen-core/build.gradle.kts +++ b/sourcegen-core/build.gradle.kts @@ -3,10 +3,10 @@ plugins { } dependencies { - compileOnly(mn.micronaut.inject.java) - implementation(projects.sourcegenAnnotations) - implementation(libs.javapoet) - implementation(libs.kotlinpoet) - implementation(libs.kotlinpoet.javapoet) - testAnnotationProcessor(project) +// compileOnly(mn.micronaut.inject.java) +// implementation(projects.sourcegenAnnotations) +// implementation(libs.managed.javapoet) +// implementation(libs.managed.kotlinpoet) +// implementation(libs.managed.kotlinpoet.javapoet) +// testAnnotationProcessor(project) } diff --git a/sourcegen-core/src/main/java/io/micronaut/sourcegen/visitors/JavaBuilderTypeElementVisitor.java b/sourcegen-core/src/main/java/io/micronaut/sourcegen/visitors/JavaBuilderTypeElementVisitor.java deleted file mode 100644 index 487f7d8c..00000000 --- a/sourcegen-core/src/main/java/io/micronaut/sourcegen/visitors/JavaBuilderTypeElementVisitor.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2003-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.sourcegen.visitors; - -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.FieldSpec; -import com.squareup.javapoet.JavaFile; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.ParameterSpec; -import com.squareup.javapoet.TypeSpec; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.Element; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.PropertyElement; -import io.micronaut.inject.visitor.TypeElementVisitor; -import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.inject.writer.GeneratedSourceFile; -import io.micronaut.sourcegen.ann.Builder; - -import javax.lang.model.element.Modifier; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class JavaBuilderTypeElementVisitor implements TypeElementVisitor { - private final Map typeBuilders = new HashMap<>(); - - private TypeSpec.Builder findBuilderFor(ClassElement element) { - var simpleName = element.getSimpleName() + "Builder"; - var builderName = element.getPackageName() + "." + simpleName; - return typeBuilders.computeIfAbsent(builderName, name -> TypeSpec.classBuilder(simpleName).addModifiers(Modifier.PUBLIC)); - } - - private List orderedPropertyNames(ClassElement element) { - return element.getBeanProperties() - .stream() - .map(Element::getSimpleName) - .toList(); - } - - @Override - public void visitClass(ClassElement element, VisitorContext context) { - var builderTypeName = element.getSimpleName() + "Builder"; - var builder = findBuilderFor(element); - var builderClassName = ClassName.get(element.getPackageName(), builderTypeName); - builder.addMethod(MethodSpec.methodBuilder("builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(builderClassName) - .addStatement("return new $T(" + String.join(", ", defaultValuesFor(element.getBeanProperties())) + ")", builderClassName) - .build()); - for (PropertyElement beanProperty : element.getBeanProperties()) { - var propertyType = beanProperty.getType(); - var propertyName = beanProperty.getSimpleName(); - var propertyTypeName = ClassName.get(propertyType.getPackageName(), propertyType.getSimpleName()); - builder.addField(FieldSpec.builder( - propertyTypeName, - propertyName, - Modifier.PRIVATE, Modifier.FINAL - ).build()); - builder.addMethod(MethodSpec.methodBuilder(propertyName) - .addModifiers(Modifier.PUBLIC) - .addParameter(propertyTypeName, propertyName) - .returns(builderClassName) - .addStatement("return new " + builderTypeName + "(" + String.join(", ", orderedPropertyNames(element)) + ")") - .build()); - } - } - - private List defaultValuesFor(List beanProperties) { - return beanProperties.stream() - .map(p -> { - if (p.isPrimitive()) { - return "0"; - } - return "null"; - }).toList(); - } - - @Override - public void visitConstructor(ConstructorElement element, VisitorContext context) { - if (element.isPublic()) { - var owningType = element.getOwningType(); - var builder = findBuilderFor(owningType); - var args = Arrays.stream(element.getParameters()) - .map(ParameterElement::getSimpleName) - .collect(Collectors.joining(",")); - var buildMethodBuilder = MethodSpec.methodBuilder("build") - .addModifiers(Modifier.PUBLIC) - .returns(ClassName.get(owningType.getPackageName(), owningType.getSimpleName())); - buildMethodBuilder.addStatement( - "return new " + owningType.getSimpleName() + "(" + - args + - ")" - ); - var ctorBuilder = MethodSpec.constructorBuilder() - .addModifiers(Modifier.PRIVATE) - .addParameters( - Arrays.stream(element.getParameters()) - .map(p -> ParameterSpec.builder( - ClassName.get(p.getType().getPackageName(), p.getType().getSimpleName()), - p.getName() - ).build()) - .toList() - ); - for (String propertyName : orderedPropertyNames(owningType)) { - ctorBuilder.addStatement("this.$L=$L", propertyName, propertyName); - } - builder.addMethod(ctorBuilder.build()); - builder.addMethod(buildMethodBuilder.build()); - } - } - - @Override - public void finish(VisitorContext visitorContext) { - for (Map.Entry entry : typeBuilders.entrySet()) { - var fqn = entry.getKey(); - var typeSpecBuilder = entry.getValue(); - var packageName = fqn.substring(0, fqn.lastIndexOf(".")); - var fileName = fqn.substring(fqn.lastIndexOf(".") + 1); - var generatedSourceFile = visitorContext.visitGeneratedSourceFile(packageName, fileName); - generatedSourceFile.ifPresent(generatedFile -> { - var javaFile = JavaFile.builder(packageName, typeSpecBuilder.build()).build(); - try { - generatedFile.visitLanguage(GeneratedSourceFile.Language.JAVA, javaFile::writeTo); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - typeBuilders.clear(); - } - -} diff --git a/sourcegen-core/src/main/java/io/micronaut/sourcegen/visitors/KotlinBuilderTypeElementVisitor.java b/sourcegen-core/src/main/java/io/micronaut/sourcegen/visitors/KotlinBuilderTypeElementVisitor.java deleted file mode 100644 index 78459880..00000000 --- a/sourcegen-core/src/main/java/io/micronaut/sourcegen/visitors/KotlinBuilderTypeElementVisitor.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2003-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.sourcegen.visitors; - -import com.squareup.kotlinpoet.ClassName; -import com.squareup.kotlinpoet.FileSpec; -import com.squareup.kotlinpoet.FunSpec; -import com.squareup.kotlinpoet.KModifier; -import com.squareup.kotlinpoet.ParameterSpec; -import com.squareup.kotlinpoet.ParameterizedTypeName; -import com.squareup.kotlinpoet.PropertySpec; -import com.squareup.kotlinpoet.TypeName; -import com.squareup.kotlinpoet.TypeSpec; -import com.squareup.kotlinpoet.javapoet.J2kInteropKt; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.PropertyElement; -import io.micronaut.inject.ast.TypedElement; -import io.micronaut.inject.visitor.TypeElementVisitor; -import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.inject.writer.GeneratedSourceFile; -import io.micronaut.sourcegen.ann.Builder; -import kotlin.reflect.KClass; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -public class KotlinBuilderTypeElementVisitor implements TypeElementVisitor { - private final Map typeBuilders = new HashMap<>(); - - private TypeSpec.Builder findBuilderFor(ClassElement element) { - var simpleName = element.getSimpleName() + "Builder"; - var builderName = element.getPackageName() + "." + simpleName; - var info = new BuilderInfo(element.getCanonicalName(), builderName); - return typeBuilders.computeIfAbsent(info, name -> - TypeSpec.classBuilder(simpleName).addModifiers(KModifier.DATA) - ); - } - - private static TypeName kType(String pkg, String name, boolean nullable) { - if ("".equals(pkg)) { - TypeName typeName = switch (name) { - case "void" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.VOID); - case "byte" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.BYTE); - case "short" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.SHORT); - case "char" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.CHAR); - case "int" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.INT); - case "long" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.LONG); - case "float" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.FLOAT); - case "double" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.DOUBLE); - case "boolean" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.BOOLEAN); - default -> null; - }; - if (typeName != null) { - if (nullable) { - typeName = asNullable(typeName); - } - return typeName; - } - } - var kClassName = J2kInteropKt.toKClassName(com.squareup.javapoet.ClassName.get(pkg, name)); - if (nullable) { - kClassName = (ClassName) asNullable(kClassName); - } - return kClassName; - } - - private static TypeName asNullable(TypeName kClassName) { - return kClassName.copy(true, kClassName.getAnnotations(), kClassName.getTags()); - } - - @Override - public void visitClass(ClassElement element, VisitorContext context) { - var builderTypeName = element.getSimpleName() + "Builder"; - var builder = findBuilderFor(element); - var builderClassName = kType(element.getPackageName(), builderTypeName, false); - for (PropertyElement beanProperty : element.getBeanProperties()) { - var propertyType = beanProperty.getType(); - var propertyName = beanProperty.getSimpleName(); - var propertyTypeName = kType(propertyType.getPackageName(), propertyType.getSimpleName(), false); - - builder.addFunction(FunSpec.builder( - propertyName - ) - .addParameter(propertyName, propertyTypeName) - .returns(builderClassName) - .addStatement("return copy(%L=%L)", propertyName, propertyName) - .build()); - } - } - - private String defaultValueFor(TypedElement e) { - if (e.isPrimitive()) { - return "0"; - } - return "null"; - } - - @Override - public void visitConstructor(ConstructorElement element, VisitorContext context) { - if (element.isPublic()) { - var owningType = element.getOwningType(); - var builder = findBuilderFor(owningType); - var args = Arrays.stream(element.getParameters()) - .map(ParameterElement::getSimpleName) - .map(s -> s + "!!") - .collect(Collectors.joining(",")); - var buildMethodBuilder = FunSpec.builder("build") - .returns(kType(owningType.getPackageName(), owningType.getSimpleName(), false)); - buildMethodBuilder.addStatement( - "return " + owningType.getSimpleName() + "(" + - args + - ")" - ); - var ctorBuilder = FunSpec.constructorBuilder() - .addParameters( - Arrays.stream(element.getParameters()) - .map(p -> ParameterSpec.builder( - p.getName(), - kType(p.getType().getPackageName(), p.getType().getSimpleName(), true) - ) - .defaultValue(defaultValueFor(p)).build()) - .toList() - ); - Arrays.stream(element.getParameters()) - .map(p -> PropertySpec.builder( - p.getName(), - kType(p.getType().getPackageName(), p.getType().getSimpleName(), true) - ).initializer(p.getName()).build()) - .forEach(builder::addProperty); - builder.primaryConstructor(ctorBuilder.build()); - builder.addFunction(buildMethodBuilder.build()); - } - } - - @Override - public void finish(VisitorContext visitorContext) { - for (Map.Entry entry : typeBuilders.entrySet()) { - var info = entry.getKey(); - var fqn = info.builderTypeName(); - var typeSpecBuilder = entry.getValue(); - var packageName = fqn.substring(0, fqn.lastIndexOf(".")); - var fileName = fqn.substring(fqn.lastIndexOf(".") + 1); - var generatedSourceFile = visitorContext.visitGeneratedSourceFile(packageName, fileName); - generatedSourceFile.ifPresent(generatedFile -> { - var sourceFile = FileSpec.builder(packageName, "${fileName}.kt") - .addType(typeSpecBuilder.build()) - .addFunction(FunSpec.builder("builder") - .receiver(ParameterizedTypeName.get(ClassName.bestGuess(KClass.class.getName()), ClassName.bestGuess(info.originalTypeName))) - .returns(ClassName.bestGuess(fqn)) - .addStatement("return %T()", ClassName.bestGuess(fqn)) - .build()) - .build(); - try { - generatedFile.visitLanguage(GeneratedSourceFile.Language.KOTLIN, sourceFile::writeTo); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - typeBuilders.clear(); - } - - private record BuilderInfo( - String originalTypeName, - String builderTypeName - ) { - - } -} diff --git a/sourcegen-generator-java/build.gradle.kts b/sourcegen-generator-java/build.gradle.kts new file mode 100644 index 00000000..ff85e7a9 --- /dev/null +++ b/sourcegen-generator-java/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("io.micronaut.build.internal.sourcegen-module") +} + +dependencies { + implementation(projects.sourcegenGenerator) + implementation(libs.managed.javapoet) +} diff --git a/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/GroovyPoetSourceGenerator.java b/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/GroovyPoetSourceGenerator.java new file mode 100644 index 00000000..e48fcfd7 --- /dev/null +++ b/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/GroovyPoetSourceGenerator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.visitor.VisitorContext; + +/** + * Reuse the Java source generator for Groovy. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Internal +public final class GroovyPoetSourceGenerator extends JavaPoetSourceGenerator { + + @Override + public VisitorContext.Language getLanguage() { + return VisitorContext.Language.GROOVY; + } +} diff --git a/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java b/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java new file mode 100644 index 00000000..83c93902 --- /dev/null +++ b/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java @@ -0,0 +1,142 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.generator.SourceGenerator; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; + +import java.io.IOException; +import java.io.Writer; +import java.util.stream.Collectors; + +/** + * The Java source generator. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Internal +public sealed class JavaPoetSourceGenerator implements SourceGenerator permits GroovyPoetSourceGenerator { + + @Override + public VisitorContext.Language getLanguage() { + return VisitorContext.Language.JAVA; + } + + @Override + public void write(ClassDef classDef, Writer writer) throws IOException { + TypeSpec.Builder classBuilder = TypeSpec.classBuilder(classDef.getSimpleName()); + classBuilder.addModifiers(classDef.getModifiersArray()); + for (FieldDef field : classDef.getFields()) { + classBuilder.addField( + FieldSpec.builder( + asType(field.getType()), + field.getName() + ).addModifiers(field.getModifiersArray()) + .build() + ); + } + for (MethodDef method : classDef.getMethods()) { + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(method.getName()) + .addModifiers(method.getModifiersArray()) + .returns(asType(method.getReturnType())) + .addParameters( + method.getParameters().stream() + .map(param -> ParameterSpec.builder( + asType(param.getType()), + param.getName() + ).build()) + .toList() + ); + method.getStatements().stream() + .map(st -> renderStatement(classDef, method, st)) + .forEach(methodBuilder::addStatement); + classBuilder.addMethod( + methodBuilder.build() + ); + } + JavaFile javaFile = JavaFile.builder(classDef.getPackageName(), classBuilder.build()).build(); + javaFile.writeTo(writer); + } + + private static TypeName asType(TypeDef typeDef) { + return ClassName.bestGuess(typeDef.getTypeName()); + } + + private static String renderStatement(@Nullable ClassDef classDef, MethodDef methodDef, StatementDef statementDef) { + if (statementDef instanceof StatementDef.Return aReturn) { + return "return " + renderExpression(classDef, methodDef, aReturn.expression()); + } + if (statementDef instanceof StatementDef.Assign assign) { + return renderExpression(classDef, methodDef, assign.variable()) + + " = " + + renderExpression(classDef, methodDef, assign.expression()); + } + throw new IllegalStateException("Unrecognized statement: " + statementDef); + } + + private static String renderExpression(@Nullable ClassDef classDef, MethodDef methodDef, ExpressionDef expressionDef) { + if (expressionDef instanceof ExpressionDef.NewInstance newInstance) { + return "new " + newInstance.type().getTypeName() + + "(" + newInstance.values() + .stream() + .map(exp -> renderExpression(classDef, methodDef, exp)).collect(Collectors.joining(", ")) + + ")"; + } + if (expressionDef instanceof ExpressionDef.Convert convertExpressionDef) { + return renderVariable(classDef, methodDef, convertExpressionDef.variable()); + } + if (expressionDef instanceof VariableDef variableDef) { + return renderVariable(classDef, methodDef, variableDef); + } + throw new IllegalStateException("Unrecognized expression: " + expressionDef); + } + + private static String renderVariable(@Nullable ClassDef classDef, MethodDef methodDef, VariableDef variableDef) { + if (variableDef instanceof VariableDef.MethodParameter parameterVariableDef) { + methodDef.getParameter(parameterVariableDef.name()); // Check if exists + return parameterVariableDef.name(); + } + if (variableDef instanceof VariableDef.Field field) { + classDef.getField(field.name()); // Check if exists + return renderExpression(classDef, methodDef, field.instanceVariable()) + "." + field.name(); + } + if (variableDef instanceof VariableDef.This) { + if (classDef == null) { + throw new IllegalStateException("Accessing 'this' is not available"); + } + return "this"; + } + throw new IllegalStateException("Unrecognized variable: " + variableDef); + } + +} diff --git a/sourcegen-generator-java/src/main/resources/META-INF/services/io.micronaut.sourcegen.generator.SourceGenerator b/sourcegen-generator-java/src/main/resources/META-INF/services/io.micronaut.sourcegen.generator.SourceGenerator new file mode 100644 index 00000000..a8df5829 --- /dev/null +++ b/sourcegen-generator-java/src/main/resources/META-INF/services/io.micronaut.sourcegen.generator.SourceGenerator @@ -0,0 +1,2 @@ +io.micronaut.sourcegen.JavaPoetSourceGenerator +io.micronaut.sourcegen.GroovyPoetSourceGenerator diff --git a/sourcegen-generator-kotlin/build.gradle.kts b/sourcegen-generator-kotlin/build.gradle.kts new file mode 100644 index 00000000..fd0bc0d0 --- /dev/null +++ b/sourcegen-generator-kotlin/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("io.micronaut.build.internal.sourcegen-module") +} + +dependencies { + implementation(projects.sourcegenGenerator) + implementation(libs.managed.kotlinpoet) + implementation(libs.managed.kotlinpoet.javapoet) +} diff --git a/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java b/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java new file mode 100644 index 00000000..4bce3a06 --- /dev/null +++ b/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java @@ -0,0 +1,266 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen; + +import com.squareup.kotlinpoet.FileSpec; +import com.squareup.kotlinpoet.FunSpec; +import com.squareup.kotlinpoet.KModifier; +import com.squareup.kotlinpoet.ParameterSpec; +import com.squareup.kotlinpoet.PropertySpec; +import com.squareup.kotlinpoet.TypeName; +import com.squareup.kotlinpoet.TypeSpec; +import com.squareup.kotlinpoet.javapoet.J2kInteropKt; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.generator.SourceGenerator; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; + +import javax.lang.model.element.Modifier; +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Kotlin source code generator. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Internal +public final class KotlinPoetSourceGenerator implements SourceGenerator { + + @Override + public VisitorContext.Language getLanguage() { + return VisitorContext.Language.KOTLIN; + } + + @Override + public void write(ClassDef classDef, Writer writer) throws IOException { + TypeSpec.Builder classBuilder = TypeSpec.classBuilder(classDef.getSimpleName()); + classBuilder.addModifiers(asKModifiers(classDef.getModifiers())); + TypeSpec.Builder companionBuilder = null; + for (FieldDef field : classDef.getFields()) { + Set modifiers = field.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (companionBuilder == null) { + companionBuilder = TypeSpec.companionObjectBuilder(); + } + companionBuilder.addProperty( + buildProperty(field, stripStatic(modifiers)) + ); + } else { + classBuilder.addProperty( + buildProperty(field, modifiers) + ); + } + } + for (MethodDef method : classDef.getMethods()) { + Set modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (companionBuilder == null) { + companionBuilder = TypeSpec.companionObjectBuilder(); + } + modifiers = stripStatic(modifiers); + companionBuilder.addFunction( + buildFunction(null, method, modifiers) + ); + } else { + classBuilder.addFunction( + buildFunction(classDef, method, modifiers) + ); + } + } + if (companionBuilder != null) { + classBuilder.addType(companionBuilder.build()); + } + FileSpec.builder(classDef.getPackageName(), classDef.getSimpleName() + ".kt") + .addType(classBuilder.build()) + .build() + .writeTo(writer); + } + + private static PropertySpec buildProperty(FieldDef field, Set modifiers) { + return PropertySpec.builder( + field.getName(), + asType(field.getType()), + asKModifiers(modifiers) + ) + .mutable(true) + .initializer("null").build(); + } + + private static Set stripStatic(Set modifiers) { + modifiers = new HashSet<>(modifiers); + modifiers.remove(Modifier.STATIC); + return modifiers; + } + + private static FunSpec buildFunction(ClassDef classDef, MethodDef method, Set modifiers) { + FunSpec.Builder funBuilder = FunSpec.builder(method.getName()) + .addModifiers(asKModifiers(modifiers)) + .returns(asType(method.getReturnType())) + .addParameters( + method.getParameters().stream() + .map(param -> ParameterSpec.builder( + param.getName(), + asType(param.getType()) + ).build()) + .toList() + ); + method.getStatements().stream() + .map(st -> renderStatement(classDef, method, st)) + .forEach(funBuilder::addStatement); + return funBuilder.build(); + } + + private static TypeName asType(TypeDef typeDef) { + String packageName = typeDef.getPackageName(); + String simpleName = typeDef.getSimpleName(); + if ("".equals(packageName)) { + TypeName typeName = switch (simpleName) { + case "void" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.VOID); + case "byte" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.BYTE); + case "short" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.SHORT); + case "char" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.CHAR); + case "int" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.INT); + case "long" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.LONG); + case "float" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.FLOAT); + case "double" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.DOUBLE); + case "boolean" -> J2kInteropKt.toKTypeName(com.squareup.javapoet.TypeName.BOOLEAN); + default -> null; + }; + if (typeName != null) { + if (typeDef.isNullable()) { + typeName = asNullable(typeName); + } + return typeName; + } + } + var kClassName = J2kInteropKt.toKClassName(com.squareup.javapoet.ClassName.get(packageName, simpleName)); + if (typeDef.isNullable()) { + return asNullable(kClassName); + } + return kClassName; + } + + private static TypeName asNullable(TypeName kClassName) { + return kClassName.copy(true, kClassName.getAnnotations(), kClassName.getTags()); + } + + private static List asKModifiers(Collection modifier) { + return modifier.stream().map(m -> switch (m) { + case PUBLIC -> KModifier.PUBLIC; + case PROTECTED -> KModifier.PROTECTED; + case PRIVATE -> KModifier.PRIVATE; + case ABSTRACT -> KModifier.ABSTRACT; + case SEALED -> KModifier.SEALED; + case FINAL -> KModifier.FINAL; + default -> throw new IllegalStateException("Not supported modifier: " + m); + }).toList(); + } + + private static String renderStatement(@Nullable ClassDef classDef, MethodDef methodDef, StatementDef statementDef) { + if (statementDef instanceof StatementDef.Return aReturn) { + ExpResult expResult = renderExpression(classDef, methodDef, aReturn.expression()); + return "return " + renderWithNotNullAssertion(expResult, methodDef.getReturnType()); + } + if (statementDef instanceof StatementDef.Assign assign) { + ExpResult variableExp = renderVariable(classDef, methodDef, assign.variable()); + ExpResult valueExp = renderExpression(classDef, methodDef, assign.expression()); + return variableExp.rendered + + " = " + + renderWithNotNullAssertion(valueExp, variableExp.type); + } + throw new IllegalStateException("Unrecognized statement: " + statementDef); + } + + private static ExpResult renderExpression(@Nullable ClassDef classDef, MethodDef methodDef, ExpressionDef expressionDef) { + if (expressionDef instanceof ExpressionDef.NewInstance newInstance) { + return new ExpResult( + newInstance.type().getTypeName() + + "(" + newInstance.values() + .stream() + .map(exp -> { + TypeDef result = exp.type(); + ExpResult expResult = renderExpression(classDef, methodDef, exp); + return renderWithNotNullAssertion(expResult, result); + }).collect(Collectors.joining(", ")) + + ")", + newInstance.type() + ); + } + if (expressionDef instanceof ExpressionDef.Convert convertExpressionDef) { + ExpResult expResult = renderVariable(classDef, methodDef, convertExpressionDef.variable()); + TypeDef resultType = convertExpressionDef.type(); + return new ExpResult( + renderWithNotNullAssertion(expResult, resultType), + resultType + ); + } + if (expressionDef instanceof VariableDef variableDef) { + return renderVariable(classDef, methodDef, variableDef); + } + throw new IllegalStateException("Unrecognized expression: " + expressionDef); + } + + private static ExpResult renderVariable(@Nullable ClassDef classDef, MethodDef methodDef, VariableDef variableDef) { + if (variableDef instanceof VariableDef.MethodParameter parameterVariableDef) { + methodDef.getParameter(parameterVariableDef.name()); // Check if exists + return new ExpResult(parameterVariableDef.name(), parameterVariableDef.type()); + } + if (variableDef instanceof VariableDef.Field field) { + classDef.getField(field.name()); // Check if exists + ExpResult expResult = renderVariable(classDef, methodDef, field.instanceVariable()); + String rendered = expResult.rendered(); + if (expResult.type().isNullable()) { + rendered += "!!"; + } + return new ExpResult( + rendered + "." + field.name(), + field.type() + ); + } + if (variableDef instanceof VariableDef.This aThis) { + if (classDef == null) { + throw new IllegalStateException("Accessing 'this' is not available"); + } + return new ExpResult("this", aThis.type()); + } + throw new IllegalStateException("Unrecognized variable: " + variableDef); + } + + private static String renderWithNotNullAssertion(ExpResult expResult, TypeDef result) { + String rendered = expResult.rendered(); + if (!result.isNullable() && expResult.type().isNullable()) { + rendered += "!!"; + } + return rendered; + } + + private record ExpResult(String rendered, TypeDef type) { + } +} diff --git a/sourcegen-generator-kotlin/src/main/resources/META-INF/services/io.micronaut.sourcegen.generator.SourceGenerator b/sourcegen-generator-kotlin/src/main/resources/META-INF/services/io.micronaut.sourcegen.generator.SourceGenerator new file mode 100644 index 00000000..45472597 --- /dev/null +++ b/sourcegen-generator-kotlin/src/main/resources/META-INF/services/io.micronaut.sourcegen.generator.SourceGenerator @@ -0,0 +1 @@ +io.micronaut.sourcegen.KotlinPoetSourceGenerator diff --git a/sourcegen-generator/build.gradle.kts b/sourcegen-generator/build.gradle.kts new file mode 100644 index 00000000..ec9f8208 --- /dev/null +++ b/sourcegen-generator/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("io.micronaut.build.internal.sourcegen-module") +} + +dependencies { + api(projects.sourcegenModel) + api(mn.micronaut.core.processor) + implementation(projects.sourcegenAnnotations) +} diff --git a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/SourceGenerator.java b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/SourceGenerator.java new file mode 100644 index 00000000..04d5c404 --- /dev/null +++ b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/SourceGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.generator; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.model.ClassDef; + +import java.io.IOException; +import java.io.Writer; + +/** + * Source code generator. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public interface SourceGenerator { + + /** + * @return The source language of the generator + */ + VisitorContext.Language getLanguage(); + + /** + * Write the source code. + * + * @param classDef The class definition + * @param writer The writer + * @throws IOException The IO exception + */ + void write(ClassDef classDef, Writer writer) throws IOException; + +} diff --git a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/SourceGenerators.java b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/SourceGenerators.java new file mode 100644 index 00000000..3eb21bf7 --- /dev/null +++ b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/SourceGenerators.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.generator; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.io.service.SoftServiceLoader; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.List; +import java.util.Optional; + +/** + * The source generators. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public final class SourceGenerators { + + private static List SOURCE_GENERATORS; + + private SourceGenerators() { + } + + @NonNull + public static synchronized List getAll() { + if (SOURCE_GENERATORS == null) { + SOURCE_GENERATORS = SoftServiceLoader.load(SourceGenerator.class).collectAll(); + } + System.out.println("SOURCE_GENERATORS " + SOURCE_GENERATORS); + return SOURCE_GENERATORS; + } + + @Nullable + public static Optional findByLanguage(VisitorContext.Language language) { + return getAll().stream().filter(s -> s.getLanguage() == language).findAny(); + } + +} diff --git a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java new file mode 100644 index 00000000..4a5010d4 --- /dev/null +++ b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java @@ -0,0 +1,175 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.generator.visitors; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.annotations.Builder; +import io.micronaut.sourcegen.generator.SourceGenerator; +import io.micronaut.sourcegen.generator.SourceGenerators; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ExpressionDef; +import io.micronaut.sourcegen.model.FieldDef; +import io.micronaut.sourcegen.model.MethodDef; +import io.micronaut.sourcegen.model.StatementDef; +import io.micronaut.sourcegen.model.TypeDef; +import io.micronaut.sourcegen.model.VariableDef; + +import javax.lang.model.element.Modifier; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * The visitor that is generation a builder. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Internal +public final class BuilderAnnotationVisitor implements TypeElementVisitor { + + private final List builderClasses = new ArrayList<>(); + + @Override + public @NonNull VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + String simpleName = element.getSimpleName() + "Builder"; + String builderClassName = element.getPackageName() + "." + simpleName; + + TypeDef builderType = TypeDef.of(builderClassName); + + ClassDef.ClassDefBuilder builder = ClassDef.builder(builderClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + List properties = element.getBeanProperties(); + for (PropertyElement beanProperty : properties) { + String propertyName = beanProperty.getSimpleName(); + TypeDef propertyTypeDef = TypeDef.of(beanProperty.getType()); + TypeDef fieldType = propertyTypeDef.makeNullable(); + builder.addField(FieldDef.builder(propertyName) + .ofType(fieldType) + .addModifiers(Modifier.PRIVATE) + .build() + ); + builder.addMethod( + MethodDef.builder(propertyName) + .addModifiers(Modifier.PUBLIC) + .returns(builderType) + .addParameter(propertyName, propertyTypeDef) + .addStatements(propertyBuilderMethod(builderType, fieldType, beanProperty)) + .build() + ); + } + builder.addMethod(MethodDef.builder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderType) + .addStatement(builderMethod(builderType)) + .build()); + + element.getPrimaryConstructor().ifPresent(constructor -> { + if (constructor.isPublic()) { + TypeDef buildType = TypeDef.of(element); + builder.addMethod(MethodDef.builder("build") + .addModifiers(Modifier.PUBLIC) + .returns(buildType) + .addStatement(buildMethod(builderType, buildType, constructor)) + .build() + ); + } + }); + + builderClasses.add(new ElementAndBuilder(element, builder.build())); + } + + @Override + public void finish(VisitorContext visitorContext) { + SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(visitorContext.getLanguage()).orElse(null); + if (sourceGenerator == null) { + return; + } + for (ElementAndBuilder tuple : builderClasses) { + ClassDef builderDef = tuple.builderDef(); + visitorContext.visitGeneratedSourceFile( + builderDef.getPackageName(), + builderDef.getSimpleName(), + tuple.element + ).ifPresent(sourceFile -> { + try { + sourceFile.write( + writer -> sourceGenerator.write(builderDef, writer) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + // Somehow finish is called twice + builderClasses.clear(); + } + + private List propertyBuilderMethod(TypeDef builderType, TypeDef fieldType, PropertyElement propertyElement) { + String name = propertyElement.getName(); + TypeDef type = TypeDef.of(propertyElement.getType()); + String name1 = propertyElement.getName(); + return List.of( + new StatementDef.Assign( + new VariableDef.Field(new VariableDef.This(builderType), name1, fieldType), + new VariableDef.MethodParameter(name, type) + ), + new StatementDef.Return( + new VariableDef.This(builderType) + ) + ); + } + + private StatementDef builderMethod(TypeDef builderType) { + return new StatementDef.Return( + new ExpressionDef.NewInstance(builderType, List.of()) + ); + } + + private StatementDef buildMethod(TypeDef builderType, + TypeDef buildType, + MethodElement constructorElement) { + List values = new ArrayList<>(); + for (ParameterElement parameter : constructorElement.getParameters()) { + String name = parameter.getName(); + TypeDef type = TypeDef.of(parameter.getType()).makeNullable(); + // We need to convert it for the correct type in Kotlin + values.add( + new ExpressionDef.Convert( + TypeDef.of(parameter.getType()), + new VariableDef.Field(new VariableDef.This(builderType), name, type) + ) + ); + } + return new StatementDef.Return(new ExpressionDef.NewInstance(buildType, values)); + } + + private record ElementAndBuilder(ClassElement element, ClassDef builderDef) { + } +} diff --git a/sourcegen-generator/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/sourcegen-generator/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 00000000..8b80d272 --- /dev/null +++ b/sourcegen-generator/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1 @@ +io.micronaut.sourcegen.generator.visitors.BuilderAnnotationVisitor diff --git a/sourcegen-model/build.gradle.kts b/sourcegen-model/build.gradle.kts new file mode 100644 index 00000000..de4c0b41 --- /dev/null +++ b/sourcegen-model/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("io.micronaut.build.internal.sourcegen-module") +} + +dependencies { + implementation(mn.micronaut.core.processor) +} diff --git a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/SourceCodeGen.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractAnnotatedElement.java similarity index 58% rename from sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/SourceCodeGen.java rename to sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractAnnotatedElement.java index b6005b7a..7e966a48 100644 --- a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/SourceCodeGen.java +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractAnnotatedElement.java @@ -1,11 +1,11 @@ /* - * Copyright 2003-2021 the original author or authors. + * Copyright 2017-2023 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.sourcegen.ann; +package io.micronaut.sourcegen.model; -public interface SourceCodeGen { +import io.micronaut.core.annotation.Experimental; + +/** + * The abstract annotated element. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +abstract sealed class AbstractAnnotatedElement permits AbstractElement, ParameterDef { } diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java new file mode 100644 index 00000000..71b274aa --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +import javax.lang.model.element.Modifier; +import java.util.Collections; +import java.util.Set; + +/** + * The abstract element. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +abstract sealed class AbstractElement extends AbstractAnnotatedElement permits ClassDef, FieldDef, MethodDef { + + protected final String name; + protected final Set modifiers; + + AbstractElement(String name, Set modifiers) { + this.name = name; + this.modifiers = Collections.unmodifiableSet(modifiers); + } + + public final String getName() { + return name; + } + + public final Set getModifiers() { + return modifiers; + } + + public final Modifier[] getModifiersArray() { + return modifiers.toArray(Modifier[]::new); + } +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java new file mode 100644 index 00000000..fede83ff --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +import javax.lang.model.element.Modifier; +import java.util.EnumSet; +import java.util.List; + +/** + * The abstract element builder. + * + * @param The type of this builder + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public sealed class AbstractElementBuilder permits ClassDef.ClassDefBuilder, FieldDef.FieldDefBuilder, MethodDef.MethodDefBuilder { + + protected final String name; + protected EnumSet modifiers = EnumSet.noneOf(Modifier.class); + + protected AbstractElementBuilder(String name) { + this.name = name; + } + + public final ThisType addModifiers(Modifier... modifiers) { + this.modifiers.addAll(List.of(modifiers)); + return (ThisType) this; + } + +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ClassDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ClassDef.java new file mode 100644 index 00000000..ac1a8fcb --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ClassDef.java @@ -0,0 +1,122 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.NameUtils; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * The class definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public final class ClassDef extends AbstractElement { + + private final List fields; + private final List methods; + + private ClassDef(String name, + EnumSet modifiers, + List fields, + List methods) { + super(name, modifiers); + this.fields = fields; + this.methods = methods; + } + + public static ClassDefBuilder builder(String name) { + return new ClassDefBuilder(name); + } + + public TypeDef asTypeDef() { + return TypeDef.of(this); + } + + public String getPackageName() { + return NameUtils.getPackageName(name); + } + + public String getSimpleName() { + return NameUtils.getSimpleName(name); + } + + public List getFields() { + return fields; + } + + public List getMethods() { + return methods; + } + + @Nullable + public FieldDef findField(String name) { + for (FieldDef field : fields) { + if (field.getName().equals(name)) { + return field; + } + } + return null; + } + + @NonNull + public FieldDef getField(String name) { + FieldDef field = findField(name); + if (field == null) { + throw new IllegalStateException("Class: " + name + " doesn't have a field: " + name); + } + return null; + } + + /** + * The class definition builder. + * + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + public static final class ClassDefBuilder extends AbstractElementBuilder { + + private final List fields = new ArrayList<>(); + private final List methods = new ArrayList<>(); + + private ClassDefBuilder(String name) { + super(name); + } + + public void addField(FieldDef fieldDef) { + fields.add(fieldDef); + } + + public void addMethod(MethodDef methodDef) { + methods.add(methodDef); + } + + public ClassDef build() { + return new ClassDef(name, modifiers, fields, methods); + } + + } + +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ExpressionDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ExpressionDef.java new file mode 100644 index 00000000..2d06091f --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ExpressionDef.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +import java.util.List; + +/** + * The expression definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public sealed interface ExpressionDef permits ExpressionDef.Convert, ExpressionDef.NewInstance, VariableDef { + + /** + * The type of the expression. + * + * @return The type + */ + TypeDef type(); + + /** + * The new instance expression. + * + * @param type The type + * @param values The constructor values + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record NewInstance(TypeDef type, + List values) implements ExpressionDef { + } + + /** + * The convert variable expression. (To support Kotlin's nullable -> not-null conversion) + * + * @param type The type + * @param variable The variable reference + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record Convert(TypeDef type, + VariableDef variable) implements ExpressionDef { + } + +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/FieldDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/FieldDef.java new file mode 100644 index 00000000..965c9b9c --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/FieldDef.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +import javax.lang.model.element.Modifier; +import java.util.EnumSet; + +/** + * The field definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public final class FieldDef extends AbstractElement { + + private final TypeDef type; + + private FieldDef(String name, EnumSet modifiers, TypeDef type) { + super(name, modifiers); + this.type = type; + } + + public static FieldDefBuilder builder(String name) { + return new FieldDefBuilder(name); + } + + public TypeDef getType() { + return type; + } + + /** + * The field builder definition. + * + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + public static final class FieldDefBuilder extends AbstractElementBuilder { + + private TypeDef type; + + private FieldDefBuilder(String name) { + super(name); + } + + public FieldDefBuilder ofType(TypeDef type) { + this.type = type; + return this; + } + + public FieldDef build() { + return new FieldDef(name, modifiers, type); + } + + } +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/MethodDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/MethodDef.java new file mode 100644 index 00000000..8a23224a --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/MethodDef.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +/** + * The method definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public final class MethodDef extends AbstractElement { + + private final TypeDef returnType; + private final List parameters; + private final List statements; + + MethodDef(String name, + EnumSet modifiers, + TypeDef returnType, + List parameters, + List statements) { + super(name, modifiers); + this.returnType = returnType; + this.parameters = Collections.unmodifiableList(parameters); + this.statements = statements; + } + + public TypeDef getReturnType() { + return returnType; + } + + public List getParameters() { + return parameters; + } + + public List getStatements() { + return statements; + } + + @Nullable + public ParameterDef findParameter(String name) { + for (ParameterDef parameter : parameters) { + if (parameter.getName().equals(name)) { + return parameter; + } + } + return null; + } + + @NonNull + public ParameterDef getParameter(String name) { + ParameterDef parameter = findParameter(name); + if (parameter == null) { + throw new IllegalStateException("Method: " + name + " doesn't have parameter: " + name); + } + return parameter; + } + + public static MethodDefBuilder builder(String name) { + return new MethodDefBuilder(name); + } + + /** + * The method builder definition. + * + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + public static final class MethodDefBuilder extends AbstractElementBuilder { + + private final List parameters = new ArrayList<>(); + private TypeDef returnType; + private final List statements = new ArrayList<>(); + + private MethodDefBuilder(String name) { + super(name); + } + + public MethodDefBuilder returns(TypeDef type) { + this.returnType = type; + return this; + } + + public MethodDefBuilder addParameter(String name, TypeDef type) { + parameters.add(new ParameterDef(name, type)); + return this; + } + + public MethodDefBuilder addStatement(StatementDef statement) { + statements.add(statement); + return this; + } + + public MethodDefBuilder addStatements(Collection newStatements) { + statements.addAll(newStatements); + return this; + } + + public MethodDef build() { + if (returnType == null) { + throw new IllegalStateException("Return type of method: " + name + " not specified!"); + } + return new MethodDef(name, modifiers, returnType, parameters, statements); + } + + } +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ParameterDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ParameterDef.java new file mode 100644 index 00000000..34a0b6a4 --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/ParameterDef.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +/** + * The parameter definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public final class ParameterDef extends AbstractAnnotatedElement { + + private final String name; + private final TypeDef type; + + ParameterDef(String name, TypeDef type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public TypeDef getType() { + return type; + } +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/StatementDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/StatementDef.java new file mode 100644 index 00000000..b857f424 --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/StatementDef.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +/** + * The statement definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public interface StatementDef { + + /** + * The return statement. + * + * @param expression The expression + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record Return(ExpressionDef expression) implements StatementDef { + } + + /** + * The assign statement. + * + * @param variable The variable to assign + * @param expression The expression + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record Assign(VariableDef variable, + ExpressionDef expression) implements StatementDef { + } + +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/TypeDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/TypeDef.java new file mode 100644 index 00000000..9c6c4f72 --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/TypeDef.java @@ -0,0 +1,211 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.ast.ClassElement; + +/** + * The type definition. + * Not-null by default. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public sealed interface TypeDef { + + /** + * @return The type name + */ + String getTypeName(); + + /** + * @return Is nullable type + */ + boolean isNullable(); + + /** + * @return A new nullable type + */ + TypeDef makeNullable(); + + /** + * @return The simple name + */ + default String getSimpleName() { + return NameUtils.getSimpleName(getTypeName()); + } + + /** + * @return The package name + */ + default String getPackageName() { + return NameUtils.getPackageName(getTypeName()); + } + + /** + * Create a new type definition. + * + * @param type The class + * @return type definition + */ + static TypeDef of(Class type) { + return new ClassTypeDef(type, false); + } + + /** + * Create a new type definition. + * + * @param className The class name + * @return type definition + */ + static TypeDef of(String className) { + return new ClassNameTypeDef(className, false); + } + + /** + * Create a new type definition. + * + * @param classElement The class element + * @return type definition + */ + static TypeDef of(ClassElement classElement) { + return new ClassElementTypeDef(classElement, false); + } + + /** + * Create a new type definition. + * + * @param classDef The class definition + * @return type definition + */ + static TypeDef of(ClassDef classDef) { + return new ClassDefTypeDef(classDef, false); + } + + /** + * The class type. + * + * @param type The type + * @param nullable Is nullable + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record ClassTypeDef(Class type, boolean nullable) implements TypeDef { + + @Override + public String getTypeName() { + return type.getTypeName(); + } + + @Override + public boolean isNullable() { + return nullable; + } + + @Override + public TypeDef makeNullable() { + return new ClassTypeDef(type, true); + } + } + + /** + * The class name type. + * + * @param className The class name + * @param nullable Is nullable + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record ClassNameTypeDef(String className, boolean nullable) implements TypeDef { + + @Override + public String getTypeName() { + return className; + } + + @Override + public boolean isNullable() { + return nullable; + } + + @Override + public TypeDef makeNullable() { + return new ClassNameTypeDef(className, true); + } + + } + + /** + * The class element type. + * + * @param classElement The class element + * @param nullable Is nullable + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record ClassElementTypeDef(ClassElement classElement, boolean nullable) implements TypeDef { + + @Override + public String getTypeName() { + return classElement.getName(); + } + + @Override + public boolean isNullable() { + return nullable; + } + + @Override + public TypeDef makeNullable() { + return new ClassElementTypeDef(classElement, true); + } + + } + + /** + * The class definition type. + * + * @param classDef The class definition + * @param nullable Is nullable + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record ClassDefTypeDef(ClassDef classDef, boolean nullable) implements TypeDef { + + @Override + public String getTypeName() { + return classDef.getName(); + } + + @Override + public boolean isNullable() { + return nullable; + } + + @Override + public TypeDef makeNullable() { + return new ClassDefTypeDef(classDef, true); + } + + } +} diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/VariableDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/VariableDef.java new file mode 100644 index 00000000..c3fdef33 --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/VariableDef.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +/** + * The variable definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public sealed interface VariableDef extends ExpressionDef { + + /** + * The variable of a method parameter. + * + * @param name The name + * @param type The type + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record MethodParameter(String name, TypeDef type) implements VariableDef { + } + + /** + * The variable of a field. + * + * @param instanceVariable The instance variable + * @param name The name + * @param type The type + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record Field(VariableDef instanceVariable, + String name, + TypeDef type) implements VariableDef { + } + + /** + * The variable of `this`. + * + * @param type The type + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + record This(TypeDef type) implements VariableDef { + } + +} diff --git a/sourcegen-core/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/sourcegen-model/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor similarity index 100% rename from sourcegen-core/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor rename to sourcegen-model/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor diff --git a/test-suite-groovy/build.gradle.kts b/test-suite-groovy/build.gradle.kts new file mode 100644 index 00000000..35440fc8 --- /dev/null +++ b/test-suite-groovy/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("io.micronaut.build.internal.sourcegen-testsuite") + id("groovy") +} + +dependencies { + compileOnly(mn.micronaut.inject.groovy) + compileOnly(projects.sourcegenGenerator) + compileOnly(projects.sourcegenGeneratorJava) + +// implementation(mn.micronaut.inject.groovy) +// implementation(projects.sourcegenGeneratorJava) +// implementation(projects.sourcegenGenerator) + +// annotationProcessor(mn.micronaut.inject.groovy) +// annotationProcessor(projects.sourcegenGeneratorJava) +// annotationProcessor(projects.sourcegenGenerator) + + implementation(projects.sourcegenAnnotations) + + testImplementation(mnTest.micronaut.test.junit5) + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2") +} diff --git a/test-suite-groovy/src/main/groovy/io/micronaut/sourcegen/visitors/Person.groovy b/test-suite-groovy/src/main/groovy/io/micronaut/sourcegen/visitors/Person.groovy new file mode 100644 index 00000000..fafa4c85 --- /dev/null +++ b/test-suite-groovy/src/main/groovy/io/micronaut/sourcegen/visitors/Person.groovy @@ -0,0 +1,8 @@ +package io.micronaut.sourcegen.visitors + + +import io.micronaut.sourcegen.annotations.Builder + +@Builder +record Person(Long id, String name) { +} diff --git a/test-suite-groovy/src/main/groovy/io/micronaut/sourcegen/visitors/Person1.groovy b/test-suite-groovy/src/main/groovy/io/micronaut/sourcegen/visitors/Person1.groovy new file mode 100644 index 00000000..86c1b591 --- /dev/null +++ b/test-suite-groovy/src/main/groovy/io/micronaut/sourcegen/visitors/Person1.groovy @@ -0,0 +1,10 @@ +package io.micronaut.sourcegen.visitors + + +import io.micronaut.sourcegen.annotations.Builder + +@Builder +class Person1 { + Long id + String name +} diff --git a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/SourceCodeGenerator.java b/test-suite-groovy/src/test/groovy/io/micronaut/sourcegen/visitors/PersonBuilderTest.groovy similarity index 60% rename from sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/SourceCodeGenerator.java rename to test-suite-groovy/src/test/groovy/io/micronaut/sourcegen/visitors/PersonBuilderTest.groovy index 2cf7f20f..52fb4005 100644 --- a/sourcegen-annotations/src/main/java/io/micronaut/sourcegen/ann/SourceCodeGenerator.java +++ b/test-suite-groovy/src/test/groovy/io/micronaut/sourcegen/visitors/PersonBuilderTest.groovy @@ -13,18 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.sourcegen.ann; +package io.micronaut.sourcegen.visitors -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; +import org.junit.jupiter.api.Test -import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals -@Documented -@Retention(RUNTIME) -@Target({ElementType.ANNOTATION_TYPE}) -public @interface SourceCodeGenerator { - Class value(); +class PersonBuilderTest { + @Test + void buildsPerson() { + var person = PersonBuilder.builder() + .id(123L) + .name("Cédric") + .build() + assertEquals("Cédric", person.name()) + assertEquals(123L, person.id()) + } } diff --git a/test-suite-java/build.gradle.kts b/test-suite-java/build.gradle.kts index 3d44c226..437f33e2 100644 --- a/test-suite-java/build.gradle.kts +++ b/test-suite-java/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { annotationProcessor(mn.micronaut.inject.java) - annotationProcessor(projects.sourcegenCore) + annotationProcessor(projects.sourcegenGeneratorJava) implementation(projects.sourcegenAnnotations) testImplementation(mnTest.micronaut.test.junit5) testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2") diff --git a/test-suite-java/src/main/java/io/micronaut/sourcegen/visitors/Person.java b/test-suite-java/src/main/java/io/micronaut/sourcegen/visitors/Person.java index 9a3f30b3..30c67587 100644 --- a/test-suite-java/src/main/java/io/micronaut/sourcegen/visitors/Person.java +++ b/test-suite-java/src/main/java/io/micronaut/sourcegen/visitors/Person.java @@ -1,7 +1,22 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.micronaut.sourcegen.visitors; -import io.micronaut.sourcegen.ann.Builder; +import io.micronaut.sourcegen.annotations.Builder; @Builder public record Person(Long id, String name) { diff --git a/test-suite-kotlin/build.gradle.kts b/test-suite-kotlin/build.gradle.kts index db867f89..3bcd66c7 100644 --- a/test-suite-kotlin/build.gradle.kts +++ b/test-suite-kotlin/build.gradle.kts @@ -7,7 +7,7 @@ plugins { dependencies { ksp(mn.micronaut.inject.kotlin) - ksp(projects.sourcegenCore) + ksp(projects.sourcegenGeneratorKotlin) implementation(mn.micronaut.inject.kotlin) implementation(projects.sourcegenAnnotations) testImplementation(mnTest.micronaut.test.kotest5) diff --git a/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/visitors/Person.kt b/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/visitors/Person.kt index 33cf496c..94229f1e 100644 --- a/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/visitors/Person.kt +++ b/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/visitors/Person.kt @@ -1,6 +1,21 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.micronaut.sourcegen.visitors -import io.micronaut.sourcegen.ann.Builder +import io.micronaut.sourcegen.annotations.Builder @Builder data class Person(val id: Long, val name: String) diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/visitors/PersonBuilderTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/visitors/PersonBuilderTest.kt index 4d9d6b52..a45acb81 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/visitors/PersonBuilderTest.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/visitors/PersonBuilderTest.kt @@ -1,15 +1,16 @@ package io.micronaut.sourcegen.visitors -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -class PersonBuilderTest : StringSpec({ - "should create a person" { - val person = Person::class.builder() - .id(123) +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PersonBuilderTest { + @Test + fun buildsPerson() { + val person: Person = PersonBuilder.builder() + .id(123L) .name("Cédric") .build() - person.id shouldBe 123 - person.name shouldBe "Cédric" + assertEquals("Cédric", person.name) + assertEquals(123L, person.id) } - -}) +}