diff --git a/maddi-cst-api/src/main/java/io/codelaser/maddi/cst/api/element/DetailedSources.java b/maddi-cst-api/src/main/java/io/codelaser/maddi/cst/api/element/DetailedSources.java index 1179d22fe..d197c96f3 100644 --- a/maddi-cst-api/src/main/java/io/codelaser/maddi/cst/api/element/DetailedSources.java +++ b/maddi-cst-api/src/main/java/io/codelaser/maddi/cst/api/element/DetailedSources.java @@ -73,6 +73,14 @@ public interface DetailedSources { * Source range spanning the entire field declaration (type + name + initialiser). */ Object FIELD_DECLARATION = new Object(); + /** + * Source range of a javadoc comment as written, from its opening {@code /**} through its closing + * {@code */}, set on the {@link JavaDoc}'s own source. That source spans the comment's TEXT only, and + * consumers placing a word inside the comment count from it. This range is for a consumer that removes or moves + * the comment itself and needs its first character -- which it cannot infer when the line also holds code, as + * in {@code /** the second */ B,}. + */ + Object WHOLE_COMMENT = new Object(); /** * Position of the comma that precedes an element in a comma-separated list. * Computed for formal type parameter lists, formal method parameter lists, field declaration lists, and diff --git a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanCompilationUnit.java b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanCompilationUnit.java index 9ffb30dd9..bb91beb92 100644 --- a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanCompilationUnit.java +++ b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanCompilationUnit.java @@ -117,7 +117,8 @@ private record BlockData(Block.Builder blockBuilder, String index, int numberOfS classSymbolScanner.syntheticListField()); DocSourcePositions docSourcePositions = docTrees.getSourcePositions(); - this.scanJavaDoc = new ScanJavaDoc(runtime, typeData, docSourcePositions, compilationUnitTree, lineMap); + this.scanJavaDoc = new ScanJavaDoc(runtime, typeData, docSourcePositions, compilationUnitTree, lineMap, + new ScanJavaDoc.SourceText(compilationUnitTree)); convertType = classSymbolScanner; convertType.startCompilationUnit(this, elementStack); diff --git a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanJavaDoc.java b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanJavaDoc.java index e8ccbaa9d..31c3efc98 100644 --- a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanJavaDoc.java +++ b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/ScanJavaDoc.java @@ -7,11 +7,14 @@ import com.sun.source.util.DocTreeScanner; import com.sun.tools.javac.tree.DCTree; import com.sun.tools.javac.util.JCDiagnostic; +import io.codelaser.maddi.cst.api.element.DetailedSources; import io.codelaser.maddi.cst.api.element.JavaDoc; import io.codelaser.maddi.cst.api.element.Source; import io.codelaser.maddi.cst.api.runtime.Runtime; +import org.jetbrains.annotations.Nullable; import javax.tools.Diagnostic; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -20,16 +23,69 @@ public record ScanJavaDoc(Runtime runtime, TypeData typeData, DocSourcePositions docSourcePositions, CompilationUnitTree compilationUnitTree, - LineMap lineMap) { + LineMap lineMap, + SourceText sourceText) { public JavaDoc scan(DocCommentTree docCommentTree) { MyScanner myScanner = new MyScanner(docCommentTree); Source source = myScanner.source(docCommentTree); if (source == runtime.noSource()) source = rawCommentSource(docCommentTree); + Source wholeComment = wholeCommentSource(docCommentTree); + if (wholeComment != null && source != runtime.noSource()) { + source = source.withDetailedSources(runtime.newDetailedSourcesBuilder() + .put(DetailedSources.WHOLE_COMMENT, wholeComment).build()); + } myScanner.scan(docCommentTree, null); return runtime.newJavaDoc(source, myScanner.comment.toString(), List.copyOf(myScanner.tags)); } + /** + * The text of the compilation unit, read once and only when a javadoc asks for it: {@link #scan} runs for every + * javadoc of the unit, and the file object may go back to the disk on each read. + */ + static final class SourceText { + private final CompilationUnitTree compilationUnitTree; + private String text; + private boolean read; + + SourceText(CompilationUnitTree compilationUnitTree) { + this.compilationUnitTree = compilationUnitTree; + } + + @Nullable String get() { + if (!read) { + read = true; + try { + text = compilationUnitTree.getSourceFile().getCharContent(true).toString(); + } catch (IOException | RuntimeException e) { + text = null; + } + } + return text; + } + } + + /** + * The comment as written, {@code /**} through {@code */} (see {@link DetailedSources#WHOLE_COMMENT}). The + * tokenizer's comment knows where its {@code /**} is, as in {@link #rawCommentSource}; the closing + * {@code */} is the first one after it, since a comment cannot contain one. Null when either end cannot be + * found. + */ + private @Nullable Source wholeCommentSource(DocCommentTree docCommentTree) { + if (!(docCommentTree instanceof DCTree.DCDocComment dc) || dc.comment == null) return null; + JCDiagnostic.DiagnosticPosition pos = dc.comment.getPos(); + if (pos == null) return null; + int start = pos.getStartPosition(); + if (start == Diagnostic.NOPOS) return null; + String text = sourceText.get(); + if (text == null || !text.startsWith("/**", start)) return null; + int close = text.indexOf("*/", start + 3); + if (close < 0) return null; + int lastChar = close + 1; // inclusive: the '/' of '*/' + return runtime.newParserSource("-", (int) lineMap.getLineNumber(start), (int) lineMap.getColumnNumber(start), + (int) lineMap.getLineNumber(lastChar), (int) lineMap.getColumnNumber(lastChar)); + } + /** * #31: javac positions a doc comment tree by its BODY, so an empty {@code /** */} has no position at all, * and the JavaDoc read as line 0 to every consumer placing something relative to a member's comments. The diff --git a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java index 75861dd39..cd0e816b8 100644 --- a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java +++ b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java @@ -370,12 +370,41 @@ private void scanTypeBody(Node body) { case CompactConstructorDeclaration ccd -> scanMethodDeclaration(ccd); case MethodDeclaration _, AnnotationMethodDeclaration _ -> scanMethodDeclaration(node2); case FieldDeclaration fd -> scanFieldDeclaration(fd); + case EnumConstant ec -> scanEnumConstant(ec); default -> { } } } } + /** + * ⛔ AN ENUM CONSTANT HAD NO COMMENTS AT ALL. This switch had no case for it, so a {@code //} or {@code /* */} + * comment in front of a constant was recorded for nothing, and a consumer removing the constant could not tell + * that the comment belonged to it: removing B from + *
+ * // the first + * A, + * // the second + * B, + * // the third + * C; + *+ * deleted from B's name to C's name, which took "// the third" and left "// the second" above C. + *
+ * A single-line comment after the constant on its last line -- {@code B, // the second} -- belongs to B as
+ * well. The comma (or the ';' that ends the list) stands between B and that comment, so the lookahead looks
+ * past it: the comment is a preceding token of whatever comes after the separator, and without this the next
+ * constant takes it as its own leading comment.
+ */
+ private void scanEnumConstant(EnumConstant ec) {
+ Node after = ec.nextSibling();
+ if (after != null && (after.getType() == Token.TokenType.COMMA
+ || after.getType() == Token.TokenType.SEMICOLON)) {
+ after = after.nextSibling();
+ }
+ addComments(ec, after);
+ }
+
private void scanModifiers(Modifiers modifiers) {
for (Node node : modifiers.children()) {
String string = node.getSource();
@@ -689,19 +718,24 @@ private void visit(Node node, Predicate
+ * And a javadoc carries where it is WRITTEN, {@code /**} through {@code */}, as
+ * {@link DetailedSources.WHOLE_COMMENT} on its source. The javadoc's own source spans its text only, so on a line
+ * shared with code -- {@code /** the second */ B,} -- nothing said where the comment began.
+ */
+public class TestEnumConstantComments extends CommonTest {
+
+ @Language("java")
+ private static final String INPUT = """
+ package a.b;
+ public enum E {
+ // above A
+ A, // after A
+ /* above B */
+ B,
+ /** before C */ C,
+ /**
+ * Above D.
+ */
+ D; // after D
+ }
+ """;
+
+ @DisplayName("each enum constant has the comments written for it, and a javadoc knows where it is written")
+ @Test
+ public void test() {
+ TypeInfo e = scan("a.b.E", INPUT);
+
+ assertEquals("[SingleLineComment at 3:5, SingleLineComment at 4:8]", comments(e, "A"),
+ "A: '// above A' on the line above it (line 3, column 5), then '// after A' after it on its own"
+ + " line (line 4, column 8)");
+ assertEquals("[MultiLineComment at 5:5]", comments(e, "B"),
+ "B: '/* above B */' on the line above it (line 5, column 5), and not A's '// after A'");
+ assertEquals("[JavaDoc at 7:9]", comments(e, "C"),
+ "C: its javadoc, whose source starts at its text 'before C' (line 7, column 9)");
+ assertEquals("[JavaDoc at 9:8, SingleLineComment at 11:8]", comments(e, "D"),
+ "D: its javadoc block, whose source starts at its text 'Above D.' (line 9, column 8), then"
+ + " '// after D' after the ';' on its line (line 11, column 8)");
+
+ assertEquals("7:5-7:19", wholeComment(e, "C"),
+ "C's javadoc as written: '/** before C */' runs from column 5 to column 19 of line 7");
+ assertEquals("8:5-10:7", wholeComment(e, "D"),
+ "D's javadoc as written: '/**' at line 8 column 5 through '*/' at line 10 columns 6-7");
+ }
+
+ /** Each comment of the constant as its kind and the line:column where its source begins. */
+ private static String comments(TypeInfo e, String constant) {
+ List