Conversation

YongGoose

Related issue: #655

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting a draft PR is a great approach!

The Gradle tests are quite slow. I would create a ReviewDogGenerator in the lib-extra project, running tests there is much faster than in the Gradle project. Once we can generate a ReviewDog file, then we can wire it up with Gradle.

As for the wiring, this is how it works now

graph TD
    spotlessJava --> spotlessJavaCheck
    spotlessJava --> spotlessJavaApply
    spotlessJavaCheck --> spotlessCheck
    spotlessJavaApply --> spotlessApply

    spotlessKotlin --> spotlessKotlinCheck
    spotlessKotlin --> spotlessKotlinApply
    spotlessKotlinCheck --> spotlessCheck
    spotlessKotlinApply --> spotlessApply
Loading

All of the work happens in spotlessJava, and it happens in a way which supports caching and up-to-dateness, etc

@TaskAction
public void performAction(InputChanges inputs) throws Exception {
IdeHook.State ideHook = getIdeHookState().getOrNull();
if (ideHook != null && ideHook.path != null) {
IdeHook.performHook(this, ideHook);
return;
}
SpotlessTaskService taskService = getTaskService().get();
taskService.registerSourceAlreadyRan(this);
if (target == null) {
throw new GradleException("You must specify 'Iterable<File> target'");
}
if (!inputs.isIncremental()) {
getLogger().info("Not incremental: removing prior outputs");
getFs().delete(d -> d.delete(cleanDirectory));
getFs().delete(d -> d.delete(lintsDirectory));
Files.createDirectories(cleanDirectory.toPath());
Files.createDirectories(lintsDirectory.toPath());
}

Then spotlessJavaCheck and spotlessJavaApply just look at those folders and copy them around

private void performAction(boolean isTest) throws IOException {
ConfigurableFileTree cleanFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessCleanDirectory().get());
ConfigurableFileTree lintsFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessLintsDirectory().get());
if (cleanFiles.isEmpty() && lintsFiles.isEmpty()) {
getState().setDidWork(sourceDidWork());
} else if (!isTest && applyHasRun()) {
// if our matching apply has already run, then we don't need to do anything
getState().setDidWork(false);
} else {
List<File> unformattedFiles = getUncleanFiles(cleanFiles);

public void performAction() {
getTaskService().get().registerApplyAlreadyRan(this);
ConfigurableFileTree cleanFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessCleanDirectory().get());
ConfigurableFileTree lintsFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessLintsDirectory().get());
if (cleanFiles.isEmpty() && lintsFiles.isEmpty()) {
getState().setDidWork(sourceDidWork());
} else {

I would expect spotlessReviewDog to work in that same structure. But probably we should get the guts working in lib-extra, and also write some docs for plugin-gradle, before we spend too much time implementing the wiring.

task.setGroup(TASK_GROUP);
task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME);
});
rootCheckTask.configure(task -> task.dependsOn(diffTask));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think everybody needs the diff task.

@YongGoose

@nedtwigg

I’m planning to implement this feature in three steps:

  1. Create the ReviewDogGenerator class
  2. Add a task in the Gradle-plugin that uses the information stored by ReviewDogGenerator
  3. Write test codes

This dc52b26 includes only the implementation of step 1.

Also, I have a question!

After ReviewDogGenerator generates a diff for ReviewDog, I’d like to store that information in a separate directory (like SpotlessTaskImpl does).
However, it seems that classes from gradle.api can’t be used within the lib-extra directory.

Would you be able to offer some guidance on this?

@YongGooseYongGoose requested a review from nedtwigg May 11, 2025 12:19
@nedtwigg

Here is my suggestion:

class ReviewDog {
  public static String rdjsonlDiff(String path, String actualContent, String formattedContent)
  public static String rdjsonlLints(String path, String formattedContent, List<FormatterStep> steps, List<List<Lint>> lintsPerStep)
}

You can test this easily in lib-extra

class ReviewDogTest {
  @Test
  public void diffSingleLine() {
    expectSelfie(ReviewDog.rdjsonlDiff("test.txt", "dirty", "clean")).toBe_TODO()
  }
  ... etc
}

So you can build and test the ReviewDog features in isolation, no gradle API required. Once these are working, you can pipe them together in a Gradle task.

@YongGoose

Here is my suggestion:

class ReviewDog {
  public static String rdjsonlDiff(String path, String actualContent, String formattedContent)
  public static String rdjsonlLints(String path, String formattedContent, List<FormatterStep> steps, List<List<Lint>> lintsPerStep)
}

Sorry for the late reply.

Would you mind explaining the purpose of the ReviewDog class?
I’m still having a bit of trouble understanding it.
To be more specific, I’m curious where actualContext and formattedContent are coming from.

Would it be correct to understand this test as one meant to verify the functionality in isolation?

@nedtwigg

I’m curious where actualContext and formattedContent are coming from

  • actualContent is the file as it exists
  • formattedContent is the formatter version of the file

Depending on the plugin integration, they will come from different places. But I promise they are available! If you build it this way, the ReviewDog stuff is decoupled from Formatter etc. You don't have to make fake and irrelevant Formatter instances to test it.

@YongGoose

I’m curious where actualContext and formattedContent are coming from

  • actualContent is the file as it exists
  • formattedContent is the formatter version of the file

Depending on the plugin integration, they will come from different places. But I promise they are available! If you build it this way, the ReviewDog stuff is decoupled from Formatter etc. You don't have to make fake and irrelevant Formatter instances to test it.

Based on the comments, I removed the internal formatter and refactored it into a utility class.
PTAL ⚡️

Since I had worked on it previously, I was able to make the changes quickly. 😁

@YongGoose

@nedtwigg

Feel free to update the code if you think any changes are necessary. 🚀

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great progress! Next step is probably the docs. Try updating the plugin-gradle/README.md with some kind of flag so that failed spotlessCheck will generate the reviewDog files in the appropriate place. Link to setup instructions for reviewDog, etc.

Comment on lines 58 to 61
assertNotNull(result);
assertTrue(result.contains("\"path\":\"src/main.java\""));
assertTrue(result.contains("-Dirty line"));
assertTrue(result.contains("+Clean line"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, just do Selfie.expectSelfie(result).toBe_TODO(). When you run it, it will replace the TODO with the actual content. Makes the assertion easier to read, write, and maintain.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a awesome feature!!!
Done in 5724b73 , 745810b !

- use Selfie.expectSelfie(result).toBe_TODO()
- use Selfie.expectSelfie(result).toBe_TODO()

Signed-off-by: yongjunhong <[email protected]>
Signed-off-by: yongjunhong <[email protected]>
Signed-off-by: yongjunhong <[email protected]>
@YongGoose

@nedtwigg

I've added an initial version of the README. e7015df
I think the gradle code will need to be updated after we complete the implementation.

Please take a look :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good! If you're okay with the docs as specified, the next step is

  • add reviewDog property to SpotlessExtension
  • pipe that property to spotlessCheck
  • you already have the data you need for ReviewDogGenerator
    • List<File> unformattedFiles = getUncleanFiles(cleanFiles);
      if (!unformattedFiles.isEmpty()) {
      // if any files are unformatted, we show those
      throw new GradleException(DiffMessageFormatter.builder()
      .runToFix(getRunToFixMessage().get())
      .formatterFolder(
      getProjectDir().get().getAsFile().toPath(),
      getSpotlessCleanDirectory().get().toPath(),
      getEncoding().get())
      .problemFiles(unformattedFiles)
      .getMessage());
      } else {
      // We only show lints if there are no unformatted files.
      // This is because lint line numbers are relative to the
      // formatted content, and formatting often fixes lints.
      boolean detailed = false;
      throw new GradleException(super.allLintsErrorMsgDetailed(lintsFiles, detailed));
      }

Comment on lines 1788 to 1803
### Automatic report generation on check failure

To generate reports when `spotlessCheck` fails:

```gradle
tasks.named('spotlessCheck').configure {
ignoreFailures = true
finalizedBy tasks.register('generateReviewdogReport') {
doLast {
if (spotlessCheck.outcome.failure) {
logger.lifecycle("Spotless check failed - Reviewdog report generated")
}
}
}
}
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have a separate generateReviewdogReport. If reviewdogOutput is set, then we should generate that output in spotlessCheck

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a94b357 !

I’ll revise it again after all implementations are complete.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nedtwigg

I’ve left comments on the parts where I need help or think we should discuss further.
Please take a look :)

* @param lintsPerStep The list of lints produced by each step
* @return A string in rdjsonl format representing the lints
*/
public static String rdjsonlLints(String path, List<FormatterStep> steps, List<List<Lint>> lintsPerStep) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should formattedContent also be output in the ReviewDog format in rdjsonlLints?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduced use of deps by returning early.
Did this for readability, but I can revert if you think the previous version was better.
Let me know what you think!

Comment on lines 99 to 103
if (getReviewDog().get()) {
for (File file : lintsFiles.getFiles()) {
String path = file.getPath();
// TODO : send or save the output of ReviewDogGenerator.rdjsonlLints
ReviewDogGenerator.rdjsonlLints(path, null, null);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure how to get and pass the List<FormatterStep> steps and List<List<Lint>> lintsPerStep objects here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this is a bit tricky. I would do it like so

  • ReviewDogGenerator currently takes List<FormatterStep> steps, but it could also take List<String> stepNames
  • Create the stepNames method, and make it the "real" one. The List<FormatterStep> one should just create List<String> and delegate
  • this is because Gradle has some hiccups that make it a lot easier to have a List<String> as an input
  • Add a property to SpotlessCheck and set it up here
    • TaskProvider<SpotlessCheck> checkTask = tasks.register(taskName + CHECK, SpotlessCheck.class, task -> {
      SpotlessTaskImpl source = spotlessTask.get();
      task.setGroup(TASK_GROUP);
      task.init(source);
      task.setEnabled(ideHook.path == null);
      task.dependsOn(source);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a7d515c !

PTAL ⚡️

@YongGooseYongGoose requested a review from nedtwigg May 21, 2025 14:04

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great progress!

Comment on lines +284 to +286
public void setReviewDog(boolean reviewDog) {
this.reviewDog = reviewDog;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the docs:

reviewdogOutput file("${buildDir}/spotless-reviewdog.json")

The boolean you used here is probably better. Consider a multi-project build and each project has both a java and a kotlin reviewdog thing. Probably want something like this, which is easy to do if it's just a boolean flag. Specifying an output file for every task would be very tedious.

app/build/spotless-reviewdog/java/spotless-reviewdog.json
app/build/spotless-reviewdog/kotlin/spotless-reviewdog.json
lib/build/spotless-reviewdog/java/spotless-reviewdog.json
lib/build/spotless-reviewdog/kotlin/spotless-reviewdog.json

And for a successful spotlessCheck, are these files empty? Or do they not exist at all?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5ae21df !

PTAL ⚡️

Comment on lines 99 to 103
if (getReviewDog().get()) {
for (File file : lintsFiles.getFiles()) {
String path = file.getPath();
// TODO : send or save the output of ReviewDogGenerator.rdjsonlLints
ReviewDogGenerator.rdjsonlLints(path, null, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this is a bit tricky. I would do it like so

  • ReviewDogGenerator currently takes List<FormatterStep> steps, but it could also take List<String> stepNames
  • Create the stepNames method, and make it the "real" one. The List<FormatterStep> one should just create List<String> and delegate
  • this is because Gradle has some hiccups that make it a lot easier to have a List<String> as an input
  • Add a property to SpotlessCheck and set it up here
    • TaskProvider<SpotlessCheck> checkTask = tasks.register(taskName + CHECK, SpotlessCheck.class, task -> {
      SpotlessTaskImpl source = spotlessTask.get();
      task.setGroup(TASK_GROUP);
      task.init(source);
      task.setEnabled(ideHook.path == null);
      task.dependsOn(source);

@YongGooseYongGoose requested a review from nedtwigg May 28, 2025 00:26
@YongGoose

@nedtwigg

Hello! Hope you’re having a great day 🙂
When you have some time, would you mind taking a look and sharing your feedback?

@YongGoose

Any updates?

Sign up for free to join this conversation on . Already have an account? Sign in to comment
None yet
None yet

Successfully merging this pull request may close these issues.