openrewrite / rewrite

Automated mass refactoring of source code.
https://docs.openrewrite.org
Apache License 2.0
1.98k stars 293 forks source link

Change to Rewrite's Maven Resolution & Semantic Model #1373

Closed josemariavillar closed 2 years ago

josemariavillar commented 2 years ago

Good afternoon,

I have upgraded to version 7.18.0-SNAPSHOT to have access to the latest developments and I have started to get compilation errors in my own recipes as the Maven.java class has disappeared from rewrite-maven/src/main/java/org/openrewrite/maven/tree/. Should they be replaced by another class? and if not, how should I access the Maven object from now on?

Thank you very much for everything

tkvangorder commented 2 years ago

Hi @josemariavillar ,

So previously, Maven was just extending an Xml.Document.

During the refactoring, we realized that we were not getting much by extending Xml.Document. Therefore, we removed this class and now the MavenVisitor is just an Xml.Visitor that operates on an Xml.Document that just happens to have the MavenResolutionResults as a marker.

So, to answer your question, anywhere you were using Maven, replace that with Xml.Document, and visitMaven becomes visitDocument.

Here is an example of what this refactoring looks like:

https://github.com/openrewrite/rewrite-spring/commit/2a64e71ad14ace1e94559e1e0540d03d85ad5e1b#diff-52c7dfd8c6cc4c7a1e2a29064cf0966d66c4d9451a15152987fb1acc80b9f10aR64-R69

There is also a MavenVisitor.getResolutionResult() that will return the semantic model for the Maven XML document.

If you were using an Maven class outside the context of a MavenVisitor, you will still change this to Xml.Document and you can get access to the model by looking for the marker on the class :

Xml.Document maven = .....

MavenResolutionResult model = maven.getMarkers().findFirst(MavenResolutionResult.class)
                .orElseThrow(() -> new IllegalStateException("Maven visitors should not be visiting XML documents without a Maven marker"));
tkvangorder commented 2 years ago

We had a good a couple of additional good questions from @josemariavillar (thank you!) :

How could I check now if a transitive dependency exists?

Each Pom.xml has a marker on it called MavenResolutionResult, this model object has a reference to the ResolvedPom and it also has a Map<Scope, List<ResolvedDependency>> which represents the list of resolved dependencies for the given scope. The list of dependencies in each scope represents all dependencies (including transitive dependencies) and there is no need to navigate to any parents.

Additionally, the list of dependencies under each scope will follow the rules defined in the maven documentation :

As an example:

List<ResolvedDependency> dependencies = results.getDependencies().get(Scope.Test)

The list of dependencies will include all dependencies that are in test scope, compile scope, and runtime scope. This also means that the same dependency may be duplicated within the resolve dependencies map.

tkvangorder commented 2 years ago

How do you populate the Maven settings file within the ExecutionContext while writing unit tests?

There is a MavenExecutionContextView that acts as a wrapper around an existing execution context that provides some helper methods for setting messages on the context that are used by the underlying Maven infrastructure.

If you are extending RecipeTest, you can override the RecipeTest.getExecutionContext() method and provide a context that includes various maven settings:

    override val executionContext: ExecutionContext
        get() = MavenExecutionContextView(super.executionContext).apply {
            setMavenSettings(MavenSettings.parse(Parser.Input(Paths.get("settings.xml")) {//language=xml
                """
                <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
                    <!-- define settings here -->
                </settings>
                """.trimIndent().byteInputStream()
            }, this))
        }

Alternatively, you can use the finer-grained setters that are provided:

    override val executionContext: ExecutionContext
        get() = MavenExecutionContextView(super.executionContext).apply {
            mirrors = listOf(
                MavenRepositoryMirror("custom", "https://localhost:8443/repository", "central", true, true))
        }

Finally, if you only want to add maven settings for a specific test, you can override the named parameter used by assertChanged/assertUnchanged:

    fun myTest() = assertChanged(
        executionContext = MavenExecutionContextView(super.executionContext).apply {
            mirrors = listOf(
                MavenRepositoryMirror("custom", "https://localhost:8443/repository", "central", true, true))
        },
        before = """
        """,
        ...
tkvangorder commented 2 years ago

For those that run into serialization errors similar to the following when using rewrite's maven plugin:

Failed to parse pom: Cannot construct instance of `org.openrewrite.maven.tree.ProfileActivation` (although at least one Creator exists): no default no-argument constructor found

This can happen when older versions of rewrite have stored serialized representations of the pom files into the Rockdb cache.

The solution to this problem is to remove all contents from the directory: ~/.rewrite-cache

The next time you run the maven plugin, rewrite will recreate the cache using the new serialization format.