diff --git a/src/main/groovy/org/assertj/generator/gradle/tasks/AssertJGenerationTask.groovy b/src/main/groovy/org/assertj/generator/gradle/tasks/AssertJGenerationTask.groovy deleted file mode 100644 index 6dde0f5..0000000 --- a/src/main/groovy/org/assertj/generator/gradle/tasks/AssertJGenerationTask.groovy +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright 2017. assertj-generator-gradle-plugin contributors. - * - * 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 - * - * http://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 org.assertj.generator.gradle.tasks - -import com.google.common.collect.Sets -import com.google.common.reflect.TypeToken -import org.assertj.assertions.generator.AssertionsEntryPointType -import org.assertj.assertions.generator.BaseAssertionGenerator -import org.assertj.assertions.generator.description.ClassDescription -import org.assertj.assertions.generator.description.converter.ClassToClassDescriptionConverter -import org.assertj.assertions.generator.util.ClassUtil -import org.assertj.generator.gradle.internal.tasks.AssertionsGeneratorReport -import org.assertj.generator.gradle.tasks.config.AssertJGeneratorExtension -import org.assertj.generator.gradle.tasks.config.SerializedTemplate -import org.gradle.api.file.* -import org.gradle.api.logging.Logging -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ListProperty -import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty -import org.gradle.api.tasks.* -import org.gradle.api.tasks.incremental.IncrementalTaskInputs - -import javax.inject.Inject -import java.nio.file.Path -import java.nio.file.Paths - -import static com.google.common.collect.Sets.newLinkedHashSet - -/** - * Executes AssertJ generation against provided sources using the configured templates. - */ -@CacheableTask -class AssertJGenerationTask extends SourceTask { - - private static final logger = Logging.getLogger(AssertJGenerationTask) - - @InputFiles - @Classpath - final ConfigurableFileCollection generationClasspath - - @OutputDirectory - final DirectoryProperty outputDir - - // TODO `internal` when Konverted - @Input - final Property skip - - // TODO `internal` when Konverted - @Input - final Property hierarchical - - // TODO `internal` when Konverted - @Input - final SetProperty entryPoints - - // TODO `internal` when Konverted - @Input - @Optional - final Property entryPointsClassPackage - - // TODO `internal` when Konverted - @InputFiles - @Classpath - final FileCollection templateFiles - - // TODO `internal` when Konverted - @Input - final ListProperty generatorTemplates - - @Inject - AssertJGenerationTask(ObjectFactory objects, SourceSet sourceSet) { - description = "Generates AssertJ assertions for the ${sourceSet.name} sources." - - def assertJOptions = sourceSet.extensions.getByType(AssertJGeneratorExtension) - source(sourceSet.allJava) - dependsOn sourceSet.compileJavaTaskName - - this.generationClasspath = objects.fileCollection() - .from(sourceSet.runtimeClasspath) - - this.skip = objects.property(Boolean).tap { - set(project.provider { assertJOptions.skip }) - } - - this.hierarchical = objects.property(Boolean).tap { - set(project.provider { assertJOptions.hierarchical }) - } - - this.entryPoints = objects.setProperty(AssertionsEntryPointType).tap { - set(project.provider { assertJOptions.entryPoints.entryPoints }) - } - - this.entryPointsClassPackage = objects.property(String).tap { - set(project.provider { assertJOptions.entryPoints.classPackage }) - } - - this.outputDir = assertJOptions.outputDir - // TODO Make `templates.templateFiles` `internal` once `AssertJGenerationTask` is Kotlin - this.templateFiles = assertJOptions.templates.templateFiles - // TODO Make `templates.generatorTemplates` `internal` once `AssertJGenerationTask` is Kotlin - this.generatorTemplates = assertJOptions.templates.generatorTemplates - } - - @TaskAction - def execute(IncrementalTaskInputs inputs) { - if (skip.get()) { - return - } - - Set sourceFiles = source.files - - def classesToGenerate = [] - def fullRegenRequired = false - inputs.outOfDate { change -> - if (generationClasspath.contains(change.file)) { - // file is part of classpath - fullRegenRequired = true - } else if (sourceFiles.contains(change.file)) { - // source file changed - classesToGenerate += change.file - } else if (templateFiles.contains(change.file)) { - fullRegenRequired = true - } - } - - inputs.removed { change -> - // TODO Handle deleted file -// def targetFile = project.file("$outputDir/${change.file.name}") -// if (targetFile.exists()) { -// targetFile.delete() -// } - } - - if (fullRegenRequired || !inputs.incremental) { - project.delete(outputDir.asFileTree.files) - classesToGenerate = sourceFiles - } - - def classLoader = new URLClassLoader(generationClasspath.collect { it.toURI().toURL() } as URL[]) - - def inputClassNames = getClassNames() - Set> classes = ClassUtil.collectClasses( - classLoader, - inputClassNames.values().flatten() as String[], - ) - - def classesByTypeName = classes.collectEntries { [(it.type.typeName): it] } - - def inputClassesToFile = inputClassNames - .collectMany { file, classDefs -> - classDefs.collect { - [(classesByTypeName[it]): file] - } - } - .collectEntries() as Map, File> - - inputClassesToFile.values().removeAll { - !classesToGenerate.contains(it) - } - - runGeneration(classes, inputClassNames, inputClassesToFile) - } - - private def runGeneration( - Set> allClasses, - Map> inputClassNames, - Map, File> inputClassesToFile - ) { - BaseAssertionGenerator generator = new BaseAssertionGenerator() - ClassToClassDescriptionConverter converter = new ClassToClassDescriptionConverter() - - def absOutputDir = project.rootDir.toPath().resolve(this.outputDir.getAsFile().get().toPath()).toFile() - - Set> filteredClasses = removeAssertClasses(allClasses) - def report = new AssertionsGeneratorReport( - absOutputDir, - inputClassNames.values().flatten().collect { it.toString() }, - allClasses - filteredClasses, - ) - - def templates = generatorTemplates.get().collect { it.maybeLoadTemplate() }.findAll() - for (template in templates) { - generator.register(template) - } - - try { - generator.directoryWhereAssertionFilesAreGenerated = absOutputDir - - def classDescriptions - if (hierarchical.get()) { - classDescriptions = generateHierarchical(converter, generator, report, filteredClasses) - } else { - classDescriptions = generateFlat(converter, generator, report, filteredClasses, inputClassesToFile) - } - - if (!inputClassesToFile.isEmpty()) { - // only generate the entry points if there are classes that have changed (or exist..) - for (assertionsEntryPointType in entryPoints.get()) { - File assertionsEntryPointFile = generator.generateAssertionsEntryPointClassFor( - classDescriptions.toSet(), - assertionsEntryPointType, - entryPointsClassPackage.getOrNull(), - ) - report.reportEntryPointGeneration(assertionsEntryPointType, assertionsEntryPointFile) - } - } - } catch (Exception e) { - report.exception = e - } - - logger.info(report.getReportContent()) - } - - private static Collection generateHierarchical( - ClassToClassDescriptionConverter converter, - BaseAssertionGenerator generator, - AssertionsGeneratorReport report, - Set> classes - ) { - classes.collect { clazz -> - def classDescription = converter.convertToClassDescription(clazz) - def generatedCustomAssertionFiles = generator.generateHierarchicalCustomAssertionFor( - classDescription, - classes, - ) - report.addGeneratedAssertionFiles(generatedCustomAssertionFiles) - classDescription - } - } - - private static Collection generateFlat( - ClassToClassDescriptionConverter converter, - BaseAssertionGenerator generator, - AssertionsGeneratorReport report, - Set> classes, - Map, File> inputClassesToFile - ) { - classes.collect { clazz -> - def classDescription = converter.convertToClassDescription(clazz) - - if (inputClassesToFile.containsKey(clazz)) { - def generatedCustomAssertionFile = generator.generateCustomAssertionFor(classDescription) - report.addGeneratedAssertionFiles(generatedCustomAssertionFile) - } - - classDescription - } - } - - /** - * Returns the source for this task, after the include and exclude patterns have been applied. Ignores source files which do not exist. - * - * @return The source. - */ - // This method is here as the Gradle DSL generation can't handle properties with setters and getters in different classes. - @InputFiles - @SkipWhenEmpty - FileTree getSource() { - super.getSource() - } - - private Map> getClassNames() { - Map> fullyQualifiedNames = new HashMap<>(source.files.size()) - - source.visit { FileVisitDetails fileVisitDetails -> - Path file = fileVisitDetails.file.toPath() - fullyQualifiedNames[fileVisitDetails.file] = AssertJGenerationTask.getClassesInFile(file) - } - - fullyQualifiedNames.removeAll { it.value.isEmpty() } - fullyQualifiedNames - } - - private static Set> removeAssertClasses(Set> classList) { - Set> filteredClassList = newLinkedHashSet() - for (TypeToken clazz : classList) { - String classSimpleName = clazz.rawType.simpleName - if (!classSimpleName.endsWith("Assert") && !classSimpleName.endsWith("Assertions")) { - filteredClassList.add(clazz) - } - } - return filteredClassList - } - - private static def PACKAGE_JAVA_PATH = Paths.get("package-info.java") - - private static Set getClassesInFile(Path path) { - if (path.toFile().isDirectory()) return Sets.newHashSet() - - // Ignore package-info.java, it's not supposed to be included. - if (path.fileName == PACKAGE_JAVA_PATH) return Sets.newHashSet() - def fileName = path.fileName.toString() - def extension = fileName.substring(fileName.findLastIndexOf { it == '.' } + 1) - def fileNameWithoutExtension = fileName.substring(0, fileName.size() - extension.size() - 1) - - String packageName - Set classNames - - switch (extension) { - case "java": - def lines = path.readLines() - def packageLine = lines.find { it.startsWith("package ") } - - packageName = packageLine.substring("package ".size(), packageLine.size() - 1) - classNames = Sets.newHashSet(fileNameWithoutExtension) - break - - default: throw new IllegalStateException("Unsupported path: $path") - } - - return classNames.collect { "$packageName.$it" }.toSet() - } -} diff --git a/src/main/kotlin/org/assertj/generator/gradle/tasks/AssertJGenerationTask.kt b/src/main/kotlin/org/assertj/generator/gradle/tasks/AssertJGenerationTask.kt new file mode 100644 index 0000000..d724aa0 --- /dev/null +++ b/src/main/kotlin/org/assertj/generator/gradle/tasks/AssertJGenerationTask.kt @@ -0,0 +1,316 @@ +/* + * Copyright 2023. assertj-generator-gradle-plugin contributors. + * + * 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 + * + * http://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 org.assertj.generator.gradle.tasks + +import com.google.common.collect.Sets +import com.google.common.reflect.TypeToken +import org.assertj.assertions.generator.AssertionsEntryPointType +import org.assertj.assertions.generator.BaseAssertionGenerator +import org.assertj.assertions.generator.description.ClassDescription +import org.assertj.assertions.generator.description.converter.ClassToClassDescriptionConverter +import org.assertj.assertions.generator.util.ClassUtil +import org.assertj.generator.gradle.internal.tasks.AssertionsGeneratorReport +import org.assertj.generator.gradle.tasks.config.AssertJGeneratorExtension +import org.assertj.generator.gradle.tasks.config.SerializedTemplate +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileTree +import org.gradle.api.file.FileVisitDetails +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceTask +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.incremental.IncrementalTaskInputs +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.property +import org.gradle.kotlin.dsl.setProperty +import java.io.File +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import javax.inject.Inject +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.useLines + +/** + * Executes AssertJ generation against provided sources using the configured templates. + */ +@CacheableTask +open class AssertJGenerationTask @Inject internal constructor( + objects: ObjectFactory, + sourceSet: SourceSet, +) : SourceTask() { + + @get:InputFiles + @get:Classpath + val generationClasspath: ConfigurableFileCollection = objects.fileCollection() + .from(sourceSet.runtimeClasspath) + + @OutputDirectory + val outputDir: DirectoryProperty + + @get:Input + internal val skip: Property = objects.property() + + @get:Input + internal val hierarchical: Property = objects.property() + + @get:Input + internal val entryPoints: SetProperty = objects.setProperty() + + @get:Input + @get:Optional + internal val entryPointsClassPackage: Property = objects.property() + + @get:InputFiles + @get:Classpath + internal val templateFiles: FileCollection + + @get:Input + internal val generatorTemplates: ListProperty + + init { + description = "Generates AssertJ assertions for the ${sourceSet.name} sources." + + source(sourceSet.allJava) + dependsOn(sourceSet.compileJavaTaskName) + + val assertJOptions = sourceSet.extensions.getByType() + + outputDir = assertJOptions.outputDir + templateFiles = assertJOptions.templates.templateFiles + generatorTemplates = assertJOptions.templates.generatorTemplates + + skip.set(project.provider { assertJOptions.skip }) + hierarchical.set(project.provider { assertJOptions.hierarchical }) + entryPoints.set(project.provider { assertJOptions.entryPoints.entryPoints }) + entryPointsClassPackage.set(project.provider { assertJOptions.entryPoints.classPackage }) + } + + @TaskAction + fun execute(inputs: IncrementalTaskInputs) { + if (skip.get()) { + return + } + + val sourceFiles = source.files + + var classesToGenerate = mutableSetOf() + var fullRegenRequired = false + inputs.outOfDate { change -> + if (generationClasspath.contains(change.file)) { + // file is part of classpath + fullRegenRequired = true + } else if (sourceFiles.contains(change.file)) { + // source file changed + classesToGenerate += change.file + } else if (templateFiles.contains(change.file)) { + fullRegenRequired = true + } + } + + inputs.removed { change -> + // TODO Handle deleted file +// def targetFile = project.file("$outputDir/${change.file.name}") +// if (targetFile.exists()) { +// targetFile.delete() +// } + } + + if (fullRegenRequired || !inputs.isIncremental) { + project.delete(outputDir.asFileTree.files) + classesToGenerate = sourceFiles + } + + val classLoader = URLClassLoader(generationClasspath.map { it.toURI().toURL() }.toTypedArray()) + + val inputClassNames = getClassNames() + + @Suppress("SpreadOperator") // Java interop + val classes = ClassUtil.collectClasses( + classLoader, + *inputClassNames.values.flatten().toTypedArray(), + ) + + val classesByTypeName = classes.associateBy { it.type.typeName } + + val inputClassesToFile = inputClassNames.asSequence() + .flatMap { (file, classDefs) -> + classDefs.map { classesByTypeName.getValue(it) to file } + } + .filter { (_, file) -> file in classesToGenerate } + .toMap() + + runGeneration(classes, inputClassNames, inputClassesToFile) + } + + private fun runGeneration( + allClasses: Set>, + inputClassNames: Map>, + inputClassesToFile: Map, File> + ) { + val generator = BaseAssertionGenerator() + val converter = ClassToClassDescriptionConverter() + + val absOutputDir = project.rootDir.toPath().resolve(this.outputDir.asFile.get().toPath()).toFile() + + val filteredClasses = removeAssertClasses(allClasses) + val report = AssertionsGeneratorReport( + directoryPathWhereAssertionFilesAreGenerated = absOutputDir, + inputClasses = inputClassNames.values.flatten(), + excludedClassesFromAssertionGeneration = allClasses - filteredClasses, + ) + + val templates = generatorTemplates.get().mapNotNull { it.maybeLoadTemplate() } + for (template in templates) { + generator.register(template) + } + + try { + generator.setDirectoryWhereAssertionFilesAreGenerated(absOutputDir) + + val classDescriptions = if (hierarchical.get()) { + generateHierarchical(converter, generator, report, filteredClasses) + } else { + generateFlat(generator, converter, report, filteredClasses, inputClassesToFile) + }.toSet() + + if (inputClassesToFile.isNotEmpty()) { + // only generate the entry points if there are classes that have changed (or exist..) + for (assertionsEntryPointType in entryPoints.get()) { + val assertionsEntryPointFile = generator.generateAssertionsEntryPointClassFor( + classDescriptions, + assertionsEntryPointType, + entryPointsClassPackage.orNull, + ) + report.reportEntryPointGeneration(assertionsEntryPointType, assertionsEntryPointFile) + } + } + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + report.exception = e + } + + logger.info(report.getReportContent()) + } + + private fun generateHierarchical( + converter: ClassToClassDescriptionConverter, + generator: BaseAssertionGenerator, + report: AssertionsGeneratorReport, + classes: Set>, + ): Collection { + return classes.map { clazz -> + val classDescription = converter.convertToClassDescription(clazz) + val generatedCustomAssertionFiles = generator.generateHierarchicalCustomAssertionFor( + classDescription, + classes, + ) + report.addGeneratedAssertionFiles(generatedCustomAssertionFiles = generatedCustomAssertionFiles) + classDescription + } + } + + private fun generateFlat( + generator: BaseAssertionGenerator, + converter: ClassToClassDescriptionConverter, + report: AssertionsGeneratorReport, + classes: Set>, + inputClassesToFile: Map, File>, + ): Collection { + return classes.map { clazz -> + val classDescription = converter.convertToClassDescription(clazz) + + if (clazz in inputClassesToFile) { + val generatedCustomAssertionFile = generator.generateCustomAssertionFor(classDescription) + report.addGeneratedAssertionFiles(generatedCustomAssertionFile) + } + + classDescription + } + } + + /** + * Returns the source for this task, after the include and exclude patterns have been applied. Ignores source files + * which do not exist. + * + * @return The source. + */ + // This method is here as the Gradle DSL generation can't handle properties with setters and getters in different + // classes. + @InputFiles + @SkipWhenEmpty + override fun getSource(): FileTree = super.getSource() + + private fun getClassNames(): Map> { + val fullyQualifiedNames = mutableMapOf>() + + source.visit { fileVisitDetails: FileVisitDetails -> + val file = fileVisitDetails.file.toPath() + fullyQualifiedNames[fileVisitDetails.file] = getClassesInFile(file) + } + + return fullyQualifiedNames.filterValues { it.isNotEmpty() } + } +} + +private fun removeAssertClasses(classList: Set>): Set> { + val filteredClassList = mutableSetOf>() + for (clazz in classList) { + val classSimpleName = clazz.rawType.simpleName + if (!classSimpleName.endsWith("Assert") && !classSimpleName.endsWith("Assertions")) { + filteredClassList.add(clazz) + } + } + return filteredClassList +} + +private val PACKAGE_JAVA_PATH = Paths.get("package-info.java") + +private fun getClassesInFile(path: Path): Set { + if (path.isDirectory()) return setOf() + + // Ignore package-info.java, it's not supposed to be included. + if (path.fileName == PACKAGE_JAVA_PATH) return setOf() + + val extension = path.extension + val fileNameWithoutExtension = path.fileName.nameWithoutExtension + + val (packageName, classNames) = when (extension) { + "java" -> { + path.useLines { lines -> + val packageLine = lines.first { it.startsWith("package ") } + + val packageName = packageLine.removePrefix("package ").trimEnd(';') + val classNames = Sets.newHashSet(fileNameWithoutExtension) + Pair(packageName, classNames) + } + } + + else -> error("Unsupported extension: $extension") + } + + return classNames.asSequence().map { "$packageName.$it" }.toSet() +} diff --git a/src/main/kotlin/org/assertj/generator/gradle/tasks/config/SerializedTemplate.kt b/src/main/kotlin/org/assertj/generator/gradle/tasks/config/SerializedTemplate.kt index 02b6ae5..3d9bda3 100644 --- a/src/main/kotlin/org/assertj/generator/gradle/tasks/config/SerializedTemplate.kt +++ b/src/main/kotlin/org/assertj/generator/gradle/tasks/config/SerializedTemplate.kt @@ -20,7 +20,7 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable -class SerializedTemplate( +internal class SerializedTemplate( type: Type, content: String?, file: File?, diff --git a/src/main/kotlin/org/assertj/generator/gradle/tasks/config/Templates.kt b/src/main/kotlin/org/assertj/generator/gradle/tasks/config/Templates.kt index 91330fe..6cf5b63 100644 --- a/src/main/kotlin/org/assertj/generator/gradle/tasks/config/Templates.kt +++ b/src/main/kotlin/org/assertj/generator/gradle/tasks/config/Templates.kt @@ -30,9 +30,6 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider -import org.gradle.api.tasks.Classpath -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFiles import org.gradle.kotlin.dsl.listProperty import org.gradle.kotlin.dsl.newInstance import org.gradle.kotlin.dsl.property @@ -81,17 +78,12 @@ open class Templates @Inject internal constructor(objects: ObjectFactory) { /** * All files associated with templates. This is used for building up dependencies. */ - // TODO Make this `internal` once `AssertJGenerationTask` is Kotlin - @get:InputFiles - @get:Classpath - val templateFiles: FileCollection + internal val templateFiles: FileCollection /** * All template data that has been set by a user. */ - // TODO Make this `internal` once `AssertJGenerationTask` is Kotlin - @get:Input - val generatorTemplates: ListProperty + internal val generatorTemplates: ListProperty init { classes = objects.newInstance()