From f27e778563d0223e9e1b8e6dc189801c29de95e8 Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Thu, 14 Dec 2023 21:02:50 -0500 Subject: [PATCH] Support Locating AST elements by Line/Column Signed-off-by: Jonathan Leitschuh --- .../analysis/util/CoordinateLocator.java | 214 ++++++++++++++++++ .../analysis/util/package-info.java | 25 ++ .../analysis/util/CoordinateLocatorTest.java | 190 ++++++++++++++++ 3 files changed, 429 insertions(+) create mode 100644 src/main/java/org/openrewrite/analysis/util/CoordinateLocator.java create mode 100644 src/main/java/org/openrewrite/analysis/util/package-info.java create mode 100644 src/test/java/org/openrewrite/analysis/util/CoordinateLocatorTest.java diff --git a/src/main/java/org/openrewrite/analysis/util/CoordinateLocator.java b/src/main/java/org/openrewrite/analysis/util/CoordinateLocator.java new file mode 100644 index 000000000..0f60e94ea --- /dev/null +++ b/src/main/java/org/openrewrite/analysis/util/CoordinateLocator.java @@ -0,0 +1,214 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.analysis.util; + +import fj.data.Option; +import lombok.RequiredArgsConstructor; +import org.openrewrite.Cursor; +import org.openrewrite.Incubating; +import org.openrewrite.PrintOutputCapture; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaPrinter; +import org.openrewrite.java.tree.Comment; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.java.tree.Space; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +@Incubating(since = "2.1.6") +public class CoordinateLocator { + + /** + * Find the first element in the AST at the given line and column. + *

+ * NOTE: line and column numbers are 1-based, which matches the behavior of most editors. + *

+ * + * @param sourceFile The source file to search. + * @param line The line number to search. 1-based. + * @param column The column number to search. 1-based. + * @return The first element found at the given line and column, or {@link Option#none()} if no element was found. + */ + public static Option findCoordinate(JavaSourceFile sourceFile, int line, int column) { + if (line < 1 || column < 1) { + throw new IllegalArgumentException("Line and column numbers must be 1-based"); + } + AtomicReference found = new AtomicReference<>(); + CoordinateLocatorVisitor locatorVisitor = new CoordinateLocatorVisitor<>(line, column, found); + locatorVisitor.visit( + sourceFile, + locatorVisitor.new CoordinateLocatorPrinter(0), new Cursor(null, "root") + ); + return Option.fromNull(found.get()); + } + + + @RequiredArgsConstructor + private static class CoordinateLocatorVisitor

extends JavaPrinter

{ + private final int line; + private final int column; + private final AtomicReference found; + private int foundLineNumber = 1; + private int foundColumnNumber = 1; + private boolean foundLine = false; + private boolean foundColumn = false; + + @Override + public @Nullable J preVisit(J tree, PrintOutputCapture

printOutputCapture) { + if (foundLine && foundColumn && found.get() == null) { + found.set(tree); + stopAfterPreVisit(); + return tree; + } + return tree; + } + + class CoordinateLocatorPrinter extends PrintOutputCapture

{ + + public CoordinateLocatorPrinter(P p) { + super(p); + } + + @Override + public PrintOutputCapture

append(@Nullable String text) { + if (found.get() != null) { + // Optimization to avoid printing the rest of the file once we've found the element + return this; + } + if (text == null) { + return this; + } + for (int i = 0; i < text.length(); i++) { + append(text.charAt(i)); + } + return this; + } + + @Override + public PrintOutputCapture

append(char c) { + if (found.get() != null) { + // Optimization to avoid printing the rest of the file once we've found the element + return this; + } + if (isNewLine(c)) { + if (foundLine && !foundColumn) { + throw new IllegalStateException("Found line " + line + " but did not find column " + column); + } + foundLineNumber++; + } + if (foundLineNumber == line) { + foundLine = true; + } + if (foundLine && !isNewLine(c)) { + foundColumnNumber++; + } + if (foundLine && foundColumnNumber == column) { + foundColumn = true; + } + // Actually appending isn't necessary + return this; + } + } + } + + /** + * Find all elements in the AST at the given line. + *

+ * NOTE: line number is 1-based, which matches the behavior of most editors. + *

+ * + * @param sourceFile The source file to search. + * @param line The line number to search. 1-based. + * @return The elements found at the given line, or an empty collection if no elements were found. + */ + public static Collection findLine(JavaSourceFile sourceFile, int line) { + if (line < 1) { + throw new IllegalArgumentException("Line numbers must be 1-based"); + } + Set found = Collections.newSetFromMap(new IdentityHashMap<>()); + LineLocator locatorVisitor = new LineLocator<>(line, found); + locatorVisitor.visit( + sourceFile, + locatorVisitor.new LineLocatorPrinter(0), new Cursor(null, "root") + ); + return Collections.unmodifiableSet(found); + } + + @RequiredArgsConstructor + private static class LineLocator

extends JavaPrinter

{ + private final int line; + private final Set found; + private int foundLineNumber = 1; + + private boolean foundLine() { + return foundLineNumber == line; + } + + @Override + public @Nullable J preVisit(J tree, PrintOutputCapture

pPrintOutputCapture) { + if (tree.getPrefix().getWhitespace().chars().anyMatch(CoordinateLocator::isNewLine)) { + // If the element has a newline prefix, then it's on a new line + return tree; + } + if (tree.getPrefix().getComments().stream().anyMatch(Comment::isMultiline)) { + // If the element has a multiline comment prefix, then it's on a new line + return tree; + } + if (foundLine()) { + found.add(tree); + } + if (foundLineNumber > line) { + // Optimization to avoid visiting the rest of the file once we've found the element + stopAfterPreVisit(); + return tree; + } + return tree; + } + + class LineLocatorPrinter extends PrintOutputCapture

{ + + public LineLocatorPrinter(P p) { + super(p); + } + + @Override + public PrintOutputCapture

append(@Nullable String text) { + if (text == null) { + return this; + } + for (int i = 0; i < text.length(); i++) { + append(text.charAt(i)); + } + return this; + } + + @Override + public PrintOutputCapture

append(char c) { + if (isNewLine(c)) { + foundLineNumber++; + } + // Actually appending isn't necessary + return this; + } + } + } + + private static boolean isNewLine(int c) { + return c == '\n' || c == '\r'; + } +} diff --git a/src/main/java/org/openrewrite/analysis/util/package-info.java b/src/main/java/org/openrewrite/analysis/util/package-info.java new file mode 100644 index 000000000..1aa0bd322 --- /dev/null +++ b/src/main/java/org/openrewrite/analysis/util/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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. + */ +/** + * This package is used to analyze the dataflow through a program. + *

+ * The primary entry point for interacting with this logic is + * {@link org.openrewrite.analysis.dataflow.Dataflow#startingAt(org.openrewrite.Cursor)}. + */ +@NonNullApi +package org.openrewrite.analysis.util; + +import org.openrewrite.internal.lang.NonNullApi; diff --git a/src/test/java/org/openrewrite/analysis/util/CoordinateLocatorTest.java b/src/test/java/org/openrewrite/analysis/util/CoordinateLocatorTest.java new file mode 100644 index 000000000..bf70e0482 --- /dev/null +++ b/src/test/java/org/openrewrite/analysis/util/CoordinateLocatorTest.java @@ -0,0 +1,190 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.analysis.util; + +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.test.RewriteTest; + +import java.util.Collection; +import java.util.function.Supplier; + +import static org.openrewrite.java.Assertions.java; + +public class CoordinateLocatorTest implements RewriteTest { + + private static Supplier> locateCoordinate(int line, int column) { + return () -> new JavaIsoVisitor<>() { + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext executionContext) { + J found = CoordinateLocator.findCoordinate(cu, line, column).some(); + //noinspection DataFlowIssue + return (J.CompilationUnit) cu.accept(new JavaIsoVisitor<>() { + @Override + public @Nullable J preVisit(J tree, ExecutionContext executionContext) { + if (tree.isScope(found)) { + return SearchResult.found(tree, tree.getClass().getSimpleName()); + } + return super.preVisit(tree, executionContext); + } + }, executionContext); + } + }; + } + + private static Supplier> locateLine(int line) { + return () -> new JavaIsoVisitor<>() { + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext executionContext) { + Collection found = CoordinateLocator.findLine(cu, line); + + //noinspection DataFlowIssue + return (J.CompilationUnit) cu.accept(new JavaIsoVisitor<>() { + @Override + public @Nullable J preVisit(J tree, ExecutionContext executionContext) { + if (found.contains(tree)) { + return SearchResult.found(tree, tree.getClass().getSimpleName()); + } + return super.preVisit(tree, executionContext); + } + }, executionContext); + } + }; + } + + @Test + void findCoordinatePassedString() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(locateCoordinate(3, 28))), + java( + """ + class Test { + void test() { + System.out.println("Hello World!"); + } + } + """, + """ + class Test { + void test() { + System.out.println(/*~~(Literal)~~>*/"Hello World!"); + } + } + """ + ) + ); + } + + @Test + void findCoordinateMethodInvocation() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(locateCoordinate(3, 20))), + java( + """ + class Test { + void test() { + System.out.println("Hello World!"); + } + } + """, + """ + class Test { + void test() { + System.out./*~~(Identifier)~~>*/println("Hello World!"); + } + } + """ + ) + ); + } + + @Test + void findMethodCallLineNumber() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(locateLine(3))), + java( + """ + class Test { + void test() { + System.out.println("Hello World!"); + } + } + """, + """ + class Test { + void test() { + /*~~(FieldAccess)~~>*//*~~(Identifier)~~>*/System./*~~(Identifier)~~>*/out./*~~(Identifier)~~>*/println(/*~~(Literal)~~>*/"Hello World!"); + } + } + """ + ) + ); + } + + @Test + void findClassHeaderLine() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(locateLine(1))), + java( + """ + class Test { + void test() { + System.out.println("Hello World!"); + } + } + """, + """ + /*~~(ClassDeclaration)~~>*/class /*~~(Identifier)~~>*/Test /*~~(Block)~~>*/{ + void test() { + System.out.println("Hello World!"); + } + } + """ + ) + ); + } + + @Test + void findClassHeaderWithMultilneCommentLine() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(locateLine(1))), + java( + """ + class Test /* + */ { + void test() { + System.out.println("Hello World!"); + } + } + """, + """ + /*~~(ClassDeclaration)~~>*/class /*~~(Identifier)~~>*/Test /* + */ { + void test() { + System.out.println("Hello World!"); + } + } + """ + ) + ); + } +}