diff --git a/java-frontend/pom.xml b/java-frontend/pom.xml index 6f3a4b1cd43..41c4770f98b 100644 --- a/java-frontend/pom.xml +++ b/java-frontend/pom.xml @@ -14,10 +14,293 @@ Code Analyzer for Java :: Front-end (Java Parser) + 262.10968.63 + 2.4.0 + 1.10.2-intellij-2 + 0.5.0 + 8.5.18-jb1 + 1.48.0 3.2.1 + + + com.jetbrains.intellij.java + java-psi + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.java + java-psi-impl + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.java + java-syntax + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.java + java-frontback-psi + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.java + java-frontback-psi-impl + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + core + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + core-impl + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + diagnostic + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + diagnostic-telemetry + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + extensions + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + syntax + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + syntax-i18-n + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + syntax-psi + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + syntax-util + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + util + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + util-base + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + util-base-multiplatform + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + util-coroutines + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + util-multiplatform + ${intellij.version} + + + * + * + + + + + com.jetbrains.intellij.platform + util-rt + ${intellij.version} + + + * + * + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${intellij.kotlin.version} + + + * + * + + + + + org.jetbrains.intellij.deps.kotlinx + kotlinx-coroutines-core-jvm + ${intellij.coroutines.version} + + + * + * + + + + + org.jetbrains.kotlinx + kotlinx-collections-immutable-jvm + ${intellij.collections-immutable.version} + + + * + * + + + + + org.jetbrains.intellij.deps.fastutil + intellij-deps-fastutil + ${intellij.fastutil.version} + + + * + * + + + + + io.opentelemetry + opentelemetry-api + ${intellij.opentelemetry.version} + + + * + * + + + + ${project.groupId} jdt-package @@ -145,6 +428,17 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + -Dnet.bytebuddy.experimental=true -javaagent:"${test-java-agent-directory}/${jacoco-agent-jar}=destfile=${project.build.directory}/jacoco.exec" -javaagent:"${test-java-agent-directory}/${mockito-agent-jar}" --add-opens=java.base/java.lang=ALL-UNNAMED + + true + ${project.build.directory}/idea-config + + + org.apache.maven.plugins maven-dependency-plugin diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 6ddf24dee0d..cadc110c7c2 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -21,7 +21,6 @@ import java.util.Collections; import java.util.Deque; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -348,7 +347,7 @@ static JavaTree.CompilationUnitTreeImpl convert(String version, String unitName, ASTUtils.mayTolerateMissingType(astNode.getAST()); - setParents(tree); + JavaTreeFinalizer.setParents(tree); return tree; } @@ -357,23 +356,6 @@ static TokenManager createTokenManager(String version, String unitName, String s return new TokenManager(lex(version, unitName, source.toCharArray()), source, new DefaultCodeFormatterOptions(new HashMap<>())); } - private static void setParents(Tree node) { - Iterator childrenIterator = iteratorFor(node); - while (childrenIterator.hasNext()) { - Tree child = childrenIterator.next(); - ((JavaTree) child).setParent(node); - setParents(child); - } - } - - private static Iterator iteratorFor(Tree node) { - if (node.kind() == Tree.Kind.INFERED_TYPE || node.kind() == Tree.Kind.TOKEN) { - // getChildren throws exception in this case - return Collections.emptyIterator(); - } - return ((JavaTree) node).getChildren().iterator(); - } - @VisibleForTesting static List lex(String version, String unitName, char[] sourceChars) { List tokens = new ArrayList<>(); diff --git a/java-frontend/src/main/java/org/sonar/java/model/JavaTreeFinalizer.java b/java-frontend/src/main/java/org/sonar/java/model/JavaTreeFinalizer.java new file mode 100644 index 00000000000..5752c91b5f3 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/JavaTreeFinalizer.java @@ -0,0 +1,44 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model; + +import java.util.Collections; +import java.util.Iterator; +import org.sonar.plugins.java.api.tree.Tree; + +public final class JavaTreeFinalizer { + + private JavaTreeFinalizer() { + } + + public static void setParents(Tree node) { + Iterator childrenIterator = iteratorFor(node); + while (childrenIterator.hasNext()) { + Tree child = childrenIterator.next(); + ((JavaTree) child).setParent(node); + setParents(child); + } + } + + private static Iterator iteratorFor(Tree node) { + if (node.kind() == Tree.Kind.INFERED_TYPE || node.kind() == Tree.Kind.TOKEN) { + return Collections.emptyIterator(); + } + return ((JavaTree) node).getChildren().iterator(); + } + +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/KeywordSuper.java b/java-frontend/src/main/java/org/sonar/java/model/KeywordSuper.java index d3027535538..8b67de1d512 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/KeywordSuper.java +++ b/java-frontend/src/main/java/org/sonar/java/model/KeywordSuper.java @@ -19,9 +19,9 @@ import org.sonar.java.model.expression.IdentifierTreeImpl; import org.sonar.plugins.java.api.semantic.Type; -final class KeywordSuper extends IdentifierTreeImpl { +public final class KeywordSuper extends IdentifierTreeImpl { - KeywordSuper(InternalSyntaxToken token) { + public KeywordSuper(InternalSyntaxToken token) { super(token); } diff --git a/java-frontend/src/main/java/org/sonar/java/model/KeywordThis.java b/java-frontend/src/main/java/org/sonar/java/model/KeywordThis.java index 43f7031baa8..c37f42476ea 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/KeywordThis.java +++ b/java-frontend/src/main/java/org/sonar/java/model/KeywordThis.java @@ -18,9 +18,9 @@ import org.sonar.java.model.expression.IdentifierTreeImpl; -final class KeywordThis extends IdentifierTreeImpl { +public final class KeywordThis extends IdentifierTreeImpl { - KeywordThis(InternalSyntaxToken token) { + public KeywordThis(InternalSyntaxToken token) { super(token); } diff --git a/java-frontend/src/main/java/org/sonar/java/model/psi/PsiSourceSnapshot.java b/java-frontend/src/main/java/org/sonar/java/model/psi/PsiSourceSnapshot.java new file mode 100644 index 00000000000..2f8a24279e3 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/psi/PsiSourceSnapshot.java @@ -0,0 +1,148 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import java.util.Arrays; +import java.util.Objects; + +/** Keeps Java Unicode translation and PSI backslash protection separate from original source locations. */ +public final class PsiSourceSnapshot { + + private final String original; + private final String translated; + private final String psiText; + private final int[] originalOffsets; + private final int[] translatedOffsets; + + private PsiSourceSnapshot(String original, String translated, String psiText, int[] originalOffsets, int[] translatedOffsets) { + this.original = original; + this.translated = translated; + this.psiText = psiText; + this.originalOffsets = originalOffsets; + this.translatedOffsets = translatedOffsets; + } + + static PsiSourceSnapshot identity(String source) { + return new PsiSourceSnapshot(source, source, source, null, null); + } + + /** Applies JLS 3.3 once, then shields remaining backslashes from PSI's own Unicode translation. */ + public static PsiSourceSnapshot from(String original) { + Objects.requireNonNull(original); + if (original.indexOf("\\u") < 0) { + return identity(original); + } + var text = new StringBuilder(original.length()); + int[] boundaries = new int[original.length() + 1]; + int backslashes = 0; + boolean previousWasUnicode = false; + boolean changed = false; + int offset = 0; + while (offset < original.length()) { + char c = original.charAt(offset); + int end = offset + 1; + boolean unicode = c == '\\' && (previousWasUnicode || backslashes % 2 == 0) && end < original.length() && original.charAt(end) == 'u'; + if (unicode) { + while (end < original.length() && original.charAt(end) == 'u') { + end++; + } + if (original.length() - end < 4) { + throw malformed(offset); + } + int value = 0; + for (int i = 0; i < 4; i++) { + char digit = original.charAt(end++); + int hex = digit >= '0' && digit <= '9' ? digit - '0' + : digit >= 'a' && digit <= 'f' ? digit - 'a' + 10 : digit >= 'A' && digit <= 'F' ? digit - 'A' + 10 : -1; + if (hex < 0) { + throw malformed(offset); + } + value = (value << 4) | hex; + } + c = (char) value; + changed = true; + } + text.append(c); + boundaries[text.length()] = end; + backslashes = c == '\\' ? backslashes + 1 : 0; + previousWasUnicode = unicode; + offset = end; + } + if (!changed) { + return identity(original); + } + return protect(original, text.toString(), boundaries); + } + + private static PsiSourceSnapshot protect(String original, String translated, int[] boundaries) { + int slashCount = (int) translated.chars().filter(c -> c == '\\').count(); + if (slashCount == 0) { + return new PsiSourceSnapshot(original, translated, translated, Arrays.copyOf(boundaries, translated.length() + 1), null); + } + int length = Math.addExact(translated.length(), Math.multiplyExact(slashCount, 5)); + var protectedText = new StringBuilder(length); + int[] originalOffsets = new int[length + 1]; + int[] translatedOffsets = new int[length + 1]; + for (int i = 0; i < translated.length(); i++) { + char c = translated.charAt(i); + if (c == '\\') { + protectedText.append(c).append("u005c"); + } else { + protectedText.append(c); + } + int end = protectedText.length(); + int start = end - (c == '\\' ? 6 : 1); + Arrays.fill(originalOffsets, start, end, boundaries[i]); + Arrays.fill(translatedOffsets, start, end, i); + originalOffsets[end] = boundaries[i + 1]; + translatedOffsets[end] = i + 1; + } + return new PsiSourceSnapshot(original, translated, protectedText.toString(), originalOffsets, translatedOffsets); + } + + private static IllegalArgumentException malformed(int offset) { + return new IllegalArgumentException("Malformed Java Unicode escape at original offset " + offset); + } + + public String psiText() { + return psiText; + } + + String original() { + return original; + } + + String translated() { + return translated; + } + + /** Synthetic buffer interiors map to the original character's start; the final boundary maps to its exclusive end. */ + int originalOffset(int psiOffset) { + Objects.checkIndex(psiOffset, psiText.length() + 1); + return originalOffsets == null ? psiOffset : originalOffsets[psiOffset]; + } + + String originalText(int psiStart, int psiEnd) { + Objects.checkFromToIndex(psiStart, psiEnd, psiText.length()); + return original.substring(originalOffset(psiStart), originalOffset(psiEnd)); + } + + String logicalText(int psiStart, int psiEnd) { + Objects.checkFromToIndex(psiStart, psiEnd, psiText.length()); + return translated.substring(translatedOffsets == null ? psiStart : translatedOffsets[psiStart], translatedOffsets == null ? psiEnd : translatedOffsets[psiEnd]); + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/psi/PsiTokenIndex.java b/java-frontend/src/main/java/org/sonar/java/model/psi/PsiTokenIndex.java new file mode 100644 index 00000000000..88229ad4a4a --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/psi/PsiTokenIndex.java @@ -0,0 +1,211 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.javadoc.PsiDocComment; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import org.sonar.java.model.InternalSyntaxToken; +import org.sonar.java.model.InternalSyntaxTrivia; +import org.sonar.java.model.LineColumnConverter; +import org.sonar.plugins.java.api.tree.SyntaxTrivia; + +final class PsiTokenIndex { + + private final PsiSourceSnapshot source; + private final LineColumnConverter positions; + private final List entries = new ArrayList<>(); + private final List comments = new ArrayList<>(); + private final Map logicalTexts = new IdentityHashMap<>(); + private final Map closingAnglePositions = new HashMap<>(); + + PsiTokenIndex(PsiJavaFile file, String source) { + this(file, PsiSourceSnapshot.identity(source)); + } + + PsiTokenIndex(PsiJavaFile file, PsiSourceSnapshot source) { + if (!file.getText().equals(source.psiText())) { + throw new IllegalArgumentException("PSI contents differ from the source snapshot"); + } + this.source = source; + positions = new LineColumnConverter(source.original()); + collect(file); + add(source.psiText().length(), source.psiText().length(), true); + } + + private void collect(PsiElement element) { + int start = element.getTextRange().getStartOffset(); + int end = element.getTextRange().getEndOffset(); + if (element instanceof PsiWhiteSpace || start == end) { + return; + } + if (element instanceof PsiComment comment) { + String text = source.originalText(start, end); + SyntaxTrivia.CommentKind kind = comment instanceof PsiDocComment doc + ? doc.isMarkdownComment() ? SyntaxTrivia.CommentKind.MARKDOWN : SyntaxTrivia.CommentKind.JAVADOC + : comment.getTokenType() == JavaTokenType.END_OF_LINE_COMMENT ? SyntaxTrivia.CommentKind.LINE : SyntaxTrivia.CommentKind.BLOCK; + if (kind == SyntaxTrivia.CommentKind.LINE) { + text = source.originalText(start, lineCommentEnd(end)); + } + var pos = positions.toPos(source.originalOffset(start)); + comments.add(new InternalSyntaxTrivia(kind, text, pos.line(), pos.columnOffset())); + } else if (element.getFirstChild() == null) { + add(start, end, false); + } else { + for (PsiElement child = element.getFirstChild(); child != null; child = child.getNextSibling()) { + collect(child); + } + } + } + + private void add(int start, int end, boolean eof) { + var pos = positions.toPos(source.originalOffset(start)); + var token = new InternalSyntaxToken(pos.line(), pos.columnOffset(), source.originalText(start, end), List.copyOf(comments), eof); + entries.add(new Entry(start, end, token)); + logicalTexts.put(token, source.logicalText(start, end)); + comments.clear(); + if (matches(token, ">")) { + closingAnglePositions.put(source.originalOffset(start), source.originalOffset(end) - 1); + } + } + + /** Matches ECJ's inclusion of escaped line terminators, including a physical CR paired with an escaped LF. */ + private int lineCommentEnd(int end) { + if (end == source.psiText().length()) { + return end; + } + if (source.psiText().charAt(end) == '\r' && end + 1 < source.psiText().length() && source.psiText().charAt(end + 1) == '\n' + && source.originalOffset(end + 2) - source.originalOffset(end + 1) > 1) { + return end + 2; + } + return source.originalOffset(end + 1) - source.originalOffset(end) > 1 ? end + 1 : end; + } + + InternalSyntaxToken token(PsiElement element) { + return at(element.getTextRange().getStartOffset()); + } + + InternalSyntaxToken at(int offset) { + int index = lowerBound(offset); + if (index < entries.size() && entries.get(index).start == offset) { + return entries.get(index).token; + } + throw new IllegalArgumentException("No PSI token at offset " + offset); + } + + private int lowerBound(int offset) { + int low = 0; + int high = entries.size(); + while (low < high) { + int middle = (low + high) >>> 1; + if (entries.get(middle).start < offset) { + low = middle + 1; + } else { + high = middle; + } + } + return low; + } + + InternalSyntaxToken first(PsiElement element, String text) { + return find(element.getTextRange().getStartOffset(), element.getTextRange().getEndOffset(), text, false); + } + + InternalSyntaxToken last(PsiElement element, String text) { + return find(element.getTextRange().getStartOffset(), element.getTextRange().getEndOffset(), text, true); + } + + InternalSyntaxToken find(int start, int end, String text, boolean last) { + int from = lowerBound(start); + int to = lowerBound(end); + for (int i = last ? to - 1 : from; i >= from && i < to; i += last ? -1 : 1) { + Entry entry = entries.get(i); + if (entry.end <= end && matches(entry.token, text)) { + return entry.token; + } + } + throw new IllegalArgumentException("Missing PSI token '" + text + "' in [" + start + ", " + end + ")"); + } + + InternalSyntaxToken before(PsiElement element, String text) { + return find(0, element.getTextRange().getStartOffset(), text, true); + } + + InternalSyntaxToken after(PsiElement element, String text) { + return find(element.getTextRange().getEndOffset(), source.psiText().length(), text, false); + } + + InternalSyntaxToken next(PsiElement element) { + return entries.get(lowerBound(element.getTextRange().getEndOffset())).token; + } + + int originalOffset(int psiOffset) { + return source.originalOffset(psiOffset); + } + + boolean matches(InternalSyntaxToken token, String text) { + return text.equals(logicalTexts.get(token)); + } + + /** ECJ keeps the starting offset when its closing-angle scan encounters trivia instead of a greater-than token. */ + int closingAnglePosition(int originalOffset) { + return closingAnglePositions.getOrDefault(originalOffset, originalOffset); + } + + InternalSyntaxToken closingAngle(PsiElement element) { + int from = lowerBound(element.getTextRange().getStartOffset()); + int to = lowerBound(element.getTextRange().getEndOffset()); + for (int i = to - 1; i >= from; i--) { + var entry = entries.get(i); + if (matches(entry.token, ">")) { + var position = positions.toPos(source.originalOffset(entry.end) - 1); + return new InternalSyntaxToken(position.line(), position.columnOffset(), ">", entry.token.trivias(), false); + } + } + throw new IllegalArgumentException("Missing PSI closing angle in " + element.getTextRange()); + } + + InternalSyntaxToken syntheticAtOriginalOffset(int originalOffset, String text) { + var position = positions.toPos(originalOffset); + return new InternalSyntaxToken(position.line(), position.columnOffset(), text, List.of(), false); + } + + InternalSyntaxToken eof() { + return entries.getLast().token; + } + + List within(PsiElement element) { + int start = element.getTextRange().getStartOffset(); + int end = element.getTextRange().getEndOffset(); + return entries.subList(lowerBound(start), lowerBound(end)).stream().map(Entry::token).toList(); + } + + List tokens() { + return entries.stream().map(Entry::token).toList(); + } + + private record Entry(int start, int end, InternalSyntaxToken token) { + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/psi/PsiTreeConverter.java b/java-frontend/src/main/java/org/sonar/java/model/psi/PsiTreeConverter.java new file mode 100644 index 00000000000..14068eb484f --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/psi/PsiTreeConverter.java @@ -0,0 +1,1270 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiAnnotationMethod; +import com.intellij.psi.PsiAnonymousClass; +import com.intellij.psi.PsiArrayAccessExpression; +import com.intellij.psi.PsiArrayInitializerExpression; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.PsiAssertStatement; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiBreakStatement; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassInitializer; +import com.intellij.psi.PsiClassObjectAccessExpression; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiConditionalExpression; +import com.intellij.psi.PsiContinueStatement; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiDeconstructionPattern; +import com.intellij.psi.PsiDefaultCaseLabelElement; +import com.intellij.psi.PsiDoWhileStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiEmptyStatement; +import com.intellij.psi.PsiEnumConstant; +import com.intellij.psi.PsiErrorElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiExpressionListStatement; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiForeachStatement; +import com.intellij.psi.PsiIfStatement; +import com.intellij.psi.PsiImplicitClass; +import com.intellij.psi.PsiImportList; +import com.intellij.psi.PsiImportModuleStatement; +import com.intellij.psi.PsiImportStatementBase; +import com.intellij.psi.PsiImportStaticStatement; +import com.intellij.psi.PsiInstanceOfExpression; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiJavaModule; +import com.intellij.psi.PsiJavaToken; +import com.intellij.psi.PsiKeyword; +import com.intellij.psi.PsiLabeledStatement; +import com.intellij.psi.PsiLambdaExpression; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiMethodReferenceExpression; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiPackageAccessibilityStatement; +import com.intellij.psi.PsiPackageStatement; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiParenthesizedExpression; +import com.intellij.psi.PsiPattern; +import com.intellij.psi.PsiPolyadicExpression; +import com.intellij.psi.PsiPostfixExpression; +import com.intellij.psi.PsiPrefixExpression; +import com.intellij.psi.PsiProvidesStatement; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiReferenceList; +import com.intellij.psi.PsiReferenceParameterList; +import com.intellij.psi.PsiRequiresStatement; +import com.intellij.psi.PsiResourceExpression; +import com.intellij.psi.PsiResourceListElement; +import com.intellij.psi.PsiResourceVariable; +import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiSuperExpression; +import com.intellij.psi.PsiSwitchExpression; +import com.intellij.psi.PsiSwitchLabelStatementBase; +import com.intellij.psi.PsiSwitchLabeledRuleStatement; +import com.intellij.psi.PsiSwitchStatement; +import com.intellij.psi.PsiSynchronizedStatement; +import com.intellij.psi.PsiThisExpression; +import com.intellij.psi.PsiThrowStatement; +import com.intellij.psi.PsiTryStatement; +import com.intellij.psi.PsiTypeCastExpression; +import com.intellij.psi.PsiTypeElement; +import com.intellij.psi.PsiTypeParameterList; +import com.intellij.psi.PsiTypeTestPattern; +import com.intellij.psi.PsiUnnamedPattern; +import com.intellij.psi.PsiUsesStatement; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.PsiWhileStatement; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.PsiYieldStatement; +import com.intellij.psi.util.PsiTreeUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import org.sonar.java.ast.parser.ArgumentListTreeImpl; +import org.sonar.java.ast.parser.FormalParametersListTreeImpl; +import org.sonar.java.ast.parser.InitializerListTreeImpl; +import org.sonar.java.ast.parser.ModuleNameListTreeImpl; +import org.sonar.java.ast.parser.ModuleNameTreeImpl; +import org.sonar.java.ast.parser.QualifiedIdentifierListTreeImpl; +import org.sonar.java.ast.parser.ResourceListTreeImpl; +import org.sonar.java.ast.parser.StatementListTreeImpl; +import org.sonar.java.ast.parser.TypeParameterListTreeImpl; +import org.sonar.java.model.ArrayDimensionTreeImpl; +import org.sonar.java.model.InternalSyntaxToken; +import org.sonar.java.model.JavaTree; +import org.sonar.java.model.JavaTreeFinalizer; +import org.sonar.java.model.KeywordSuper; +import org.sonar.java.model.KeywordThis; +import org.sonar.java.model.SemanticValues; +import org.sonar.java.model.TypeParameterTreeImpl; +import org.sonar.java.model.declaration.AnnotationTreeImpl; +import org.sonar.java.model.declaration.ClassTreeImpl; +import org.sonar.java.model.declaration.EnumConstantTreeImpl; +import org.sonar.java.model.declaration.ExportsDirectiveTreeImpl; +import org.sonar.java.model.declaration.MethodTreeImpl; +import org.sonar.java.model.declaration.ModifierKeywordTreeImpl; +import org.sonar.java.model.declaration.ModifiersTreeImpl; +import org.sonar.java.model.declaration.ModuleDeclarationTreeImpl; +import org.sonar.java.model.declaration.OpensDirectiveTreeImpl; +import org.sonar.java.model.declaration.ProvidesDirectiveTreeImpl; +import org.sonar.java.model.declaration.RequiresDirectiveTreeImpl; +import org.sonar.java.model.declaration.UsesDirectiveTreeImpl; +import org.sonar.java.model.declaration.VariableTreeImpl; +import org.sonar.java.model.expression.ArrayAccessExpressionTreeImpl; +import org.sonar.java.model.expression.AssignmentExpressionTreeImpl; +import org.sonar.java.model.expression.BinaryExpressionTreeImpl; +import org.sonar.java.model.expression.ConditionalExpressionTreeImpl; +import org.sonar.java.model.expression.IdentifierTreeImpl; +import org.sonar.java.model.expression.InstanceOfTreeImpl; +import org.sonar.java.model.expression.InternalPostfixUnaryExpression; +import org.sonar.java.model.expression.InternalPrefixUnaryExpression; +import org.sonar.java.model.expression.LambdaExpressionTreeImpl; +import org.sonar.java.model.expression.LiteralTreeImpl; +import org.sonar.java.model.expression.MemberSelectExpressionTreeImpl; +import org.sonar.java.model.expression.MethodInvocationTreeImpl; +import org.sonar.java.model.expression.MethodReferenceTreeImpl; +import org.sonar.java.model.expression.NewArrayTreeImpl; +import org.sonar.java.model.expression.NewClassTreeImpl; +import org.sonar.java.model.expression.ParenthesizedTreeImpl; +import org.sonar.java.model.expression.TypeArgumentListTreeImpl; +import org.sonar.java.model.expression.TypeCastExpressionTreeImpl; +import org.sonar.java.model.expression.VarTypeTreeImpl; +import org.sonar.java.model.pattern.DefaultPatternTreeImpl; +import org.sonar.java.model.pattern.GuardedPatternTreeImpl; +import org.sonar.java.model.pattern.NullPatternTreeImpl; +import org.sonar.java.model.pattern.RecordPatternTreeImpl; +import org.sonar.java.model.pattern.TypePatternTreeImpl; +import org.sonar.java.model.statement.AssertStatementTreeImpl; +import org.sonar.java.model.statement.BlockTreeImpl; +import org.sonar.java.model.statement.BreakStatementTreeImpl; +import org.sonar.java.model.statement.CaseGroupTreeImpl; +import org.sonar.java.model.statement.CaseLabelTreeImpl; +import org.sonar.java.model.statement.CatchTreeImpl; +import org.sonar.java.model.statement.ContinueStatementTreeImpl; +import org.sonar.java.model.statement.DoWhileStatementTreeImpl; +import org.sonar.java.model.statement.EmptyStatementTreeImpl; +import org.sonar.java.model.statement.ExpressionStatementTreeImpl; +import org.sonar.java.model.statement.ForEachStatementImpl; +import org.sonar.java.model.statement.ForStatementTreeImpl; +import org.sonar.java.model.statement.IfStatementTreeImpl; +import org.sonar.java.model.statement.LabeledStatementTreeImpl; +import org.sonar.java.model.statement.ReturnStatementTreeImpl; +import org.sonar.java.model.statement.StaticInitializerTreeImpl; +import org.sonar.java.model.statement.SwitchExpressionTreeImpl; +import org.sonar.java.model.statement.SwitchStatementTreeImpl; +import org.sonar.java.model.statement.SynchronizedStatementTreeImpl; +import org.sonar.java.model.statement.ThrowStatementTreeImpl; +import org.sonar.java.model.statement.TryStatementTreeImpl; +import org.sonar.java.model.statement.WhileStatementTreeImpl; +import org.sonar.java.model.statement.YieldStatementTreeImpl; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.ArrayDimensionTree; +import org.sonar.plugins.java.api.tree.CatchTree; +import org.sonar.plugins.java.api.tree.EnumConstantTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.ImportClauseTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.Modifier; +import org.sonar.plugins.java.api.tree.ModifierTree; +import org.sonar.plugins.java.api.tree.ModuleDeclarationTree; +import org.sonar.plugins.java.api.tree.ModuleDirectiveTree; +import org.sonar.plugins.java.api.tree.PackageDeclarationTree; +import org.sonar.plugins.java.api.tree.PatternTree; +import org.sonar.plugins.java.api.tree.StatementTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.Tree.Kind; +import org.sonar.plugins.java.api.tree.TypeTree; +import org.sonar.plugins.java.api.tree.VariableTree; + +/** Converts a PSI source snapshot to the existing Sonar syntax model without resolving semantics. */ +public final class PsiTreeConverter { + + private final PsiTokenIndex tokens; + private final Map types = new IdentityHashMap<>(); + private final Map modifierLists = new IdentityHashMap<>(); + + private PsiTreeConverter(PsiJavaFile file, PsiSourceSnapshot source) { + tokens = new PsiTokenIndex(file, source); + } + + /** The caller owns the PSI read action and may dispose its environment after conversion. Sources containing Unicode escapes must use a snapshot. */ + public static JavaTree.CompilationUnitTreeImpl convert(PsiJavaFile file, String source) { + var snapshot = PsiSourceSnapshot.from(source); + if (!snapshot.psiText().equals(source)) { + throw new IllegalArgumentException("Source contains Unicode escapes; convert a PsiSourceSnapshot instead"); + } + return convert(file, snapshot); + } + + /** Converts the exact parsing text of a translated snapshot while retaining its original token spelling and positions. */ + public static JavaTree.CompilationUnitTreeImpl convert(PsiJavaFile file, PsiSourceSnapshot source) { + var converter = new PsiTreeConverter(file, source); + requireNoParseErrors(file); + var result = converter.compilationUnit(file); + JavaTreeFinalizer.setParents(result); + return result; + } + + /** PSI reports a module named {@code transitive} as a missing requires name; {@link #module} converts that form like ECJ. */ + private static void requireNoParseErrors(PsiJavaFile file) { + for (var error : PsiTreeUtil.findChildrenOfType(file, PsiErrorElement.class)) { + if (!(error.getParent() instanceof PsiRequiresStatement)) { + throw new IllegalArgumentException("PSI contains parse errors: " + error.getErrorDescription() + " at " + error.getTextRange()); + } + } + } + + private JavaTree.CompilationUnitTreeImpl compilationUnit(PsiJavaFile file) { + var declaration = file.getPackageStatement(); + PackageDeclarationTree packageTree = declaration == null ? null : new JavaTree.PackageDeclarationTreeImpl( + annotations(declaration.getAnnotationList()), first(declaration, "package"), (ExpressionTree) reference(declaration.getPackageReference()), last(declaration, ";")); + List imports = new ArrayList<>(); + List types = new ArrayList<>(); + var importList = file.getImportList(); + boolean hasImports = importList != null && importList.getAllImportStatements().length != 0; + List emptyStatements = hasImports || file.getClasses().length != 0 ? null : imports; + for (PsiElement child : file.getChildren()) { + if (child instanceof PsiImportList list) { + for (PsiElement entry : list.getChildren()) { + if (entry instanceof PsiImportStatementBase imp) { + imports.add(importTree(imp)); + emptyStatements = imports; + } else if (is(entry, ";")) { + addEmptyStatement(entry, emptyStatements); + } else { + requireTrivia(entry); + } + } + } else if (child instanceof PsiClass c) { + types.add(classTree(c)); + emptyStatements = types; + } else if (is(child, ";")) { + addEmptyStatement(child, emptyStatements); + } else if (child instanceof PsiPackageStatement || child instanceof PsiJavaModule) { + emptyStatements = null; + } else { + requireTrivia(child); + } + } + return new JavaTree.CompilationUnitTreeImpl(packageTree, imports, types, module(file.getModuleDeclaration()), tokens.eof()); + } + + private JavaTree.ImportTreeImpl importTree(PsiImportStatementBase imp) { + PsiElement name = imp instanceof PsiImportModuleStatement module ? module.getModuleReference() : imp.getImportReference(); + ExpressionTree imported = imp instanceof PsiImportModuleStatement ? dottedName(name) : (ExpressionTree) reference((PsiJavaCodeReferenceElement) name); + if (imp.isOnDemand() && !(imp instanceof PsiImportModuleStatement)) { + imported = new MemberSelectExpressionTreeImpl(imported, last(imp, "."), new IdentifierTreeImpl(last(imp, "*"))); + } + return new JavaTree.ImportTreeImpl(first(imp, "import"), + imp instanceof PsiImportStaticStatement ? first(imp, "static") : null, + imp instanceof PsiImportModuleStatement ? first(imp, "module") : null, imported, last(imp, ";")); + } + + /** Like ECJ, keeps top-level semicolons only after an import, a type, or at the start of a file without either. */ + private void addEmptyStatement(PsiElement semicolon, List target) { + if (target != null) { + target.add(new EmptyStatementTreeImpl(token(semicolon))); + } + } + + private ClassTreeImpl classTree(PsiClass c) { + boolean anonymous = c instanceof PsiAnonymousClass; + boolean implicit = c instanceof PsiImplicitClass; + Kind kind = implicit ? Kind.IMPLICIT_CLASS : c.isAnnotationType() ? Kind.ANNOTATION_TYPE : c.isEnum() ? Kind.ENUM : c.isRecord() ? Kind.RECORD + : c.isInterface() ? Kind.INTERFACE : Kind.CLASS; + List members = new ArrayList<>(); + boolean body = implicit; + boolean enumSeparatorSeen = false; + for (PsiElement child : c.getChildren()) { + if (child == c.getLBrace()) { + body = true; + } else if (child == c.getRBrace()) { + body = false; + } else if (body) { + if (child instanceof PsiEnumConstant constant) { + members.add(enumConstant(constant)); + } else if (child instanceof PsiField field) { + members.add(variable(field, true)); + } else if (child instanceof PsiMethod method) { + members.add(method(method)); + } else if (child instanceof PsiClass inner) { + members.add(classTree(inner)); + } else if (child instanceof PsiClassInitializer initializer) { + var block = block(initializer.getBody()); + members.add(initializer.hasModifierProperty(PsiModifier.STATIC) + ? new StaticInitializerTreeImpl(first(initializer, "static"), (InternalSyntaxToken) block.openBraceToken(), block.body(), (InternalSyntaxToken) block.closeBraceToken()) + : new BlockTreeImpl(Kind.INITIALIZER, (InternalSyntaxToken) block.openBraceToken(), block.body(), (InternalSyntaxToken) block.closeBraceToken())); + } else if (is(child, ";")) { + if (!c.isEnum() || enumSeparatorSeen || members.isEmpty() || !(members.getLast() instanceof EnumConstantTree)) { + members.add(new EmptyStatementTreeImpl(token(child))); + } + enumSeparatorSeen = true; + } else if (!is(child, ",")) { + requireTrivia(child); + } + } + } + var result = new ClassTreeImpl(kind, nullableToken(c.getLBrace()), members, nullableToken(c.getRBrace())); + if (anonymous || implicit) { + return result; + } + String keyword = c.isAnnotationType() || c.isInterface() ? "interface" : c.isEnum() ? "enum" : c.isRecord() ? "record" : "class"; + result.complete(modifiers(c.getModifierList()), first(c, keyword), identifier(c.getNameIdentifier())); + if (c.isAnnotationType()) { + result.completeAtToken(first(c, "@")); + } + if (!c.isEnum() && !c.isAnnotationType()) { + result.completeTypeParameters(typeParameters(c.getTypeParameterList())); + } + var extended = c.getExtendsList(); + if (extended != null && extended.getReferenceElements().length != 0) { + if (c.isInterface()) { + result.completeInterfaces(first(extended, "extends"), referenceList(extended, ",")); + } else { + result.completeSuperclass(first(extended, "extends"), reference(extended.getReferenceElements()[0])); + } + } + var implemented = c.getImplementsList(); + if (implemented != null && implemented.getReferenceElements().length != 0) { + result.completeInterfaces(first(implemented, "implements"), referenceList(implemented, ",")); + } + var permitted = c.getPermitsList(); + if (permitted != null && permitted.getReferenceElements().length != 0) { + result.completePermittedTypes(first(permitted, "permits"), referenceList(permitted, ",")); + } + if (c.isRecord()) { + var header = Objects.requireNonNull(c.getRecordHeader()); + var components = header.getRecordComponents(); + List converted = new ArrayList<>(); + for (int i = 0; i < components.length; i++) { + var component = variable(components[i], false); + if (i + 1 < components.length) { + component.setEndToken(tokens.after(components[i], ",")); + } + converted.add(component); + } + result.completeRecordComponents(first(header, "("), converted, last(header, ")")); + } + return result; + } + + private MethodTreeImpl method(PsiMethod method) { + var parameterList = children(method, PsiParameterList.class).stream().findFirst().orElse(null); + boolean compact = parameterList == null; + var parameters = new FormalParametersListTreeImpl(compact ? null : first(parameterList, "("), compact ? null : last(parameterList, ")")); + PsiParameter[] declared = compact ? new PsiParameter[0] : parameterList.getParameters(); + for (int i = 0; i < declared.length; i++) { + var parameter = variable(declared[i], false); + if (i + 1 < declared.length) { + parameter.setEndToken(tokens.after(declared[i], ",")); + } + parameters.add(parameter); + } + TypeTree returnType = method.getReturnTypeElement() == null ? null : extraDimensions(type(method.getReturnTypeElement()), method.getNameIdentifier(), method); + MethodTreeImpl result; + if (method instanceof PsiAnnotationMethod annotation) { + var value = annotation.getDefaultValue(); + result = new MethodTreeImpl(parameters, value == null ? null : tokens.before(value, "default"), value == null ? null : annotationValue(value)) + .complete(returnType, identifier(method.getNameIdentifier()), last(method, ";")); + } else { + var thrown = method.getThrowsList(); + var body = method.getBody(); + result = new MethodTreeImpl(returnType, identifier(method.getNameIdentifier()), parameters, + thrown.getReferenceElements().length == 0 ? null : first(thrown, "throws"), referenceList(thrown, ","), + body == null ? null : block(body), body == null ? last(method, ";") : null); + } + return result.completeWithModifiers(modifiers(method.getModifierList())).completeWithTypeParameters(typeParameters(method.getTypeParameterList())); + } + + private VariableTreeImpl variable(PsiVariable variable, boolean terminated) { + var name = identifier(variable.getNameIdentifier()); + var typeElement = variable.getTypeElement(); + var result = new VariableTreeImpl(name); + if (typeElement != null && typeElement.getTextLength() != 0) { + result.completeModifiersAndType(modifiers(variable.getModifierList()), extraDimensions(type(typeElement), variable.getNameIdentifier(), variable)); + } + var initializer = variable.getInitializer(); + if (initializer != null) { + result.completeTypeAndInitializer(result.type(), tokens.before(initializer, "="), expression(initializer)); + } + if (terminated) { + var next = tokens.next(variable); + result.setEndToken(tokens.matches(next, ",") || tokens.matches(next, ";") ? next : last(variable, ";")); + } + return result; + } + + private TypeTree extraDimensions(TypeTree base, PsiElement name, PsiElement owner) { + List annotations = new ArrayList<>(); + boolean afterName = false; + for (PsiElement child : owner.getChildren()) { + if (child == name) { + afterName = true; + } else if (afterName && child instanceof PsiAnnotation annotation) { + annotations.add(annotation(annotation)); + } else if (afterName && is(child, "[")) { + base = new JavaTree.ArrayTypeTreeImpl(base, List.copyOf(annotations), token(child), tokens.after(child, "]")); + annotations.clear(); + } else if (afterName && (is(child, "=") || is(child, ",") || is(child, ";"))) { + break; + } + } + return base; + } + + private EnumConstantTreeImpl enumConstant(PsiEnumConstant constant) { + var name = identifier(constant.getNameIdentifier()); + var initializer = new NewClassTreeImpl(name, arguments(constant.getArgumentList()), + constant.getInitializingClass() == null ? null : classTree(constant.getInitializingClass())); + var next = tokens.next(constant); + return new EnumConstantTreeImpl(modifiers(constant.getModifierList()), name, initializer, + tokens.matches(next, ",") || tokens.matches(next, ";") ? next : null); + } + + private ModifiersTreeImpl modifiers(PsiModifierList list) { + if (list != null && modifierLists.containsKey(list)) { + return modifierLists.get(list); + } + List modifiers = new ArrayList<>(); + if (list != null) { + for (PsiElement child : list.getChildren()) { + if (child instanceof PsiAnnotation annotation) { + modifiers.add(annotation(annotation)); + } else if (child instanceof PsiKeyword) { + Modifier modifier = Modifier.valueOf(child.getText().toUpperCase(Locale.ROOT).replace('-', '_')); + modifiers.add(new ModifierKeywordTreeImpl(modifier, token(child))); + } else { + requireTrivia(child); + } + } + } + var result = new ModifiersTreeImpl(modifiers); + if (list != null) { + modifierLists.put(list, result); + } + return result; + } + + private List annotations(PsiElement list) { + List result = new ArrayList<>(); + if (list != null) { + for (PsiElement child : list.getChildren()) { + if (child instanceof PsiAnnotation annotation) { + result.add(annotation(annotation)); + } + } + } + return result; + } + + private AnnotationTreeImpl annotation(PsiAnnotation annotation) { + var list = annotation.getParameterList(); + var values = ArgumentListTreeImpl.emptyList(); + var attributes = list.getAttributes(); + for (int i = 0; i < attributes.length; i++) { + var attribute = attributes[i]; + var value = annotationValue(attribute.getValue()); + if (attribute.getNameIdentifier() != null) { + value = new AssignmentExpressionTreeImpl(Kind.ASSIGNMENT, identifier(attribute.getNameIdentifier()), first(attribute, "="), value); + } + values.add(value); + if (i + 1 < attributes.length) { + values.separators().add(tokens.after(attribute, ",")); + } + } + if (list.getTextLength() != 0) { + values.complete(first(list, "("), last(list, ")")); + } + return new AnnotationTreeImpl(first(annotation, "@"), reference(annotation.getNameReferenceElement()), values); + } + + private ExpressionTree annotationValue(PsiAnnotationMemberValue value) { + if (value instanceof PsiExpression expression) { + return expression(expression); + } + if (value instanceof PsiAnnotation annotation) { + return annotation(annotation); + } + if (value instanceof PsiArrayInitializerMemberValue array) { + var values = InitializerListTreeImpl.emptyList(); + for (var item : array.getInitializers()) { + values.add(annotationValue(item)); + if (tokens.matches(tokens.next(item), ",")) { + values.separators().add(tokens.next(item)); + } + } + return new NewArrayTreeImpl(List.of(), values).completeWithCurlyBraces(first(array, "{"), last(array, "}")); + } + throw unsupported(value); + } + + private TypeParameterListTreeImpl typeParameters(PsiTypeParameterList list) { + if (list == null || list.getTypeParameters().length == 0) { + return new TypeParameterListTreeImpl(); + } + var result = new TypeParameterListTreeImpl(first(list, "<"), tokens.closingAngle(list)); + var parameters = list.getTypeParameters(); + for (int i = 0; i < parameters.length; i++) { + var parameter = parameters[i]; + var name = identifier(parameter.getNameIdentifier()); + name.complete(annotations(parameter.getModifierList())); + var bounds = parameter.getExtendsList(); + result.add(new TypeParameterTreeImpl(name, bounds.getReferenceElements().length == 0 ? null : first(bounds, "extends"), referenceList(bounds, "&"))); + if (i + 1 < parameters.length) { + result.separators().add(tokens.after(parameter, ",")); + } + } + return result; + } + + private QualifiedIdentifierListTreeImpl referenceList(PsiReferenceList list, String separator) { + var result = QualifiedIdentifierListTreeImpl.emptyList(); + if (list != null) { + var elements = list.getReferenceElements(); + for (int i = 0; i < elements.length; i++) { + result.add(reference(elements[i])); + if (i + 1 < elements.length) { + result.separators().add(tokens.after(elements[i], separator)); + } + } + } + return result; + } + + private TypeTree type(PsiTypeElement element) { + TypeTree result = types.get(element); + if (result == null) { + result = convertType(element); + types.put(element, result); + } + return result; + } + + private TypeTree convertType(PsiTypeElement element) { + List parts = children(element, PsiTypeElement.class); + if (direct(element, "|") != null) { + var alternatives = QualifiedIdentifierListTreeImpl.emptyList(); + for (int i = 0; i < parts.size(); i++) { + alternatives.add(type(parts.get(i))); + if (i + 1 < parts.size()) { + alternatives.separators().add(tokens.after(parts.get(i), "|")); + } + } + return new JavaTree.UnionTypeTreeImpl(alternatives); + } + if (direct(element, "?") != null) { + JavaTree.WildcardTreeImpl result; + if (parts.isEmpty()) { + result = new JavaTree.WildcardTreeImpl(first(element, "?")); + } else { + boolean upper = direct(element, "extends") != null; + result = new JavaTree.WildcardTreeImpl(upper ? Kind.EXTENDS_WILDCARD : Kind.SUPER_WILDCARD, + first(element, upper ? "extends" : "super"), type(parts.getFirst())).complete(first(element, "?")); + } + result.complete(annotations(element)); + return result; + } + TypeTree result = null; + List annotations = new ArrayList<>(); + for (PsiElement child : element.getChildren()) { + if (child instanceof PsiAnnotation annotation) { + annotations.add(annotation(annotation)); + } else if (child instanceof PsiTypeElement nested) { + result = type(nested); + } else if (child instanceof PsiJavaCodeReferenceElement ref) { + result = reference(ref); + if (!annotations.isEmpty()) { + ((JavaTree.AnnotatedTypeTree) result).complete(new ArrayList<>(annotations)); + annotations.clear(); + } + } else if (is(child, "[")) { + result = new JavaTree.ArrayTypeTreeImpl(result, List.copyOf(annotations), token(child), tokens.after(child, "]")); + annotations.clear(); + } else if (is(child, "...")) { + result = new JavaTree.ArrayTypeTreeImpl(result, List.copyOf(annotations), token(child)); + annotations.clear(); + } else if (child instanceof PsiKeyword keyword) { + result = is(keyword, "var") ? new VarTypeTreeImpl(token(keyword)) : new JavaTree.PrimitiveTypeTreeImpl(token(keyword)); + ((JavaTree.AnnotatedTypeTree) result).complete(new ArrayList<>(annotations)); + annotations.clear(); + } else if (!is(child, "]")) { + requireTrivia(child); + } + } + if (result == null) { + throw unsupported(element); + } + return result; + } + + private TypeTree reference(PsiJavaCodeReferenceElement ref) { + if (ref == null) { + throw new IllegalArgumentException("Missing PSI reference"); + } + var name = identifier(ref.getReferenceNameElement()); + TypeTree result; + if (ref.getQualifier() instanceof PsiJavaCodeReferenceElement qualifier) { + result = new MemberSelectExpressionTreeImpl((ExpressionTree) reference(qualifier), tokens.before(ref.getReferenceNameElement(), "."), name); + } else { + result = name; + } + name.complete(annotations(ref)); + var parameters = typeArguments(ref.getParameterList(), true); + return parameters == null ? result : new JavaTree.ParameterizedTypeTreeImpl(result, parameters); + } + + private ExpressionTree dottedName(PsiElement element) { + ExpressionTree result = null; + InternalSyntaxToken dot = null; + for (var token : tokens.within(element)) { + if (tokens.matches(token, ".")) { + dot = token; + } else { + var name = new IdentifierTreeImpl(token); + result = result == null ? name : new MemberSelectExpressionTreeImpl(result, dot, name); + } + } + return Objects.requireNonNull(result); + } + + private TypeArgumentListTreeImpl typeArguments(PsiReferenceParameterList list, boolean parameterizedType) { + if (list == null || list.getTextLength() == 0) { + return null; + } + var parameters = Arrays.stream(list.getTypeParameterElements()).filter(p -> p.getTextLength() != 0).toArray(PsiTypeElement[]::new); + var close = parameterizedType && parameters.length != 0 + ? tokens.syntheticAtOriginalOffset(tokens.closingAnglePosition(typeEnd(parameters[parameters.length - 1])), ">") : tokens.closingAngle(list); + var result = new TypeArgumentListTreeImpl(first(list, "<"), close); + for (int i = 0; i < parameters.length; i++) { + result.add(type(parameters[i])); + if (i + 1 < parameters.length) { + result.separators().add(tokens.after(parameters[i], ",")); + } + } + return result; + } + + /** Preserves ECJ's parameterized-type end position, including its treatment of intervening trivia. */ + private int typeEnd(PsiTypeElement type) { + if (type.getLastChild() instanceof PsiJavaCodeReferenceElement ref) { + var parameters = ref.getParameterList().getTypeParameterElements(); + if (parameters.length != 0) { + return tokens.closingAnglePosition(typeEnd(parameters[parameters.length - 1])) + 1; + } + } + return tokens.originalOffset(type.getTextRange().getEndOffset()); + } + + private ArgumentListTreeImpl arguments(PsiExpressionList list) { + var result = ArgumentListTreeImpl.emptyList(); + if (list != null && list.getTextLength() != 0) { + result.complete(first(list, "("), last(list, ")")); + var expressions = list.getExpressions(); + for (int i = 0; i < expressions.length; i++) { + result.add(expression(expressions[i])); + if (i + 1 < expressions.length) { + result.separators().add(tokens.after(expressions[i], ",")); + } + } + } + return result; + } + + private ExpressionTree expression(PsiExpression e) { + if (e == null) { + return null; + } + if (e instanceof PsiMethodReferenceExpression ref) { + var qualifier = ref.getQualifierType(); + Tree target = qualifier == null ? expression(ref.getQualifierExpression()) : type(qualifier); + var result = new MethodReferenceTreeImpl(target, first(ref, "::")); + result.complete(typeArguments(ref.getParameterList(), false), identifier(ref.getReferenceNameElement())); + return result; + } + if (e instanceof PsiReferenceExpression ref) { + var qualifier = ref.getQualifierExpression(); + var name = identifier(ref.getReferenceNameElement()); + return qualifier == null ? name : new MemberSelectExpressionTreeImpl(expression(qualifier), tokens.before(ref.getReferenceNameElement(), "."), name); + } + if (e instanceof PsiLiteralExpression literal) { + Kind kind; + var tokenType = literal.getFirstChild().getNode().getElementType(); + if (tokenType == JavaTokenType.NULL_KEYWORD) { + kind = Kind.NULL_LITERAL; + } else if (tokenType == JavaTokenType.TRUE_KEYWORD || tokenType == JavaTokenType.FALSE_KEYWORD) { + kind = Kind.BOOLEAN_LITERAL; + } else if (tokenType == JavaTokenType.CHARACTER_LITERAL) { + kind = Kind.CHAR_LITERAL; + } else if (tokenType == JavaTokenType.TEXT_BLOCK_LITERAL) { + kind = Kind.TEXT_BLOCK; + } else if (tokenType == JavaTokenType.STRING_LITERAL) { + kind = Kind.STRING_LITERAL; + } else if (tokenType == JavaTokenType.LONG_LITERAL) { + kind = Kind.LONG_LITERAL; + } else if (tokenType == JavaTokenType.FLOAT_LITERAL) { + kind = Kind.FLOAT_LITERAL; + } else if (tokenType == JavaTokenType.DOUBLE_LITERAL) { + kind = Kind.DOUBLE_LITERAL; + } else { + kind = Kind.INT_LITERAL; + } + return new LiteralTreeImpl(kind, token(literal)); + } + if (e instanceof PsiParenthesizedExpression p) { + return new ParenthesizedTreeImpl(first(p, "("), expression(p.getExpression()), last(p, ")")); + } + if (e instanceof PsiMethodCallExpression call) { + return new MethodInvocationTreeImpl(expression(call.getMethodExpression()), typeArguments(call.getMethodExpression().getParameterList(), false), arguments(call.getArgumentList())); + } + if (e instanceof PsiNewExpression creation) { + return newExpression(creation); + } + if (e instanceof PsiArrayInitializerExpression array) { + var initializers = InitializerListTreeImpl.emptyList(); + for (var value : array.getInitializers()) { + initializers.add(expression(value)); + if (tokens.matches(tokens.next(value), ",")) { + initializers.separators().add(tokens.next(value)); + } + } + return new NewArrayTreeImpl(List.of(), initializers).completeWithCurlyBraces(first(array, "{"), last(array, "}")); + } + if (e instanceof PsiAssignmentExpression assignment) { + return new AssignmentExpressionTreeImpl(operator(assignment.getOperationSign().getText()), expression(assignment.getLExpression()), + token(assignment.getOperationSign()), expression(assignment.getRExpression())); + } + if (e instanceof PsiPolyadicExpression polyadic) { + var operands = polyadic.getOperands(); + ExpressionTree result = expression(operands[0]); + for (int i = 1; i < operands.length; i++) { + var sign = polyadic.getTokenBeforeOperand(operands[i]); + result = new BinaryExpressionTreeImpl(operator(sign.getText()), result, token(sign), expression(operands[i])); + } + return result; + } + if (e instanceof PsiPrefixExpression prefix) { + Kind kind = switch (prefix.getOperationSign().getText()) { + case "+" -> Kind.UNARY_PLUS; + case "-" -> Kind.UNARY_MINUS; + case "!" -> Kind.LOGICAL_COMPLEMENT; + case "~" -> Kind.BITWISE_COMPLEMENT; + case "++" -> Kind.PREFIX_INCREMENT; + case "--" -> Kind.PREFIX_DECREMENT; + default -> throw unsupported(prefix); + }; + return new InternalPrefixUnaryExpression(kind, token(prefix.getOperationSign()), expression(prefix.getOperand())); + } + if (e instanceof PsiPostfixExpression postfix) { + return new InternalPostfixUnaryExpression(postfix.getOperationSign().textMatches("++") ? Kind.POSTFIX_INCREMENT : Kind.POSTFIX_DECREMENT, + expression(postfix.getOperand()), token(postfix.getOperationSign())); + } + if (e instanceof PsiConditionalExpression conditional) { + return new ConditionalExpressionTreeImpl(expression(conditional.getCondition()), first(conditional, "?"), expression(conditional.getThenExpression()), + tokens.before(conditional.getElseExpression(), ":"), expression(conditional.getElseExpression())); + } + if (e instanceof PsiTypeCastExpression cast) { + var type = cast.getCastType(); + var parts = children(type, PsiTypeElement.class); + if (direct(type, "&") != null) { + var bounds = QualifiedIdentifierListTreeImpl.emptyList(); + for (int i = 1; i < parts.size(); i++) { + bounds.add(type(parts.get(i))); + if (i + 1 < parts.size()) { + bounds.separators().add(tokens.after(parts.get(i), "&")); + } + } + return new TypeCastExpressionTreeImpl(first(cast, "("), type(parts.getFirst()), tokens.after(parts.getFirst(), "&"), bounds, + tokens.before(cast.getOperand(), ")"), expression(cast.getOperand())); + } + return new TypeCastExpressionTreeImpl(first(cast, "("), type(type), tokens.before(cast.getOperand(), ")"), expression(cast.getOperand())); + } + if (e instanceof PsiArrayAccessExpression access) { + return new ArrayAccessExpressionTreeImpl(expression(access.getArrayExpression()), new ArrayDimensionTreeImpl(tokens.after(access.getArrayExpression(), "["), + expression(access.getIndexExpression()), last(access, "]"))); + } + if (e instanceof PsiClassObjectAccessExpression classObject) { + return new MemberSelectExpressionTreeImpl((ExpressionTree) type(classObject.getOperand()), last(classObject, "."), new IdentifierTreeImpl(last(classObject, "class"))); + } + if (e instanceof PsiThisExpression thisExpression) { + var keyword = new KeywordThis(last(e, "this")); + return thisExpression.getQualifier() == null ? keyword : new MemberSelectExpressionTreeImpl((ExpressionTree) reference(thisExpression.getQualifier()), last(e, "."), keyword); + } + if (e instanceof PsiSuperExpression superExpression) { + var keyword = new KeywordSuper(last(e, "super")); + return superExpression.getQualifier() == null ? keyword : new MemberSelectExpressionTreeImpl((ExpressionTree) reference(superExpression.getQualifier()), last(e, "."), keyword); + } + if (e instanceof PsiInstanceOfExpression instanceOf) { + return instanceOf.getPattern() == null + ? new InstanceOfTreeImpl(expression(instanceOf.getOperand()), first(e, "instanceof"), type(instanceOf.getCheckType())) + : new InstanceOfTreeImpl(expression(instanceOf.getOperand()), first(e, "instanceof"), pattern(instanceOf.getPattern())); + } + if (e instanceof PsiLambdaExpression lambda) { + List parameters = new ArrayList<>(); + var source = lambda.getParameterList().getParameters(); + for (int i = 0; i < source.length; i++) { + var parameter = variable(source[i], false); + if (i + 1 < source.length) { + parameter.setEndToken(tokens.after(source[i], ",")); + } + parameters.add(parameter); + } + var list = lambda.getParameterList(); + var body = lambda.getBody(); + boolean parenthesized = is(list.getFirstChild(), "("); + return new LambdaExpressionTreeImpl(parenthesized ? first(list, "(") : null, parameters, + parenthesized ? last(list, ")") : null, first(lambda, "->"), body instanceof PsiCodeBlock block ? block(block) : expression((PsiExpression) body)); + } + if (e instanceof PsiSwitchExpression expression) { + return new SwitchExpressionTreeImpl(first(e, "switch"), first(e, "("), expression(expression.getExpression()), tokens.before(expression.getBody(), ")"), + first(expression.getBody(), "{"), switchGroups(expression.getBody()), last(expression.getBody(), "}")); + } + throw unsupported(e); + } + + private static Kind operator(String text) { + return switch (text) { + case "=" -> Kind.ASSIGNMENT; + case "+=" -> Kind.PLUS_ASSIGNMENT; + case "-=" -> Kind.MINUS_ASSIGNMENT; + case "*=" -> Kind.MULTIPLY_ASSIGNMENT; + case "/=" -> Kind.DIVIDE_ASSIGNMENT; + case "%=" -> Kind.REMAINDER_ASSIGNMENT; + case "&=" -> Kind.AND_ASSIGNMENT; + case "|=" -> Kind.OR_ASSIGNMENT; + case "^=" -> Kind.XOR_ASSIGNMENT; + case "<<=" -> Kind.LEFT_SHIFT_ASSIGNMENT; + case ">>=" -> Kind.RIGHT_SHIFT_ASSIGNMENT; + case ">>>=" -> Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT; + case "+" -> Kind.PLUS; + case "-" -> Kind.MINUS; + case "*" -> Kind.MULTIPLY; + case "/" -> Kind.DIVIDE; + case "%" -> Kind.REMAINDER; + case "&&" -> Kind.CONDITIONAL_AND; + case "||" -> Kind.CONDITIONAL_OR; + case "&" -> Kind.AND; + case "|" -> Kind.OR; + case "^" -> Kind.XOR; + case "==" -> Kind.EQUAL_TO; + case "!=" -> Kind.NOT_EQUAL_TO; + case "<" -> Kind.LESS_THAN; + case "<=" -> Kind.LESS_THAN_OR_EQUAL_TO; + case ">" -> Kind.GREATER_THAN; + case ">=" -> Kind.GREATER_THAN_OR_EQUAL_TO; + case "<<" -> Kind.LEFT_SHIFT; + case ">>" -> Kind.RIGHT_SHIFT; + case ">>>" -> Kind.UNSIGNED_RIGHT_SHIFT; + default -> throw new IllegalArgumentException("Unsupported PSI operator: " + text); + }; + } + + private ExpressionTree newExpression(PsiNewExpression e) { + var ref = e.getClassOrAnonymousClassReference(); + if (e.getArgumentList() != null) { + var result = new NewClassTreeImpl(reference(ref), arguments(e.getArgumentList()), e.getAnonymousClass() == null ? null : classTree(e.getAnonymousClass())) + .completeWithNewKeyword(first(e, "new")).completeWithTypeArguments(typeArguments(e.getTypeArgumentList(), false)); + if (e.getQualifier() != null) { + result.completeWithEnclosingExpression(expression(e.getQualifier())); + result.completeWithDotToken(tokens.after(e.getQualifier(), ".")); + } + return result; + } + TypeTree base = ref == null ? null : reference(ref); + List dimensions = new ArrayList<>(); + List annotations = new ArrayList<>(); + for (PsiElement child : e.getChildren()) { + if (child instanceof PsiKeyword keyword && !is(keyword, "new")) { + var primitive = new JavaTree.PrimitiveTypeTreeImpl(token(child)); + primitive.complete(List.copyOf(annotations)); + annotations.clear(); + base = primitive; + } else if (child instanceof PsiAnnotation annotation) { + annotations.add(annotation(annotation)); + } else if (is(child, "[")) { + PsiElement next = nextSignificant(child); + var dimension = new ArrayDimensionTreeImpl(token(child), next instanceof PsiExpression expression ? expression(expression) : null, + tokens.after(next instanceof PsiExpression ? next : child, "]")) + .completeAnnotations(List.copyOf(annotations)); + annotations.clear(); + if (next instanceof PsiExpression || e.getArrayInitializer() != null) { + dimensions.add(dimension); + } + } + } + if (base == null) { + throw unsupported(e); + } + var initializer = e.getArrayInitializer(); + return initializer == null + ? new NewArrayTreeImpl(dimensions, InitializerListTreeImpl.emptyList()).complete(base).completeWithNewKeyword(first(e, "new")) + : ((NewArrayTreeImpl) expression(initializer)).complete(base).completeDimensions(dimensions).completeWithNewKeyword(first(e, "new")); + } + + private BlockTreeImpl block(PsiCodeBlock block) { + List statements = new ArrayList<>(); + for (var statement : block.getStatements()) { + addStatement(statement, statements, true); + } + return new BlockTreeImpl(first(block, "{"), statements, last(block, "}")); + } + + private void addStatement(PsiStatement statement, List result, boolean terminated) { + if (statement instanceof PsiDeclarationStatement declaration) { + for (var element : declaration.getDeclaredElements()) { + if (element instanceof PsiVariable variable) { + result.add(variable(variable, terminated)); + } else if (element instanceof PsiClass c) { + result.add(classTree(c)); + } else { + throw unsupported(element); + } + } + } else if (statement instanceof PsiExpressionListStatement list) { + for (var expression : list.getExpressionList().getExpressions()) { + result.add(new ExpressionStatementTreeImpl(expression(expression), null)); + } + } else if (!terminated && statement instanceof PsiExpressionStatement expression) { + result.add(new ExpressionStatementTreeImpl(expression(expression.getExpression()), null)); + } else { + result.add(statement(statement)); + } + } + + private StatementTree statement(PsiStatement s) { + if (s instanceof PsiBlockStatement block) { + return block(block.getCodeBlock()); + } + if (s instanceof PsiEmptyStatement) { + return new EmptyStatementTreeImpl(first(s, ";")); + } + if (s instanceof PsiExpressionStatement expression) { + return new ExpressionStatementTreeImpl(expression(expression.getExpression()), last(s, ";")); + } + if (s instanceof PsiReturnStatement returned) { + return new ReturnStatementTreeImpl(first(s, "return"), expression(returned.getReturnValue()), last(s, ";")); + } + if (s instanceof PsiThrowStatement thrown) { + return new ThrowStatementTreeImpl(first(s, "throw"), expression(thrown.getException()), last(s, ";")); + } + if (s instanceof PsiYieldStatement yielded) { + return new YieldStatementTreeImpl(first(s, "yield"), expression(yielded.getExpression()), last(s, ";")); + } + if (s instanceof PsiIfStatement branch) { + var then = branch.getThenBranch(); + var otherwise = branch.getElseBranch(); + return new IfStatementTreeImpl(first(s, "if"), first(s, "("), expression(branch.getCondition()), tokens.after(branch.getCondition(), ")"), statement(then), + otherwise == null ? null : tokens.after(then, "else"), otherwise == null ? null : statement(otherwise)); + } + if (s instanceof PsiWhileStatement loop) { + return new WhileStatementTreeImpl(first(s, "while"), first(s, "("), expression(loop.getCondition()), tokens.after(loop.getCondition(), ")"), statement(loop.getBody())); + } + if (s instanceof PsiDoWhileStatement loop) { + return new DoWhileStatementTreeImpl(first(s, "do"), statement(loop.getBody()), tokens.after(loop.getBody(), "while"), tokens.before(loop.getCondition(), "("), + expression(loop.getCondition()), tokens.after(loop.getCondition(), ")"), last(s, ";")); + } + if (s instanceof PsiForStatement loop) { + var init = statementList(loop.getInitialization()); + var update = statementList(loop.getUpdate()); + var semicolons = Arrays.stream(loop.getChildren()).filter(c -> is(c, ";")).toList(); + var firstSemicolon = semicolons.size() == 2 ? token(semicolons.getFirst()) : last(loop.getInitialization(), ";"); + var secondSemicolon = token(semicolons.getLast()); + return new ForStatementTreeImpl(first(s, "for"), first(s, "("), init, firstSemicolon, expression(loop.getCondition()), secondSemicolon, + update, tokens.before(loop.getBody(), ")"), statement(loop.getBody())); + } + if (s instanceof PsiForeachStatement loop) { + return new ForEachStatementImpl(first(s, "for"), first(s, "("), variable(loop.getIterationParameter(), false), first(s, ":"), + expression(loop.getIteratedValue()), tokens.before(loop.getBody(), ")"), statement(loop.getBody())); + } + if (s instanceof PsiBreakStatement broken) { + return new BreakStatementTreeImpl(first(s, "break"), broken.getLabelIdentifier() == null ? null : identifier(broken.getLabelIdentifier()), last(s, ";")); + } + if (s instanceof PsiContinueStatement continued) { + return new ContinueStatementTreeImpl(first(s, "continue"), continued.getLabelIdentifier() == null ? null : identifier(continued.getLabelIdentifier()), last(s, ";")); + } + if (s instanceof PsiLabeledStatement labeled) { + return new LabeledStatementTreeImpl(identifier(labeled.getLabelIdentifier()), first(s, ":"), statement(labeled.getStatement())); + } + if (s instanceof PsiAssertStatement assertion) { + var result = new AssertStatementTreeImpl(first(s, "assert"), expression(assertion.getAssertCondition()), last(s, ";")); + return assertion.getAssertDescription() == null ? result : result.complete(tokens.before(assertion.getAssertDescription(), ":"), expression(assertion.getAssertDescription())); + } + if (s instanceof PsiSynchronizedStatement sync) { + return new SynchronizedStatementTreeImpl(first(s, "synchronized"), first(s, "("), expression(sync.getLockExpression()), tokens.before(sync.getBody(), ")"), block(sync.getBody())); + } + if (s instanceof PsiTryStatement attempt) { + return tryStatement(attempt); + } + if (s instanceof PsiSwitchStatement selection) { + return new SwitchStatementTreeImpl(first(s, "switch"), first(s, "("), expression(selection.getExpression()), tokens.before(selection.getBody(), ")"), + first(selection.getBody(), "{"), switchGroups(selection.getBody()), last(selection.getBody(), "}")); + } + throw unsupported(s); + } + + private StatementListTreeImpl statementList(PsiStatement statement) { + var result = StatementListTreeImpl.emptyList(); + if (statement != null && !(statement instanceof PsiEmptyStatement)) { + addStatement(statement, result, false); + if (statement instanceof PsiExpressionListStatement expressions) { + var values = expressions.getExpressionList().getExpressions(); + for (int i = 0; i + 1 < values.length; i++) { + result.separators().add(tokens.after(values[i], ",")); + } + } else if (statement instanceof PsiDeclarationStatement declaration) { + var elements = declaration.getDeclaredElements(); + for (int i = 0; i + 1 < elements.length; i++) { + ((VariableTreeImpl) result.get(i)).setEndToken(tokens.after(elements[i], ",")); + } + } + } + return result; + } + + private TryStatementTreeImpl tryStatement(PsiTryStatement s) { + var resources = ResourceListTreeImpl.emptyList(); + var list = s.getResourceList(); + if (list != null) { + for (PsiResourceListElement resource : list) { + if (resource instanceof PsiResourceVariable variable) { + resources.add(variable(variable, false)); + } else if (resource instanceof PsiResourceExpression expression) { + resources.add(expression(expression.getExpression())); + } else { + throw unsupported(resource); + } + if (tokens.matches(tokens.next(resource), ";")) { + resources.separators().add(tokens.next(resource)); + } + } + } + List catches = new ArrayList<>(); + for (var section : s.getCatchSections()) { + catches.add(new CatchTreeImpl(first(section, "catch"), first(section, "("), variable(section.getParameter(), false), + tokens.before(section.getCatchBlock(), ")"), block(section.getCatchBlock()))); + } + return new TryStatementTreeImpl(first(s, "try"), list == null ? null : first(list, "("), resources, list == null ? null : last(list, ")"), block(s.getTryBlock()), catches, + s.getFinallyBlock() == null ? null : tokens.before(s.getFinallyBlock(), "finally"), s.getFinallyBlock() == null ? null : block(s.getFinallyBlock())); + } + + private List switchGroups(PsiCodeBlock block) { + List groups = new ArrayList<>(); + List labels = new ArrayList<>(); + var body = StatementListTreeImpl.emptyList(); + for (var statement : block.getStatements()) { + if (statement instanceof PsiSwitchLabelStatementBase label) { + if (!body.isEmpty()) { + groups.add(new CaseGroupTreeImpl(labels, body)); + labels = new ArrayList<>(); + body = StatementListTreeImpl.emptyList(); + } + List values = new ArrayList<>(); + if (label.getCaseLabelElementList() != null) { + for (var element : label.getCaseLabelElementList().getElements()) { + ExpressionTree value; + if (element instanceof PsiPattern pattern) { + value = pattern(pattern); + if (label.getGuardExpression() != null) { + value = new GuardedPatternTreeImpl((PatternTree) value, tokens.before(label.getGuardExpression(), "when"), expression(label.getGuardExpression()), SemanticValues.UNKNOWN_TYPE); + } + } else if (element instanceof PsiDefaultCaseLabelElement) { + value = new DefaultPatternTreeImpl(token(element), SemanticValues.UNKNOWN_TYPE); + } else { + value = expression((PsiExpression) element); + if (value.is(Kind.NULL_LITERAL)) { + value = new NullPatternTreeImpl((LiteralTreeImpl) value); + } + } + values.add(value); + } + } + labels.add(new CaseLabelTreeImpl(first(label, label.isDefaultCase() ? "default" : "case"), values, + label instanceof PsiSwitchLabeledRuleStatement ? first(label, "->") : last(label, ":"))); + if (label instanceof PsiSwitchLabeledRuleStatement rule) { + var ruleBody = rule.getBody(); + if (ruleBody instanceof PsiExpressionStatement expression) { + body.add(new YieldStatementTreeImpl(null, expression(expression.getExpression()), last(ruleBody, ";"))); + } else { + addStatement(ruleBody, body, true); + } + } + } else { + addStatement(statement, body, true); + } + } + if (!labels.isEmpty()) { + groups.add(new CaseGroupTreeImpl(labels, body)); + } + return groups; + } + + private PatternTree pattern(PsiPattern pattern) { + if (pattern instanceof PsiTypeTestPattern test) { + return new TypePatternTreeImpl(variable(test.getPatternVariable(), false), SemanticValues.UNKNOWN_TYPE); + } + if (pattern instanceof PsiDeconstructionPattern record) { + List components = Arrays.stream(record.getDeconstructionList().getDeconstructionComponents()).map(this::pattern).toList(); + return new RecordPatternTreeImpl(type(record.getTypeElement()), first(record.getDeconstructionList(), "("), components, last(record.getDeconstructionList(), ")")); + } + if (pattern instanceof PsiUnnamedPattern unnamed) { + var variable = new VariableTreeImpl(new IdentifierTreeImpl(last(unnamed, "_"), true)); + if (unnamed.getTypeElement().getTextLength() != 0) { + variable.completeType(type(unnamed.getTypeElement())); + } + return new TypePatternTreeImpl(variable, SemanticValues.UNKNOWN_TYPE); + } + throw unsupported(pattern); + } + + private ModuleDeclarationTree module(PsiJavaModule module) { + if (module == null) { + return null; + } + List directives = new ArrayList<>(); + for (PsiElement child : module.getChildren()) { + if (child instanceof PsiRequiresStatement requires) { + var reference = requires.getReferenceElement(); + var modifiers = modifiers(requires.getModifierList()); + ModuleNameTreeImpl name; + if (reference == null) { + var sourceTokens = tokens.within(requires); + var nameToken = sourceTokens.get(sourceTokens.size() - 2); + if (!tokens.matches(nameToken, "transitive")) { + throw unsupported(requires); + } + name = ModuleNameTreeImpl.emptyList(); + name.add(new IdentifierTreeImpl(nameToken)); + modifiers = new ModifiersTreeImpl(modifiers.stream().filter(m -> m.firstToken() != nameToken).toList()); + } else { + name = moduleName(reference); + } + directives.add(new RequiresDirectiveTreeImpl(first(requires, "requires"), modifiers, name, last(requires, ";"))); + } else if (child instanceof PsiPackageAccessibilityStatement access) { + var names = ModuleNameListTreeImpl.emptyList(); + for (var ref : access.getModuleReferences()) { + if (!names.isEmpty()) { + names.separators().add(tokens.before(ref, ",")); + } + names.add(moduleName(ref)); + } + var to = names.isEmpty() ? null : first(access, "to"); + directives.add(access.getRole() == PsiPackageAccessibilityStatement.Role.EXPORTS + ? new ExportsDirectiveTreeImpl(first(access, "exports"), (ExpressionTree) reference(access.getPackageReference()), to, names, last(access, ";")) + : new OpensDirectiveTreeImpl(first(access, "opens"), (ExpressionTree) reference(access.getPackageReference()), to, names, last(access, ";"))); + } else if (child instanceof PsiUsesStatement uses) { + directives.add(new UsesDirectiveTreeImpl(first(uses, "uses"), reference(uses.getClassReference()), last(uses, ";"))); + } else if (child instanceof PsiProvidesStatement provides) { + directives.add(new ProvidesDirectiveTreeImpl(first(provides, "provides"), reference(provides.getInterfaceReference()), first(provides, "with"), + referenceList(provides.getImplementationList(), ","), last(provides, ";"))); + } else if (!(child instanceof PsiJavaToken) && child != module.getModifierList() && child != module.getNameIdentifier()) { + requireTrivia(child); + } + } + return new ModuleDeclarationTreeImpl(annotations(module.getModifierList()), module.hasModifierProperty(PsiModifier.OPEN) ? first(module, "open") : null, + first(module, "module"), moduleName(module.getNameIdentifier()), first(module, "{"), directives, last(module, "}")); + } + + private ModuleNameTreeImpl moduleName(PsiElement element) { + var result = ModuleNameTreeImpl.emptyList(); + var name = dottedName(element); + addModuleName(name, result); + return result; + } + + private void addModuleName(ExpressionTree name, ModuleNameTreeImpl result) { + if (name instanceof MemberSelectExpressionTree member) { + addModuleName(member.expression(), result); + result.add(member.identifier()); + } else { + result.add((IdentifierTree) name); + } + } + + private IdentifierTreeImpl identifier(PsiElement element) { + return new IdentifierTreeImpl(token(element), element.textMatches("_")); + } + + private InternalSyntaxToken token(PsiElement element) { + return tokens.token(Objects.requireNonNull(element)); + } + + private InternalSyntaxToken nullableToken(PsiElement element) { + return element == null ? null : token(element); + } + + private InternalSyntaxToken first(PsiElement element, String text) { + var direct = direct(element, text); + return direct == null ? tokens.first(element, text) : token(direct); + } + + private InternalSyntaxToken last(PsiElement element, String text) { + return tokens.last(element, text); + } + + private static boolean is(PsiElement element, String text) { + return element instanceof PsiJavaToken && element.textMatches(text); + } + + private static PsiElement direct(PsiElement element, String text) { + return Arrays.stream(element.getChildren()).filter(c -> is(c, text)).findFirst().orElse(null); + } + + private static List children(PsiElement element, Class type) { + return Arrays.stream(element.getChildren()).filter(type::isInstance).map(type::cast).toList(); + } + + private static PsiElement nextSignificant(PsiElement element) { + PsiElement next = element.getNextSibling(); + while (next instanceof PsiWhiteSpace || next instanceof PsiComment) { + next = next.getNextSibling(); + } + return next; + } + + private static void requireTrivia(PsiElement element) { + if (!(element instanceof PsiWhiteSpace) && !(element instanceof PsiComment) && element.getTextLength() != 0) { + throw unsupported(element); + } + } + + private static IllegalArgumentException unsupported(PsiElement element) { + return new IllegalArgumentException("Unsupported PSI syntax " + (element == null ? "null" : element.getNode().getElementType() + " at " + element.getTextRange())); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/psi/JavaPsiApplication.java b/java-frontend/src/test/java/org/sonar/java/model/psi/JavaPsiApplication.java new file mode 100644 index 00000000000..3a9d5a5c2f2 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/psi/JavaPsiApplication.java @@ -0,0 +1,167 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import com.intellij.core.CoreApplicationEnvironment; +import com.intellij.core.CoreJavaDirectoryService; +import com.intellij.core.CorePsiPackageImplementationHelper; +import com.intellij.ide.highlighter.JavaClassFileType; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.java.frontback.psi.impl.syntax.JavaSyntaxDefinitionExtension; +import com.intellij.lang.LanguageASTFactory; +import com.intellij.lang.java.JavaLanguage; +import com.intellij.lang.java.JavaParserDefinition; +import com.intellij.lang.java.syntax.JavaElementTypeConverterExtension; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.extensions.ExtensionPoint; +import com.intellij.openapi.extensions.ExtensionsArea; +import com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers; +import com.intellij.openapi.projectRoots.JavaVersionService; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.impl.jar.CoreJarFileSystem; +import com.intellij.platform.syntax.psi.CommonElementTypeConverterFactory; +import com.intellij.platform.syntax.psi.ElementTypeConverters; +import com.intellij.platform.syntax.psi.LanguageSyntaxDefinitions; +import com.intellij.platform.syntax.psi.PsiSyntaxBuilderFactory; +import com.intellij.platform.syntax.psi.PsiSyntaxBuilderFactoryImpl; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.ClassFileViewProviderFactory; +import com.intellij.psi.FileTypeFileViewProviders; +import com.intellij.psi.JavaDirectoryService; +import com.intellij.psi.PsiSubstitutorFactory; +import com.intellij.psi.compiled.ClassFileDecompilers; +import com.intellij.psi.impl.JavaClassSupersImpl; +import com.intellij.psi.impl.LanguageConstantExpressionEvaluator; +import com.intellij.psi.impl.PsiExpressionEvaluator; +import com.intellij.psi.impl.PsiSubstitutorFactoryImpl; +import com.intellij.psi.impl.compiled.ClassFileStubBuilder; +import com.intellij.psi.impl.compiled.ClsDecompilerImpl; +import com.intellij.psi.impl.file.PsiPackageImplementationHelper; +import com.intellij.psi.impl.source.tree.JavaASTFactory; +import com.intellij.psi.stubs.BinaryFileStubBuilders; +import com.intellij.psi.stubs.StubElementRegistryServiceImplKt; +import com.intellij.psi.util.JavaClassSupers; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Element; + +final class JavaPsiApplication extends CoreApplicationEnvironment { + + private static JavaPsiApplication shared; + private static int users; + private static final List DESCRIPTORS = List.of( + "META-INF/Core.xml", "META-INF/Core.analyzer.xml", "META-INF/CoreImpl.xml", "META-INF/CoreImpl.analyzer.xml", + "intellij.java.frontback.psi.xml", "intellij.java.frontback.psi.impl.xml", + "intellij.java.psi.xml", "intellij.java.psi.impl.xml"); + + private JavaPsiApplication(Disposable owner) { + super(owner, true); + registerExtensionPoints(application.getExtensionArea(), false); + Registry.get("java.highest.language.level").setValue(Integer.toString(LanguageLevel.HIGHEST.feature())); + registerFileType(JavaClassFileType.INSTANCE, "class"); + registerFileType(JavaFileType.INSTANCE, "java"); + addExtension(StubElementRegistryServiceImplKt.STUB_REGISTRY_EP, new JavaPsiStubFactories()); + addExplicitExtension(FileTypeFileViewProviders.INSTANCE, JavaClassFileType.INSTANCE, new ClassFileViewProviderFactory()); + addExplicitExtension(BinaryFileStubBuilders.INSTANCE, JavaClassFileType.INSTANCE, new ClassFileStubBuilder()); + addExplicitExtension(LanguageASTFactory.INSTANCE, JavaLanguage.INSTANCE, new JavaASTFactory()); + addExplicitExtension(LanguageSyntaxDefinitions.getINSTANCE(), JavaLanguage.INSTANCE, new JavaSyntaxDefinitionExtension()); + addExplicitExtension(ElementTypeConverters.getInstance(), JavaLanguage.INSTANCE, new CommonElementTypeConverterFactory()); + addExplicitExtension(ElementTypeConverters.getInstance(), JavaLanguage.INSTANCE, new JavaElementTypeConverterExtension()); + registerParserDefinition(new JavaParserDefinition()); + addExplicitExtension(LanguageConstantExpressionEvaluator.INSTANCE, JavaLanguage.INSTANCE, new PsiExpressionEvaluator()); + registerApplicationService(PsiPackageImplementationHelper.class, new CorePsiPackageImplementationHelper()); + registerApplicationService(PsiSubstitutorFactory.class, new PsiSubstitutorFactoryImpl()); + registerApplicationService(JavaClassSupers.class, new JavaClassSupersImpl()); + registerApplicationService(JavaDirectoryService.class, new CoreJavaDirectoryService()); + registerApplicationService(JavaVersionService.class, new JavaVersionService()); + registerApplicationService(PsiSyntaxBuilderFactory.class, new PsiSyntaxBuilderFactoryImpl()); + registerApplicationDynamicExtensionPoint("com.intellij.psi.classFileDecompiler", ClassFileDecompilers.Decompiler.class); + addExtension(ClassFileDecompilers.STATIC_EP_NAME, new ClsDecompilerImpl()); + BinaryFileTypeDecompilers.getInstance(); + } + + static synchronized JavaPsiApplication acquire() { + var current = ApplicationManager.getApplication(); + if (current != null && (shared == null || current != shared.getApplication())) { + throw new IllegalStateException("Cannot replace an existing IntelliJ application"); + } + if (shared == null) { + var owner = Disposer.newDisposable("SonarJava PSI application"); + try { + shared = new JavaPsiApplication(owner); + } catch (RuntimeException | Error failure) { + try { + Disposer.dispose(owner); + } catch (RuntimeException | Error cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } finally { + ApplicationManager.setApplication(null); + } + throw failure; + } + } + users++; + return shared; + } + + static synchronized void release() { + if (--users == 0) { + var previous = shared; + shared = null; + try { + ((CoreJarFileSystem) previous.getJarFileSystem()).clearHandlersCache(); + Disposer.dispose(previous.getParentDisposable()); + } finally { + ApplicationManager.setApplication(null); + } + } + } + + static void registerExtensionPoints(ExtensionsArea area, boolean project) { + for (var resource : DESCRIPTORS) { + try (var input = JavaPsiApplication.class.getClassLoader().getResourceAsStream(resource)) { + if (input == null) { + throw new IllegalStateException("Missing PSI descriptor: " + resource); + } + var factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + var document = factory.newDocumentBuilder().parse(input); + var points = document.getElementsByTagName("extensionPoint"); + for (int i = 0; i < points.getLength(); i++) { + var point = (Element) points.item(i); + if (project != "IDEA_PROJECT".equals(point.getAttribute("area"))) { + continue; + } + var name = point.hasAttribute("qualifiedName") ? point.getAttribute("qualifiedName") : "com.intellij." + point.getAttribute("name"); + if (!area.hasExtensionPoint(name)) { + var bean = point.hasAttribute("beanClass"); + area.registerExtensionPoint(name, point.getAttribute(bean ? "beanClass" : "interface"), + bean ? ExtensionPoint.Kind.BEAN_CLASS : ExtensionPoint.Kind.INTERFACE); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (javax.xml.parsers.ParserConfigurationException | org.xml.sax.SAXException e) { + throw new IllegalStateException("Cannot register PSI extensions", e); + } + } + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/psi/JavaPsiStubFactories.java b/java-frontend/src/test/java/org/sonar/java/model/psi/JavaPsiStubFactories.java new file mode 100644 index 00000000000..b4f4fdcfec7 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/psi/JavaPsiStubFactories.java @@ -0,0 +1,96 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import com.intellij.psi.impl.java.stubs.JavaStubElementTypePsiElementMappingRegistry; +import com.intellij.psi.impl.java.stubs.JavaStubElementTypes; +import com.intellij.psi.impl.java.stubs.factories.JavaAnnotationParameterListStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaAnnotationStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaClassInitializerStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaClassReferenceListStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaClassStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaFieldStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaImportListStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaImportStatementStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaLiteralExpressionStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaMethodStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaModifierListStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaModuleStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaNameValuePairStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaPackageAccessibilityStatementStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaPackageStatementStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaParameterListStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaParameterStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaProvidesStatementStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaRecordComponentStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaRecordHeaderStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaRequiresStatementStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaTypeParameterListStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaTypeParameterStubFactory; +import com.intellij.psi.impl.java.stubs.factories.JavaUsesStatementStubFactory; +import com.intellij.psi.impl.java.stubs.factories.LambdaExpressionStubFactory; +import com.intellij.psi.impl.java.stubs.factories.MethodReferenceStubFactory; +import com.intellij.psi.stubs.StubRegistry; +import com.intellij.psi.stubs.StubRegistryExtension; + +/** Registers in-memory Java stub factories without persistent index serializers. */ +final class JavaPsiStubFactories implements StubRegistryExtension { + + @Override + public void register(StubRegistry registry) { + JavaStubElementTypePsiElementMappingRegistry.getInstance(); + registry.registerLightStubFactory(JavaStubElementTypes.LITERAL_EXPRESSION, new JavaLiteralExpressionStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.MODIFIER_LIST, new JavaModifierListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.ANNOTATION, new JavaAnnotationStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.ANNOTATION_PARAMETER_LIST, new JavaAnnotationParameterListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.NAME_VALUE_PAIR, new JavaNameValuePairStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.LAMBDA_EXPRESSION, new LambdaExpressionStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.METHOD_REF_EXPRESSION, new MethodReferenceStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.PARAMETER_LIST, new JavaParameterListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.PARAMETER, new JavaParameterStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.TYPE_PARAMETER, new JavaTypeParameterStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.TYPE_PARAMETER_LIST, new JavaTypeParameterListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.CLASS_INITIALIZER, new JavaClassInitializerStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.IMPORT_LIST, new JavaImportListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.MODULE, new JavaModuleStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.REQUIRES_STATEMENT, new JavaRequiresStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.USES_STATEMENT, new JavaUsesStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.PROVIDES_STATEMENT, new JavaProvidesStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.RECORD_COMPONENT, new JavaRecordComponentStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.RECORD_HEADER, new JavaRecordHeaderStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.PACKAGE_STATEMENT, new JavaPackageStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.EXPORTS_STATEMENT, new JavaPackageAccessibilityStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.OPENS_STATEMENT, new JavaPackageAccessibilityStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.CLASS, new JavaClassStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.IMPLICIT_CLASS, new JavaClassStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.ANONYMOUS_CLASS, new JavaClassStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.ENUM_CONSTANT_INITIALIZER, new JavaClassStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.METHOD, new JavaMethodStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.ANNOTATION_METHOD, new JavaMethodStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.FIELD, new JavaFieldStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.ENUM_CONSTANT, new JavaFieldStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.EXTENDS_LIST, new JavaClassReferenceListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.PERMITS_LIST, new JavaClassReferenceListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.IMPLEMENTS_LIST, new JavaClassReferenceListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.THROWS_LIST, new JavaClassReferenceListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.EXTENDS_BOUND_LIST, new JavaClassReferenceListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.PROVIDES_WITH_LIST, new JavaClassReferenceListStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.IMPORT_STATEMENT, new JavaImportStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.IMPORT_STATIC_STATEMENT, new JavaImportStatementStubFactory()); + registry.registerLightStubFactory(JavaStubElementTypes.IMPORT_MODULE_STATEMENT, new JavaImportStatementStubFactory()); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/psi/PsiSourceSnapshotTest.java b/java-frontend/src/test/java/org/sonar/java/model/psi/PsiSourceSnapshotTest.java new file mode 100644 index 00000000000..5cfb991c37a --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/psi/PsiSourceSnapshotTest.java @@ -0,0 +1,119 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PsiSourceSnapshotTest { + + private static final String BACKSLASH = Character.toString(92); + + @Test + void unchanged_sources_use_identity_coordinates() { + for (String source : List.of("", "class A {}", "String s = \"" + BACKSLASH.repeat(2) + "u0061\";", BACKSLASH + "x")) { + var snapshot = PsiSourceSnapshot.from(source); + assertThat(snapshot.psiText()).isSameAs(source); + assertThat(snapshot.translated()).isSameAs(source); + for (int i = 0; i <= source.length(); i++) { + assertThat(snapshot.originalOffset(i)).isEqualTo(i); + } + } + } + + @Test + void translates_eligible_backslashes_using_result_provenance() { + for (int count = 1; count <= 8; count++) { + String source = BACKSLASH.repeat(count) + "u0061"; + String expected = count % 2 == 0 ? source : BACKSLASH.repeat(count - 1) + "a"; + assertThat(PsiSourceSnapshot.from(source).translated()).as(source).isEqualTo(expected); + } + assertTranslation(escape("005c") + escape("0061"), BACKSLASH + "a"); + assertTranslation(escape("005c") + BACKSLASH + "u0061", BACKSLASH + "a"); + assertTranslation(escape("005c") + BACKSLASH.repeat(2) + "u0061", BACKSLASH.repeat(2) + "a"); + assertTranslation(escape("005c") + BACKSLASH.repeat(3) + "u0061", BACKSLASH.repeat(4) + "u0061"); + } + + @Test + void never_recursively_translates_a_generated_backslash() { + var snapshot = PsiSourceSnapshot.from(escape("005c") + "u0061"); + assertThat(snapshot.translated()).isEqualTo(BACKSLASH + "u0061"); + assertThat(snapshot.psiText()).isEqualTo(escape("005c") + "u0061"); + } + + @Test + void repeated_u_characters_and_surrogate_units_retain_original_boundaries() { + var repeated = PsiSourceSnapshot.from(BACKSLASH + "uuuu0041"); + assertThat(repeated.translated()).isEqualTo("A"); + assertThat(repeated.originalOffset(1)).isEqualTo(9); + var supplementary = PsiSourceSnapshot.from(escape("D801") + escape("dc00")); + assertThat(supplementary.translated()).isEqualTo(new String(Character.toChars(0x10400))); + assertThat(supplementary.originalOffset(1)).isEqualTo(6); + assertThat(supplementary.originalOffset(2)).isEqualTo(12); + } + + @Test + void maps_contraction_and_backslash_protection_to_original_utf16_boundaries() { + String source = "x" + escape("0061") + "b" + escape("005c") + "u000aZ"; + var snapshot = PsiSourceSnapshot.from(source); + assertThat(snapshot.translated()).isEqualTo("xab" + BACKSLASH + "u000aZ"); + assertThat(snapshot.psiText()).isEqualTo("xab" + escape("005c") + "u000aZ"); + assertThat(snapshot.originalText(1, 2)).isEqualTo(escape("0061")); + assertThat(snapshot.logicalText(1, 2)).isEqualTo("a"); + assertThat(snapshot.originalText(3, 9)).isEqualTo(escape("005c")); + assertThat(snapshot.logicalText(3, 9)).isEqualTo(BACKSLASH); + int previous = 0; + for (int i = 0; i <= snapshot.psiText().length(); i++) { + int offset = snapshot.originalOffset(i); + assertThat(offset).isBetween(previous, source.length()); + previous = offset; + } + assertThat(previous).isEqualTo(source.length()); + assertThat(snapshot.originalText(0, snapshot.psiText().length())).isEqualTo(source); + assertThat(snapshot.logicalText(0, snapshot.psiText().length())).isEqualTo(snapshot.translated()); + } + + @Test + void rejects_malformed_eligible_escapes_with_original_offsets() { + for (String suffix : List.of("u", "uu", "u123", "u12x4", "u1234")) { + assertThatThrownBy(() -> PsiSourceSnapshot.from("abc" + BACKSLASH + suffix)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("Malformed Java Unicode escape at original offset 3"); + String ineligible = BACKSLASH.repeat(2) + suffix; + assertThat(PsiSourceSnapshot.from(ineligible).translated()).isEqualTo(ineligible); + } + } + + @Test + void offset_and_slice_bounds_are_checked() { + var snapshot = PsiSourceSnapshot.from(escape("0061")); + assertThatThrownBy(() -> snapshot.originalOffset(-1)).isInstanceOf(IndexOutOfBoundsException.class); + assertThatThrownBy(() -> snapshot.originalOffset(2)).isInstanceOf(IndexOutOfBoundsException.class); + assertThatThrownBy(() -> snapshot.originalText(1, 0)).isInstanceOf(IndexOutOfBoundsException.class); + assertThatThrownBy(() -> snapshot.logicalText(0, 2)).isInstanceOf(IndexOutOfBoundsException.class); + } + + private static void assertTranslation(String source, String expected) { + assertThat(PsiSourceSnapshot.from(source).translated()).as(source).isEqualTo(expected); + } + + private static String escape(String hex) { + return BACKSLASH + "u" + hex; + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/psi/PsiTestSession.java b/java-frontend/src/test/java/org/sonar/java/model/psi/PsiTestSession.java new file mode 100644 index 00000000000..f806eabdcba --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/psi/PsiTestSession.java @@ -0,0 +1,65 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import com.intellij.core.JavaCoreProjectEnvironment; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.roots.LanguageLevelProjectExtension; +import com.intellij.openapi.util.Disposer; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiManager; +import com.intellij.testFramework.LightVirtualFile; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; +import org.sonar.java.model.JavaTree; + +final class PsiTestSession implements AutoCloseable { + + private final JavaPsiApplication application = JavaPsiApplication.acquire(); + private final com.intellij.openapi.Disposable owner = Disposer.newDisposable("PSI syntax test"); + private final JavaCoreProjectEnvironment environment = new JavaCoreProjectEnvironment(owner, application); + + PsiTestSession() { + JavaPsiApplication.registerExtensionPoints(environment.getProject().getExtensionArea(), true); + ApplicationManager.getApplication().runWriteAction(() -> + LanguageLevelProjectExtension.getInstance(environment.getProject()).setLanguageLevel(LanguageLevel.JDK_26_PREVIEW)); + } + + T read(String name, String source, Function action) { + return ApplicationManager.getApplication().runReadAction((com.intellij.openapi.util.Computable) () -> { + var virtualFile = new LightVirtualFile(name, JavaFileType.INSTANCE, source, StandardCharsets.UTF_8, 0); + virtualFile.setWritable(false); + return action.apply((PsiJavaFile) PsiManager.getInstance(environment.getProject()).findFile(virtualFile)); + }); + } + + JavaTree.CompilationUnitTreeImpl convert(String name, String source) { + var snapshot = PsiSourceSnapshot.from(source); + return read(name, snapshot.psiText(), file -> PsiTreeConverter.convert(file, snapshot)); + } + + @Override + public void close() { + try { + Disposer.dispose(owner); + } finally { + JavaPsiApplication.release(); + } + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/model/psi/PsiTreeConverterTest.java b/java-frontend/src/test/java/org/sonar/java/model/psi/PsiTreeConverterTest.java new file mode 100644 index 00000000000..d0956eff121 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/psi/PsiTreeConverterTest.java @@ -0,0 +1,543 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.model.psi; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiMethodCallExpression; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.sonar.java.model.InternalSyntaxToken; +import org.sonar.java.model.JParserTestUtils; +import org.sonar.java.model.JavaTree; +import org.sonar.plugins.java.api.tree.SyntaxToken; +import org.sonar.plugins.java.api.tree.Tree; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PsiTreeConverterTest { + + private static PsiTestSession session; + private static final Set observedKinds = java.util.EnumSet.noneOf(Tree.Kind.class); + + @BeforeAll + static void open() { + session = new PsiTestSession(); + } + + @AfterAll + static void close() { + if (session != null) { + session.close(); + } + var expected = java.util.EnumSet.allOf(Tree.Kind.class); + expected.remove(Tree.Kind.OTHER); + assertThat(observedKinds).containsAll(expected); + } + + static List sources() { + return List.of( + "", + "// comment only\n", + "/* header */ package sample; import java.util.*; import static java.util.List.of; class A {} // trailing", + "import module java.base; class A {}", + "class A { public static final int VALUE = 1; String name; void m(int x, String y) { return; } }", + "class A { int a=1,b=2; void m(){ int x=3,y=4; x=y; } }", + "class A { ; static { foo(); } { foo(); } A() { this(1); } A(int n) { super(); } void foo() {} }", + "@interface A { int value() default 1; String[] names() default {\"a\",\"b\",}; }", + "@Deprecated class A> extends B implements C, D {} class B {} interface C {} interface D {}", + "class A { java.util.Map> map; java.util.List numbers; java.util.List strings; java.util.List any; }", + "class A { int[] a; int b[]; int[][] c; void m(String... args) {} }", + "record A(int x, String y) implements java.io.Serializable { A { } A(int x) { this(x, null); } }", + "enum A { X, Y(1) { void m() {} }; A() {} A(int x) {} }", + "class A { Object m() { return new Runnable() { public void run() {} }; } }", + "class A { Object m() { class Local {} return new Local(); } }", + "class A { void m() { if (true) foo(); else bar(); while (true) { break; } do { continue; } while (false); } }", + "class A { void m() { for (int i=0,j=1;i<10;i++,j--) { } for(;;); for(String s : values) foo(s); } }", + "class A { void m() { label: while(true) { if (true) break label; continue label; } assert true; assert false : \"detail\"; synchronized(this) {} throw new RuntimeException(); } }", + "class A { void m() { try { foo(); } catch (IllegalArgumentException | IllegalStateException ex) {} finally {} } }", + "class A { void m() { try (var a = open(); var b = open();) {} } }", + "class A { void m() { switch(x) { case 1: case 2: foo(); break; default: bar(); } } }", + "class A { int m() { return switch(x) { case 1, 2 -> 3; default -> { yield 4; } }; } }", + "class A { void m() { int a = 1+2*3-4/5%6; boolean b = a<2 && a>=1 || a==3; a += 2; a >>>= 1; ++a; a--; Object x = b ? a : null; } }", + "class A { Object m(Object x) { return (Runnable & java.io.Serializable) x; } boolean test(Object x) { return x instanceof String s; } }", + "class A { Object a = (x,y) -> x; Object b = x -> x; Object c = (String x) -> { return x; }; Object d = String::valueOf; Object e = A::new; }", + "class A { int[][] a = new int[2][]; String[] b = new String[]{\"a\",\"b\",}; Object c = int[].class; int d = a[0][1]; }", + "class A { Object a = this; Object b = A.this; void m() { super.foo(); this.foo(); foo(); obj.foo(); } }", + "class A { void m(int a, int b) { a = +a * -b / a % b + a - b; a = ~a; boolean x = !true || false && ab && a<=b && a>=b && a==b && a!=b; a = a & b | a ^ b; a = a << b >> a >>> b; a*=b; a/=b; a%=b; a+=b; a-=b; a<<=b; a>>=b; a>>>=b; a&=b; a^=b; a|=b; ++a; --a; a++; a--; } }", + "class A { long a = -9223372036854775808L; int b = -2147483648; float c = 1f; double d = 0x1.fp2; char e = 'a'; boolean f = true; }", + "class A { String text = \"\"\"\nhello\nworld\n\"\"\"; }", + "sealed interface A permits B {} final class B implements A {}", + "class A { int m(Object o) { return switch(o) { case String s when !s.isEmpty() -> 1; case null, default -> 0; }; } }", + "record Pair(int x, int y) {} class A { boolean m(Object o) { return o instanceof Pair(int x, int y); } }", + "void main() { System.out.println(\"hello\"); }", + "class A { A(int x) { int y=x+1; super(); } }", + "/// heading\n/// description\nclass A {}", + "package p; ;", + "package p; ; class A {}", + "enum E { ; }", + "enum E { ; void m() {} }", + "class A { int \u0061; String emoji=\"😀\"; }"); + } + + @ParameterizedTest + @MethodSource("sources") + void matches_ecj_syntax(String source) { + compare("File.java", source); + } + + static List existingModelSources() throws IOException { + String code = Files.readString(Path.of("src/test/java/org/sonar/java/model/JavaTreeModelTest.java")); + List sources = session.read("JavaTreeModelTest.java", code, file -> { + List result = new ArrayList<>(); + collectModelSources(file, result); + return result.stream().distinct().toList(); + }); + assertThat(sources).hasSizeGreaterThan(150); + return sources; + } + + private static void collectModelSources(PsiElement element, List sources) { + if (element instanceof PsiMethodCallExpression call + && Set.of("firstType", "firstTypeMember", "firstMethodFirstStatement", "expressionOfReturnStatement", "compilationUnit", "parse", + "annotations", "firstMethodInvocation", "firstConstructorInvocation", "firstMethodParameter") + .contains(call.getMethodExpression().getReferenceName()) + && call.getArgumentList().getExpressions().length != 0 + && call.getArgumentList().getExpressions()[0] instanceof PsiLiteralExpression literal + && literal.getValue() instanceof String source) { + sources.add(source); + } + for (PsiElement child : element.getChildren()) { + collectModelSources(child, sources); + } + } + + @ParameterizedTest + @MethodSource("existingModelSources") + void existing_model_syntax(String source) { + compare("File.java", source); + } + + @Test + void module_declarations() { + compare("package-info.java", "@Deprecated package sample;"); + compare("module-info.java", "@Deprecated open module sample.module { requires static transitive java.base; exports sample.api to other.module, another; opens sample.impl; uses sample.Service; provides sample.Service with sample.Impl, sample.Other; }"); + } + + @Test + void physical_line_endings_and_comments() { + for (String newline : List.of("\n", "\r\n", "\r")) { + compare("File.java", "/* header */" + newline + "class A { // line" + newline + "\tString s = \"😀\"; /* tail */ }" + newline + "// eof"); + } + } + + @Test + void existing_token_completeness_source() throws IOException { + compare("TreeTokenCompletenessTest.java", Files.readString(Path.of("src/test/java/org/sonar/java/model/TreeTokenCompletenessTest.java"))); + } + + static List existingSourceFiles() throws IOException { + List result = new ArrayList<>(); + for (String directory : List.of("model", "ast", "highlighter", "cfg")) { + try (var files = Files.walk(Path.of("src/test/files", directory))) { + result.addAll(files.filter(path -> path.toString().endsWith(".java")).sorted().toList()); + } + } + return result; + } + + @ParameterizedTest + @MethodSource("existingSourceFiles") + void existing_source_files(Path file) throws IOException { + compare(file.getFileName().toString(), Files.readString(file)); + } + + @Test + void unicode_identifiers_use_the_source_bridge_while_raw_psi_remains_unsupported() { + String escape = Character.toString(92) + "u0061"; + String source = "class A { int " + escape + " = 1; int b = " + escape + "; }"; + assertThat(JParserTestUtils.parse(source).types()).hasSize(1); + session.read("File.java", source, file -> { + assertThat(com.intellij.psi.util.PsiTreeUtil.findChildOfType(file, com.intellij.psi.PsiErrorElement.class)).isNotNull(); + assertThatThrownBy(() -> PsiTreeConverter.convert(file, source)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unicode escapes"); + assertThatThrownBy(() -> PsiTreeConverter.convert(file, PsiSourceSnapshot.identity(source))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("parse errors"); + return null; + }); + compare("File.java", source); + } + + static List unicodeSources() { + String slash = Character.toString(92); + return List.of( + "cl" + escape("0061") + "ss " + escape("0041") + " " + escape("007b") + " " + escape("0069") + "nt " + escape("0061") + " = " + escape("0031") + escape("003b") + " int b = " + escape("0061") + "; " + escape("007d"), + "class A { int na" + escape("006d") + "e; int m" + escape("0065") + "thod() { return na" + escape("006d") + "e; } }", + "class A { int " + slash + "uuuu0061; int " + escape("D801") + escape("DC00") + "; }", + "class A { int a" + escape("0000") + "b; int a" + escape("200c") + "c; }", + "class A { String s = \"" + escape("0061") + "\"; char c = '" + escape("0062") + "'; }", + "class A { int " + escape("0061") + "; String s = \"" + slash.repeat(2) + "u0061\"; String n = \"" + slash + "n\"; }", + "class A { char c = '" + escape("005c") + escape("005c") + "'; }", + "class A { String s = \"" + escape("005c") + escape("005c") + "u0061\"; }", + "class A { // " + escape("005c") + "u000a int hidden;\n int visible; }", + "class A { // comment" + escape("000a") + "int a; }", + "class A { // comment" + escape("000d") + "int a; }", + "class A { /* before " + escape("000a") + " after */ int a; } // " + escape("0061"), + "/// heading " + escape("0061") + "\n/// next\nclass A {}", + "import java" + escape("002e") + "util.List; class A { java" + escape("002e") + "util.List a; }", + "class A { java.util.List a; }", + "class A { java.util.List escapedModelSources() throws IOException { + List result = new ArrayList<>(); + for (String source : existingModelSources()) { + for (char c : new char[] {'a', '>', '.', ';', ',', ' '}) { + if (source.indexOf(c) >= 0) { + result.add(source.replace(Character.toString(c), escape(String.format("%04x", (int) c)))); + } + } + } + return result; + } + + @ParameterizedTest + @MethodSource("escapedModelSources") + void escaped_existing_model_syntax(String source) { + compare("File.java", source); + } + + static List unicodeLineEndings() { + return List.of(escape("000d") + escape("000a"), escape("000d") + "\n", "\r" + escape("000a"), + escape("000a") + "\r\n", escape("000d") + " ", escape("000a") + escape("000a")); + } + + @ParameterizedTest + @MethodSource("unicodeLineEndings") + void unicode_line_comment_terminators(String ending) { + compare("File.java", "class A { // comment" + ending + "int a; }"); + } + + @Test + void unicode_backslash_eligibility_in_comments() { + String slash = Character.toString(92); + for (String prefix : List.of("", escape("005c"), escape("005c") + slash, escape("005c") + slash.repeat(2))) { + for (int count = 0; count <= 8; count++) { + compare("File.java", "class A { // " + prefix + slash.repeat(count) + "u000a\n int a; }"); + } + } + } + + @Test + void escaped_module_names_and_contextual_keywords() { + compare("module-info.java", "open mod" + escape("0075") + "le sample" + escape("002e") + "module { requires trans" + escape("0069") + "tive; }"); + compare("module-info.java", "module sample { requires static trans" + escape("0069") + "tive other" + escape("002e") + "module; }"); + compare("File.java", "import mod" + escape("0075") + "le java" + escape("002e") + "base; class A {}"); + } + + @Test + void escaped_comment_delimiters_retain_the_ecj_validation_failure() { + for (String source : List.of(escape("002f") + "/ comment\nclass A {}", "/* comment *" + escape("002f") + "class A {}", + "/" + escape("002a") + "* javadoc */ class A {}", escape("002f") + "// markdown\nclass A {}")) { + assertThatThrownBy(() -> JParserTestUtils.parse(source)).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Invalid comment kind"); + assertThatThrownBy(() -> session.convert("File.java", source)).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Invalid comment kind"); + } + } + + static List unicodeGenericClosers() { + return List.of( + "class A { List a; }", + "class A { List /* outer */ " + escape("003e") + " a; }", + "class A { Object x = obj. /* outer */ > field; int i = 8 >> 1 >>> 1; }", + "class A { String s = \"\"\"\nfirst" + Character.toString(92) + "\nsecond\n\"\"\"; }", + "class A { Object f = new java.util.ArrayList<>(); Object g = int[]::new; Object h = this::m; Object i = super::m; }", + "record P(int a, int b) {} class A { boolean m(Object o) { return o instanceof P(int _, _); } }", + "class A { void m(Object o) { switch(o) { case String _ -> foo(); default -> bar(); } } }", + "class A { void m() { Object resource = null; try (resource) {} } }", + "import java.util.List;; class A {};;")) { + compare("File.java", source); + } + } + + @Test + void nested_punctuation_and_keywords() { + for (String source : List.of( + "@interface Ann { Class value(); } @Ann(String.class) class A {}", + "class A { boolean a,b,c,d,e; boolean value = (a ? b : c) ? d : e; }", + "class A { Object x; boolean value = f(x instanceof String) instanceof Object; Object f(boolean b) { return null; } }", + "class A { int[] a; int[][] b = new int[a[0]][a[1]]; }", + "class A { String[] a = new @Ann String[2]; String[][] b = new @Ann String @Ann [2] @Ann [3]; }", + "class A { class B {} Object b = new A().new B(); }", + "class A { void m() { for (@Ann(true ? 1 : 2) String s : values) {} } } @interface Ann { int value(); }")) { + compare("File.java", source); + } + } + + @Test + void psi_tokens_reconstruct_the_original_snapshot() { + for (String newline : List.of("\n", "\r\n", "\r")) { + String source = "\t/* leading */" + newline + "/// markdown" + newline + "/// continuation" + newline + + "class A { /** javadoc */ String s = \"😀\"; String block = \"\"\"" + newline + + "first" + Character.toString(92) + newline + "second" + newline + "\"\"\"; } // trailing" + newline; + for (String original : List.of(source, source.replace("class", "cl" + escape("0061") + "ss").replace(" ", escape("0020")))) { + var snapshot = PsiSourceSnapshot.from(original); + session.read("File.java", snapshot.psiText(), file -> { + var index = new PsiTokenIndex(file, snapshot); + int previousEnd = 0; + StringBuilder reconstructed = new StringBuilder(); + for (var token : index.tokens()) { + for (var trivia : token.trivias()) { + previousEnd = appendPiece(original, previousEnd, trivia.range(), trivia.comment(), reconstructed); + } + previousEnd = appendPiece(original, previousEnd, token.range(), token.text(), reconstructed); + } + assertThat(index.eof().range().start()).isEqualTo(new org.sonar.java.model.LineColumnConverter(original).toPosition(original.length())); + assertThat(reconstructed).hasToString(original); + return null; + }); + } + } + } + + private static int appendPiece(String source, int previousEnd, org.sonar.plugins.java.api.location.Range range, String text, StringBuilder result) { + int start = absoluteOffset(source, range.start()); + int end = absoluteOffset(source, range.end()); + assertThat(start).isGreaterThanOrEqualTo(previousEnd); + assertThat(PsiSourceSnapshot.from(source.substring(previousEnd, start)).translated().chars().allMatch(Character::isWhitespace)).isTrue(); + assertThat(source.substring(start, end)).isEqualTo(text); + result.append(source, previousEnd, start).append(text); + return end; + } + + private static int absoluteOffset(String source, org.sonar.plugins.java.api.location.Position position) { + var matcher = java.util.regex.Pattern.compile("\\r\\n?|\\n").matcher(source); + int start = 0; + for (int line = 1; line < position.line(); line++) { + assertThat(matcher.find()).isTrue(); + start = matcher.end(); + } + return start + position.columnOffset(); + } + + @Test + void mapping_inventory_covers_the_ecj_converter() throws IOException { + String parser = Files.readString(Path.of("src/main/java/org/sonar/java/model/JParser.java")); + var matcher = java.util.regex.Pattern.compile("ASTNode\\.([A-Z_]+)").matcher(parser); + Set branches = new java.util.TreeSet<>(); + while (matcher.find()) { + branches.add(matcher.group(1)); + } + try (var input = getClass().getResourceAsStream("/psi-syntax-mapping.json")) { + var ledger = com.google.gson.JsonParser.parseReader(new java.io.InputStreamReader(java.util.Objects.requireNonNull(input), java.nio.charset.StandardCharsets.UTF_8)); + assertThat(ledger.getAsJsonObject().getAsJsonObject("ecjNodes").keySet()).containsExactlyInAnyOrderElementsOf(branches); + } + } + + @Test + void rejects_mismatched_snapshot() { + session.read("A.java", "class A {}", file -> { + assertThatThrownBy(() -> PsiTreeConverter.convert(file, "class B {}")) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("snapshot"); + return null; + }); + String original = "class A { int " + escape("0061") + "; }"; + var snapshot = PsiSourceSnapshot.from(original); + session.read("A.java", original, file -> { + assertThatThrownBy(() -> PsiTreeConverter.convert(file, snapshot)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("snapshot"); + return null; + }); + } + + @Test + void rejects_parse_errors() { + String source = "class A { void m() { int x = 1 } }"; + session.read("A.java", source, file -> { + assertThatThrownBy(() -> PsiTreeConverter.convert(file, source)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("parse errors"); + return null; + }); + } + + @Test + void converted_tree_outlives_psi() { + String source = "class A { int " + escape("0061") + "; String s = \"" + escape("005c") + escape("005c") + "\"; }"; + JavaTree result; + try (var isolated = new PsiTestSession()) { + result = isolated.convert("A.java", source); + } + assertThat(snapshot(result)).isEqualTo(snapshot((JavaTree) JParserTestUtils.parse(source))); + } + + @Test + void mapped_psi_leaves_reconstruct_the_original_source() { + for (String original : unicodeSources()) { + var source = PsiSourceSnapshot.from(original); + session.read("File.java", source.psiText(), file -> { + var reconstructed = new StringBuilder(); + appendOriginalLeaves(file, source, reconstructed); + assertThat(reconstructed).hasToString(original); + return null; + }); + } + } + + private static void appendOriginalLeaves(PsiElement element, PsiSourceSnapshot source, StringBuilder result) { + if (element.getFirstChild() == null) { + result.append(source.originalText(element.getTextRange().getStartOffset(), element.getTextRange().getEndOffset())); + } else { + for (PsiElement child = element.getFirstChild(); child != null; child = child.getNextSibling()) { + appendOriginalLeaves(child, source, result); + } + } + } + + private static void compare(String name, String source) { + JavaTree expected = (JavaTree) JParserTestUtils.parse(name, source, JParserTestUtils.DEFAULT_CLASSPATH); + JavaTree actual = session.convert(name, source); + assertThat(snapshot(actual)).as(source).isEqualTo(snapshot(expected)); + } + + private static List snapshot(JavaTree root) { + List result = new ArrayList<>(); + snapshot(root, "root", result); + var paths = new java.util.IdentityHashMap(); + collectPaths(root, "root", paths); + appendParents(root, "root", paths, result); + return result; + } + + private static void collectPaths(JavaTree tree, String path, java.util.IdentityHashMap paths) { + paths.putIfAbsent(tree, path); + if (!tree.isLeaf()) { + var children = tree.getChildren(); + for (int i = 0; i < children.size(); i++) { + collectPaths((JavaTree) children.get(i), path + "/" + i, paths); + } + } + } + + private static void appendParents(JavaTree tree, String path, java.util.IdentityHashMap paths, List result) { + result.add(path + " parent=" + paths.get(tree.parent())); + if (!tree.isLeaf()) { + var children = tree.getChildren(); + for (int i = 0; i < children.size(); i++) { + appendParents((JavaTree) children.get(i), path + "/" + i, paths, result); + } + } + } + + private static void appendAccessors(JavaTree tree, String path, List result) { + var methods = Arrays.stream(tree.kind().getAssociatedInterface().getMethods()) + .filter(method -> method.getDeclaringClass().isInterface() && method.getParameterCount() == 0 && !method.getName().equals("parent")) + .filter(method -> Tree.class.isAssignableFrom(method.getReturnType()) + || Iterable.class.isAssignableFrom(method.getReturnType()) && method.getGenericReturnType().getTypeName().contains("org.sonar.plugins.java.api.tree.")) + .sorted(java.util.Comparator.comparing(java.lang.reflect.Method::getName)).toList(); + for (var method : methods) { + try { + Object value = method.invoke(tree); + if (value instanceof Iterable values && !(value instanceof SyntaxToken)) { + List items = new ArrayList<>(); + for (Object item : values) { + items.add(describe((Tree) item)); + } + result.add(path + " " + method.getName() + "=" + items); + } else { + result.add(path + " " + method.getName() + "=" + describe((Tree) value)); + } + } catch (ReflectiveOperationException e) { + throw new AssertionError(path + " " + method, e); + } + } + } + + private static String describe(Tree tree) { + if (tree == null) { + return "null"; + } + if (tree instanceof org.sonar.plugins.java.api.tree.SyntaxTrivia trivia) { + return trivia.commentKind() + "@" + trivia.range() + ":" + trivia.comment(); + } + if (tree instanceof SyntaxToken token) { + return token.text() + "@" + token.range(); + } + return tree.kind() + "[" + describe(tree.firstToken()) + "," + describe(tree.lastToken()) + "]"; + } + + private static void snapshot(JavaTree tree, String path, List result) { + observedKinds.add(tree.kind()); + appendAccessors(tree, path, result); + String prefix = path + " " + tree.getClass().getSimpleName() + " " + tree.kind(); + if (tree instanceof SyntaxToken token) { + result.add(prefix + " " + token.text() + " " + token.range() + " EOF=" + ((InternalSyntaxToken) token).isEOF()); + for (var trivia : token.trivias()) { + observedKinds.add(Tree.Kind.TRIVIA); + result.add(path + " trivia " + trivia.commentKind() + " " + trivia.comment() + " " + trivia.range()); + } + } else if (!tree.isLeaf()) { + result.add(prefix); + var children = tree.getChildren(); + for (int i = 0; i < children.size(); i++) { + var child = (JavaTree) children.get(i); + snapshot(child, path + "/" + i, result); + } + } else { + result.add(prefix); + } + } +} diff --git a/sonar-java-plugin/pom.xml b/sonar-java-plugin/pom.xml index af8cd45089c..7db7c116425 100644 --- a/sonar-java-plugin/pom.xml +++ b/sonar-java-plugin/pom.xml @@ -184,8 +184,8 @@ - 21500000 - 19000000 + 41000000 + 38000000 ${project.build.directory}/${project.build.finalName}.jar @@ -252,6 +252,19 @@ ** + + + com.jetbrains.intellij.java:* + + ** + + + + com.jetbrains.intellij.platform:* + + ** + +