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 super EmptyStatementTreeImpl> 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 super EmptyStatementTreeImpl> 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 extends Number> numbers; java.util.List super String> 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:*
+
+ **
+
+