diff --git a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/GradleProject.java b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/GradleProject.java index 3598798ad..9ba00eea5 100644 --- a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/GradleProject.java +++ b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/GradleProject.java @@ -1,7 +1,5 @@ package org.dddjava.jig.gradle; -import org.dddjava.jig.domain.model.sources.filesystem.SourceBasePath; -import org.dddjava.jig.domain.model.sources.filesystem.SourceBasePaths; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.DependencySet; @@ -22,12 +20,13 @@ public class GradleProject { final Project project; public GradleProject(Project project) { - if (isNonJavaProject(project)) { - throw new IllegalStateException("Java プラグインが適用されていません。"); - } this.project = project; } + public static boolean isJavaProject(Project project) { + return findJavaPluginExtension(project).isPresent(); + } + public Set classPaths() { return sourceSets() .map(SourceSet::getOutput) @@ -60,16 +59,24 @@ private Stream sourceSets() { .filter(sourceSet -> !sourceSet.getName().equals(SourceSet.TEST_SOURCE_SET_NAME))); } - public SourceBasePaths rawSourceLocations() { + /** + * 依存プロジェクトを含むすべてのクラスパスを返す + */ + public Set allClassPaths() { return allDependencyProjectsFrom(project) .map(GradleProject::new) - .map(gradleProject -> - new SourceBasePaths( - new SourceBasePath(gradleProject.classPaths()), - new SourceBasePath(gradleProject.sourcePaths()) - )) - .reduce(SourceBasePaths::merge) - .orElseThrow(() -> new IllegalStateException("対象プロジェクトが見つかりません。")); + .flatMap(gp -> gp.classPaths().stream()) + .collect(toSet()); + } + + /** + * 依存プロジェクトを含むすべてのソースパスを返す + */ + public Set allSourcePaths() { + return allDependencyProjectsFrom(project) + .map(GradleProject::new) + .flatMap(gp -> gp.sourcePaths().stream()) + .collect(toSet()); } private Stream allDependencyProjectsFrom(Project currentProject) { diff --git a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigConfig.java b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigConfig.java index 12e9382d5..a010be03c 100644 --- a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigConfig.java +++ b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigConfig.java @@ -1,17 +1,10 @@ package org.dddjava.jig.gradle; import org.dddjava.jig.domain.model.documents.documentformat.JigDiagramFormat; -import org.dddjava.jig.domain.model.documents.documentformat.JigDocument; -import org.dddjava.jig.infrastructure.configuration.JigProperties; -import org.gradle.api.Project; -import java.nio.file.Path; import java.nio.file.Paths; -import java.time.Duration; -import java.util.Optional; import java.util.ArrayList; import java.util.List; -import java.util.StringJoiner; public class JigConfig { @@ -22,66 +15,17 @@ public class JigConfig { String outputDirectory = ""; - String outputOmitPrefix = ".+\\.(service|domain\\.(model|type))\\."; - JigDiagramFormat diagramFormat = JigDiagramFormat.SVG; String dotTimeout = "10s"; boolean diagramTransitiveReduction = true; - List documentTypes() { - List toExclude = documentTypesToExclude(); - return documentTypesToInclude().stream() - .filter(each -> !toExclude.contains(each)) - .toList(); - } - - List documentTypesToExclude() { - if (documentTypesExclude.isEmpty()) return List.of(); - return documentTypesExclude.stream() - .map(JigDocument::valueOf) - .toList(); - } - - List documentTypesToInclude() { - if (documentTypes.isEmpty()) return JigDocument.canonical(); - return documentTypes.stream() - .map(JigDocument::valueOf) - .toList(); + public List getDocumentTypesExclude() { + return documentTypesExclude; } - public JigProperties toJigProperties(Project project) { - return new JigProperties( - documentTypes(), - Optional.ofNullable(modelPattern).filter(s -> !s.isEmpty()), resolveOutputDirectory(project), - diagramFormat, - diagramTransitiveReduction, - parseDotTimeout() - ); - } - - private Duration parseDotTimeout() { - if (dotTimeout.endsWith("ms")) { - return Duration.ofMillis(Long.parseLong(dotTimeout.substring(0, dotTimeout.length() - 2))); - } - if (dotTimeout.endsWith("s")) { - return Duration.ofSeconds(Long.parseLong(dotTimeout.substring(0, dotTimeout.length() - 1))); - } - throw new IllegalArgumentException("dotTimeout must be end with ms or s. " + dotTimeout + " is invalid."); - } - - private Path resolveOutputDirectory(Project project) { - if (this.outputDirectory.isEmpty()) { - return defaultOutputDirectory(project); - } - return Paths.get(this.outputDirectory); - } - - private Path defaultOutputDirectory(Project project) { - Path path = Paths.get(getOutputDirectory()); - if (path.isAbsolute()) return path; - var buildDirectory = project.getLayout().getBuildDirectory(); - return buildDirectory.getAsFile().get().toPath().resolve("jig"); + public void setDocumentTypesExclude(List documentTypesExclude) { + this.documentTypesExclude = documentTypesExclude; } public String getModelPattern() { @@ -135,23 +79,4 @@ public void setDiagramTransitiveReduction(boolean diagramTransitiveReduction) { this.diagramTransitiveReduction = diagramTransitiveReduction; } - public String getOutputOmitPrefix() { - return outputOmitPrefix; - } - - public void setOutputOmitPrefix(String outputOmitPrefix) { - this.outputOmitPrefix = outputOmitPrefix; - } - - public String propertiesText() { - return new StringJoiner("\n\t", "jig {\n\t", "\n}") - .add("documentTypes = '" + documentTypes + '\'') - .add("modelPattern = '" + modelPattern + '\'') - .add("outputDirectory = '" + outputDirectory + '\'') - .add("diagramFormat= '" + diagramFormat + '\'') - .add("dotTimeout= '" + dotTimeout + '\'') - .add("diagramTransitiveReduction= '" + diagramTransitiveReduction + '\'') - .add("outputOmitPrefix = '" + outputOmitPrefix + '\'') - .toString(); - } } diff --git a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigGradlePlugin.java b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigGradlePlugin.java index 00febd8f3..db6099c03 100644 --- a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigGradlePlugin.java +++ b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigGradlePlugin.java @@ -5,17 +5,46 @@ import org.gradle.api.plugins.ExtensionContainer; import org.gradle.api.tasks.TaskContainer; +import java.nio.file.Path; + public class JigGradlePlugin implements Plugin { @Override public void apply(Project project) { ExtensionContainer extensions = project.getExtensions(); - extensions.create("jig", JigConfig.class); + JigConfig config = extensions.create("jig", JigConfig.class); TaskContainer tasks = project.getTasks(); tasks.register("jigReports", JigReportsTask.class).configure(task -> { task.setGroup("JIG"); task.setDescription("Generates JIG documentation for the main source code."); + + // JigConfig のプロパティをタスクプロパティにワイヤリング + task.getModelPattern().convention(project.provider(config::getModelPattern)); + task.getDocumentTypes().convention(project.provider(config::getDocumentTypes)); + task.getDocumentTypesExclude().convention(project.provider(config::getDocumentTypesExclude)); + task.getDiagramFormat().convention(project.provider(() -> config.getDiagramFormat().name())); + task.getDiagramTransitiveReduction().convention(project.provider(config::isDiagramTransitiveReduction)); + task.getDotTimeout().convention(project.provider(config::getDotTimeout)); + + // 出力ディレクトリ(project をキャプチャしないよう configuration phase で解決) + String outputDir = config.getOutputDirectory(); + if (outputDir.isEmpty()) { + task.getOutputDirectory().convention(project.getLayout().getBuildDirectory().dir("jig")); + } else { + task.getOutputDirectory().set(project.getLayout().getProjectDirectory().dir(outputDir)); + } + + // Java プラグインの適用状態(configuration phase で解決) + task.getJavaPluginApplied().convention(GradleProject.isJavaProject(project)); + + // ソース/クラスパス(configuration phase で解決し直接設定。 + // Provider でラップすると project をキャプチャしてしまい、Gradle 8 の configuration cache でシリアライズできないため。) + if (GradleProject.isJavaProject(project)) { + GradleProject gp = new GradleProject(project); + task.getClassFiles().from(gp.allClassPaths().stream().map(Path::toFile).toList()); + task.getSourceFiles().from(gp.allSourcePaths().stream().map(Path::toFile).toList()); + } }); } } diff --git a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigReportsTask.java b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigReportsTask.java index 81270f217..a8944c851 100644 --- a/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigReportsTask.java +++ b/jig-gradle-plugin/src/main/java/org/dddjava/jig/gradle/JigReportsTask.java @@ -3,35 +3,106 @@ import org.dddjava.jig.HandleResult; import org.dddjava.jig.JigExecutor; import org.dddjava.jig.JigResult; +import org.dddjava.jig.domain.model.documents.documentformat.JigDiagramFormat; +import org.dddjava.jig.domain.model.documents.documentformat.JigDocument; +import org.dddjava.jig.domain.model.sources.filesystem.SourceBasePath; import org.dddjava.jig.domain.model.sources.filesystem.SourceBasePaths; import org.dddjava.jig.infrastructure.configuration.Configuration; +import org.dddjava.jig.infrastructure.configuration.JigProperties; import org.gradle.api.DefaultTask; -import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.TaskAction; import org.gradle.work.DisableCachingByDefault; +import java.io.File; +import java.nio.file.Path; +import java.time.Duration; import java.util.List; +import java.util.Optional; +import java.util.Set; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; -@DisableCachingByDefault(because = "JigReportsTask depends on JigExecutor and cannot define output") -public class JigReportsTask extends DefaultTask { +@DisableCachingByDefault(because = "JigReportsTask generates files via JigExecutor whose outputs are not fully declarable") +public abstract class JigReportsTask extends DefaultTask { + + @Input + public abstract Property getModelPattern(); + + @Input + public abstract ListProperty getDocumentTypes(); + + @Input + public abstract ListProperty getDocumentTypesExclude(); + + @Input + public abstract Property getDiagramFormat(); + + @Input + public abstract Property getDiagramTransitiveReduction(); + + @Input + public abstract Property getDotTimeout(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getClassFiles(); + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getSourceFiles(); + + @Internal + public abstract Property getJavaPluginApplied(); + + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); @TaskAction void outputReports() { - Project project = getProject(); - JigConfig config = project.getExtensions().findByType(JigConfig.class); - if (config == null) { - getLogger().warn("jig-gradle-pluginの設定が取得できません。通常は起こらないはずで、疑われるのはプラグイン側の実装ミスです。続行できないため終了します。"); - return; + if (!getJavaPluginApplied().getOrElse(false)) { + throw new IllegalStateException("Java プラグインが適用されていません。"); } - Configuration configuration = Configuration.from(config.toJigProperties(getProject())); + List documentTypes = resolveDocumentTypes(); + Path outputDirectory = getOutputDirectory().getAsFile().get().toPath(); - getLogger().info("-- configuration -------------------------------------------\n{}\n------------------------------------------------------------", config.propertiesText()); + JigProperties jigProperties = new JigProperties( + documentTypes, + Optional.ofNullable(getModelPattern().getOrNull()).filter(s -> !s.isEmpty()), + outputDirectory, + JigDiagramFormat.valueOf(getDiagramFormat().get()), + getDiagramTransitiveReduction().get(), + parseDotTimeout(getDotTimeout().get()) + ); + + Configuration configuration = Configuration.from(jigProperties); + + getLogger().info("-- configuration -------------------------------------------\n{}\n------------------------------------------------------------", + jigProperties); long startTime = System.currentTimeMillis(); - SourceBasePaths sourceBasePaths = new GradleProject(project).rawSourceLocations(); + + Set classPaths = getClassFiles().getFiles().stream() + .map(File::toPath) + .collect(toSet()); + Set sourcePaths = getSourceFiles().getFiles().stream() + .map(File::toPath) + .collect(toSet()); + SourceBasePaths sourceBasePaths = new SourceBasePaths( + new SourceBasePath(classPaths), + new SourceBasePath(sourcePaths) + ); JigResult jigResult = JigExecutor.standard(configuration, sourceBasePaths); List handleResultList = jigResult.listResult(); @@ -48,4 +119,28 @@ void outputReports() { System.currentTimeMillis() - startTime, resultLog); } + private List resolveDocumentTypes() { + List toExclude = getDocumentTypesExclude().get().stream() + .map(JigDocument::valueOf) + .toList(); + + List includeTypes = getDocumentTypes().get(); + List toInclude = includeTypes.isEmpty() + ? JigDocument.canonical() + : includeTypes.stream().map(JigDocument::valueOf).toList(); + + return toInclude.stream() + .filter(each -> !toExclude.contains(each)) + .toList(); + } + + private Duration parseDotTimeout(String dotTimeout) { + if (dotTimeout.endsWith("ms")) { + return Duration.ofMillis(Long.parseLong(dotTimeout.substring(0, dotTimeout.length() - 2))); + } + if (dotTimeout.endsWith("s")) { + return Duration.ofSeconds(Long.parseLong(dotTimeout.substring(0, dotTimeout.length() - 1))); + } + throw new IllegalArgumentException("dotTimeout must be end with ms or s. " + dotTimeout + " is invalid."); + } } diff --git a/jig-gradle-plugin/src/test/java/jig/JigPluginFunctionalTest.java b/jig-gradle-plugin/src/test/java/jig/JigPluginFunctionalTest.java index 880897071..9c0999ee1 100644 --- a/jig-gradle-plugin/src/test/java/jig/JigPluginFunctionalTest.java +++ b/jig-gradle-plugin/src/test/java/jig/JigPluginFunctionalTest.java @@ -12,6 +12,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -235,12 +237,72 @@ private void buildGradle(String buildScript) throws IOException { Files.writeString(testProjectDir.resolve("build.gradle"), buildScript); } - private GradleRunner runner(String version) throws IOException { + @ParameterizedTest + @MethodSource("supportGradleVersion") + void コンフィグレーションキャッシュが有効でも実行できる(String version) throws IOException { + settingsGradle(""" + rootProject.name = 'my-test' + """); + buildGradle(""" + plugins { + id 'java' + id 'org.dddjava.jig-gradle-plugin' + } + """); + + // 1回目: キャッシュ保存 + var result1 = runner(version, "--configuration-cache").build(); + var taskResult1 = Objects.requireNonNull(result1.task(":jigReports")); + assertEquals(TaskOutcome.SUCCESS, taskResult1.getOutcome()); + assertTrue(result1.getOutput().contains("[JIG] all JIG documents completed: "), result1.getOutput()); + + // 2回目: キャッシュ再利用(タスクはUP_TO_DATEになりうる) + var result2 = runner(version, "--configuration-cache").build(); + assertNotNull(result2.task(":jigReports")); + assertTrue(result2.getOutput().contains("Reusing configuration cache"), result2.getOutput()); + } + + @ParameterizedTest + @MethodSource("supportGradleVersion") + void コンフィグレーションキャッシュが有効でもマルチプロジェクトで実行できる(String version) throws IOException { + settingsGradle(""" + rootProject.name = 'my-test' + include 'a', 'b' + """); + buildGradle("a", """ + plugins { + id 'java' + id 'org.dddjava.jig-gradle-plugin' + } + dependencies { + implementation project(':b'); + } + """); + buildGradle("b", """ + plugins { + id 'java' + } + """); + + // 1回目: キャッシュ保存 + var result1 = runner(version, "--configuration-cache").build(); + var taskResult1 = Objects.requireNonNull(result1.task(":a:jigReports")); + assertEquals(TaskOutcome.SUCCESS, taskResult1.getOutcome()); + + // 2回目: キャッシュ再利用(タスクはUP_TO_DATEになりうる) + var result2 = runner(version, "--configuration-cache").build(); + assertNotNull(result2.task(":a:jigReports")); + assertTrue(result2.getOutput().contains("Reusing configuration cache"), result2.getOutput()); + } + + private GradleRunner runner(String version, String... additionalArgs) throws IOException { + List args = new ArrayList<>(List.of("jig", "--info")); + args.addAll(List.of(additionalArgs)); return GradleRunner.create() .withGradleVersion(version) .withProjectDir(testProjectDir.toFile()) - .withArguments("jig", "--info") + .withArguments(args) .withPluginClasspath(); } }