|
17 | 17 |
|
18 | 18 | import lombok.RequiredArgsConstructor; |
19 | 19 | import org.jspecify.annotations.Nullable; |
| 20 | +import org.openrewrite.Cursor; |
20 | 21 | import org.openrewrite.ExecutionContext; |
21 | 22 | import org.openrewrite.Tree; |
22 | 23 | import org.openrewrite.marker.Markers; |
|
27 | 28 | import org.openrewrite.xml.tree.Xml; |
28 | 29 |
|
29 | 30 | import java.util.ArrayList; |
| 31 | +import java.util.Comparator; |
30 | 32 | import java.util.List; |
31 | 33 | import java.util.concurrent.atomic.AtomicBoolean; |
32 | | -import java.util.stream.IntStream; |
33 | 34 |
|
34 | 35 | import static java.util.Collections.emptyList; |
35 | 36 |
|
@@ -138,69 +139,113 @@ public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { |
138 | 139 | "</dependency>" |
139 | 140 | ); |
140 | 141 |
|
141 | | - doAfterVisit(new AddToTagVisitor<>(tag, dependencyTag, |
| 142 | + // Add the dependency with optional comment in a single operation to ensure |
| 143 | + // the comment stays with its associated dependency regardless of sorting order |
| 144 | + doAfterVisit(new AddDependencyWithCommentVisitor(tag, dependencyTag, because, |
142 | 145 | new InsertDependencyComparator(tag.getContent() == null ? emptyList() : tag.getContent(), dependencyTag))); |
143 | | - |
144 | | - // If because is provided, add a visitor to prepend the comment before the dependency |
145 | | - if (because != null) { |
146 | | - doAfterVisit(new AddCommentBeforeDependency(groupId, artifactId, because)); |
147 | | - } |
148 | 146 | } |
149 | | - return super. |
150 | | - |
151 | | - visitTag(tag, ctx); |
| 147 | + return super.visitTag(tag, ctx); |
152 | 148 | } |
153 | 149 | } |
154 | 150 |
|
155 | 151 | @RequiredArgsConstructor |
156 | | - private static class AddCommentBeforeDependency extends MavenIsoVisitor<ExecutionContext> { |
157 | | - private final String groupId; |
158 | | - private final String artifactId; |
| 152 | + private static class AddDependencyWithCommentVisitor extends MavenIsoVisitor<ExecutionContext> { |
| 153 | + private final Xml.Tag scope; |
| 154 | + private final Xml.Tag dependencyTag; |
| 155 | + @Nullable |
159 | 156 | private final String because; |
| 157 | + private final Comparator<Content> tagComparator; |
160 | 158 |
|
161 | 159 | @Override |
162 | | - public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { |
163 | | - if (MANAGED_DEPENDENCIES_MATCHER.matches(getCursor()) && tag.getContent() != null) { |
164 | | - List<Content> contents = new ArrayList<>(tag.getContent()); |
165 | | - |
166 | | - return IntStream.range(0, contents.size()) |
167 | | - .filter(i -> contents.get(i) instanceof Xml.Tag) |
168 | | - .mapToObj(i -> new IndexedTag(i, (Xml.Tag) contents.get(i))) |
169 | | - .filter(pair -> matchesDependency(pair.tag, groupId, artifactId)) |
170 | | - .findFirst() |
171 | | - .map(pair -> addComment(tag, contents, pair)) |
172 | | - .orElse(tag); |
| 160 | + public Xml.Tag visitTag(Xml.Tag t, ExecutionContext ctx) { |
| 161 | + if (scope.isScope(t)) { |
| 162 | + t = ensureClosingTag(t, ctx); |
| 163 | + Xml.Tag formattedDependencyTag = formatDependencyTag(ctx); |
| 164 | + |
| 165 | + List<Content> content = t.getContent() == null ? new ArrayList<>() : new ArrayList<>(t.getContent()); |
| 166 | + int insertIndex = findInsertIndex(content); |
| 167 | + |
| 168 | + if (because != null) { |
| 169 | + addComment(content, insertIndex, because, formattedDependencyTag.getPrefix()); |
| 170 | + addDependency(content, insertIndex + 1, formattedDependencyTag); |
| 171 | + } else { |
| 172 | + addDependency(content, insertIndex, formattedDependencyTag); |
| 173 | + } |
| 174 | + |
| 175 | + t = t.withContent(content); |
173 | 176 | } |
174 | | - return super.visitTag(tag, ctx); |
| 177 | + |
| 178 | + return super.visitTag(t, ctx); |
| 179 | + } |
| 180 | + |
| 181 | + /** |
| 182 | + * Ensures the tag has a proper closing tag (not self-closing) with a newline prefix. |
| 183 | + */ |
| 184 | + private Xml.Tag ensureClosingTag(Xml.Tag t, ExecutionContext ctx) { |
| 185 | + if (t.getClosing() == null) { |
| 186 | + t = t.withClosing(autoFormat(new Xml.Tag.Closing(Tree.randomId(), "\n", |
| 187 | + Markers.EMPTY, t.getName(), ""), null, ctx, getCursor())) |
| 188 | + .withBeforeTagDelimiterPrefix(""); |
| 189 | + } |
| 190 | + if (!t.getClosing().getPrefix().contains("\n")) { |
| 191 | + t = t.withClosing(t.getClosing().withPrefix("\n")); |
| 192 | + } |
| 193 | + return t; |
175 | 194 | } |
176 | 195 |
|
177 | | - private Xml.Tag addComment(Xml.Tag tag, List<Content> contents, IndexedTag pair) { |
178 | | - // Extract the prefix (should be like "\n ") |
179 | | - String prefix = pair.tag.getPrefix(); |
| 196 | + /** |
| 197 | + * Formats the dependency tag with proper newline prefix and indentation. |
| 198 | + */ |
| 199 | + private Xml.Tag formatDependencyTag(ExecutionContext ctx) { |
| 200 | + Xml.Tag formatted = dependencyTag; |
| 201 | + if (!formatted.getPrefix().contains("\n")) { |
| 202 | + formatted = formatted.withPrefix("\n"); |
| 203 | + } |
| 204 | + return autoFormat(formatted, null, ctx, getCursor()); |
| 205 | + } |
180 | 206 |
|
181 | | - // Create comment with the same prefix as the dependency tag |
182 | | - // Add spaces around the comment text for proper XML formatting |
| 207 | + /** |
| 208 | + * Find the insertion position by comparing only with dependency tags. |
| 209 | + * This ensures we don't insert between a comment and its associated dependency. |
| 210 | + */ |
| 211 | + private int findInsertIndex(List<Content> content) { |
| 212 | + for (int i = 0; i < content.size(); i++) { |
| 213 | + Content item = content.get(i); |
| 214 | + // Only compare with dependency tags, skip comments |
| 215 | + if (item instanceof Xml.Tag && tagComparator.compare(item, dependencyTag) > 0) { |
| 216 | + // Found a dependency that should come after ours. |
| 217 | + // We want to insert before this dependency AND any preceding comments. |
| 218 | + return findInsertPositionBeforePrecedingComments(content, i); |
| 219 | + } |
| 220 | + } |
| 221 | + return content.size(); |
| 222 | + } |
| 223 | + |
| 224 | + private void addComment(List<Content> content, int insertIndex, String because, String prefix) { |
183 | 225 | Xml.Comment comment = new Xml.Comment( |
184 | 226 | Tree.randomId(), |
185 | 227 | prefix, |
186 | 228 | Markers.EMPTY, |
187 | 229 | " " + because + " " |
188 | 230 | ); |
| 231 | + content.add(insertIndex, comment); |
| 232 | + } |
189 | 233 |
|
190 | | - // Insert comment before the dependency |
191 | | - contents.add(pair.index, comment); |
192 | | - |
193 | | - // Update the dependency to have the same prefix (newline + indentation) |
194 | | - // so it appears on the next line after the comment |
195 | | - contents.set(pair.index + 1, pair.tag.withPrefix(prefix)); |
196 | | - |
197 | | - return tag.withContent(contents); |
| 234 | + private void addDependency(List<Content> content, int insertIndex, Xml.Tag dependencyTag) { |
| 235 | + content.add(insertIndex, dependencyTag); |
198 | 236 | } |
199 | 237 |
|
200 | | - @RequiredArgsConstructor |
201 | | - private static class IndexedTag { |
202 | | - final int index; |
203 | | - final Xml.Tag tag; |
| 238 | + /** |
| 239 | + * Given an index of a dependency tag, find the insertion position that's before |
| 240 | + * any preceding comments that belong to that dependency. |
| 241 | + */ |
| 242 | + private int findInsertPositionBeforePrecedingComments(List<Content> content, int tagIndex) { |
| 243 | + int insertPos = tagIndex; |
| 244 | + // Walk backwards from the tag, skipping any immediately preceding comments |
| 245 | + while (insertPos > 0 && content.get(insertPos - 1) instanceof Xml.Comment) { |
| 246 | + insertPos--; |
| 247 | + } |
| 248 | + return insertPos; |
204 | 249 | } |
205 | 250 | } |
206 | 251 | } |
0 commit comments