Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start KSP dual implementation #722

Merged
merged 18 commits into from
Aug 1, 2023
Merged

Conversation

ZacSweers
Copy link
Collaborator

@ZacSweers ZacSweers commented Jul 7, 2023

This implements dual support for anvil code gen and KSP code gen using ContributesBinding and AnvilAnnotationDetectorCheck.

The general design is like this

object {Feature}CodeGen {

  <impl>

  @AutoService(CodeGenerator::class)
  class EmbeddedGenerator { ... }

  class KspGenerator {
    <impl>
     
    @AutoService(SymbolProcessorProvider::class)
    class Provider { ... }
  }
}

Where the common code gen implementation lives in the parent object and frontend-specific impls live in nested classes. The impl is agnostic to KSP and Anvil+PSI.

To support this, I've also added new testing support to anvil compilation tests for KSP. This primarily runs through a new AnvilCompilationMode API, which supports two modes: Embedded (the traditional anvil way) and Ksp. This PR's a little larger due to this ground work, but future PRs should be simpler as we continue to replicate some of these common utilities and infra.

@ZacSweers ZacSweers changed the title Demo KSP dual implementation Start KSP dual implementation w/ ContributesBinding code gen. Jul 29, 2023
)
addAll(mode.symbolProcessorProviders)
}
kspWithCompilation = true
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is how we force KSP to compile in-process during the kotlinc invocation, just for testing.

Copy link
Member

Choose a reason for hiding this comment

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

This is how we force KSP to compile in-process during the kotlinc invocation, just for testing.

Maybe worth adding this as a code comment? Unless that's already documented on the KCT side

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's on the kdoc iirc

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added one in d51fc90 (#722)

Comment on lines +85 to +103
pluginOptions +=
listOf(
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "src-gen-dir",
optionValue = File(workingDir, "build/anvil").absolutePath
),
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "generate-dagger-factories",
optionValue = generateDaggerFactories.toString()
),
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "generate-dagger-factories-only",
optionValue = generateDaggerFactoriesOnly.toString()
)
)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I gated these off now that they may not be necessary in all compilations

@@ -61,6 +65,13 @@ class AnvilCommandLineProcessor : CommandLineProcessor {
"@MergeComponent or @MergeSubcomponent.",
required = false,
allowMultipleOccurrences = false
),
CliOption(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added this option so we could correctly gate the modes, but have not wired it up in the gradle plugin yet. I figure we can wait on that until everything's fully implemented

@ZacSweers ZacSweers changed the title Start KSP dual implementation w/ ContributesBinding code gen. Start KSP dual implementation Jul 30, 2023
@@ -0,0 +1,9 @@
package com.squareup.anvil.compiler.api

public interface AnvilApplicabilityChecker {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The goal with this is to be able to hoist up the applicability checks to shared locations

Copy link
Member

@JoelWilcox JoelWilcox left a comment

Choose a reason for hiding this comment

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

High level looks great so far!

I wasn't quite able to make it through the whole review yet, leaving some initial questions and comments for now.

@@ -34,7 +38,7 @@ class AnvilCommandLineProcessor : CommandLineProcessor {
optionName = srcGenDirName,
valueDescription = "<file-path>",
description = "Path to directory in which Anvil specific code should be generated",
required = true,
required = false,
Copy link
Member

Choose a reason for hiding this comment

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

Why is this optional now? Do we lose the ability to configure this with KSP?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We don't need it at all in KSP

),
CliOption(
optionName = compilationModeName,
valueDescription = "<embedded|ksp>",
Copy link
Member

Choose a reason for hiding this comment

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

Could you add constants for both of these values? Looks like we could probably utilize the existing types you created in AnvilCompilationMode or at least house the values there if we don't want them directly tied to the Type names. Just wanna make sure these can all be updated in one place if they change since they're already referenced in a couple places (CommandLineOptions, AnvilComponentRegistrar, and based on the enum name in AnvilCompilation)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Extracted in 4ed03de

@@ -77,6 +88,8 @@ class AnvilCommandLineProcessor : CommandLineProcessor {
configuration.put(generateDaggerFactoriesOnlyKey, value.toBoolean())
disableComponentMergingName ->
configuration.put(disableComponentMergingKey, value.toBoolean())
compilationModeName ->
configuration.put(compilationModeKey, value)
Copy link
Member

Choose a reason for hiding this comment

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

Do you know if CommandLineProcessor automatically validates inputs based on the provided value description? I didn't see anything from a quick search, but if it's not handled automatically I think we should add a quick check that the input matches one of the expected values.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does not validate

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added some + a stronger typed parsing in 4ed03de

)
addAll(mode.symbolProcessorProviders)
}
kspWithCompilation = true
Copy link
Member

Choose a reason for hiding this comment

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

This is how we force KSP to compile in-process during the kotlinc invocation, just for testing.

Maybe worth adding this as a code comment? Unless that's already documented on the KCT side

}
kspWithCompilation = true
kspArgs["generate-dagger-factories"] = generateDaggerFactories.toString()
kspArgs["generate-dagger-factories-only"] = generateDaggerFactoriesOnly.toString()
Copy link
Member

Choose a reason for hiding this comment

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

Why do we not use PluginOption for these still / why is there this API difference for KSP?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

KSP uses its own API yeah. Modeled after how annotation processing options are

block: Result.() -> Unit = { },
): Result {
block: KotlinCompilation.Result.() -> Unit = { },
): KotlinCompilation.Result {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Any reason to make these Result usages more qualified? I see we're still importing com.tschuchort.compiletesting.KotlinCompilation.Result and there are other usages left unqualified. I'm good with either style but I think it's worth keeping them consistent

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because kotlin.Result is an implicit import and they cause issues. This is why I changed the name (and updated in the KCT update PR)

}

@AutoService(CodeGenerator::class)
internal class AnvilGenerator : CodeGenerator {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about matching this up with the compilation mode naming?

Suggested change
internal class AnvilGenerator : CodeGenerator {
internal class EmbeddedGenerator : CodeGenerator {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good to me 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in 074d5cb (#722)


override fun processChecked(resolver: Resolver): List<KSAnnotated> {
resolver
.getSymbolsWithAnnotation(contributesBindingFqName.asString())
Copy link
Member

Choose a reason for hiding this comment

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

Starting to get familiar with the KSP apis as part of this review and I see there's also Resolver#getAllFiles() used in some of the samples, which looks very similar to our previous logic around iterating over all project files. Does getSymbolsWithAnnotation handle checking all files as well as inner classes that may be annotated? It seems that way but just want to clarify

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah this API is the preferred one and handles all that logic for us in a heavily cached and optimized way

Comment on lines +100 to +102
env.logger.error(
"Only classes can be annotated with @ContributesBinding.", annotated
)
Copy link
Member

Choose a reason for hiding this comment

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

Nice

spec.writeTo(
env.codeGenerator,
aggregating = false,
originatingKSFiles = listOf(clazz.containingFile!!)
Copy link
Member

Choose a reason for hiding this comment

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

It's awesome that this is already built into the API (for both KSP and KotlinPoet) 🙌

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup one of the first things I implemented for KP's KSP support 😁

import com.squareup.anvil.compiler.qualifierFqName
import org.jetbrains.kotlin.name.FqName

internal fun <T : KSAnnotation> List<T>.checkNoDuplicateScopeAndBoundType(
Copy link
Member

Choose a reason for hiding this comment

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

Part of me is wondering if we should add some light layer of abstraction to share the larger utility extensions between ksp and embedded/reference impls, but I think this is fine for now and we can do some combining later if it there ends up being enough duplication to be worth it.

@@ -13,6 +13,10 @@ dependencyResolutionManagement {
String keyString = key.toString()
if (keyString.startsWith("override_")) {
overrides.put(keyString, value.toString())
if (keyString.endsWith("_kotlin")) {
// TODO hardcoded to match what's in libs.versions.toml, but kinda ugly
Copy link
Member

Choose a reason for hiding this comment

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

No need to change in this PR but in a followup could you add more detail around why we need to do this / when it will no longer be necessary? Is this to work around Kotlin 1.8 support?

@JoelWilcox JoelWilcox merged commit 2545677 into square:main Aug 1, 2023
19 checks passed
@ZacSweers ZacSweers mentioned this pull request Sep 27, 2023
15 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants