diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/AddStopBubblingToLombokConfig.java b/src/main/java/org/openrewrite/java/migrate/lombok/AddStopBubblingToLombokConfig.java new file mode 100644 index 0000000000..36f8eb2b1a --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/AddStopBubblingToLombokConfig.java @@ -0,0 +1,120 @@ +/* + * Copyright 2026 the original author or authors. + *
+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * https://docs.moderne.io/licensing/moderne-source-available-license + *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.migrate.lombok;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.ScanningRecipe;
+import org.openrewrite.SourceFile;
+import org.openrewrite.Tree;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.text.PlainText;
+import org.openrewrite.text.PlainTextParser;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.STOP_BUBBLING;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.append;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.declaresStopBubbling;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.expandImports;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.isConfig;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.isLombokConfig;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.parse;
+
+@Value
+@EqualsAndHashCode(callSuper = false)
+public class AddStopBubblingToLombokConfig extends ScanningRecipe
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.migrate.lombok;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.ScanningRecipe;
+import org.openrewrite.SourceFile;
+import org.openrewrite.Tree;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.text.PlainText;
+import org.openrewrite.text.PlainTextParser;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import static java.util.Comparator.comparingInt;
+import static java.util.Objects.requireNonNull;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.LOMBOK_CONFIG;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.STOP_BUBBLING_KEY;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.append;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.expandImports;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.isConfig;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.isLombokConfig;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.parse;
+import static org.openrewrite.java.migrate.lombok.LombokConfig.resolveImport;
+
+@Value
+@EqualsAndHashCode(callSuper = false)
+public class ConsolidateLombokConfig extends ScanningRecipe
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.migrate.lombok;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.SourceFile;
+import org.openrewrite.binary.Binary;
+import org.openrewrite.quark.Quark;
+import org.openrewrite.remote.Remote;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Shared handling of Lombok's configuration format: which files Lombok reads, how a line of one is read, and how
+ * lines are written back to it.
+ */
+final class LombokConfig {
+
+ static final String LOMBOK_CONFIG = "lombok.config";
+
+ private static final String CONFIG_EXTENSION = ".config";
+
+ /**
+ * Tells Lombok to stop looking at parent directories, so it is scoped to the directory that declares it.
+ */
+ static final String STOP_BUBBLING = "config.stopBubbling";
+
+ static final String STOP_BUBBLING_KEY = normalizeKey(STOP_BUBBLING);
+
+ /**
+ * The line grammar of {@code lombok.core.configuration.ConfigurationParser}; anything else is an invalid line.
+ */
+ private static final Pattern IMPORT = Pattern.compile("import\\s+(.+)");
+
+ private static final Pattern CLEAR = Pattern.compile("clear\\s+([^=]+)");
+
+ private static final Pattern ASSIGNMENT = Pattern.compile("(\\S+?)\\s*([+-]?=)\\s*(.*)");
+
+ private LombokConfig() {
+ }
+
+ enum Kind {
+ BLANK, COMMENT, IMPORT, CLEAR, ASSIGN, ADD, REMOVE, INVALID
+ }
+
+ /**
+ * One line of a {@code lombok.config}, classified the way Lombok classifies it and kept as it was written, so
+ * that merging never has to reformat it.
+ */
+ static class Line {
+ final Kind kind;
+
+ final String text;
+
+ final @Nullable String key;
+
+ final @Nullable String value;
+
+ /**
+ * The key as Lombok compares it; {@code null} for a line that names no key, an {@code import} included, as it
+ * names a path.
+ */
+ final @Nullable String normalizedKey;
+
+ Line(Kind kind, String text, @Nullable String key, @Nullable String value) {
+ this.kind = kind;
+ this.text = text;
+ this.key = key;
+ this.value = value;
+ this.normalizedKey = key == null || kind == Kind.IMPORT ? null : normalizeKey(key);
+ }
+
+ /**
+ * The directive as Lombok compares it, so that {@code key=value}, {@code key = value} and {@code KEY = value}
+ * dedupe against each other; {@code null} for a line that is not a directive.
+ */
+ @Nullable
+ String canonical() {
+ switch (kind) {
+ case ASSIGN:
+ return normalizedKey + "=" + value;
+ case ADD:
+ return normalizedKey + "+=" + value;
+ case REMOVE:
+ return normalizedKey + "-=" + value;
+ default:
+ return null;
+ }
+ }
+ }
+
+ static boolean isLombokConfig(SourceFile sourceFile) {
+ return isConfig(sourceFile) && LOMBOK_CONFIG.equals(sourceFile.getSourcePath().getFileName().toString());
+ }
+
+ /**
+ * Whether a file is one Lombok reads, of its own accord or because another file imports it. Only a file parsed as
+ * text can be read.
+ */
+ static boolean isConfig(SourceFile sourceFile) {
+ return !(sourceFile instanceof Quark || sourceFile instanceof Remote || sourceFile instanceof Binary) &&
+ sourceFile.getSourcePath().getFileName().toString().endsWith(CONFIG_EXTENSION);
+ }
+
+ /**
+ * A key as Lombok compares it. {@code ConfigurationKey.registeredKeys()} is ordered by
+ * {@link String#CASE_INSENSITIVE_ORDER}, so {@code lombok.val.flagUsage} and {@code Lombok.Val.FlagUsage} are the
+ * same key.
+ */
+ static String normalizeKey(String key) {
+ return key.toLowerCase(Locale.ROOT);
+ }
+
+ static List
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.migrate.lombok;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.test.SourceSpecs.text;
+
+class AddStopBubblingToLombokConfigTest implements RewriteTest {
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new AddStopBubblingToLombokConfig());
+ }
+
+ @DocumentExample
+ @Test
+ void addStopBubblingToRootConfig() {
+ rewriteRun(
+ text(
+ """
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ """,
+ """
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigThatAlreadyStopsBubblingIsNotChanged() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigThatTurnsOffStopBubblingIsLeftAlone() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = false
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void stopBubblingSpelledWithADifferentCaseIsNotAddedAgain() {
+ rewriteRun(
+ text(
+ """
+ config.stopbubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void stopBubblingDeclaredByAnImportedFileIsNotAddedAgain() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("shared/base.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigImportingAFileThatIsNotAmongTheSourcesIsLeftAlone() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void importsAreFollowedThroughTheFilesTheyName() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import deeper.config
+ """,
+ spec -> spec.path("shared/base.config")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("shared/deeper.config")
+ )
+ );
+ }
+
+ @Test
+ void importsThatLeadBackAroundLeaveTheRootAlone() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import ../lombok.config
+ """,
+ spec -> spec.path("shared/base.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigsAreNotChanged() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigIsNotTreatedAsTheRootConfig() {
+ rewriteRun(
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void doNothingWhenLombokConfigIsAbsent() {
+ rewriteRun(
+ text(
+ """
+ This is a README file.
+ """,
+ spec -> spec.path("README.md")
+ )
+ );
+ }
+
+ @Test
+ void unrelatedConfigFileIsNotChanged() {
+ rewriteRun(
+ text(
+ """
+ whatever=true
+ """,
+ spec -> spec.path("unrelated.config")
+ )
+ );
+ }
+
+ @Test
+ void emptyRootConfig() {
+ rewriteRun(
+ text(
+ "",
+ """
+ config.stopBubbling = true
+
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigWithoutATrailingNewLine() {
+ rewriteRun(
+ text(
+ "lombok.val.flagUsage = error",
+ """
+ lombok.val.flagUsage = error
+ config.stopBubbling = true""",
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigWithWindowsLineEndingsIsAppendedToWithWindowsLineEndings() {
+ rewriteRun(
+ text(
+ "lombok.val.flagUsage = error\r\nlombok.var.flagUsage = error",
+ "lombok.val.flagUsage = error\r\nlombok.var.flagUsage = error\r\nconfig.stopBubbling = true",
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigIsNotReformatted() {
+ rewriteRun(
+ text(
+ """
+ # Keep the comments and the odd spacing exactly as they are.
+ lombok.val.flagUsage=error
+
+ lombok.var.flagUsage = error
+ """,
+ """
+ # Keep the comments and the odd spacing exactly as they are.
+ lombok.val.flagUsage=error
+
+ lombok.var.flagUsage = error
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+}
diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/ConsolidateLombokConfigTest.java b/src/test/java/org/openrewrite/java/migrate/lombok/ConsolidateLombokConfigTest.java
new file mode 100755
index 0000000000..4e87e80dc0
--- /dev/null
+++ b/src/test/java/org/openrewrite/java/migrate/lombok/ConsolidateLombokConfigTest.java
@@ -0,0 +1,1549 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.migrate.lombok;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+import static org.openrewrite.test.SourceSpecs.text;
+
+class ConsolidateLombokConfigTest implements RewriteTest {
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new ConsolidateLombokConfig());
+ }
+
+ @DocumentExample
+ @Test
+ void mergeNestedConfigsIntoRootConfig() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ lombok.anyConstructor.addConstructorProperties = true
+ lombok.extern.findbugs.addSuppressFBWarnings = true
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ """,
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ lombok.anyConstructor.addConstructorProperties = true
+ lombok.extern.findbugs.addSuppressFBWarnings = true
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ a1=a1value
+ a2=a2value
+ other=foobar
+ b1=b1value
+ b2=b2value
+ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ a1=a1value
+ a2=a2value
+ other=foobar
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ b1=b1value
+ b2=b2value
+ other=foobar
+ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
+ """,
+ doesNotExist(),
+ spec -> spec.path("b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void conflictingDirectiveInNestedConfig() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation = false
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void conflictingDirectivesInNestedConfigs() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation = false
+ """,
+ spec -> spec.path("b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void listDirectivesWithDifferentValuesDoNotConflict() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
+ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
+ """,
+ doesNotExist(),
+ spec -> spec.path("b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void spacingAroundTheOperatorDoesNotPreventDeduplication() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation=true
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void indentedDirectivesAreNormalized() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ lombok.copyableAnnotations += com.example.Ann
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ // The first line is left unindented so that `trimIndent` does not strip the indentation under test.
+ text(
+ """
+ lombok.copyableAnnotations += com.example.Ann
+ lombok.addLombokGeneratedAnnotation = true
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void singleConfigIsNotChanged() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ lombok.anyConstructor.addConstructorProperties = true
+ lombok.extern.findbugs.addSuppressFBWarnings = true
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void ignoreUnrelatedConfigFile() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ whatever=true
+ """,
+ spec -> spec.path("unrelated/unrelated.config")
+ )
+ );
+ }
+
+ @Test
+ void doNothingWhenLombokConfigIsAbsent() {
+ rewriteRun(
+ text(
+ """
+ This is a README file.
+ """,
+ spec -> spec.path("README.md")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigIsNotReformatted() {
+ rewriteRun(
+ text(
+ """
+ # Lombok configuration for this project.
+
+ # val is fine, var is not.
+ lombok.val.flagUsage = allow
+ lombok.var.flagUsage=error
+
+ config.stopBubbling = true
+ """,
+ """
+ # Lombok configuration for this project.
+
+ # val is fine, var is not.
+ lombok.val.flagUsage = allow
+ lombok.var.flagUsage=error
+
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigImportingAFileThatIsNotAmongTheSourcesIsLeftAlone() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void importInRootConfigIsPreservedAtTheTopOfTheFile() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ """,
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("shared/base.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void directiveTheRootImportsAlreadyDeclaresIsNotRepeated() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("shared/base.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void directiveConflictingWithWhatTheRootImportsAbortsTheRecipe() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = warning
+ """,
+ spec -> spec.path("shared/base.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void importsAreFollowedThroughTheFilesTheyName() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import more.config
+ """,
+ spec -> spec.path("shared/base.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("shared/more.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void importsThatLeadBackAroundAbortTheRecipe() {
+ rewriteRun(
+ text(
+ """
+ import shared/base.config
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import ../lombok.config
+ """,
+ spec -> spec.path("shared/base.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void configImportedByAnotherConfigIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import ../b/lombok.config
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("b/lombok.config")
+ ),
+ text(
+ """
+ lombok.var.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("c/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void clearInRootConfigIsPreserved() {
+ rewriteRun(
+ text(
+ """
+ clear lombok.copyableAnnotations
+ config.stopBubbling = true
+ """,
+ """
+ clear lombok.copyableAnnotations
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void linesLombokCannotReadArePreserved() {
+ rewriteRun(
+ text(
+ """
+ this is not a directive
+ config.stopBubbling = true
+ """,
+ """
+ this is not a directive
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigDeclaringStopBubblingIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("generated/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigTurningOffStopBubblingIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ config.stopBubbling = false
+ a=aValue
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedImportIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import base.config
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedClearIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ clear lombok.copyableAnnotations
+ lombok.copyableAnnotations += com.example.Ann
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedRemoveIsLeftInPlaceBecauseItsOrderMatters() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations -= com.example.Ann
+ lombok.copyableAnnotations += com.example.Ann
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedLinesLombokCannotReadAreLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ this is not a directive
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigWithNothingToHoistIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ # Nothing to see here.
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedCommentsAreHoistedWithTheirDirectives() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ # Generated sources need the annotation.
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ # Generated sources need the annotation.
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void commentAtTheEndOfANestedConfigIsCarriedOver() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ # TODO revisit once module a no longer uses val.
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ # TODO revisit once module a no longer uses val.
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void commentAtTheEndOfARedundantNestedConfigIsCarriedOver() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ # TODO revisit once module a no longer uses val.
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ # TODO revisit once module a no longer uses val.
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void additionsOfADirectoryAboveAnotherAreAppendedFirstToKeepTheirOrderInTheList() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.copyableAnnotations += com.example.A
+ lombok.copyableAnnotations += com.example.B
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += com.example.A
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += com.example.B
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootClearOfAKeyBlocksHoistingThatKey() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ clear lombok.copyableAnnotations
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += com.example.Ann
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootRemovalOfAValueBlocksHoistingThatKey() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.copyableAnnotations -= com.example.Ann
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += com.example.Ann
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void redundantNestedConfigIsDeletedWithoutChangingTheRoot() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void deeplyNestedConfigsAreMergedInAPredictableOrder() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ b=bValue
+ a=aValue
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ a=aValue
+ """,
+ doesNotExist(),
+ spec -> spec.path("modules/z/deep/lombok.config")
+ ),
+ text(
+ """
+ b=bValue
+ """,
+ doesNotExist(),
+ spec -> spec.path("modules/a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigsAreLeftInPlaceWithoutARootConfig() {
+ rewriteRun(
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("module/lombok.config")
+ ),
+ text(
+ """
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("module/nested/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nothingIsChangedWhenNothingCanBeHoisted() {
+ rewriteRun(
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import base.config
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void hoistableAndUnhoistableNestedConfigsAreHandledIndependently() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ b=bValue
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import base.config
+ a=aValue
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ b=bValue
+ """,
+ doesNotExist(),
+ spec -> spec.path("b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void commentSyntaxLombokDoesNotSupportIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ // Lombok comments start with a hash, so Lombok cannot read this line.
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigWithOnlyBlankLinesIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+
+
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void blankLinesInNestedConfigsAreNotHoisted() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+
+ lombok.var.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void spacingAroundTheListOperatorDoesNotPreventDeduplication() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.copyableAnnotations += com.example.Ann
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations+=com.example.Ann
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void valueContainingAnEqualsSignIsReadAsPartOfTheValue() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.nonNull.exceptionType = a=b
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.nonNull.exceptionType=a=b
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void emptyRootConfig() {
+ rewriteRun(
+ text(
+ "",
+ """
+ lombok.val.flagUsage = error
+
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigWithoutATrailingNewLine() {
+ rewriteRun(
+ text(
+ "config.stopBubbling = true",
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error""",
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigWithWindowsLineEndingsIsAppendedToWithWindowsLineEndings() {
+ rewriteRun(
+ text(
+ "config.stopBubbling = true\r\nlombok.var.flagUsage = error",
+ "config.stopBubbling = true\r\nlombok.var.flagUsage = error\r\nlombok.val.flagUsage = error",
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void rootConfigEndingWithABlankLineKeepsItsTrailingNewLine() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigUnderAStopBubblingAncestorIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("generated/lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("generated/nested/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void commentIsNotHoistedWhenItsDirectiveIsAlreadyDeclaredByTheRoot() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ # val is banned here too.
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void keyAssignedTwiceInOneFileIsNotAConflict() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = allow
+ lombok.val.flagUsage = error
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = allow
+ lombok.val.flagUsage = error
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void hoistingContinuesBelowADirectoryThatTurnsOffStopBubbling() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ config.stopBubbling = false
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.var.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/nested/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void onlyCommentsWhoseDirectiveIsHoistedAreCarriedOver() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ # var is banned here as well.
+ lombok.var.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ # val is banned everywhere.
+ lombok.val.flagUsage = error
+ # var is banned here as well.
+ lombok.var.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void keyAssignedTwiceInANestedConfigKeepsOnlyTheAssignmentThatWins() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = allow
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void assignmentSupersededInItsOwnFileDoesNotOutrankTheRoot() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = warning
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void assignmentSupersededInItsOwnFileDoesNotOutrankAnotherNestedConfig() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = warning
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedStopBubblingSpelledWithADifferentCaseIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ config.stopbubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void conflictingValuesForAKeySpelledWithADifferentCaseAbortTheRecipe() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.addLombokGeneratedAnnotation = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.addlombokgeneratedannotation = false
+ """,
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void keySpelledWithADifferentCaseIsRecognizedAsAlreadyDeclaredByTheRoot() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ Lombok.Val.FlagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigShadowedByAnAncestorAssignmentIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ clear lombok.copyableAnnotations
+ lombok.fieldDefaults.defaultFinal = false
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.fieldDefaults.defaultFinal = true
+ """,
+ spec -> spec.path("a/b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigWhoseAdditionAnAncestorClearsIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ clear lombok.copyableAnnotations
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.copyableAnnotations += com.example.Ann
+ """,
+ spec -> spec.path("a/b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigBelowAnAncestorThatImportsIsLeftInPlace() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ import base.config
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void nestedConfigIsHoistedWhenAnAncestorLeftInPlaceSaysTheSameThing() {
+ rewriteRun(
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ clear lombok.copyableAnnotations
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("a/lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("a/b/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void configWithNoJavaSourceBelowItIsLeftInPlace() {
+ rewriteRun(
+ java(
+ """
+ class Foo {
+ }
+ """,
+ spec -> spec.path("src/main/java/Foo.java")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("src/test/resources/fixtures/lombok.config")
+ )
+ );
+ }
+
+ @Test
+ void configGoverningTestSourcesIsHoisted() {
+ rewriteRun(
+ java(
+ """
+ package com.foo;
+
+ class Foo {
+ }
+ """,
+ spec -> spec.path("src/test/java/com/foo/Foo.java")
+ ),
+ text(
+ """
+ config.stopBubbling = true
+ """,
+ """
+ config.stopBubbling = true
+ lombok.val.flagUsage = error
+ """,
+ spec -> spec.path("lombok.config")
+ ),
+ text(
+ """
+ lombok.val.flagUsage = error
+ """,
+ doesNotExist(),
+ spec -> spec.path("src/test/java/com/foo/lombok.config")
+ )
+ );
+ }
+}
> leftInPlaceAbove(Path path,
+ SortedMap
> leftInPlace = new ArrayList<>();
+ for (Path directory = requireNonNull(path.getParent()).getParent(); directory != null; directory = directory.getParent()) {
+ Path config = directory.resolve(LOMBOK_CONFIG);
+ List
> leftInPlaceAbove) {
+ Set
> files = new ArrayList<>();
+ files.add(rootLines);
+ files.addAll(hoistable.values());
+
+ Map