Repository: apple/pkl
File: pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java (Java)

Code before:
  @Optional
  public abstract Property<String> getExpression();

  @Inject
  public abstract ObjectFactory getObjectFactory();

  private CliEvaluator getCliEvaluator() {
    return new CliEvaluator(
        new CliEvaluatorOptions(
            getCliBaseOptions(),
            getOutputFile().get().getAsFile().getAbsolutePath(),
            getOutputFormat().get(),
            getModuleOutputSeparator().get(),
            mapAndGetOrNull(getMultipleFileOutputDir(), (it) -> it.getAsFile().getAbsolutePath()),
            getExpression().get()));
  }

  @OutputFiles
  @Optional
  public SetProperty<File> getOutputPaths() {
    var ret = getObjectFactory().setProperty(File.class);
    ret.value(getCliEvaluator().getOutputFiles()).disallowChanges();
    return ret;
  }

  @OutputDirectories
  @Optional
  public SetProperty<File> getMultipleFileOutputPaths() {
    var ret = getObjectFactory().setProperty(File.class);
    ret.value(getCliEvaluator().getOutputDirectories()).disallowChanges();
    return ret;
  }

  @Override
  protected void doRunTask() {
    //noinspection ResultOfMethodCallIgnored
    getOutputs().getPreviousOutputFiles().forEach(File::delete);
    getCliEvaluator().run();
  }
}


Review comment (bug):
Unfortunately this approach has some issues.

Gradle lazy property objects can be accessed at any time, including at the configuration phase. Therefore, their creation must not be predicated on any complex logic, and especially not on values of other lazy properties.

In this case, however, just creating the `SetProperty` instance requires creating the `CliEvaluator` object, which in turn evaluates other properties in the task. This is, unfortunately, does not follow Gradle's model of lazy properties properly.

Here is a limited and artificial example which illustrates the point:
```java
// buildSrc/src/main/java/tasks/EvalTask.java

public abstract class EvalTask extends DefaultTask {
  @Internal
  public abstract RegularFileProperty getOutputFile();

  @Inject
  public abstract ObjectFactory getObjectFactory();

  private Set<File> getCliEvaluatorFiles() {
    return Set.of(getOutputFile().get().getAsFile());
  }

  @OutputFiles
  @Optional
  public SetProperty<File> getOutputPaths() {
    var ret = getObjectFactory().setProperty(File.class);
    ret.value(getCliEvaluatorFiles()).disallowChanges();
    return ret;
  }
}
```
```kotlin
// build.gradle.kts

import tasks.EvalTask

val evalSomething by tasks.creating(EvalTask::class) {
    // outputFile is not set
}

val copySomewhere by tasks.creating(Copy::class) {
    from(evalSomething.outputPaths)
    into("whatever")
}

evalSomething.run {
    outputFile = layout.projectDirectory.file("settings.gradle.kts")
}
```
This build will fail at configuration time with an exception like this:
```
* What went wrong:
Cannot query the value of task ':evalSomething' property 'outputFile' because it has no value available.
```

This is actually the reason why in that link that was submitted in the earlier discussion (on Gradle forums) it was done differently, along these lines:
```
@OutputFile
abstract RegularFileProperty getOutputFile()

Producer() {
    outputFile.value(getOutputFileName().flatMap(name -> project.layout.buildDirectory.file(name))).disallowChanges()
}
```

Not only is the actual instance of the property instantiated by Gradle here (via abstract method injection), it is configured with a provider (derived from a property here), not with a directly computed value, like in this PR.

Code after:

  @Input
  public abstract Property<String> getExpression();

  private final Provider<CliEvaluator> cliEvaluator =
      getProviders()
          .provider(
              () ->
                  new CliEvaluator(
                      new CliEvaluatorOptions(
                          getCliBaseOptions(),
                          getOutputFile().get().getAsFile().getAbsolutePath(),
                          getOutputFormat().get(),
                          getModuleOutputSeparator().get(),
                          mapAndGetOrNull(
                              getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
                          getExpression().get())));

  @SuppressWarnings("unused")
  @OutputFiles
  @Optional
  public FileCollection getEffectiveOutputFiles() {
    return getObjects()
        .fileCollection()
        .from(cliEvaluator.map(e -> nullToEmpty(e.getOutputFiles())));
  }

  @OutputDirectories
  @Optional
  public FileCollection getEffectiveOutputDirs() {
    return getObjects()
        .fileCollection()
        .from(cliEvaluator.map(e -> nullToEmpty(e.getOutputDirectories())));
  }

  private static <T> Set<T> nullToEmpty(@Nullable Set<T> set) {
    return set == null ? Collections.emptySet() : set;
  }

  @Override
  protected void doRunTask() {
    //noinspection ResultOfMethodCallIgnored
    getOutputs().getPreviousOutputFiles().forEach(File::delete);
    cliEvaluator.get().run();
  }
}