diff --git a/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java index a539841b022..cbc71c27d49 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java @@ -64,7 +64,7 @@ private static class EraseSymbols extends BaseTreeVisitor { @Override public void visitVariable(VariableTree tree) { if (tree instanceof VariableTreeImpl varImpl) { - varImpl.variableBinding = null; + varImpl.variableSymbol = null; } super.visitVariable(tree); } diff --git a/java-checks/src/test/java/org/sonar/java/checks/ParsingErrorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/ParsingErrorCheckTest.java index debf45c33a4..59936381065 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/ParsingErrorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/ParsingErrorCheckTest.java @@ -47,12 +47,9 @@ void test() { assertThat(issue.getMessage()).isEqualTo("Parse error"); } - /** - * Related to SONARJAVA-3486: discrepancies between JLS, javac (more tolerant than JLS) and ECJ (strict to JLS). - * According to JLS (§7.3 Compilation Units), empty statements are not allowed in import section. - */ + /** An empty statement among the imports is kept and the file is still analyzed. */ @Test - void javacEmptyStatementsInImportsBug() { + void empty_statement_in_imports_is_analyzed() { SonarComponents sonarComponents = mock(SonarComponents.class); when(sonarComponents.inputFileContents(any())).thenCallRealMethod(); @@ -60,10 +57,6 @@ void javacEmptyStatementsInImportsBug() { .withSonarComponents(sonarComponents) .build(); JavaAstScanner.scanSingleFileForTests(TestUtils.inputFile("src/test/files/checks/parsing/EmptyStatementsInImportsBug.java"), visitorsBridge); - Set issues = visitorsBridge.testContexts().get(0).getIssues(); - assertThat(issues).hasSize(1); - AnalyzerMessage issue = issues.iterator().next(); - assertThat(issue.getLine()).isEqualTo(3); - assertThat(issue.getMessage()).isEqualTo("Parse error"); + assertThat(visitorsBridge.testContexts().get(0).getIssues()).isEmpty(); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java index fd512787b13..4cb739ca49f 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java @@ -19,7 +19,6 @@ import com.google.common.io.Files; import java.io.File; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.sonar.java.model.JParser; @@ -83,9 +82,9 @@ public static CompilationUnitTree parse(File file) { } public static CompilationUnitTree parse(String source) { - List classpath = Arrays.asList(new File("target/test-classes"), new File("target/classes")); + List classpath = List.of(new File("target/test-classes"), new File("target/classes")); JavaVersion version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION; - return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), "test", source); + return JParser.parse(version.toString(), "test", source, classpath); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java index e5c715f9739..8a2f5168e9e 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/SpringContextModelTestUtils.java @@ -174,6 +174,6 @@ private static CompilationUnitTree parse(File file, List classpath) { throw new IllegalStateException("Unable to read file '" + file.getAbsolutePath() + "'", e); } JavaVersion version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION; - return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), file.getName(), source); + return JParser.parse(version.toString(), file.getName(), source); } } diff --git a/java-frontend/pom.xml b/java-frontend/pom.xml index 016dbbde0b9..648b147ab45 100644 --- a/java-frontend/pom.xml +++ b/java-frontend/pom.xml @@ -18,12 +18,6 @@ - - ${project.groupId} - jdt-package - 1.9.0.1840 - - org.sonarsource.api.plugin sonar-plugin-api @@ -92,6 +86,16 @@ com.google.code.gson gson + + org.apache.commons + commons-compress + 1.27.1 + + + org.tukaani + xz + 1.10 + org.sonarsource.java @@ -144,6 +148,18 @@ + + + src/main/resources + + + ${maven.multiModuleProjectDirectory}/semsitter-bridge/prebuilt + + bridge-*.xz + + bridge + + org.apache.maven.plugins diff --git a/java-frontend/src/main/java/org/eclipse/jdt/core/dom/ASTUtils.java b/java-frontend/src/main/java/org/eclipse/jdt/core/dom/ASTUtils.java deleted file mode 100644 index 80bc65622b0..00000000000 --- a/java-frontend/src/main/java/org/eclipse/jdt/core/dom/ASTUtils.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.eclipse.jdt.core.dom; - -import java.util.Objects; -import javax.annotation.Nullable; -import org.eclipse.jdt.core.compiler.CharOperation; -import org.eclipse.jdt.internal.compiler.env.IBinaryAnnotation; -import org.eclipse.jdt.internal.compiler.env.IBinaryType; -import org.eclipse.jdt.internal.compiler.env.INameEnvironment; -import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer; -import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding; -import org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding; -import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; -import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding; -import org.eclipse.jdt.internal.compiler.lookup.TypeConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class ASTUtils { - - private static final Logger LOG = LoggerFactory.getLogger(ASTUtils.class); - - private static final IAnnotationBinding[] NO_ANNOTATIONS = new IAnnotationBinding[0]; - - private ASTUtils() { - } - - public static void mayTolerateMissingType(AST ast) { - ast.getBindingResolver().lookupEnvironment().mayTolerateMissingType = true; - } - - public static Runnable getEnvironmentCleaner(AST ast) { - return new EnvironmentCleaner(ast.getBindingResolver().lookupEnvironment().nameEnvironment); - } - - @Nullable - public static ITypeBinding resolveType(AST ast, String name) { - try { - BindingResolver bindingResolver = ast.getBindingResolver(); - ReferenceBinding referenceBinding = bindingResolver - .lookupEnvironment() - .getType(CharOperation.splitOn('.', name.toCharArray())); - return bindingResolver.getTypeBinding(referenceBinding); - } catch (Exception e) { - // exception on ECJ side when trying to resolve a Type, recover on null type - LOG.error("ECJ Unable to resolve type " + name, e); - return null; - } - } - - public static IAnnotationBinding[] resolvePackageAnnotations(AST ast, String packageName) { - // See org.eclipse.jdt.core.dom.PackageBinding#getAnnotations() - BindingResolver bindingResolver = ast.getBindingResolver(); - LookupEnvironment lookupEnvironment = bindingResolver.lookupEnvironment(); - NameEnvironmentAnswer answer = lookupEnvironment.nameEnvironment.findType( - TypeConstants.PACKAGE_INFO_NAME, - CharOperation.splitOn('.', packageName.toCharArray()) - ); - if (answer == null) { - return NO_ANNOTATIONS; - } - IBinaryType type = answer.getBinaryType(); - if (type == null) { - // Can happen for instance with ant, as ant only generates 'package-info.class' - // when there is annotations in the package-info.java file. - return NO_ANNOTATIONS; - } - IBinaryAnnotation[] binaryAnnotations = type.getAnnotations(); - AnnotationBinding[] binaryInstances = - BinaryTypeBinding.createAnnotations(binaryAnnotations, lookupEnvironment, type.getMissingTypeNames()); - AnnotationBinding[] allInstances = - AnnotationBinding.addStandardAnnotations(binaryInstances, type.getTagBits(), lookupEnvironment); - - IAnnotationBinding[] domInstances = new IAnnotationBinding[allInstances.length]; - for (int i = 0; i < allInstances.length; i++) { - // FIXME can be null if annotation can not be resolved e.g. due to incomplete classpath - domInstances[i] = bindingResolver.getAnnotationInstance(allInstances[i]); - } - return domInstances; - } - - public static String signature(IMethodBinding methodBinding) { - char[] signature = ((MethodBinding) methodBinding).binding.signature(); - return new String(signature); - } - - private static class EnvironmentCleaner implements Runnable { - private final INameEnvironment env; - - private EnvironmentCleaner(INameEnvironment env) { - this.env = env; - } - - @Override - public void run() { - env.cleanup(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - EnvironmentCleaner that = (EnvironmentCleaner) o; - return Objects.equals(env, that.env); - } - - @Override - public int hashCode() { - return Objects.hash(env); - } - } - -} diff --git a/java-frontend/src/main/java/org/eclipse/jdt/core/dom/package-info.java b/java-frontend/src/main/java/org/eclipse/jdt/core/dom/package-info.java deleted file mode 100644 index 41ae9dc837b..00000000000 --- a/java-frontend/src/main/java/org/eclipse/jdt/core/dom/package-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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.eclipse.jdt.core.dom; diff --git a/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java b/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java index 74e3e81452c..dd1c71e52a2 100644 --- a/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java +++ b/java-frontend/src/main/java/org/sonar/java/ProgressMonitor.java @@ -19,12 +19,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; -import org.eclipse.core.runtime.IProgressMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.java.annotations.VisibleForTesting; -public class ProgressMonitor implements IProgressMonitor, Runnable { +public class ProgressMonitor implements Runnable { private final Logger logger; private final long period; @@ -84,7 +83,6 @@ public void run() { } } - @Override public void beginTask(String name, int totalWork) { if (totalWork <= 0) { unknownTotalWork = true; @@ -96,7 +94,6 @@ public void beginTask(String name, int totalWork) { thread.start(); } - @Override public void done() { if (success && analysisProgress.isLastBatch()) { log("100% analyzed"); @@ -107,7 +104,6 @@ public void done() { join(); } - @Override public boolean isCanceled() { if (isCanceled.getAsBoolean()) { log("Batch processing: Cancelled!"); @@ -116,7 +112,6 @@ public boolean isCanceled() { return false; } - @Override public void setCanceled(boolean value) { // do nothing } @@ -129,7 +124,6 @@ private void join() { } } - @Override public void worked(int work) { processedWork += work; if (processedWork == totalWork) { @@ -137,17 +131,14 @@ public void worked(int work) { } } - @Override public void internalWorked(double work) { // do nothing } - @Override public void setTaskName(String name) { // do nothing } - @Override public void subTask(String name) { // do nothing } diff --git a/java-frontend/src/main/java/org/sonar/java/model/AbstractTypedTree.java b/java-frontend/src/main/java/org/sonar/java/model/AbstractTypedTree.java index f2d6def6b5e..52ddb407b57 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/AbstractTypedTree.java +++ b/java-frontend/src/main/java/org/sonar/java/model/AbstractTypedTree.java @@ -16,10 +16,8 @@ */ package org.sonar.java.model; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.sonar.plugins.java.api.semantic.Type; - import javax.annotation.Nullable; +import org.sonar.plugins.java.api.semantic.Type; /** * This class is intended for internal use during semantic analysis and should not be used in checks. @@ -27,12 +25,10 @@ public abstract class AbstractTypedTree extends JavaTree { @Nullable - public ITypeBinding typeBinding; + public Type type; public Type symbolType() { - return typeBinding != null - ? root.sema.type(typeBinding) - : Type.UNKNOWN; + return type != null ? type : Type.UNKNOWN; } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/CompilerWarnings.java b/java-frontend/src/main/java/org/sonar/java/model/CompilerWarnings.java new file mode 100644 index 00000000000..f23ee46b93a --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/CompilerWarnings.java @@ -0,0 +1,266 @@ +/* + * 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.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +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.ImportTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.Tree; + +/** + * The compiler diagnostics the checks read from the compilation unit, derived from the tree and its symbols: + * unused imports and assignments without effect. + */ +final class CompilerWarnings extends BaseTreeVisitor { + + private final Map> warnings = new EnumMap<>(JProblem.Type.class); + private final List identifiers = new ArrayList<>(); + private final JSema sema; + + private CompilerWarnings(JSema sema) { + this.sema = sema; + } + + static void addTo(JavaTree.CompilationUnitTreeImpl unit) { + CompilerWarnings visitor = new CompilerWarnings(unit.sema); + visitor.scan(unit.packageDeclaration()); + visitor.scan(unit.types()); + unit.imports().forEach(visitor::scan); + visitor.reportUnusedImports(unit); + unit.addWarnings(visitor.warnings); + } + + private void add(JProblem.Type type, String message, Tree tree) { + warnings.computeIfAbsent(type, key -> new LinkedHashSet<>()).add(new JWarning(message, type, tree)); + } + + @Override + public void visitImport(ImportTree tree) { + // names inside an import are not uses + } + + @Override + public void visitIdentifier(IdentifierTree tree) { + identifiers.add(tree); + } + + /** Only simple names use an import: the member of a qualified name is not one. */ + @Override + public void visitMemberSelectExpression(MemberSelectExpressionTree tree) { + scan(tree.expression()); + if (tree.identifier() instanceof JavaTree.AnnotatedTypeTree annotated) { + scan(annotated.annotations()); + } + } + + @Override + public void visitTypeCast(org.sonar.plugins.java.api.tree.TypeCastTree tree) { + Type cast = tree.type().symbolType(); + Type operand = tree.expression().symbolType(); + if (!cast.isUnknown() && !operand.isUnknown() && operand.isSubtypeOf(cast)) { + if (cast.erasure().fullyQualifiedName().equals(operand.erasure().fullyQualifiedName())) { + add(JProblem.Type.REDUNDANT_CAST, "Unnecessary cast to " + cast.erasure().name(), tree); + } + } + super.visitTypeCast(tree); + } + + @Override + public void visitAssignmentExpression(AssignmentExpressionTree tree) { + if (tree.is(Tree.Kind.ASSIGNMENT)) { + Symbol target = assignedVariable(tree.variable()); + if (target != null && target.equals(assignedVariable(tree.expression()))) { + add(JProblem.Type.ASSIGNMENT_HAS_NO_EFFECT, "The assignment to variable " + target.name() + " has no effect", tree); + } + } + super.visitAssignmentExpression(tree); + } + + /** The variable a simple name, {@code this.name} or {@code Type.name} designates. */ + private static Symbol assignedVariable(ExpressionTree tree) { + ExpressionTree expression = tree; + while (expression instanceof org.sonar.plugins.java.api.tree.ParenthesizedTree parenthesized) { + expression = parenthesized.expression(); + } + IdentifierTree name = null; + if (expression instanceof IdentifierTree identifier) { + name = identifier; + } else if (expression instanceof MemberSelectExpressionTree select) { + ExpressionTree qualifier = select.expression(); + if (qualifier instanceof IdentifierTree owner && ("this".equals(owner.name()) || owner.symbol().isTypeSymbol())) { + name = select.identifier(); + } + } + if (name == null || !name.symbol().isVariableSymbol() || name.symbol().isUnknown()) { + return null; + } + return name.symbol(); + } + + private void reportUnusedImports(JavaTree.CompilationUnitTreeImpl unit) { + Set seen = new HashSet<>(); + String currentPackage = unit.packageDeclaration() == null ? "" : dotted(unit.packageDeclaration().packageName()); + for (ImportClauseTree clause : unit.imports()) { + if (!(clause instanceof ImportTree importTree)) { + continue; + } + String name = dotted(importTree.qualifiedIdentifier()); + String key = (importTree.isStatic() ? "static " : "") + name; + boolean duplicate = !seen.add(key); + if (isResolvable(importTree, name, currentPackage) && (duplicate || isOwnPackage(importTree, name, currentPackage) || !isUsed(importTree, name))) { + add(JProblem.Type.UNUSED_IMPORT, "The import " + (name.endsWith(".*") ? name.substring(0, name.length() - 2) : name) + " is never used", importTree); + } + } + } + + /** Like the compiler, only an import that resolves can be reported as unused. */ + private boolean isResolvable(ImportTree importTree, String name, String currentPackage) { + if (importTree instanceof JavaTree.ImportTreeImpl impl && impl.isModule()) { + return true; + } + boolean onDemand = name.endsWith(".*"); + String qualifier = onDemand ? name.substring(0, name.length() - 2) : name; + if (onDemand && !importTree.isStatic() && qualifier.equals(currentPackage)) { + return true; + } + return resolvesType(importTree.isStatic() && !onDemand ? qualifier.substring(0, Math.max(0, qualifier.lastIndexOf('.'))) : qualifier); + } + + private boolean resolvesType(String dotted) { + return binaryName(dotted) != null; + } + + /** The binary name of the type a dotted name denotes, trying each dot as a nesting boundary. */ + private String binaryName(String dotted) { + String candidate = dotted; + while (!candidate.isEmpty()) { + if (sema.declaresType(candidate)) { + return candidate; + } + int dot = candidate.lastIndexOf('.'); + if (dot < 0) { + return null; + } + candidate = candidate.substring(0, dot) + "$" + candidate.substring(dot + 1); + } + return null; + } + + /** The package's own types are imported implicitly, so such an import is never counted as used. */ + private static boolean isOwnPackage(ImportTree importTree, String name, String currentPackage) { + if (importTree.isStatic() || currentPackage.isEmpty()) { + return false; + } + String qualifier = name.endsWith(".*") ? name.substring(0, name.length() - 2) : name.substring(0, Math.max(0, name.lastIndexOf('.'))); + return (qualifier.equals(currentPackage) && !name.endsWith(".*")) || name.equals(currentPackage + ".*"); + } + + private boolean isUsed(ImportTree importTree, String name) { + if (importTree instanceof JavaTree.ImportTreeImpl impl && impl.isModule()) { + return true; + } + boolean onDemand = name.endsWith(".*"); + String qualifier = onDemand ? name.substring(0, name.length() - 2) : name.substring(0, Math.max(0, name.lastIndexOf('.'))); + for (IdentifierTree identifier : identifiers) { + Symbol symbol = identifier.symbol(); + // a symbol found in the scope of the file does not consult the import + boolean inScope = !symbol.isUnknown() && symbol.declaration() != null; + if (!inScope && uses(importTree, name, qualifier, identifier)) { + return true; + } + } + return false; + } + + private boolean uses(ImportTree importTree, String name, String qualifier, IdentifierTree identifier) { + Symbol symbol = identifier.symbol(); + if (name.endsWith(".*")) { + return declaredIn(symbol, qualifier, importTree.isStatic()); + } + if (!identifier.name().equals(name.substring(name.lastIndexOf('.') + 1))) { + return false; + } + if (symbol.isUnknown()) { + return true; + } + return importTree.isStatic() ? isMemberOf(symbol, qualifier) : denotesType(symbol, name, identifier); + } + + /** A static import also brings in the members the named type inherits. */ + private boolean isMemberOf(Symbol symbol, String qualifier) { + if (!(symbol.owner() instanceof Symbol.TypeSymbol owner)) { + return false; + } + if (dottedType(owner).equals(qualifier)) { + return true; + } + String binary = binaryName(qualifier); + return binary != null && sema.getClassType(binary).isSubtypeOf(owner.type()); + } + + private static boolean denotesType(Symbol symbol, String name, IdentifierTree identifier) { + if (symbol.isTypeSymbol()) { + return dottedType(symbol).equals(name); + } + if (!(symbol.isMethodSymbol() && "".equals(symbol.name()))) { + return false; + } + if (symbol.owner() instanceof Symbol.TypeSymbol owner && dottedType(owner).equals(name)) { + return true; + } + Type written = identifier.symbolType(); + return written != null && !written.isUnknown() && written.erasure().fullyQualifiedName().replace('$', '.').equals(name); + } + + private static boolean declaredIn(Symbol symbol, String qualifier, boolean isStatic) { + if (symbol.isUnknown()) { + return false; + } + if (isStatic) { + return symbol.owner() instanceof Symbol.TypeSymbol owner && dottedType(owner).equals(qualifier); + } + Symbol type = symbol; + if ("".equals(symbol.name())) { + type = symbol.owner(); + } + return type.isTypeSymbol() && !type.isUnknown() && dottedType(type).startsWith(qualifier + "."); + } + + private static String dottedType(Symbol symbol) { + return symbol.type().fullyQualifiedName().replace('$', '.'); + } + + private static String dotted(Tree tree) { + if (tree instanceof MemberSelectExpressionTree select) { + return dotted(select.expression()) + "." + select.identifier().name(); + } + return tree instanceof IdentifierTree identifier ? identifier.name() : ""; + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/EffectivelyFinalVariables.java b/java-frontend/src/main/java/org/sonar/java/model/EffectivelyFinalVariables.java new file mode 100644 index 00000000000..eeb101c9909 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/EffectivelyFinalVariables.java @@ -0,0 +1,92 @@ +/* + * 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.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.UnaryExpressionTree; +import org.sonar.plugins.java.api.tree.VariableTree; + +/** + * Marks the local variables and parameters that are never reassigned (JLS 4.12.4), as the compiler does. + * A variable declared without a value stays effectively final after one plain assignment. + */ +final class EffectivelyFinalVariables extends BaseTreeVisitor { + + private final Map assignments = new IdentityHashMap<>(); + private final List variables = new ArrayList<>(); + + private EffectivelyFinalVariables() { + } + + static void mark(JavaTree.CompilationUnitTreeImpl unit) { + EffectivelyFinalVariables visitor = new EffectivelyFinalVariables(); + visitor.scan(unit); + for (VariableTree variable : visitor.variables) { + if (variable.symbol() instanceof JVariableSymbol symbol) { + int[] counts = visitor.assignments.getOrDefault(symbol, new int[2]); + boolean unassigned = variable.initializer() == null && isLocalDeclaration(variable); + symbol.effectivelyFinal(counts[1] == 0 && counts[0] <= (unassigned ? 1 : 0)); + } + } + } + + private static boolean isLocalDeclaration(VariableTree variable) { + Tree parent = variable.parent(); + return parent != null && parent.is(Tree.Kind.BLOCK, Tree.Kind.CASE_GROUP, Tree.Kind.STATIC_INITIALIZER, Tree.Kind.INITIALIZER, Tree.Kind.FOR_STATEMENT); + } + + @Override + public void visitVariable(VariableTree tree) { + if (tree.symbol() instanceof JVariableSymbol symbol && (symbol.isLocalVariable() || symbol.isParameter())) { + variables.add(tree); + } + super.visitVariable(tree); + } + + @Override + public void visitAssignmentExpression(AssignmentExpressionTree tree) { + countAssignment(tree.variable(), tree.is(Tree.Kind.ASSIGNMENT) ? 0 : 1); + super.visitAssignmentExpression(tree); + } + + @Override + public void visitUnaryExpression(UnaryExpressionTree tree) { + if (tree.is(Tree.Kind.PREFIX_INCREMENT, Tree.Kind.PREFIX_DECREMENT, Tree.Kind.POSTFIX_INCREMENT, Tree.Kind.POSTFIX_DECREMENT)) { + countAssignment(tree.expression(), 1); + } + super.visitUnaryExpression(tree); + } + + private void countAssignment(ExpressionTree target, int slot) { + ExpressionTree expression = target; + while (expression instanceof org.sonar.plugins.java.api.tree.ParenthesizedTree parenthesized) { + expression = parenthesized.expression(); + } + if (expression instanceof IdentifierTree identifier) { + assignments.computeIfAbsent(identifier.symbol(), key -> new int[2])[slot]++; + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/JMethodSymbol.java b/java-frontend/src/main/java/org/sonar/java/model/JMethodSymbol.java index 3f15b5f9a6b..8cbde9dbd22 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JMethodSymbol.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JMethodSymbol.java @@ -16,186 +16,365 @@ */ package org.sonar.java.model; +import com.google.gson.JsonObject; import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Predicate; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ASTUtils; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.Modifier; -import org.sonar.java.annotations.VisibleForTesting; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.ArrayTypeTree; import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.TypeTree; import org.sonar.plugins.java.api.tree.VariableTree; final class JMethodSymbol extends JSymbol implements Symbol.MethodSymbol { - /** - * Cache for {@link #parameterTypes()}. - */ - private List parameterTypes; + private List parameterTypes = List.of(); + private TypeSymbol returnTypeSymbol = TypeSymbol.UNKNOWN_TYPE; + private boolean varArgs; + private boolean lambda; + private boolean parametrized; + private final String signature; + @Nullable + private Integer returnTypeParameter; + private boolean returnsSelf; + @Nullable + private List parameterVariables; + @Nullable + private List declaredParameters; + private boolean declaredReturnLoaded; + @Nullable + private JsonObject declaredReturn; + private List overridden; + @Nullable + private List thrownTypes; + @Nullable + private String descriptorSignature; - private List parameters; + JMethodSymbol(JSema sema, @Nullable String id, String name) { + super(sema, Kind.METHOD, id, name); + this.signature = id == null ? name : JSema.erase(id); + } - /** - * Cache for {@link #returnType()}. - */ - private TypeSymbol returnType; + void parameterTypes(List parameterTypes) { + this.parameterTypes = List.copyOf(parameterTypes); + } - /** - * Cache for {@link #thrownTypes()}. - */ - private List thrownTypes; + void returnTypeSymbol(TypeSymbol returnTypeSymbol) { + this.returnTypeSymbol = returnTypeSymbol == null ? TypeSymbol.UNKNOWN_TYPE : returnTypeSymbol; + } - /** - * Cache for {@link #overriddenSymbols()}. - */ - private List overriddenSymbols; + void varArgs(boolean varArgs) { + this.varArgs = varArgs; + } - private final String signature; + void lambda(boolean lambda) { + this.lambda = lambda; + } - JMethodSymbol(JSema sema, IMethodBinding methodBinding) { - super(sema, methodBinding); - this.signature = methodBinding().getDeclaringClass().getBinaryName() - + "#" + name() - + ASTUtils.signature(methodBinding().getMethodDeclaration()); + boolean isLambda() { + return lambda; + } + + void parametrized(boolean parametrized) { + this.parametrized = parametrized; + } + + void returnTypeParameter(@Nullable Integer returnTypeParameter) { + this.returnTypeParameter = returnTypeParameter; + } + + @Nullable + Integer returnTypeParameter() { + return returnTypeParameter; + } + + void returnsSelf(boolean returnsSelf) { + this.returnsSelf = returnsSelf; + } + + boolean returnsSelf() { + return returnsSelf; + } + + void parameterVariables(List parameterVariables) { + this.parameterVariables = parameterVariables; + } + + @Nullable + List parameterVariables() { + return parameterVariables; + } + + void declaredParameters(List declaredParameters) { + this.declaredParameters = declaredParameters; + } + + @Nullable + List declaredParameters() { + return declaredParameters; + } + + void declaredReturn(@Nullable JsonObject declaredReturn) { + this.declaredReturnLoaded = true; + this.declaredReturn = declaredReturn; + } + + boolean declaredReturnLoaded() { + return declaredReturnLoaded; + } + + @Nullable + JsonObject declaredReturn() { + return declaredReturn; } - IMethodBinding methodBinding() { - return (IMethodBinding) binding; + /** + * A per-invocation view with substituted parameter and return types. Equality stays on the SemSitter id, + * so the view still denotes the same method as the declaration. + */ + JMethodSymbol withSignature(List parameterTypes, Type returnType) { + JMethodSymbol view = new JMethodSymbol(sema, id, name()); + view.owner(owner()); + view.addModifiers(modifiersCopy()); + view.parameterTypes = List.copyOf(parameterTypes); + view.parameterVariables = parameterVariables; + view.declaredParameters = declaredParameters; + view.declaredReturnLoaded = declaredReturnLoaded; + view.declaredReturn = declaredReturn; + view.returnTypeSymbol = returnType.symbol(); + view.varArgs = varArgs; + view.lambda = lambda; + view.parametrized = parametrized || !returnType.equals(returnTypeSymbol.type()) || !view.parameterTypes.equals(this.parameterTypes); + view.returnTypeParameter = returnTypeParameter; + view.returnsSelf = returnsSelf; + view.thrownTypes = thrownTypes; + view.unknown(isUnknown()); + view.deprecated(isDeprecated()); + view.descriptorSignature = signature(); + return view; } - public boolean isLambda() { - return methodBinding().getDeclaringMember() != null; + private List modifiersCopy() { + List copy = new ArrayList<>(); + if (isPublic()) { + copy.add("public"); + } + if (isPrivate()) { + copy.add("private"); + } + if (isProtected()) { + copy.add("protected"); + } + if (isStatic()) { + copy.add("static"); + } + if (isFinal()) { + copy.add("final"); + } + if (hasModifier("abstract")) { + copy.add("abstract"); + } + if (hasModifier("default")) { + copy.add("default"); + } + if (hasModifier("native")) { + copy.add("native"); + } + if (hasModifier("synchronized")) { + copy.add("synchronized"); + } + return copy; } @Override public List parameterTypes() { - if (parameterTypes == null) { - parameterTypes = sema.types(methodBinding().getParameterTypes()); - } return parameterTypes; } @Override public List declarationParameters() { - if (parameters == null) { - MethodTree declaration = declaration(); - if (declaration != null && !isCompactConstructor(declaration)) { - parameters = declaration.parameters().stream().map(VariableTree::symbol).toList(); - } else { - parameters = new ArrayList<>(); - IMethodBinding methodBinding = methodBinding(); - ITypeBinding[] parameterTypeBindings = methodBinding.getParameterTypes(); - for (int i = 0; i < parameterTypeBindings.length; i++) { - parameters.add(new JVariableSymbol.ParameterPlaceholderSymbol(i, sema, methodBinding.getMethodDeclaration(), parameterTypeBindings[i])); - } - } + if (declaration() instanceof MethodTree method && method.closeParenToken() != null) { + return method.parameters().stream().map(parameter -> (Symbol) parameter.symbol()).toList(); } - return parameters; - } - - private static boolean isCompactConstructor(MethodTree methodTree) { - return methodTree.closeParenToken() == null; + List placeholders = new ArrayList<>(); + for (int i = 0; i < parameterTypes.size(); i++) { + placeholders.add(new JVariableSymbol.ParameterPlaceholderSymbol(i, this, parameterTypes.get(i))); + } + return placeholders; } - /** - * @since 6.0 returns void type for constructors instead of {@code null} - */ @Override public TypeSymbol returnType() { - if (returnType == null) { - ITypeBinding methodBindingReturnType = methodBinding().getReturnType(); - // In rare circumstances, when the semantic information is incomplete, methodBindingReturnType can be null. - if (methodBindingReturnType == null) { - return TypeSymbol.UNKNOWN_TYPE; - } - this.returnType = sema.typeSymbol(methodBindingReturnType); - } - return returnType; + return returnTypeSymbol; + } + + void thrownTypes(List thrownTypes) { + this.thrownTypes = List.copyOf(thrownTypes); } @Override public List thrownTypes() { - if (thrownTypes == null) { - thrownTypes = sema.types(methodBinding().getExceptionTypes()); + if (thrownTypes != null) { + return thrownTypes; + } + MethodTree method = declaration(); + if (method == null) { + return Collections.emptyList(); } - return thrownTypes; + return method.throwsClauses().stream().map(TypeTree::symbolType).toList(); } @Override public List overriddenSymbols() { - if (overriddenSymbols == null) { - overriddenSymbols = findOverriddenSymbols(); + if (overridden == null) { + overridden = findOverridden(); } - return overriddenSymbols; + return overridden; } - private List findOverriddenSymbols() { - Set results = new LinkedHashSet<>(); - IMethodBinding methodBinding = methodBinding(); - findOverridesInParentTypes(results, methodBinding::overrides, methodBinding.getDeclaringClass()); - return new ArrayList<>(results); + private List findOverridden() { + if (!(owner() instanceof JTypeSymbol typeSymbol) || isUnknown() || isStatic() || isPrivate() || "".equals(name())) { + return List.of(); + } + List found = new ArrayList<>(); + Set visited = new HashSet<>(); + if (typeSymbol.isInterface()) { + match(found, sema.typeSymbol("java.lang.Object")); + } + walk(found, visited, typeSymbol.superClass()); + for (Type iface : typeSymbol.interfaces()) { + walk(found, visited, iface); + } + return found; } - @VisibleForTesting - void findOverridesInParentTypes(Collection accumulator, Predicate overridesCondition, ITypeBinding type) { - if (type.isInterface()) { - // check Object for interfaces forcing overrides from Object - findOverridesInTypes(accumulator, overridesCondition, sema.resolveType("java.lang.Object")); - } else if (!"java.lang.Object".equals(type.getQualifiedName())) { - findOverridesInTypes(accumulator, overridesCondition, type.getSuperclass()); + private void walk(List found, Set visited, @Nullable Type type) { + if (type == null || type.isUnknown() || !visited.add(type.fullyQualifiedName())) { + return; + } + if (type.symbol() instanceof JTypeSymbol parent) { + match(found, parent); + walk(found, visited, parent.superClass()); + for (Type iface : parent.interfaces()) { + walk(found, visited, iface); + } } - findOverridesInTypes(accumulator, overridesCondition, type.getInterfaces()); } - private void findOverridesInTypes(Collection accumulator, Predicate overridesCondition, ITypeBinding... types) { - for (ITypeBinding type : types) { - if (type != null) { - for (IMethodBinding declaredMethod : type.getDeclaredMethods()) { - if (overridesCondition.test(declaredMethod)) { - accumulator.add(sema.methodSymbol(declaredMethod)); - break; - } - } - findOverridesInParentTypes(accumulator, overridesCondition, type); + private void match(List found, TypeSymbol parent) { + for (Symbol member : parent.memberSymbols()) { + if (member.isMethodSymbol() && name().equals(member.name()) && sameSignature((MethodSymbol) member)) { + found.add((MethodSymbol) member); + return; + } + } + } + + private boolean sameSignature(MethodSymbol other) { + List theirs = other.parameterTypes(); + if (parameterTypes.size() != theirs.size()) { + return false; + } + for (int i = 0; i < parameterTypes.size(); i++) { + if (!parameterTypes.get(i).erasure().is(theirs.get(i).erasure().fullyQualifiedName())) { + return false; } } + return true; } + /** + * The ECJ form, {@code binaryOwner#name(descriptors)returnDescriptor}, from the erased parameter and return + * types. Falls back to the SemSitter id when the owner is not a type. + */ @Override public String signature() { - return signature; + if (descriptorSignature == null) { + descriptorSignature = descriptorSignature(); + } + return descriptorSignature; + } + + private String descriptorSignature() { + Symbol owner = owner(); + if (!(owner instanceof TypeSymbol ownerType) || isUnknown()) { + return signature; + } + StringBuilder out = new StringBuilder(ownerType.type().fullyQualifiedName()).append('#').append(name()).append('('); + parameterTypes.forEach(parameter -> out.append(descriptor(parameter))); + out.append(')'); + if ("".equals(name())) { + out.append('V'); + } else { + out.append(descriptor(returnTypeSymbol.type())); + } + return out.toString(); + } + + private static String descriptor(@Nullable Type type) { + if (type == null || type.isUnknown()) { + return "Ljava/lang/Object;"; + } + Type erased = type.erasure(); + if (erased.isArray()) { + return "[" + descriptor(((Type.ArrayType) erased).elementType()); + } + return switch (erased.fullyQualifiedName()) { + case "byte" -> "B"; + case "char" -> "C"; + case "double" -> "D"; + case "float" -> "F"; + case "int" -> "I"; + case "long" -> "J"; + case "short" -> "S"; + case "boolean" -> "Z"; + case "void" -> "V"; + default -> "L" + erased.fullyQualifiedName().replace('.', '/') + ";"; + }; } @Override public boolean isOverridable() { - return !isUnknown() && !(isPrivate() || isStatic() || isFinal() || owner().isFinal()); + Symbol owner = owner(); + return !isUnknown() && !(isPrivate() || isStatic() || isFinal() || (owner != null && owner.isFinal())); } @Override public boolean isParametrizedMethod() { - return !isUnknown() && (methodBinding().isParameterizedMethod() || methodBinding().isGenericMethod()); + return !isUnknown() && parametrized; } @Override public boolean isDefaultMethod() { - return !isUnknown() && Modifier.isDefault(binding.getModifiers()); + return !isUnknown() && hasModifier("default"); } @Override public boolean isSynchronizedMethod() { - return !isUnknown() && Modifier.isSynchronized(binding.getModifiers()); + return !isUnknown() && hasModifier("synchronized"); } @Override public boolean isVarArgsMethod() { - return !isUnknown() && methodBinding().isVarargs(); + if (isUnknown()) { + return false; + } + if (varArgs) { + return true; + } + if (declaration() instanceof MethodTree method) { + List parameters = method.parameters(); + if (parameters.isEmpty()) { + return false; + } + return parameters.get(parameters.size() - 1).type() instanceof ArrayTypeTree arrayType + && arrayType.ellipsisToken() != null; + } + return !parameterTypes.isEmpty() && parameterTypes.get(parameterTypes.size() - 1).isArray(); } @Nullable @@ -206,12 +385,6 @@ public MethodTree declaration() { @Override public boolean isNativeMethod() { - return !isUnknown() && Modifier.isNative(binding.getModifiers()); - } - - /** This is for debugging and doesn't follow a guaranteed format. */ - @Override - public String toString() { - return binding.getKey(); + return !isUnknown() && hasModifier("native"); } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JPackageSymbol.java b/java-frontend/src/main/java/org/sonar/java/model/JPackageSymbol.java index 1f7680c909e..bd313da3d9d 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JPackageSymbol.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JPackageSymbol.java @@ -16,12 +16,14 @@ */ package org.sonar.java.model; -import org.eclipse.jdt.core.dom.IPackageBinding; - final class JPackageSymbol extends JSymbol { - JPackageSymbol(JSema sema, IPackageBinding binding) { - super(sema, binding); + JPackageSymbol(JSema sema, String name) { + super(sema, Kind.PACKAGE, name, name); } + @Override + public boolean isUnknown() { + return false; + } } 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 e58aeb7f024..21c7ccb338c 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 @@ -16,2890 +16,136 @@ */ package org.sonar.java.model; +import com.google.gson.JsonObject; import com.sonar.sslr.api.RecognitionException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; import javax.annotation.Nullable; -import org.eclipse.jdt.core.compiler.IProblem; -import org.eclipse.jdt.core.compiler.InvalidInputException; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTNode; -import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.ASTUtils; -import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; -import org.eclipse.jdt.core.dom.Annotation; -import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; -import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration; -import org.eclipse.jdt.core.dom.AnonymousClassDeclaration; -import org.eclipse.jdt.core.dom.ArrayAccess; -import org.eclipse.jdt.core.dom.ArrayCreation; -import org.eclipse.jdt.core.dom.ArrayInitializer; -import org.eclipse.jdt.core.dom.ArrayType; -import org.eclipse.jdt.core.dom.AssertStatement; -import org.eclipse.jdt.core.dom.Assignment; -import org.eclipse.jdt.core.dom.Block; -import org.eclipse.jdt.core.dom.BodyDeclaration; -import org.eclipse.jdt.core.dom.BooleanLiteral; -import org.eclipse.jdt.core.dom.BreakStatement; -import org.eclipse.jdt.core.dom.CastExpression; -import org.eclipse.jdt.core.dom.CatchClause; -import org.eclipse.jdt.core.dom.CharacterLiteral; -import org.eclipse.jdt.core.dom.ClassInstanceCreation; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.ConditionalExpression; -import org.eclipse.jdt.core.dom.ConstructorInvocation; -import org.eclipse.jdt.core.dom.ContinueStatement; -import org.eclipse.jdt.core.dom.CreationReference; -import org.eclipse.jdt.core.dom.Dimension; -import org.eclipse.jdt.core.dom.DoStatement; -import org.eclipse.jdt.core.dom.EmptyStatement; -import org.eclipse.jdt.core.dom.EnhancedForStatement; -import org.eclipse.jdt.core.dom.EnumConstantDeclaration; -import org.eclipse.jdt.core.dom.EnumDeclaration; -import org.eclipse.jdt.core.dom.ExportsDirective; -import org.eclipse.jdt.core.dom.Expression; -import org.eclipse.jdt.core.dom.ExpressionMethodReference; -import org.eclipse.jdt.core.dom.ExpressionStatement; -import org.eclipse.jdt.core.dom.FieldAccess; -import org.eclipse.jdt.core.dom.FieldDeclaration; -import org.eclipse.jdt.core.dom.ForStatement; -import org.eclipse.jdt.core.dom.GuardedPattern; -import org.eclipse.jdt.core.dom.IBinding; -import org.eclipse.jdt.core.dom.IExtendedModifier; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; -import org.eclipse.jdt.core.dom.IfStatement; -import org.eclipse.jdt.core.dom.ImportDeclaration; -import org.eclipse.jdt.core.dom.InfixExpression; -import org.eclipse.jdt.core.dom.Initializer; -import org.eclipse.jdt.core.dom.InstanceofExpression; -import org.eclipse.jdt.core.dom.IntersectionType; -import org.eclipse.jdt.core.dom.LabeledStatement; -import org.eclipse.jdt.core.dom.LambdaExpression; -import org.eclipse.jdt.core.dom.MemberValuePair; -import org.eclipse.jdt.core.dom.MethodDeclaration; -import org.eclipse.jdt.core.dom.MethodInvocation; -import org.eclipse.jdt.core.dom.ModuleDeclaration; -import org.eclipse.jdt.core.dom.ModuleDirective; -import org.eclipse.jdt.core.dom.ModuleModifier; -import org.eclipse.jdt.core.dom.Name; -import org.eclipse.jdt.core.dom.NameQualifiedType; -import org.eclipse.jdt.core.dom.NormalAnnotation; -import org.eclipse.jdt.core.dom.NullLiteral; -import org.eclipse.jdt.core.dom.NumberLiteral; -import org.eclipse.jdt.core.dom.OpensDirective; -import org.eclipse.jdt.core.dom.ParameterizedType; -import org.eclipse.jdt.core.dom.ParenthesizedExpression; -import org.eclipse.jdt.core.dom.Pattern; -import org.eclipse.jdt.core.dom.PatternInstanceofExpression; -import org.eclipse.jdt.core.dom.PostfixExpression; -import org.eclipse.jdt.core.dom.PrefixExpression; -import org.eclipse.jdt.core.dom.PrimitiveType; -import org.eclipse.jdt.core.dom.ProvidesDirective; -import org.eclipse.jdt.core.dom.QualifiedName; -import org.eclipse.jdt.core.dom.QualifiedType; -import org.eclipse.jdt.core.dom.RecordDeclaration; -import org.eclipse.jdt.core.dom.RecordPattern; -import org.eclipse.jdt.core.dom.RequiresDirective; -import org.eclipse.jdt.core.dom.ReturnStatement; -import org.eclipse.jdt.core.dom.SimpleName; -import org.eclipse.jdt.core.dom.SimpleType; -import org.eclipse.jdt.core.dom.SingleMemberAnnotation; -import org.eclipse.jdt.core.dom.SingleVariableDeclaration; -import org.eclipse.jdt.core.dom.Statement; -import org.eclipse.jdt.core.dom.StringLiteral; -import org.eclipse.jdt.core.dom.SuperConstructorInvocation; -import org.eclipse.jdt.core.dom.SuperFieldAccess; -import org.eclipse.jdt.core.dom.SuperMethodInvocation; -import org.eclipse.jdt.core.dom.SuperMethodReference; -import org.eclipse.jdt.core.dom.SwitchCase; -import org.eclipse.jdt.core.dom.SwitchExpression; -import org.eclipse.jdt.core.dom.SwitchStatement; -import org.eclipse.jdt.core.dom.SynchronizedStatement; -import org.eclipse.jdt.core.dom.TextBlock; -import org.eclipse.jdt.core.dom.ThisExpression; -import org.eclipse.jdt.core.dom.ThrowStatement; -import org.eclipse.jdt.core.dom.TryStatement; -import org.eclipse.jdt.core.dom.Type; -import org.eclipse.jdt.core.dom.TypeDeclaration; -import org.eclipse.jdt.core.dom.TypeDeclarationStatement; -import org.eclipse.jdt.core.dom.TypeLiteral; -import org.eclipse.jdt.core.dom.TypeMethodReference; -import org.eclipse.jdt.core.dom.TypeParameter; -import org.eclipse.jdt.core.dom.TypePattern; -import org.eclipse.jdt.core.dom.UnionType; -import org.eclipse.jdt.core.dom.UsesDirective; -import org.eclipse.jdt.core.dom.VariableDeclaration; -import org.eclipse.jdt.core.dom.VariableDeclarationExpression; -import org.eclipse.jdt.core.dom.VariableDeclarationFragment; -import org.eclipse.jdt.core.dom.VariableDeclarationStatement; -import org.eclipse.jdt.core.dom.WhileStatement; -import org.eclipse.jdt.core.dom.WildcardType; -import org.eclipse.jdt.core.dom.YieldStatement; -import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; -import org.eclipse.jdt.internal.compiler.parser.Scanner; -import org.eclipse.jdt.internal.compiler.parser.TerminalToken; -import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions; -import org.eclipse.jdt.internal.formatter.Token; -import org.eclipse.jdt.internal.formatter.TokenManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.java.annotations.VisibleForTesting; -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.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.ArrayTypeTree; -import org.sonar.plugins.java.api.tree.CatchTree; -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.InferedTypeTree; -import org.sonar.plugins.java.api.tree.ListTree; -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.SyntaxToken; -import org.sonar.plugins.java.api.tree.SyntaxTrivia; -import org.sonar.plugins.java.api.tree.SyntaxTrivia.CommentKind; -import org.sonar.plugins.java.api.tree.Tree; -import org.sonar.plugins.java.api.tree.TypeParameterTree; -import org.sonar.plugins.java.api.tree.TypeTree; -import org.sonar.plugins.java.api.tree.VariableTree; +import org.sonar.java.semsitter.BridgeExtractor; +import org.sonar.java.semsitter.PlatformDetection; +import org.sonar.java.semsitter.SemsitterBridge; -@SuppressWarnings({"rawtypes", "unchecked"}) -public class JParser { +public final class JParser { - private static final Logger LOG = LoggerFactory.getLogger(JParser.class); - - private static final Set WRONGLY_CATEGORIZED_AS_SYNTAX_ERROR = Set.of( - // Accept missing default clause, it may be due to missing semantic information of the switch expression, - // in this case, an enum fully covered with the switch cases will be seen as something that is not an enum - // when it is unknown, and the parser will wrongly consider the missing default clause as a syntax error. - IProblem.SwitchExpressionsYieldMissingDefaultCase, - // Accept missing default clause, it may be due the switch expression being an enum from a wrong dependency. - // In this case, the parser will wrongly consider the missing default clause as a syntax error. - IProblem.SwitchExpressionsYieldMissingEnumConstantCase - ); - - private static final Predicate IS_SYNTAX_ERROR = error -> ((error.getID() & IProblem.Syntax) != 0) && - !WRONGLY_CATEGORIZED_AS_SYNTAX_ERROR.contains(error.getID()); - - private static final Predicate IS_UNDEFINED_TYPE_ERROR = error -> (error.getID() & IProblem.UndefinedType) != 0; + private JParser() { + } /** - * @param unitName see {@link ASTParser#setUnitName(String)} - * @throws RecognitionException in case of syntax errors + * Parses and attributes one compilation unit on its own, against the running JDK only. Batch analysis goes + * through {@link JParserConfig}, which scans every file of the project before translating. */ - public static JavaTree.CompilationUnitTreeImpl parse(ASTParser astParser, String version, String unitName, String source) { - astParser.setUnitName(unitName); - astParser.setSource(source.toCharArray()); - - CompilationUnit astNode; - try { - astNode = (CompilationUnit) astParser.createAST(null); - } catch (Exception e) { - LOG.error("ECJ: Unable to parse file", e); - throw new RecognitionException(-1, "ECJ: Unable to parse file.", e); - } - - return convert(version, unitName, source, astNode); - } - - static JavaTree.CompilationUnitTreeImpl convert(String version, String unitName, String source, CompilationUnit astNode) { - List errors = Stream.of(astNode.getProblems()).filter(IProblem::isError).toList(); - Optional possibleSyntaxError = errors.stream().filter(IS_SYNTAX_ERROR).findFirst(); - LineColumnConverter lineColumnConverter = new LineColumnConverter(source); - if (possibleSyntaxError.isPresent()) { - IProblem syntaxError = possibleSyntaxError.get(); - LineColumnConverter.Pos pos = lineColumnConverter.toPos(syntaxError.getSourceStart()); - String message = String.format("Parse error at line %d column %d: %s", pos.line(), pos.columnOffset() + 1, syntaxError.getMessage()); - // interrupt parsing - throw new RecognitionException(pos.line(), message); - } - - Set undefinedTypes = errors.stream() - .filter(IS_UNDEFINED_TYPE_ERROR) - .map(i -> new JProblem( - i.getMessage(), - (i.getID() & IProblem.PreviewFeatureUsed) == 0 ? JProblem.Type.UNDEFINED_TYPE : JProblem.Type.PREVIEW_FEATURE_USED)) - .collect(Collectors.toSet()); - - JParser converter = new JParser(); - converter.sema = new JSema(astNode.getAST()); - converter.sema.undefinedTypes.addAll(undefinedTypes); - converter.compilationUnit = astNode; - converter.tokenManager = createTokenManager(version, unitName, source); - converter.lineColumnConverter = lineColumnConverter; - - JavaTree.CompilationUnitTreeImpl tree = converter.convertCompilationUnit(astNode); - tree.sema = converter.sema; - JWarning.Mapper.warningsFor(astNode, converter.lineColumnConverter).mappedInto(tree); - - ASTUtils.mayTolerateMissingType(astNode.getAST()); - - setParents(tree); - return tree; + public static JavaTree.CompilationUnitTreeImpl parse(String version, String unitName, String source) { + return parse(version, unitName, source, List.of()); } - @VisibleForTesting - static TokenManager createTokenManager(String version, String unitName, String source) { - 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<>(); - Scanner scanner = new Scanner( - true, - false, - false, - CompilerOptions.versionToJdkLevel(version), - null, - null, - false - ); - scanner.fakeInModule = "module-info.java".equals(unitName); - scanner.setSource(sourceChars); - while (true) { - try { - TerminalToken tokenType = scanner.getNextToken(); - Token token = Token.fromCurrent(scanner, tokenType); - tokens.add(token); - if (tokenType == TerminalToken.TokenNameEOF) { - break; + /** {@link #parse(String, String, String)} with a classpath, as ECJ attributed single sources. */ + public static JavaTree.CompilationUnitTreeImpl parse(String version, String unitName, String source, List classpath) { + Objects.requireNonNull(version); + Objects.requireNonNull(unitName); + Objects.requireNonNull(source); + Path directory = null; + try { + SemsitterBridge bridge = unitBridge(); + directory = privateTempDirectory(); + Path file = directory.resolve(Path.of(unitName.endsWith(".java") ? unitName : (unitName + ".java")).getFileName().toString()); + Files.writeString(file, source, StandardCharsets.UTF_8); + JsonObject document; + synchronized (bridge) { + bridge.analyze(directory, List.of(file), JParserConfig.bridgeClasspath(classpath)); + document = bridge.cst(file.toString()); + } + JSema sema = new JSema(); + sema.ingestDocument(document); + // Types load lazily while rules run, so the bridge outlives this call. + sema.loadTypes(names -> { + try { + return bridge.types(names); + } catch (IOException failure) { + throw new UncheckedIOException(failure); } - } catch (InvalidInputException e) { - throw new IllegalStateException(e); - } - } - return tokens; - } - - private CompilationUnit compilationUnit; - - private TokenManager tokenManager; - private LineColumnConverter lineColumnConverter; - - private JSema sema; - - private final Deque labels = new LinkedList<>(); - - private void declaration(@Nullable IBinding binding, Tree node) { - if (binding == null) { - return; - } - sema.declarations.put(binding, node); - } - - private void usage(@Nullable IBinding binding, IdentifierTree node) { - if (binding == null) { - return; - } - binding = JSema.declarationBinding(binding); - sema.usages.computeIfAbsent(binding, k -> new ArrayList<>()).add(node); - } - - private void usageLabel(@Nullable IdentifierTreeImpl node) { - if (node == null) { - return; - } - labels.stream() - .filter(symbol -> symbol.name().equals(node.name())) - .findFirst() - .ifPresent(labelSymbol -> { - labelSymbol.usages.add(node); - node.labelSymbol = labelSymbol; }); - } - - private int firstTokenIndexAfter(ASTNode e) { - int index = tokenManager.firstIndexAfter(e, ANY_TOKEN); - while (isComment(tokenManager.get(index))) { - index++; - } - return index; - } - - /** - * @param tokenType {@link TerminalToken} - */ - private int nextTokenIndex(int tokenIndex, TerminalToken tokenType) { - assert tokenType != ANY_TOKEN; - do { - tokenIndex += 1; - } while (tokenManager.get(tokenIndex).tokenType != tokenType); - return tokenIndex; - } - - /** - * @param tokenType {@link TerminalToken} - */ - private InternalSyntaxToken firstTokenBefore(ASTNode e, TerminalToken tokenType) { - return createSyntaxToken(tokenManager.firstIndexBefore(e, tokenType)); - } - - /** - * @param tokenType {@link TerminalToken} - */ - private InternalSyntaxToken firstTokenAfter(ASTNode e, TerminalToken tokenType) { - return createSyntaxToken(tokenManager.firstIndexAfter(e, tokenType)); - } - - /** - * @param tokenType {@link TerminalToken} - */ - private InternalSyntaxToken firstTokenIn(ASTNode e, TerminalToken tokenType) { - return createSyntaxToken(tokenManager.firstIndexIn(e, tokenType)); - } - - /** - * @param tokenTypeCandidateA {@link TerminalToken} - * @param tokenTypeCandidateB {@link TerminalToken} - * @return {@link TerminalToken} - */ - @VisibleForTesting - static int firstIndexIn(TokenManager tokenManager, ASTNode e, TerminalToken tokenTypeCandidateA, TerminalToken tokenTypeCandidateB) { - int first = tokenManager.firstIndexIn(e, ANY_TOKEN); - int last = tokenManager.lastIndexIn(e, ANY_TOKEN); - return IntStream.rangeClosed(first, last) - .filter(tokenIndex -> { - Token token = tokenManager.get(tokenIndex); - return token.tokenType == tokenTypeCandidateA || token.tokenType == tokenTypeCandidateB; - }) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Failed to find token " + tokenTypeCandidateA + " or " + tokenTypeCandidateB + - " in the tokens of a " + ASTNode.nodeClassForType(e.getNodeType()).getName())); - } - - /** - * @param tokenType {@link TerminalToken} - */ - private InternalSyntaxToken lastTokenIn(ASTNode e, TerminalToken tokenType) { - return createSyntaxToken(tokenManager.lastIndexIn(e, tokenType)); - } - - private InternalSyntaxToken createSyntaxToken(int tokenIndex) { - Token t = tokenManager.get(tokenIndex); - String value; - boolean isEOF; - if (t.tokenType == TerminalToken.TokenNameEOF) { - isEOF = true; - value = ""; - } else { - isEOF = false; - value = t.toString(tokenManager.getSource()); - } - LineColumnConverter.Pos pos = lineColumnConverter.toPos(t.originalStart); - return new InternalSyntaxToken(pos.line(), pos.columnOffset(), value, collectComments(tokenIndex), isEOF); - } - - private InternalSyntaxToken createSpecialToken(int tokenIndex) { - Token t = tokenManager.get(tokenIndex); - List comments = t.tokenType == TerminalToken.TokenNameGREATER - ? collectComments(tokenIndex) - : Collections.emptyList(); - LineColumnConverter.Pos pos = lineColumnConverter.toPos(t.originalEnd); - return new InternalSyntaxToken(pos.line(), pos.columnOffset(), ">", comments, false); - } - - private List collectComments(int tokenIndex) { - int commentIndex = tokenIndex; - while (commentIndex > 0 && isComment(tokenManager.get(commentIndex - 1))) { - commentIndex--; - } - return IntStream.range(commentIndex, tokenIndex).mapToObj(i -> { - Token t = tokenManager.get(i); - LineColumnConverter.Pos pos = lineColumnConverter.toPos(t.originalStart); - return new InternalSyntaxTrivia(convertTokenTypeToCommentKind(t), - t.toString(tokenManager.getSource()), - pos.line(), - pos.columnOffset() - ); - }) - .toList(); - } - - @VisibleForTesting - static CommentKind convertTokenTypeToCommentKind(Token token) { - return switch (token.tokenType) { - case TokenNameCOMMENT_BLOCK -> CommentKind.BLOCK; - case TokenNameCOMMENT_JAVADOC -> CommentKind.JAVADOC; - case TokenNameCOMMENT_LINE -> CommentKind.LINE; - case TokenNameCOMMENT_MARKDOWN -> CommentKind.MARKDOWN; - default -> throw new IllegalStateException("Unexpected value: " + token.tokenType); - }; - } - - /** - * {@link Token#isComment()} has an issue https://github.com/eclipse-jdt/eclipse.jdt.core/issues/3914 - * it does not support Markdown comments. This method has to be used instead. - */ - @VisibleForTesting - static boolean isComment(Token token) { - return switch (token.tokenType) { - case TokenNameCOMMENT_BLOCK, TokenNameCOMMENT_JAVADOC, TokenNameCOMMENT_LINE, TokenNameCOMMENT_MARKDOWN -> true; - default -> false; - }; - } - - private void addEmptyStatementsToList(int tokenIndex, List list) { - while (true) { - Token token; - do { - tokenIndex++; - token = tokenManager.get(tokenIndex); - } while (isComment(token)); - - if (token.tokenType != TerminalToken.TokenNameSEMICOLON) { - break; - } - list.add(new EmptyStatementTreeImpl(createSyntaxToken(tokenIndex))); - } - } - - private JavaTree.CompilationUnitTreeImpl convertCompilationUnit(CompilationUnit e) { - PackageDeclarationTree packageDeclaration = null; - if (e.getPackage() != null) { - packageDeclaration = new JavaTree.PackageDeclarationTreeImpl( - convertAnnotations(e.getPackage().annotations()), - firstTokenIn(e.getPackage(), TerminalToken.TokenNamepackage), - convertExpression(e.getPackage().getName()), - firstTokenIn(e.getPackage(), TerminalToken.TokenNameSEMICOLON) - ); - } - - List imports = new ArrayList<>(); - for (int i = 0; i < e.imports().size(); i++) { - ImportDeclaration e2 = (ImportDeclaration) e.imports().get(i); - ExpressionTree name = convertImportName(e2.getName()); - - boolean isModuleImport = org.eclipse.jdt.core.dom.Modifier.isModule(e2.getModifiers()); - - // "on demand" means `import pkg.*;` - if (e2.isOnDemand() && !isModuleImport) { - InternalSyntaxToken dotToken = lastTokenIn(e2, TerminalToken.TokenNameDOT); - InternalSyntaxToken identifierToken = lastTokenIn(e2, TerminalToken.TokenNameMULTIPLY); - name = new MemberSelectExpressionTreeImpl(name, dotToken, new IdentifierTreeImpl(identifierToken)); - } - - InternalSyntaxToken staticKeyword = e2.isStatic() ? firstTokenIn(e2, TerminalToken.TokenNamestatic) : null; - InternalSyntaxToken moduleKeyword = isModuleImport ? firstTokenIn(e2, TerminalToken.TokenNamemodule) : null; - - JavaTree.ImportTreeImpl t = new JavaTree.ImportTreeImpl( - firstTokenIn(e2, TerminalToken.TokenNameimport), - staticKeyword, - moduleKeyword, - name, - lastTokenIn(e2, TerminalToken.TokenNameSEMICOLON) - ); - - if (!isModuleImport) { - // There is no method to resolve bindings for a module import. - t.binding = e2.resolveBinding(); - } - - imports.add(t); - - int tokenIndex = tokenManager.lastIndexIn(e2, TerminalToken.TokenNameSEMICOLON); - addEmptyStatementsToList(tokenIndex, imports); - } - - List types = new ArrayList<>(); - for (Object type : e.types()) { - processBodyDeclaration((AbstractTypeDeclaration) type, types); - } - - if (e.imports().isEmpty() && e.types().isEmpty()) { - addEmptyStatementsToList(-1, imports); - } - - return new JavaTree.CompilationUnitTreeImpl( - packageDeclaration, - imports, - types, - convertModuleDeclaration(compilationUnit.getModule()), - firstTokenAfter(e, TerminalToken.TokenNameEOF) - ); - } - - private ExpressionTree convertImportName(Name node) { - switch (node.getNodeType()) { - case ASTNode.SIMPLE_NAME: { - return new IdentifierTreeImpl(firstTokenIn(node, TerminalToken.TokenNameIdentifier)); - } - case ASTNode.QUALIFIED_NAME: { - QualifiedName e = (QualifiedName) node; - return new MemberSelectExpressionTreeImpl( - convertImportName(e.getQualifier()), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - (IdentifierTreeImpl) convertImportName(e.getName())); - } - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); + return SemsitterTranslator.translate(sema, document); + } catch (RecognitionException syntaxError) { + throw syntaxError; + } catch (Exception failure) { + discardUnitBridge(); + throw new RecognitionException(1, "SemSitter: unable to parse " + unitName, failure); + } finally { + deleteQuietly(directory); } } @Nullable - private ModuleDeclarationTree convertModuleDeclaration(@Nullable ModuleDeclaration e) { - if (e == null) { - return null; - } - List moduleDirectives = e.moduleStatements().stream() - .map(o -> convertModuleDirective((ModuleDirective) o)) - .toList(); - return new ModuleDeclarationTreeImpl( - convertAnnotations(e.annotations()), - e.isOpen() ? firstTokenIn(e, TerminalToken.TokenNameopen) : null, - firstTokenBefore(e.getName(), TerminalToken.TokenNamemodule), - convertModuleName(e.getName()), - firstTokenAfter(e.getName(), TerminalToken.TokenNameLBRACE), - moduleDirectives, - lastTokenIn(e, TerminalToken.TokenNameRBRACE) - ); - } - - private ModuleNameTreeImpl convertModuleName(Name node) { - switch (node.getNodeType()) { - case ASTNode.QUALIFIED_NAME: { - QualifiedName e = (QualifiedName) node; - ModuleNameTreeImpl t = convertModuleName(e.getQualifier()); - t.add(new IdentifierTreeImpl(firstTokenIn(e.getName(), TerminalToken.TokenNameIdentifier))); - return t; - } - case ASTNode.SIMPLE_NAME: { - ModuleNameTreeImpl t = ModuleNameTreeImpl.emptyList(); - t.add(new IdentifierTreeImpl(firstTokenIn(node, TerminalToken.TokenNameIdentifier))); - return t; - } - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); - } - } - - private ModuleNameListTreeImpl convertModuleNames(List list) { - ModuleNameListTreeImpl t = ModuleNameListTreeImpl.emptyList(); - for (int i = 0; i < list.size(); i++) { - Name o = (Name) list.get(i); - if (i > 0) { - t.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - t.add(convertModuleName(o)); - } - return t; - } - - private ModuleDirectiveTree convertModuleDirective(ModuleDirective node) { - switch (node.getNodeType()) { - case ASTNode.REQUIRES_DIRECTIVE: { - RequiresDirective e = (RequiresDirective) node; - List modifiers = new ArrayList<>(); - for (Object o : e.modifiers()) { - switch (((ModuleModifier) o).getKeyword().toString()) { - case "static": - modifiers.add(new ModifierKeywordTreeImpl(Modifier.STATIC, firstTokenIn((ASTNode) o, ANY_TOKEN))); - break; - case "transitive": - modifiers.add(new ModifierKeywordTreeImpl(Modifier.TRANSITIVE, firstTokenIn((ASTNode) o, ANY_TOKEN))); - break; - default: - throw new IllegalStateException(); - } - } - return new RequiresDirectiveTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamerequires), - new ModifiersTreeImpl(modifiers), - convertModuleName(e.getName()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - case ASTNode.EXPORTS_DIRECTIVE: { - ExportsDirective e = (ExportsDirective) node; - return new ExportsDirectiveTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameexports), - convertExpression(e.getName()), - e.modules().isEmpty() ? null : firstTokenAfter(e.getName(), TerminalToken.TokenNameto), - convertModuleNames(e.modules()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - case ASTNode.OPENS_DIRECTIVE: { - OpensDirective e = (OpensDirective) node; - return new OpensDirectiveTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameopens), - convertExpression(e.getName()), - e.modules().isEmpty() ? null : firstTokenAfter(e.getName(), TerminalToken.TokenNameto), - convertModuleNames(e.modules()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - case ASTNode.USES_DIRECTIVE: { - UsesDirective e = (UsesDirective) node; - return new UsesDirectiveTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameuses), - (TypeTree) convertExpression(e.getName()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - case ASTNode.PROVIDES_DIRECTIVE: { - ProvidesDirective e = (ProvidesDirective) node; - QualifiedIdentifierListTreeImpl typeNames = QualifiedIdentifierListTreeImpl.emptyList(); - for (int i = 0; i < e.implementations().size(); i++) { - Name o = (Name) e.implementations().get(i); - if (i > 0) { - typeNames.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - typeNames.add((TypeTree) convertExpression(o)); - } - return new ProvidesDirectiveTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameprovides), - (TypeTree) convertExpression(e.getName()), - firstTokenAfter(e.getName(), TerminalToken.TokenNamewith), - typeNames, - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); - } - } - - /** Converts implicitly declared unnamed class at the top level of a compact compilation unit. */ - private ClassTreeImpl convertUnnamedClassDeclaration(AbstractTypeDeclaration e) { - List members = new ArrayList<>(); - - for (Object o : e.bodyDeclarations()) { - processBodyDeclaration((BodyDeclaration) o, members); - } - - ClassTreeImpl t = new ClassTreeImpl(Tree.Kind.IMPLICIT_CLASS, null, members, null); - - t.typeBinding = e.resolveBinding(); - declaration(t.typeBinding, t); - - return t; - } - - private ClassTreeImpl convertTypeDeclaration(AbstractTypeDeclaration e) { - if (e.getNodeType() == ASTNode.UNNAMED_CLASS) { - return convertUnnamedClassDeclaration(e); - } - - List members = new ArrayList<>(); - - int leftBraceTokenIndex = findLeftBraceTokenIndex(e); - addEmptyStatementsToList(leftBraceTokenIndex, members); - - for (Object o : e.bodyDeclarations()) { - processBodyDeclaration((BodyDeclaration) o, members); - } - - ModifiersTreeImpl modifiers = convertModifiers(e.modifiers()); - IdentifierTreeImpl name = createSimpleName(e.getName()); - - InternalSyntaxToken openBraceToken = createSyntaxToken(leftBraceTokenIndex); - InternalSyntaxToken closeBraceToken = lastTokenIn(e, TerminalToken.TokenNameRBRACE); - - final ClassTreeImpl t; - switch (e.getNodeType()) { - case ASTNode.TYPE_DECLARATION: - t = convertTypeDeclaration((TypeDeclaration) e, modifiers, name, openBraceToken, members, closeBraceToken); - break; - case ASTNode.ENUM_DECLARATION: - t = convertEnumDeclaration((EnumDeclaration) e, modifiers, name, openBraceToken, members, closeBraceToken); - break; - case ASTNode.RECORD_DECLARATION: - t = convertRecordDeclaration((RecordDeclaration) e, modifiers, name, openBraceToken, members, closeBraceToken); - break; - case ASTNode.ANNOTATION_TYPE_DECLARATION: - t = convertAnnotationTypeDeclaration((AnnotationTypeDeclaration) e, modifiers, name, openBraceToken, members, closeBraceToken); - break; - default: - throw new IllegalStateException(ASTNode.nodeClassForType(e.getNodeType()).toString()); - } - - // no-op for annotation-types - completeSuperInterfaces(e, t); - - t.typeBinding = e.resolveBinding(); - declaration(t.typeBinding, t); - - return t; - } - - private ClassTreeImpl convertTypeDeclaration(TypeDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, - InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { - InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), e.isInterface() ? TerminalToken.TokenNameinterface : TerminalToken.TokenNameclass); - ClassTreeImpl t = new ClassTreeImpl(e.isInterface() ? Tree.Kind.INTERFACE : Tree.Kind.CLASS, openBraceToken, members, closeBraceToken) - .complete(modifiers, declarationKeyword, name) - .completeTypeParameters(convertTypeParameters(e.typeParameters())); - - if (!e.permittedTypes().isEmpty()) { - List permittedTypes = e.permittedTypes(); - InternalSyntaxToken permitsKeyword = firstTokenBefore((Type) permittedTypes.get(0), TerminalToken.TokenNameRestrictedIdentifierpermits); - QualifiedIdentifierListTreeImpl classPermittedTypes = QualifiedIdentifierListTreeImpl.emptyList(); - - convertSeparatedTypeList(permittedTypes, classPermittedTypes); - t.completePermittedTypes(permitsKeyword, classPermittedTypes); - } - - if (!e.isInterface() && e.getSuperclassType() != null) { - Type superclassType = e.getSuperclassType(); - t.completeSuperclass(firstTokenBefore(superclassType, TerminalToken.TokenNameextends), convertType(superclassType)); - } - return t; - } - - private ClassTreeImpl convertEnumDeclaration(EnumDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, - InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { - List enumConstants = ((List) e.enumConstants()).stream() - .map(EnumConstantDeclaration.class::cast) - .map(this::processEnumConstantDeclaration) - .toList(); - members.addAll(0, enumConstants); - - InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameenum); - return new ClassTreeImpl(Tree.Kind.ENUM, openBraceToken, members, closeBraceToken) - .complete(modifiers, declarationKeyword, name); - } - - private ClassTreeImpl convertAnnotationTypeDeclaration(AnnotationTypeDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, - InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { - InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameinterface); - return new ClassTreeImpl(Tree.Kind.ANNOTATION_TYPE, openBraceToken, members, closeBraceToken) - .complete(modifiers, declarationKeyword, name) - .completeAtToken(firstTokenBefore(e.getName(), TerminalToken.TokenNameAT)); - } - - private ClassTreeImpl convertRecordDeclaration(RecordDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, - InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { - InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameRestrictedIdentifierrecord); - List recordComponents = e.recordComponents(); - InternalSyntaxToken openParen = firstTokenAfter(e.getName(), TerminalToken.TokenNameLPAREN); - InternalSyntaxToken closeParen = firstTokenAfter( - recordComponents.isEmpty() ? e.getName() : (ASTNode) recordComponents.get(recordComponents.size() - 1), - TerminalToken.TokenNameRPAREN); - return new ClassTreeImpl(Tree.Kind.RECORD, openBraceToken, members, closeBraceToken) - .complete(modifiers, declarationKeyword, name) - .completeTypeParameters(convertTypeParameters(e.typeParameters())) - .completeRecordComponents(openParen, convertRecordComponents(e), closeParen); - } - - private List convertRecordComponents(RecordDeclaration e) { - List recordComponents = new ArrayList<>(); - - for (int i = 0; i < e.recordComponents().size(); i++) { - SingleVariableDeclaration o = (SingleVariableDeclaration) e.recordComponents().get(i); - VariableTreeImpl recordComponent = convertVariable(o); - if (i < e.recordComponents().size() - 1) { - recordComponent.setEndToken(firstTokenAfter(o, TerminalToken.TokenNameCOMMA)); - } - recordComponents.add(recordComponent); - } - return recordComponents; - } - - private int findLeftBraceTokenIndex(AbstractTypeDeclaration e) { - // TODO try to simplify, note that type annotations can contain LBRACE - if (e.getNodeType() == ASTNode.ENUM_DECLARATION) { - EnumDeclaration enumDeclaration = (EnumDeclaration) e; - if (!enumDeclaration.enumConstants().isEmpty()) { - return tokenManager.firstIndexBefore((ASTNode) enumDeclaration.enumConstants().get(0), TerminalToken.TokenNameLBRACE); - } - if (!enumDeclaration.bodyDeclarations().isEmpty()) { - return tokenManager.firstIndexBefore((ASTNode) e.bodyDeclarations().get(0), TerminalToken.TokenNameLBRACE); - } - return tokenManager.lastIndexIn(e, TerminalToken.TokenNameLBRACE); - } - if (!e.bodyDeclarations().isEmpty()) { - // for records, bodyDeclarations may not be in the order encountered in file, for classes they are - List bodyDeclarations = e.bodyDeclarations(); - BodyDeclaration firstDeclaration = bodyDeclarations.get(0); - for (int i = 1; i < bodyDeclarations.size(); i++) { - BodyDeclaration declaration = bodyDeclarations.get(i); - if (firstDeclaration.getStartPosition() > declaration.getStartPosition()) { - firstDeclaration = declaration; - } - } - return tokenManager.firstIndexBefore(firstDeclaration, TerminalToken.TokenNameLBRACE); - } - return tokenManager.lastIndexIn(e, TerminalToken.TokenNameLBRACE); - } - - private void completeSuperInterfaces(AbstractTypeDeclaration e, ClassTreeImpl t) { - List superInterfaces = superInterfaceTypes(e); - if (!superInterfaces.isEmpty()) { - QualifiedIdentifierListTreeImpl interfaces = QualifiedIdentifierListTreeImpl.emptyList(); - convertSeparatedTypeList(superInterfaces, interfaces); - - ASTNode firstInterface = (ASTNode) superInterfaces.get(0); - InternalSyntaxToken keyword = firstTokenBefore(firstInterface, t.is(Tree.Kind.INTERFACE) ? TerminalToken.TokenNameextends : TerminalToken.TokenNameimplements); - t.completeInterfaces(keyword, interfaces); - } - } - - private static List superInterfaceTypes(AbstractTypeDeclaration e) { - switch (e.getNodeType()) { - case ASTNode.TYPE_DECLARATION: - return ((TypeDeclaration) e).superInterfaceTypes(); - case ASTNode.ENUM_DECLARATION: - return ((EnumDeclaration) e).superInterfaceTypes(); - case ASTNode.RECORD_DECLARATION: - return ((RecordDeclaration) e).superInterfaceTypes(); - case ASTNode.ANNOTATION_TYPE_DECLARATION, ASTNode.UNNAMED_CLASS: - return Collections.emptyList(); - default: - throw new IllegalStateException(ASTNode.nodeClassForType(e.getNodeType()).toString()); - } - } - - private void convertSeparatedTypeList(List source, ListTree target) { - for (int i = 0; i < source.size(); i++) { - Type o = source.get(i); - T tree = (T) convertType(o); - if (i > 0) { - target.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - target.add(tree); - } - } + private static SemsitterBridge unitBridge; - private EnumConstantTreeImpl processEnumConstantDeclaration(EnumConstantDeclaration e) { - final int openParTokenIndex = firstTokenIndexAfter(e.getName()); - final InternalSyntaxToken openParToken; - final InternalSyntaxToken closeParToken; - if (tokenManager.get(openParTokenIndex).tokenType == TerminalToken.TokenNameLPAREN) { - openParToken = createSyntaxToken(openParTokenIndex); - closeParToken = firstTokenAfter( - e.arguments().isEmpty() - ? e.getName() - : (ASTNode) e.arguments().get(e.arguments().size() - 1), - TerminalToken.TokenNameRPAREN); - } else { - openParToken = null; - closeParToken = null; + /** One bridge per JVM for single-source parses, stopped at shutdown. */ + private static synchronized SemsitterBridge unitBridge() throws IOException { + if (unitBridge == null) { + SemsitterBridge started = SemsitterBridge.start(bridgePath()); + Runtime.getRuntime().addShutdownHook(new Thread(started::close)); + unitBridge = started; } - - final ArgumentListTreeImpl arguments = convertArguments(openParToken, e.arguments(), closeParToken); - ClassTreeImpl classBody = null; - if (e.getAnonymousClassDeclaration() != null) { - List members = new ArrayList<>(); - for (Object o : e.getAnonymousClassDeclaration().bodyDeclarations()) { - processBodyDeclaration((BodyDeclaration) o, members); - } - classBody = new ClassTreeImpl( - Tree.Kind.CLASS, - firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), - members, - lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) - ); - classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); - declaration(classBody.typeBinding, classBody); - } - - final int separatorTokenIndex = firstTokenIndexAfter(e); - final InternalSyntaxToken separatorToken; - switch (tokenManager.get(separatorTokenIndex).tokenType) { - case TokenNameCOMMA, - TokenNameSEMICOLON: - separatorToken = createSyntaxToken(separatorTokenIndex); - break; - case TokenNameRBRACE: - separatorToken = null; - break; - default: - throw new IllegalStateException(); - } - - IdentifierTreeImpl identifier = createSimpleName(e.getName()); - if (e.getAnonymousClassDeclaration() == null) { - identifier.binding = excludeRecovery(e.resolveConstructorBinding(), arguments.size()); - } else { - identifier.binding = findConstructorForAnonymousClass(e.getAST(), identifier.typeBinding, e.resolveConstructorBinding()); - } - usage(identifier.binding, identifier); - - EnumConstantTreeImpl t = new EnumConstantTreeImpl( - convertModifiers(e.modifiers()), - identifier, - new NewClassTreeImpl(identifier, arguments, classBody), - separatorToken - ); - t.variableBinding = e.resolveVariable(); - declaration(t.variableBinding, t); - return t; - } - - private void processBodyDeclaration(BodyDeclaration node, List members) { - final int lastTokenIndex; - - switch (node.getNodeType()) { - case ASTNode.ANNOTATION_TYPE_DECLARATION, - ASTNode.ENUM_DECLARATION, - ASTNode.RECORD_DECLARATION, - ASTNode.TYPE_DECLARATION, - ASTNode.UNNAMED_CLASS: - lastTokenIndex = processTypeDeclaration((AbstractTypeDeclaration) node, members); - break; - case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION: - lastTokenIndex = processAnnotationTypeMemberDeclaration((AnnotationTypeMemberDeclaration) node, members); - break; - case ASTNode.INITIALIZER: - lastTokenIndex = processInitializerDeclaration((Initializer) node, members); - break; - case ASTNode.METHOD_DECLARATION: - lastTokenIndex = processMethodDeclaration((MethodDeclaration) node, members); - break; - case ASTNode.FIELD_DECLARATION: - lastTokenIndex = processFieldDeclaration((FieldDeclaration) node, members); - break; - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); - } - - addEmptyStatementsToList(lastTokenIndex, members); - } - - private int processTypeDeclaration(AbstractTypeDeclaration node, List members) { - members.add(convertTypeDeclaration(node)); - return tokenManager.lastIndexIn(node, TerminalToken.TokenNameRBRACE); - } - - private int processAnnotationTypeMemberDeclaration(AnnotationTypeMemberDeclaration e, List members) { - FormalParametersListTreeImpl parameters = new FormalParametersListTreeImpl( - firstTokenAfter(e.getName(), TerminalToken.TokenNameLPAREN), - firstTokenAfter(e.getName(), TerminalToken.TokenNameRPAREN)); - - Expression defaultExpression = e.getDefault(); - InternalSyntaxToken defaultToken = defaultExpression == null ? null : firstTokenBefore(defaultExpression, TerminalToken.TokenNamedefault); - ExpressionTree defaultValue = defaultExpression == null ? null : convertExpression(defaultExpression); - - MethodTreeImpl t = new MethodTreeImpl(parameters, defaultToken, defaultValue) - .complete(convertType(e.getType()), createSimpleName(e.getName()), lastTokenIn(e, TerminalToken.TokenNameSEMICOLON)) - .completeWithModifiers(convertModifiers(e.modifiers())); - - t.methodBinding = e.resolveBinding(); - declaration(t.methodBinding, t); - - members.add(t); - return tokenManager.lastIndexIn(e, TerminalToken.TokenNameSEMICOLON); - } - - private int processInitializerDeclaration(Initializer e, List members) { - BlockTreeImpl blockTree = convertBlock(e.getBody()); - if (org.eclipse.jdt.core.dom.Modifier.isStatic(e.getModifiers())) { - members.add(new StaticInitializerTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamestatic), - (InternalSyntaxToken) blockTree.openBraceToken(), - blockTree.body(), - (InternalSyntaxToken) blockTree.closeBraceToken())); - } else { - members.add(new BlockTreeImpl( - Tree.Kind.INITIALIZER, - (InternalSyntaxToken) blockTree.openBraceToken(), - blockTree.body(), - (InternalSyntaxToken) blockTree.closeBraceToken())); - } - return tokenManager.lastIndexIn(e, TerminalToken.TokenNameRBRACE); - } - - private int processMethodDeclaration(MethodDeclaration e, List members) { - List p = e.parameters(); - final FormalParametersListTreeImpl formalParameters; - if (e.isCompactConstructor()) { - // only used for records - formalParameters = new FormalParametersListTreeImpl(null, null); - } else { - InternalSyntaxToken openParen = firstTokenAfter(e.getName(), TerminalToken.TokenNameLPAREN); - InternalSyntaxToken closeParen = firstTokenAfter(p.isEmpty() ? e.getName() : (ASTNode) p.get(p.size() - 1), TerminalToken.TokenNameRPAREN); - formalParameters = new FormalParametersListTreeImpl(openParen, closeParen); - } - - for (int i = 0; i < p.size(); i++) { - SingleVariableDeclaration o = (SingleVariableDeclaration) p.get(i); - VariableTreeImpl parameter = convertVariable(o); - if (i < p.size() - 1) { - parameter.setEndToken(firstTokenAfter(o, TerminalToken.TokenNameCOMMA)); - } - formalParameters.add(parameter); - } - - QualifiedIdentifierListTreeImpl thrownExceptionTypes = QualifiedIdentifierListTreeImpl.emptyList(); - List tt = e.thrownExceptionTypes(); - convertSeparatedTypeList(tt, thrownExceptionTypes); - - Block body = e.getBody(); - Type returnType = e.getReturnType2(); - InternalSyntaxToken throwsToken = tt.isEmpty() ? null : firstTokenBefore((Type) tt.get(0), TerminalToken.TokenNamethrows); - InternalSyntaxToken semcolonToken = body == null ? lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) : null; - MethodTreeImpl t = new MethodTreeImpl( - returnType == null ? null : applyExtraDimensions(convertType(returnType), e.extraDimensions()), - createSimpleName(e.getName()), - formalParameters, - throwsToken, - thrownExceptionTypes, - body == null ? null : convertBlock(body), - semcolonToken - ).completeWithModifiers( - convertModifiers(e.modifiers()) - ).completeWithTypeParameters( - convertTypeParameters(e.typeParameters()) - ); - t.methodBinding = e.resolveBinding(); - declaration(t.methodBinding, t); - - members.add(t); - return tokenManager.lastIndexIn(e, body == null ? TerminalToken.TokenNameSEMICOLON : TerminalToken.TokenNameRBRACE); - } - - private int processFieldDeclaration(FieldDeclaration fieldDeclaration, List members) { - ModifiersTreeImpl modifiers = convertModifiers(fieldDeclaration.modifiers()); - TypeTree type = convertType(fieldDeclaration.getType()); - - for (int i = 0; i < fieldDeclaration.fragments().size(); i++) { - VariableDeclarationFragment fragment = (VariableDeclarationFragment) fieldDeclaration.fragments().get(i); - VariableTreeImpl t = new VariableTreeImpl(createSimpleName(fragment.getName())) - .completeModifiersAndType(modifiers, applyExtraDimensions(type, fragment.extraDimensions())); - - if (fragment.getInitializer() != null) { - t.completeTypeAndInitializer(t.type(), firstTokenAfter(fragment.getName(), TerminalToken.TokenNameEQUAL), convertExpression(fragment.getInitializer())); - } - - t.setEndToken(firstTokenAfter(fragment, i + 1 < fieldDeclaration.fragments().size() ? TerminalToken.TokenNameCOMMA : TerminalToken.TokenNameSEMICOLON)); - t.variableBinding = fragment.resolveBinding(); - declaration(t.variableBinding, t); - - members.add(t); - } - return tokenManager.lastIndexIn(fieldDeclaration, TerminalToken.TokenNameSEMICOLON); + return unitBridge; } - private ArgumentListTreeImpl convertArguments(@Nullable InternalSyntaxToken openParen, List list, @Nullable InternalSyntaxToken closeParen) { - ArgumentListTreeImpl arguments = ArgumentListTreeImpl.emptyList().complete(openParen, closeParen); - for (int i = 0; i < list.size(); i++) { - Expression o = (Expression) list.get(i); - arguments.add(convertExpression(o)); - if (i < list.size() - 1) { - arguments.separators().add(firstTokenAfter(o, TerminalToken.TokenNameCOMMA)); - } + private static synchronized void discardUnitBridge() { + if (unitBridge != null) { + unitBridge.close(); + unitBridge = null; } - return arguments; } - @Nullable - private TypeArgumentListTreeImpl convertTypeArguments(List list) { - if (list.isEmpty()) { - return null; - } - ASTNode last = (ASTNode) list.get(list.size() - 1); - int tokenIndex = tokenManager.firstIndexAfter(last, ANY_TOKEN); - while (isComment(tokenManager.get(tokenIndex))) { - tokenIndex++; + private static void deleteQuietly(@Nullable Path directory) { + if (directory == null) { + return; } - return convertTypeArguments( - firstTokenBefore((ASTNode) list.get(0), TerminalToken.TokenNameLESS), - list, - // TerminalToken.TokenNameUNSIGNED_RIGHT_SHIFT vs TerminalToken.TokenNameGREATER - createSpecialToken(tokenIndex) - ); - } - - private TypeArgumentListTreeImpl convertTypeArguments(InternalSyntaxToken l, List list, InternalSyntaxToken g) { - TypeArgumentListTreeImpl typeArguments = new TypeArgumentListTreeImpl(l, g); - for (int i = 0; i < list.size(); i++) { - Type o = (Type) list.get(i); - if (i > 0) { - typeArguments.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - typeArguments.add(convertType(o)); + try (Stream paths = Files.walk(directory)) { + paths.sorted(Comparator.reverseOrder()).forEach(JParser::deleteIfPresent); + } catch (IOException | UncheckedIOException ignored) { + // A leftover temporary file does not affect the analysis. } - return typeArguments; } - private TypeParameterListTreeImpl convertTypeParameters(List list) { - if (list.isEmpty()) { - return new TypeParameterListTreeImpl(); - } - ASTNode last = (ASTNode) list.get(list.size() - 1); - int tokenIndex = tokenManager.firstIndexAfter(last, ANY_TOKEN); - while (isComment(tokenManager.get(tokenIndex))) { - tokenIndex++; - } - TypeParameterListTreeImpl t = new TypeParameterListTreeImpl( - firstTokenBefore((ASTNode) list.get(0), TerminalToken.TokenNameLESS), - // TerminalToken.TokenNameUNSIGNED_RIGHT_SHIFT vs TerminalToken.TokenNameGREATER - createSpecialToken(tokenIndex) - ); - for (int i = 0; i < list.size(); i++) { - TypeParameter o = (TypeParameter) list.get(i); - if (i > 0) { - t.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - t.add(convertTypeParameter(o)); + private static void deleteIfPresent(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException failure) { + throw new UncheckedIOException(failure); } - return t; } - private TypeParameterTree convertTypeParameter(TypeParameter e) { - IdentifierTreeImpl i = createSimpleName(e.getName()); - // TODO why ECJ uses IExtendedModifier here instead of Annotation ? - i.complete(convertAnnotations(e.modifiers())); - TypeParameterTreeImpl t; - List typeBounds = e.typeBounds(); - if (typeBounds.isEmpty()) { - t = new TypeParameterTreeImpl(i); - } else { - QualifiedIdentifierListTreeImpl bounds = QualifiedIdentifierListTreeImpl.emptyList(); - for (int j = 0; j < typeBounds.size(); j++) { - Object o = typeBounds.get(j); - bounds.add(convertType((Type) o)); - if (j < typeBounds.size() - 1) { - bounds.separators().add(firstTokenAfter((ASTNode) o, TerminalToken.TokenNameAND)); - } - } - t = new TypeParameterTreeImpl( - i, - firstTokenAfter(e.getName(), TerminalToken.TokenNameextends), - bounds - ); + /** A temporary directory only the current user can read, where the file system supports it. */ + private static Path privateTempDirectory() throws IOException { + if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + return Files.createTempDirectory("sonar-java-unit", PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); } - t.typeBinding = e.resolveBinding(); - return t; + return Files.createTempDirectory("sonar-java-unit"); } - /** - * @param extraDimensions list of {@link org.eclipse.jdt.core.dom.Dimension} - */ - private TypeTree applyExtraDimensions(TypeTree type, List extraDimensions) { - ITypeBinding typeBinding = ((AbstractTypedTree) type).typeBinding; - for (int i = 0; i < extraDimensions.size(); i++) { - Dimension e = (Dimension) extraDimensions.get(i); - type = new JavaTree.ArrayTypeTreeImpl( - type, - (List) convertAnnotations(e.annotations()), - firstTokenIn(e, TerminalToken.TokenNameLBRACKET), - firstTokenIn(e, TerminalToken.TokenNameRBRACKET) - ); - if (typeBinding != null) { - ((JavaTree.ArrayTypeTreeImpl) type).typeBinding = typeBinding.createArrayType(i + 1); - } + static Path bridgePath() { + String configured = System.getProperty("sonar.java.semsitter.bridge"); + if (configured == null || configured.isBlank()) { + configured = System.getenv("SEMSITTER_BRIDGE"); } - return type; - } - - private VariableTreeImpl convertVariable(TypePattern typePattern) { - if (typePattern.getAST().apiLevel() < AST.JLS22) { - return convertVariable(typePattern.getPatternVariable()); + if (configured != null && !configured.isBlank()) { + return Path.of(configured); } - VariableDeclaration variableDeclaration = typePattern.getPatternVariable2(); - if (variableDeclaration instanceof VariableDeclarationFragment declarationFragment) { - return convertVariable(declarationFragment); - } else { - return convertVariable((SingleVariableDeclaration) variableDeclaration); + try { + Path target = Path.of(System.getProperty("java.io.tmpdir"), "sonar-java", "semsitter-bridge"); + return BridgeExtractor.extract(target, new PlatformDetection().getPlatform()); + } catch (Exception failure) { + throw new RecognitionException(1, "SemSitter bridge is unavailable: " + failure.getMessage(), failure); } } - private VariableTreeImpl convertVariable(VariableDeclarationFragment declarationFragment) { - return completeInitializerAndBinding( - new VariableTreeImpl(createSimpleName(declarationFragment.getName())), - declarationFragment); - } - - private VariableTreeImpl convertVariable(SingleVariableDeclaration e) { - // TODO are extraDimensions and varargs mutually exclusive? - TypeTree type = convertType(e.getType()); - type = applyExtraDimensions(type, e.extraDimensions()); - if (e.isVarargs()) { - ITypeBinding typeBinding = ((AbstractTypedTree) type).typeBinding; - type = new JavaTree.ArrayTypeTreeImpl( - type, - (List) convertAnnotations(e.varargsAnnotations()), - firstTokenAfter(e.getType(), TerminalToken.TokenNameELLIPSIS) - ); - if (typeBinding != null) { - ((JavaTree.ArrayTypeTreeImpl) type).typeBinding = typeBinding.createArrayType(1); - } - } - - VariableTreeImpl t = new VariableTreeImpl( - convertModifiers(e.modifiers()), - type, - createSimpleName(e.getName()) - ); - return completeInitializerAndBinding(t, e); - } - - private VariableTreeImpl completeInitializerAndBinding(VariableTreeImpl t, VariableDeclaration e) { - if (e.getInitializer() != null) { - t.completeTypeAndInitializer( - t.type(), - firstTokenAfter(e.getName(), TerminalToken.TokenNameEQUAL), - convertExpression(e.getInitializer()) - ); - } - t.variableBinding = e.resolveBinding(); - if (t.variableBinding != null) { - if (t.type() instanceof InferedTypeTree inferredType) { - inferredType.typeBinding = t.variableBinding.getType(); - } - declaration(t.variableBinding, t); - } - return t; - } - - private void addVariableToList(VariableDeclarationExpression e2, List list) { - ModifiersTreeImpl modifiers = convertModifiers(e2.modifiers()); - TypeTree type = convertType(e2.getType()); - - for (int i = 0; i < e2.fragments().size(); i++) { - VariableDeclarationFragment fragment = (VariableDeclarationFragment) e2.fragments().get(i); - VariableTreeImpl t = new VariableTreeImpl(createSimpleName(fragment.getName())); - t.completeModifiers(modifiers); - if (fragment.getInitializer() == null) { - t.completeType(applyExtraDimensions(type, fragment.extraDimensions())); - } else { - t.completeTypeAndInitializer( - applyExtraDimensions(type, fragment.extraDimensions()), - firstTokenBefore(fragment.getInitializer(), TerminalToken.TokenNameEQUAL), - convertExpression(fragment.getInitializer()) - ); - } - if (i < e2.fragments().size() - 1) { - t.setEndToken(firstTokenAfter(fragment, TerminalToken.TokenNameCOMMA)); - } - t.variableBinding = fragment.resolveBinding(); - declaration(t.variableBinding, t); - list.add(t); - } - } - - private VarTypeTreeImpl convertVarType(SimpleType simpleType) { - VarTypeTreeImpl varTree = new VarTypeTreeImpl(firstTokenIn(simpleType.getName(), TerminalToken.TokenNameIdentifier)); - varTree.typeBinding = simpleType.resolveBinding(); - return varTree; - } - - private IdentifierTreeImpl createSimpleName(SimpleName e) { - int tokenIndex = firstIndexIn(tokenManager, e, TerminalToken.TokenNameIdentifier, TerminalToken.TokenNameUNDERSCORE); - Token token = tokenManager.get(tokenIndex); - boolean isUnnamedVariable = token.tokenType == TerminalToken.TokenNameUNDERSCORE; - IdentifierTreeImpl t = new IdentifierTreeImpl(createSyntaxToken(tokenIndex), isUnnamedVariable); - t.typeBinding = e.resolveTypeBinding(); - t.binding = e.resolveBinding(); - return t; - } - - private BlockTreeImpl convertBlock(Block e) { - List statements = new ArrayList<>(); - for (Object o : e.statements()) { - addStatementToList((Statement) o, statements); - } - return new BlockTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameLBRACE), - statements, - lastTokenIn(e, TerminalToken.TokenNameRBRACE) - ); - } - - private void addStatementToList(Statement node, List statements) { - if (node.getNodeType() == ASTNode.VARIABLE_DECLARATION_STATEMENT) { - VariableDeclarationStatement e = (VariableDeclarationStatement) node; - TypeTree tType = convertType(e.getType()); - ModifiersTreeImpl modifiers = convertModifiers(e.modifiers()); - for (int i = 0; i < e.fragments().size(); i++) { - VariableDeclarationFragment fragment = (VariableDeclarationFragment) e.fragments().get(i); - VariableTreeImpl t = new VariableTreeImpl(createSimpleName(fragment.getName())) - .completeType(applyExtraDimensions(tType, fragment.extraDimensions())) - .completeModifiers(modifiers); - Expression initalizer = fragment.getInitializer(); - if (initalizer != null) { - InternalSyntaxToken equalToken = firstTokenAfter(fragment.getName(), TerminalToken.TokenNameEQUAL); - t.completeTypeAndInitializer(t.type(), equalToken, convertExpression(initalizer)); - } - TerminalToken endTokenType = i < e.fragments().size() - 1 ? TerminalToken.TokenNameCOMMA : TerminalToken.TokenNameSEMICOLON; - t.setEndToken(firstTokenAfter(fragment, endTokenType)); - - t.variableBinding = fragment.resolveBinding(); - declaration(t.variableBinding, t); - statements.add(t); - } - } else { - statements.add(createStatement(node)); - } - } - - private StatementTree createStatement(Statement node) { - switch (node.getNodeType()) { - case ASTNode.BLOCK: - return convertBlock((Block) node); - case ASTNode.EMPTY_STATEMENT: - return convertEmptyStatement((EmptyStatement) node); - case ASTNode.RETURN_STATEMENT: - return convertReturn((ReturnStatement) node); - case ASTNode.FOR_STATEMENT: - return convertFor((ForStatement) node); - case ASTNode.WHILE_STATEMENT: - return convertWhile((WhileStatement) node); - case ASTNode.IF_STATEMENT: - return convertIf((IfStatement) node); - case ASTNode.BREAK_STATEMENT: - return convertBreak((BreakStatement) node); - case ASTNode.DO_STATEMENT: - return convertDoWhile((DoStatement) node); - case ASTNode.ASSERT_STATEMENT: - return convertAssert((AssertStatement) node); - case ASTNode.SWITCH_STATEMENT: - return convertSwitchStatement((SwitchStatement) node); - case ASTNode.SYNCHRONIZED_STATEMENT: - return convertSynchronized((SynchronizedStatement) node); - case ASTNode.EXPRESSION_STATEMENT: - return convertExpressionStatement((ExpressionStatement) node); - case ASTNode.CONTINUE_STATEMENT: - return convertContinue((ContinueStatement) node); - case ASTNode.LABELED_STATEMENT: - return convertLabel((LabeledStatement) node); - case ASTNode.ENHANCED_FOR_STATEMENT: - return convertForeach((EnhancedForStatement) node); - case ASTNode.THROW_STATEMENT: - return convertThrow((ThrowStatement) node); - case ASTNode.TRY_STATEMENT: - return convertTry((TryStatement) node); - case ASTNode.TYPE_DECLARATION_STATEMENT: - return convertTypeDeclaration(((TypeDeclarationStatement) node).getDeclaration()); - case ASTNode.CONSTRUCTOR_INVOCATION: - return convertConstructorInvocation((ConstructorInvocation) node); - case ASTNode.SUPER_CONSTRUCTOR_INVOCATION: - return convertSuperConstructorInvocation((SuperConstructorInvocation) node); - case ASTNode.YIELD_STATEMENT: - return convertYield((YieldStatement) node); - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); - } - } - - private EmptyStatementTreeImpl convertEmptyStatement(EmptyStatement e) { - return new EmptyStatementTreeImpl(lastTokenIn(e, TerminalToken.TokenNameSEMICOLON)); - } - - private ReturnStatementTreeImpl convertReturn(ReturnStatement e) { - Expression expression = e.getExpression(); - return new ReturnStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamereturn), - expression == null ? null : convertExpression(expression), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private ForStatementTreeImpl convertFor(ForStatement e) { - StatementListTreeImpl forInitStatement = StatementListTreeImpl.emptyList(); - for (int i = 0; i < e.initializers().size(); i++) { - Expression o = (Expression) e.initializers().get(i); - if (i > 0) { - forInitStatement.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - if (ASTNode.VARIABLE_DECLARATION_EXPRESSION == o.getNodeType()) { - addVariableToList((VariableDeclarationExpression) o, forInitStatement); - } else { - forInitStatement.add(new ExpressionStatementTreeImpl(convertExpression(o), null)); - } - } - - StatementListTreeImpl forUpdateStatement = StatementListTreeImpl.emptyList(); - for (int i = 0; i < e.updaters().size(); i++) { - Expression o = (Expression) e.updaters().get(i); - if (i > 0) { - forUpdateStatement.separators().add(firstTokenBefore(o, TerminalToken.TokenNameCOMMA)); - } - forUpdateStatement.add(new ExpressionStatementTreeImpl(convertExpression(o), null)); - } - - final int firstSemicolonTokenIndex = e.initializers().isEmpty() - ? tokenManager.firstIndexIn(e, TerminalToken.TokenNameSEMICOLON) - : tokenManager.firstIndexAfter((ASTNode) e.initializers().get(e.initializers().size() - 1), TerminalToken.TokenNameSEMICOLON); - Expression expression = e.getExpression(); - final int secondSemicolonTokenIndex = expression == null - ? nextTokenIndex(firstSemicolonTokenIndex, TerminalToken.TokenNameSEMICOLON) - : tokenManager.firstIndexAfter(expression, TerminalToken.TokenNameSEMICOLON); - - return new ForStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamefor), - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - forInitStatement, - createSyntaxToken(firstSemicolonTokenIndex), - expression == null ? null : convertExpression(expression), - createSyntaxToken(secondSemicolonTokenIndex), - forUpdateStatement, - firstTokenBefore(e.getBody(), TerminalToken.TokenNameRPAREN), - createStatement(e.getBody()) - ); - } - - private WhileStatementTreeImpl convertWhile(WhileStatement e) { - Expression expression = e.getExpression(); - return new WhileStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamewhile), - firstTokenBefore(expression, TerminalToken.TokenNameLPAREN), - convertExpression(expression), - firstTokenAfter(expression, TerminalToken.TokenNameRPAREN), - createStatement(e.getBody()) - ); - } - - private IfStatementTreeImpl convertIf(IfStatement e) { - Expression expression = e.getExpression(); - Statement thenStatement = e.getThenStatement(); - Statement elseStatement = e.getElseStatement(); - return new IfStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameif), - firstTokenBefore(expression, TerminalToken.TokenNameLPAREN), - convertExpression(expression), - firstTokenAfter(expression, TerminalToken.TokenNameRPAREN), - createStatement(thenStatement), - elseStatement == null ? null : firstTokenAfter(thenStatement, TerminalToken.TokenNameelse), - elseStatement == null ? null : createStatement(elseStatement) - ); - } - - private BreakStatementTreeImpl convertBreak(BreakStatement e) { - IdentifierTreeImpl identifier = e.getLabel() == null ? null : createSimpleName(e.getLabel()); - usageLabel(identifier); - return new BreakStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamebreak), - identifier, - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private DoWhileStatementTreeImpl convertDoWhile(DoStatement e) { - Statement body = e.getBody(); - Expression expression = e.getExpression(); - return new DoWhileStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamedo), - createStatement(body), - firstTokenAfter(body, TerminalToken.TokenNamewhile), - firstTokenBefore(expression, TerminalToken.TokenNameLPAREN), - convertExpression(expression), - firstTokenAfter(expression, TerminalToken.TokenNameRPAREN), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private AssertStatementTreeImpl convertAssert(AssertStatement e) { - Expression message = e.getMessage(); - AssertStatementTreeImpl t = new AssertStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameassert), - convertExpression(e.getExpression()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - if (message != null) { - t.complete(firstTokenBefore(message, TerminalToken.TokenNameCOLON), convertExpression(message)); - } - return t; - } - - private SwitchStatementTreeImpl convertSwitchStatement(SwitchStatement e) { - Expression expression = e.getExpression(); - return new SwitchStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameswitch), - firstTokenBefore(expression, TerminalToken.TokenNameLPAREN), - convertExpression(expression), - firstTokenAfter(expression, TerminalToken.TokenNameRPAREN), - firstTokenAfter(expression, TerminalToken.TokenNameLBRACE), - convertSwitchStatements(e.statements()), - lastTokenIn(e, TerminalToken.TokenNameRBRACE) - ); - } - - private List convertSwitchStatements(List list) { - List groups = new ArrayList<>(); - List caselabels = null; - StatementListTreeImpl body = null; - - for (Object o : list) { - if (o instanceof SwitchCase c) { - if (caselabels == null) { - caselabels = new ArrayList<>(); - body = StatementListTreeImpl.emptyList(); - } - - List expressions = ((List) c.expressions()).stream() - .map(Expression.class::cast) - .map(this::convertExpressionFromCase) - .toList(); - - caselabels.add(new CaseLabelTreeImpl( - firstTokenIn(c, c.isDefault() ? TerminalToken.TokenNamedefault : TerminalToken.TokenNamecase), - expressions, - lastTokenIn(c, /* TerminalToken.TokenNameCOLON or TerminalToken.TokenNameARROW */ ANY_TOKEN) - )); - } else { - if (caselabels != null) { - groups.add(new CaseGroupTreeImpl(caselabels, body)); - } - caselabels = null; - addStatementToList((Statement) o, Objects.requireNonNull(body)); - } - } - if (caselabels != null) { - groups.add(new CaseGroupTreeImpl(caselabels, body)); - } - return groups; - } - - private ExpressionTree convertExpressionFromCase(Expression e) { - if (e.getNodeType() == ASTNode.CASE_DEFAULT_EXPRESSION) { - return new DefaultPatternTreeImpl(firstTokenIn(e, TerminalToken.TokenNamedefault), e.resolveTypeBinding()); - } - if (e.getNodeType() == ASTNode.NULL_LITERAL) { - return new NullPatternTreeImpl((LiteralTreeImpl) convertExpression(e)); - } - if (e instanceof Pattern pattern) { - return convertPattern(pattern); - } - return convertExpression(e); - } - - private PatternTree convertPattern(Pattern p) { - switch (p.getNodeType()) { - case ASTNode.TYPE_PATTERN: - TypePattern typePattern = (TypePattern) p; - return new TypePatternTreeImpl(convertVariable(typePattern), typePattern.resolveTypeBinding()); - case ASTNode.RECORD_PATTERN: - RecordPattern recordPattern = (RecordPattern) p; - List nestedPatterns = recordPattern.patterns().stream() - .map(this::convertPattern) - .toList(); - - TypeTree patternType = convertType(recordPattern.getPatternType()); - var openParenToken = firstTokenIn(recordPattern, TerminalToken.TokenNameLPAREN); - var closeParenToken = lastTokenIn(recordPattern, TerminalToken.TokenNameRPAREN); - return new RecordPatternTreeImpl(patternType, openParenToken, nestedPatterns, closeParenToken); - case ASTNode.GUARDED_PATTERN: - GuardedPattern g = (GuardedPattern) p; - return new GuardedPatternTreeImpl( - convertPattern(g.getPattern()), - firstTokenBefore(g.getExpression(), TerminalToken.TokenNameRestrictedIdentifierWhen), - convertExpression(g.getExpression()), - g.resolveTypeBinding()); - case ASTNode.NULL_PATTERN: - // It is not clear how to reach this one, it seems to be possible only with badly constructed AST - // fall-through. Do nothing for now. - default: - // JEP-405 (not released as part of any JDK yet): ArrayPattern, RecordPattern - throw new IllegalStateException(ASTNode.nodeClassForType(p.getNodeType()).toString()); - } - } - - private SynchronizedStatementTreeImpl convertSynchronized(SynchronizedStatement e) { - Expression expression = e.getExpression(); - return new SynchronizedStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamesynchronized), - firstTokenBefore(expression, TerminalToken.TokenNameLPAREN), - convertExpression(expression), - firstTokenAfter(expression, TerminalToken.TokenNameRPAREN), - convertBlock(e.getBody()) - ); - } - - private ExpressionStatementTreeImpl convertExpressionStatement(ExpressionStatement e) { - return new ExpressionStatementTreeImpl( - convertExpression(e.getExpression()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private ContinueStatementTreeImpl convertContinue(ContinueStatement e) { - SimpleName label = e.getLabel(); - IdentifierTreeImpl i = label == null ? null : createSimpleName(label); - usageLabel(i); - return new ContinueStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamecontinue), - i, - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private LabeledStatementTreeImpl convertLabel(LabeledStatement e) { - IdentifierTreeImpl i = createSimpleName(e.getLabel()); - - JLabelSymbol symbol = new JLabelSymbol(i.name()); - labels.push(symbol); - - LabeledStatementTreeImpl t = new LabeledStatementTreeImpl( - i, - firstTokenAfter(e.getLabel(), TerminalToken.TokenNameCOLON), - createStatement(e.getBody()) - ); - - labels.pop(); - symbol.declaration = t; - t.labelSymbol = symbol; - return t; - } - - private ForEachStatementImpl convertForeach(EnhancedForStatement e) { - SingleVariableDeclaration parameter = e.getParameter(); - Expression expression = e.getExpression(); - return new ForEachStatementImpl( - firstTokenIn(e, TerminalToken.TokenNamefor), - firstTokenBefore(parameter, TerminalToken.TokenNameLPAREN), - convertVariable(parameter), - firstTokenAfter(parameter, TerminalToken.TokenNameCOLON), - convertExpression(expression), - firstTokenAfter(expression, TerminalToken.TokenNameRPAREN), - createStatement(e.getBody()) - ); - } - - private ThrowStatementTreeImpl convertThrow(ThrowStatement e) { - return new ThrowStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNamethrow), - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameSEMICOLON) - ); - } - - private TryStatementTreeImpl convertTry(TryStatement e) { - ResourceListTreeImpl resources = convertResources(e); - List catches = convertCatchClauses(e); - - Block f = e.getFinally(); - return new TryStatementTreeImpl( - firstTokenIn(e, TerminalToken.TokenNametry), - resources.isEmpty() ? null : firstTokenIn(e, TerminalToken.TokenNameLPAREN), - resources, - resources.isEmpty() ? null : firstTokenBefore(e.getBody(), TerminalToken.TokenNameRPAREN), - convertBlock(e.getBody()), - catches, - f == null ? null : firstTokenBefore(f, TerminalToken.TokenNamefinally), - f == null ? null : convertBlock(f) - ); - } - - private ResourceListTreeImpl convertResources(TryStatement e) { - List r = e.resources(); - ResourceListTreeImpl resources = ResourceListTreeImpl.emptyList(); - for (int i = 0; i < r.size(); i++) { - Expression o = (Expression) r.get(i); - if (ASTNode.VARIABLE_DECLARATION_EXPRESSION == o.getNodeType()) { - addVariableToList((VariableDeclarationExpression) o, resources); - } else { - resources.add(convertExpression(o)); - } - addSeparatorToList(e, o, resources.separators(), i < e.resources().size() - 1); - } - return resources; - } - - private void addSeparatorToList(TryStatement tryStatement, Expression resource, List separators, boolean isLast) { - if (isLast) { - separators.add(firstTokenAfter(resource, TerminalToken.TokenNameSEMICOLON)); - } else { - int tokenIndex = tokenManager.firstIndexBefore(tryStatement.getBody(), TerminalToken.TokenNameRPAREN); - while (true) { - Token token; - do { - tokenIndex--; - token = tokenManager.get(tokenIndex); - } while (isComment(token)); - - if (token.tokenType != TerminalToken.TokenNameSEMICOLON) { - break; - } - separators.add(createSyntaxToken(tokenIndex)); - } - } - } - - private List convertCatchClauses(TryStatement e) { - List catches = new ArrayList<>(); - for (Object o : e.catchClauses()) { - CatchClause c = (CatchClause) o; - catches.add(new CatchTreeImpl( - firstTokenIn(c, TerminalToken.TokenNamecatch), - firstTokenBefore(c.getException(), TerminalToken.TokenNameLPAREN), - convertVariable(c.getException()), - firstTokenAfter(c.getException(), TerminalToken.TokenNameRPAREN), - convertBlock(c.getBody()) - )); - } - return catches; - } - - private ExpressionStatementTreeImpl convertConstructorInvocation(ConstructorInvocation e) { - ArgumentListTreeImpl arguments = convertArguments( - e.arguments().isEmpty() ? lastTokenIn(e, TerminalToken.TokenNameLPAREN) : firstTokenBefore((ASTNode) e.arguments().get(0), TerminalToken.TokenNameLPAREN), - e.arguments(), - lastTokenIn(e, TerminalToken.TokenNameRPAREN) - ); - - IdentifierTreeImpl i = new IdentifierTreeImpl(e.arguments().isEmpty() - ? lastTokenIn(e, TerminalToken.TokenNamethis) - : firstTokenBefore((ASTNode) e.arguments().get(0), TerminalToken.TokenNamethis)); - MethodInvocationTreeImpl t = new MethodInvocationTreeImpl( - i, - convertTypeArguments(e.typeArguments()), - arguments - ); - t.methodBinding = e.resolveConstructorBinding(); - if (t.methodBinding != null) { - t.typeBinding = t.methodBinding.getDeclaringClass(); - t.methodBinding = excludeRecovery(t.methodBinding, arguments.size()); - } - i.binding = t.methodBinding; - usage(i.binding, i); - return new ExpressionStatementTreeImpl( - t, - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private ExpressionStatementTreeImpl convertSuperConstructorInvocation(SuperConstructorInvocation e) { - IdentifierTreeImpl i = new IdentifierTreeImpl(firstTokenIn(e, TerminalToken.TokenNamesuper)); - ExpressionTree methodSelect = i; - if (e.getExpression() != null) { - methodSelect = new MemberSelectExpressionTreeImpl( - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameDOT), - i - ); - } - - ArgumentListTreeImpl arguments = convertArguments( - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - e.arguments(), - lastTokenIn(e, TerminalToken.TokenNameRPAREN) - ); - - MethodInvocationTreeImpl t = new MethodInvocationTreeImpl( - methodSelect, - convertTypeArguments(e.typeArguments()), - arguments - ); - t.methodBinding = e.resolveConstructorBinding(); - if (t.methodBinding != null) { - t.typeBinding = t.methodBinding.getDeclaringClass(); - t.methodBinding = excludeRecovery(t.methodBinding, arguments.size()); - } - i.binding = t.methodBinding; - usage(i.binding, i); - return new ExpressionStatementTreeImpl( - t, - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private YieldStatementTreeImpl convertYield(YieldStatement e) { - InternalSyntaxToken yieldKeyword = null; - if (!e.isImplicit()) { - yieldKeyword = firstTokenIn(e, TerminalToken.TokenNameRestrictedIdentifierYield); - } - return new YieldStatementTreeImpl( - yieldKeyword, - convertExpression(e.getExpression()), - lastTokenIn(e, TerminalToken.TokenNameSEMICOLON) - ); - } - - private ExpressionTree convertExpression(Expression node) { - ExpressionTree t = createExpression(node); - ((AbstractTypedTree) t).typeBinding = node.resolveTypeBinding(); - return t; - } - - private ExpressionTree createExpression(Expression node) { - switch (node.getNodeType()) { - case ASTNode.SIMPLE_NAME: - return convertSimpleName((SimpleName) node); - case ASTNode.QUALIFIED_NAME: - return convertQualifiedName((QualifiedName) node); - case ASTNode.FIELD_ACCESS: - return convertFieldAccess((FieldAccess) node); - case ASTNode.SUPER_FIELD_ACCESS: - return convertFieldAccess((SuperFieldAccess) node); - case ASTNode.THIS_EXPRESSION: - return convertThisExpression((ThisExpression) node); - case ASTNode.ARRAY_ACCESS: - return convertArrayAccess((ArrayAccess) node); - case ASTNode.ARRAY_CREATION: - return convertArrayCreation((ArrayCreation) node); - case ASTNode.ARRAY_INITIALIZER: - return convertArrayInitializer((ArrayInitializer) node); - case ASTNode.ASSIGNMENT: - return convertAssignment((Assignment) node); - case ASTNode.CAST_EXPRESSION: - return convertTypeCastExpression((CastExpression) node); - case ASTNode.CLASS_INSTANCE_CREATION: - return convertClassInstanceCreation((ClassInstanceCreation) node); - case ASTNode.CONDITIONAL_EXPRESSION: - return convertConditionalExpression((ConditionalExpression) node); - case ASTNode.INFIX_EXPRESSION: - return convertInfixExpression((InfixExpression) node); - case ASTNode.METHOD_INVOCATION: - return convertMethodInvocation((MethodInvocation) node); - case ASTNode.SUPER_METHOD_INVOCATION: - return convertMethodInvocation((SuperMethodInvocation) node); - case ASTNode.PARENTHESIZED_EXPRESSION: - return convertParenthesizedExpression((ParenthesizedExpression) node); - case ASTNode.POSTFIX_EXPRESSION: - return convertPostfixExpression((PostfixExpression) node); - case ASTNode.PREFIX_EXPRESSION: - return convertPrefixExpression((PrefixExpression) node); - case ASTNode.INSTANCEOF_EXPRESSION: - return convertInstanceOf((InstanceofExpression) node); - case ASTNode.PATTERN_INSTANCEOF_EXPRESSION: - return convertInstanceOf((PatternInstanceofExpression) node); - case ASTNode.LAMBDA_EXPRESSION: - return convertLambdaExpression((LambdaExpression) node); - case ASTNode.CREATION_REFERENCE: - return convertMethodReference((CreationReference) node); - case ASTNode.EXPRESSION_METHOD_REFERENCE: - return convertMethodReference((ExpressionMethodReference) node); - case ASTNode.TYPE_METHOD_REFERENCE: - return convertMethodReference((TypeMethodReference) node); - case ASTNode.SUPER_METHOD_REFERENCE: - return convertMethodReference((SuperMethodReference) node); - case ASTNode.SWITCH_EXPRESSION: - return convertSwitchExpression((SwitchExpression) node); - case ASTNode.TYPE_LITERAL: - return convertTypeLiteral((TypeLiteral) node); - case ASTNode.NULL_LITERAL: - return convertLiteral((NullLiteral) node); - case ASTNode.NUMBER_LITERAL: - return convertLiteral((NumberLiteral) node); - case ASTNode.CHARACTER_LITERAL: - return convertLiteral((CharacterLiteral) node); - case ASTNode.BOOLEAN_LITERAL: - return convertLiteral((BooleanLiteral) node); - case ASTNode.STRING_LITERAL: - return convertLiteral((StringLiteral) node); - case ASTNode.TEXT_BLOCK: - return convertTextBlock((TextBlock) node); - case ASTNode.NORMAL_ANNOTATION, - ASTNode.MARKER_ANNOTATION, - ASTNode.SINGLE_MEMBER_ANNOTATION: - return convertAnnotation((Annotation) node); - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); - } - } - - private IdentifierTreeImpl convertSimpleName(SimpleName e) { - IdentifierTreeImpl t = createSimpleName(e); - usage(t.binding, t); - return t; - } - - private MemberSelectExpressionTreeImpl convertQualifiedName(QualifiedName e) { - IdentifierTreeImpl rhs = createSimpleName(e.getName()); - usage(rhs.binding, rhs); - return new MemberSelectExpressionTreeImpl( - convertExpression(e.getQualifier()), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - rhs - ); - } - - private MemberSelectExpressionTreeImpl convertFieldAccess(FieldAccess e) { - IdentifierTreeImpl rhs = createSimpleName(e.getName()); - usage(rhs.binding, rhs); - return new MemberSelectExpressionTreeImpl( - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameDOT), - rhs - ); - } - - private MemberSelectExpressionTreeImpl convertFieldAccess(SuperFieldAccess e) { - IdentifierTreeImpl rhs = createSimpleName(e.getName()); - usage(rhs.binding, rhs); - if (e.getQualifier() == null) { - // super.name - return new MemberSelectExpressionTreeImpl( - unqualifiedKeywordSuper(e), - firstTokenIn(e, TerminalToken.TokenNameDOT), - rhs - ); - } - // qualifier.super.name - AbstractTypedTree qualifier = (AbstractTypedTree) convertExpression(e.getQualifier()); - KeywordSuper keywordSuper = new KeywordSuper(firstTokenAfter(e.getQualifier(), TerminalToken.TokenNamesuper), null); - MemberSelectExpressionTreeImpl qualifiedSuper = new MemberSelectExpressionTreeImpl( - (ExpressionTree) qualifier, - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - keywordSuper - ); - if (qualifier.typeBinding != null) { - keywordSuper.typeBinding = qualifier.typeBinding; - qualifiedSuper.typeBinding = keywordSuper.typeBinding.getSuperclass(); - } - return new MemberSelectExpressionTreeImpl( - qualifiedSuper, - firstTokenBefore(e.getName(), TerminalToken.TokenNameDOT), - rhs - ); - } - - private ExpressionTree convertThisExpression(ThisExpression e) { - if (e.getQualifier() == null) { - return new KeywordThis(firstTokenIn(e, TerminalToken.TokenNamethis), null); - } - KeywordThis keywordThis = new KeywordThis( - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNamethis), - e.resolveTypeBinding() - ); - return new MemberSelectExpressionTreeImpl( - convertExpression(e.getQualifier()), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - keywordThis - ); - } - - private MemberSelectExpressionTreeImpl convertTypeLiteral(TypeLiteral e) { - return new MemberSelectExpressionTreeImpl( - (ExpressionTree) convertType(e.getType()), - lastTokenIn(e, TerminalToken.TokenNameDOT), - new IdentifierTreeImpl( - lastTokenIn(e, TerminalToken.TokenNameclass) - ) - ); - } - - private ArrayAccessExpressionTreeImpl convertArrayAccess(ArrayAccess e) { - Expression index = e.getIndex(); - return new ArrayAccessExpressionTreeImpl( - convertExpression(e.getArray()), - new ArrayDimensionTreeImpl( - firstTokenBefore(index, TerminalToken.TokenNameLBRACKET), - convertExpression(index), - firstTokenAfter(index, TerminalToken.TokenNameRBRACKET) - ) - ); - } - - private NewArrayTreeImpl convertArrayCreation(ArrayCreation e) { - List dimensions = ((List) e.dimensions()).stream() - .map(Expression.class::cast) - .map(dimension -> new ArrayDimensionTreeImpl( - firstTokenBefore(dimension, TerminalToken.TokenNameLBRACKET), - convertExpression(dimension), - firstTokenAfter(dimension, TerminalToken.TokenNameRBRACKET) - )) - .collect(Collectors.toCollection(ArrayList::new)); - InitializerListTreeImpl initializers = InitializerListTreeImpl.emptyList(); - if (e.getInitializer() != null) { - assert dimensions.isEmpty(); - - TypeTree type = convertType(e.getType()); - while (type.is(Tree.Kind.ARRAY_TYPE)) { - ArrayTypeTree arrayType = (ArrayTypeTree) type; - ArrayDimensionTreeImpl dimension = new ArrayDimensionTreeImpl( - arrayType.openBracketToken(), - null, - arrayType.closeBracketToken() - ).completeAnnotations(arrayType.annotations()); - dimensions.add(/* TODO suboptimal */ 0, dimension); - type = arrayType.type(); - } - - return ((NewArrayTreeImpl) convertExpression(e.getInitializer())) - .completeWithNewKeyword(firstTokenIn(e, TerminalToken.TokenNamenew)) - .complete(type) - .completeDimensions(dimensions); - } - TypeTree type = convertType(e.getType()); - int index = dimensions.size() - 1; - while (type.is(Tree.Kind.ARRAY_TYPE)) { - if (!type.annotations().isEmpty()) { - ((ArrayDimensionTreeImpl) dimensions.get(index)) - .completeAnnotations(type.annotations()); - } - index--; - type = ((ArrayTypeTree) type).type(); - } - - return new NewArrayTreeImpl(dimensions, initializers) - .complete(type) - .completeWithNewKeyword(firstTokenIn(e, TerminalToken.TokenNamenew)); - } - - private NewArrayTreeImpl convertArrayInitializer(ArrayInitializer e) { - InitializerListTreeImpl initializers = InitializerListTreeImpl.emptyList(); - for (int i = 0; i < e.expressions().size(); i++) { - Expression o = (Expression) e.expressions().get(i); - initializers.add(convertExpression(o)); - final int commaTokenIndex = firstTokenIndexAfter(o); - if (tokenManager.get(commaTokenIndex).tokenType == TerminalToken.TokenNameCOMMA) { - initializers.separators().add(firstTokenAfter(o, TerminalToken.TokenNameCOMMA)); - } - } - return new NewArrayTreeImpl(Collections.emptyList(),initializers) - .completeWithCurlyBraces( - firstTokenIn(e, TerminalToken.TokenNameLBRACE), - lastTokenIn(e, TerminalToken.TokenNameRBRACE) - ); - } - - private AssignmentExpressionTreeImpl convertAssignment(Assignment e) { - Op op = operators.get(e.getOperator()); - return new AssignmentExpressionTreeImpl( - op.kind, - convertExpression(e.getLeftHandSide()), - firstTokenAfter(e.getLeftHandSide(), op.tokenType), - convertExpression(e.getRightHandSide()) - ); - } - - private TypeCastExpressionTreeImpl convertTypeCastExpression(CastExpression e) { - Type type = e.getType(); - if (type.getNodeType() == ASTNode.INTERSECTION_TYPE) { - List intersectionTypes = ((IntersectionType) type).types(); - QualifiedIdentifierListTreeImpl bounds = QualifiedIdentifierListTreeImpl.emptyList(); - for (int i = 1; i < intersectionTypes.size(); i++) { - Type o = (Type) intersectionTypes.get(i); - bounds.add(convertType(o)); - if (i < intersectionTypes.size() - 1) { - bounds.separators().add(firstTokenAfter(o, TerminalToken.TokenNameAND)); - } - } - return new TypeCastExpressionTreeImpl( - firstTokenBefore(type, TerminalToken.TokenNameLPAREN), - convertType((Type) intersectionTypes.get(0)), - firstTokenAfter((Type) intersectionTypes.get(0), TerminalToken.TokenNameAND), - bounds, - firstTokenAfter(type, TerminalToken.TokenNameRPAREN), - convertExpression(e.getExpression()) - ); - } - return new TypeCastExpressionTreeImpl( - firstTokenBefore(type, TerminalToken.TokenNameLPAREN), - convertType(type), - firstTokenAfter(type, TerminalToken.TokenNameRPAREN), - convertExpression(e.getExpression()) - ); - } - - private NewClassTreeImpl convertClassInstanceCreation(ClassInstanceCreation e) { - ArgumentListTreeImpl arguments = convertArguments( - firstTokenAfter(e.getType(), TerminalToken.TokenNameLPAREN), - e.arguments(), - firstTokenAfter(e.arguments().isEmpty() ? e.getType() : (ASTNode) e.arguments().get(e.arguments().size() - 1), TerminalToken.TokenNameRPAREN) - ); - - ClassTreeImpl classBody = null; - if (e.getAnonymousClassDeclaration() != null) { - List members = new ArrayList<>(); - for (Object o : e.getAnonymousClassDeclaration().bodyDeclarations()) { - processBodyDeclaration((BodyDeclaration) o, members); - } - classBody = new ClassTreeImpl( - Tree.Kind.CLASS, - firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), - members, - lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) - ); - classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); - declaration(classBody.typeBinding, classBody); - } - - NewClassTreeImpl t = new NewClassTreeImpl( - convertType(e.getType()), - arguments, - classBody - ).completeWithNewKeyword( - e.getExpression() == null ? firstTokenIn(e, TerminalToken.TokenNamenew) : firstTokenAfter(e.getExpression(), TerminalToken.TokenNamenew) - ).completeWithTypeArguments( - convertTypeArguments(e.typeArguments()) - ); - if (e.getExpression() != null) { - t.completeWithEnclosingExpression(convertExpression(e.getExpression())); - t.completeWithDotToken(firstTokenAfter(e.getExpression(), TerminalToken.TokenNameDOT)); - } - - IdentifierTreeImpl i = (IdentifierTreeImpl) t.getConstructorIdentifier(); - int nbArguments = arguments.size(); - if (e.getAnonymousClassDeclaration() == null) { - i.binding = excludeRecovery(e.resolveConstructorBinding(), nbArguments); - } else { - i.binding = excludeRecovery(findConstructorForAnonymousClass(e.getAST(), i.typeBinding, e.resolveConstructorBinding()), nbArguments); - } - usage(i.binding, i); - - return t; - } - - private ConditionalExpressionTreeImpl convertConditionalExpression(ConditionalExpression e) { - return new ConditionalExpressionTreeImpl( - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameQUESTION), - convertExpression(e.getThenExpression()), - firstTokenAfter(e.getThenExpression(), TerminalToken.TokenNameCOLON), - convertExpression(e.getElseExpression()) - ); - } - - private BinaryExpressionTreeImpl convertInfixExpression(InfixExpression e) { - Op op = operators.get(e.getOperator()); - BinaryExpressionTreeImpl t = new BinaryExpressionTreeImpl( - op.kind, - convertExpression(e.getLeftOperand()), - firstTokenAfter(e.getLeftOperand(), op.tokenType), - convertExpression(e.getRightOperand()) - ); - for (Object o : e.extendedOperands()) { - Expression e2 = (Expression) o; - t.typeBinding = e.resolveTypeBinding(); - t = new BinaryExpressionTreeImpl( - op.kind, - t, - firstTokenBefore(e2, op.tokenType), - convertExpression(e2) - ); - } - return t; - } - - private MethodInvocationTreeImpl convertMethodInvocation(MethodInvocation e) { - ArgumentListTreeImpl arguments = convertArguments( - firstTokenAfter(e.getName(), TerminalToken.TokenNameLPAREN), - e.arguments(), - lastTokenIn(e, TerminalToken.TokenNameRPAREN) - ); - - IdentifierTreeImpl rhs = createSimpleName(e.getName()); - ExpressionTree memberSelect; - if (e.getExpression() == null) { - memberSelect = rhs; - } else { - memberSelect = new MemberSelectExpressionTreeImpl( - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameDOT), - rhs - ); - } - MethodInvocationTreeImpl t = new MethodInvocationTreeImpl( - memberSelect, - convertTypeArguments(e.typeArguments()), - arguments - ); - t.methodBinding = excludeRecovery(e.resolveMethodBinding(), arguments.size()); - rhs.binding = t.methodBinding; - usage(rhs.binding, rhs); - return t; - } - - private MethodInvocationTreeImpl convertMethodInvocation(SuperMethodInvocation e) { - ArgumentListTreeImpl arguments = convertArguments( - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - e.arguments(), - lastTokenIn(e, TerminalToken.TokenNameRPAREN) - ); - - IdentifierTreeImpl rhs = createSimpleName(e.getName()); - - ExpressionTree outermostSelect; - if (e.getQualifier() == null) { - outermostSelect = new MemberSelectExpressionTreeImpl( - unqualifiedKeywordSuper(e), - firstTokenIn(e, TerminalToken.TokenNameDOT), - rhs - ); - } else { - final int firstDotTokenIndex = tokenManager.firstIndexAfter(e.getQualifier(), TerminalToken.TokenNameDOT); - AbstractTypedTree qualifier = (AbstractTypedTree) convertExpression(e.getQualifier()); - KeywordSuper keywordSuper = new KeywordSuper(firstTokenAfter(e.getQualifier(), TerminalToken.TokenNamesuper), null); - MemberSelectExpressionTreeImpl qualifiedSuper = new MemberSelectExpressionTreeImpl( - (ExpressionTree) qualifier, - createSyntaxToken(firstDotTokenIndex), - keywordSuper - ); - if (qualifier.typeBinding != null) { - keywordSuper.typeBinding = qualifier.typeBinding; - qualifiedSuper.typeBinding = keywordSuper.typeBinding.getSuperclass(); - } - outermostSelect = new MemberSelectExpressionTreeImpl( - qualifiedSuper, - createSyntaxToken(nextTokenIndex(firstDotTokenIndex, TerminalToken.TokenNameDOT)), - rhs - ); - } - - MethodInvocationTreeImpl t = new MethodInvocationTreeImpl( - outermostSelect, - null, - arguments - ); - t.methodBinding = excludeRecovery(e.resolveMethodBinding(), arguments.size()); - rhs.binding = t.methodBinding; - usage(rhs.binding, rhs); - return t; - } - - private ParenthesizedTreeImpl convertParenthesizedExpression(ParenthesizedExpression e) { - return new ParenthesizedTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameRPAREN) - ); - } - - private InternalPostfixUnaryExpression convertPostfixExpression(PostfixExpression e) { - Op op = operators.get(e.getOperator()); - return new InternalPostfixUnaryExpression( - op.kind, - convertExpression(e.getOperand()), - firstTokenAfter(e.getOperand(), op.tokenType) - ); - } - - private InternalPrefixUnaryExpression convertPrefixExpression(PrefixExpression e) { - Op op = operators.get(e.getOperator()); - return new InternalPrefixUnaryExpression( - op.kind, - firstTokenIn(e, op.tokenType), - convertExpression(e.getOperand()) - ); - } - - private InstanceOfTreeImpl convertInstanceOf(InstanceofExpression e) { - Expression leftOperand = e.getLeftOperand(); - InternalSyntaxToken instanceofToken = firstTokenAfter(leftOperand, TerminalToken.TokenNameinstanceof); - return new InstanceOfTreeImpl(convertExpression(leftOperand), instanceofToken, convertType(e.getRightOperand())); - } - - private InstanceOfTreeImpl convertInstanceOf(PatternInstanceofExpression e) { - Expression leftOperand = e.getLeftOperand(); - InternalSyntaxToken instanceofToken = firstTokenAfter(leftOperand, TerminalToken.TokenNameinstanceof); - return new InstanceOfTreeImpl(convertExpression(leftOperand), instanceofToken, convertPattern(e.getPattern())); - } - - private LambdaExpressionTreeImpl convertLambdaExpression(LambdaExpression e) { - List parameters = new ArrayList<>(); - for (int i = 0; i < e.parameters().size(); i++) { - VariableDeclaration o = (VariableDeclaration) e.parameters().get(i); - VariableTreeImpl t; - if (o.getNodeType() == ASTNode.VARIABLE_DECLARATION_FRAGMENT) { - t = new VariableTreeImpl(createSimpleName(o.getName())); - IVariableBinding variableBinding = o.resolveBinding(); - if (variableBinding != null) { - t.variableBinding = variableBinding; - ((InferedTypeTree) t.type()).typeBinding = variableBinding.getType(); - declaration(t.variableBinding, t); - } - } else { - t = convertVariable((SingleVariableDeclaration) o); - } - parameters.add(t); - if (i < e.parameters().size() - 1) { - t.setEndToken(firstTokenAfter(o, TerminalToken.TokenNameCOMMA)); - } - } - ASTNode body = e.getBody(); - var tree = new LambdaExpressionTreeImpl( - e.hasParentheses() ? firstTokenIn(e, TerminalToken.TokenNameLPAREN) : null, - parameters, - e.hasParentheses() ? firstTokenBefore(body, TerminalToken.TokenNameRPAREN) : null, - firstTokenBefore(body, TerminalToken.TokenNameARROW), - body.getNodeType() == ASTNode.BLOCK ? convertBlock((Block) body) : convertExpression((Expression) body) - ); - tree.methodBinding = e.resolveMethodBinding(); - return tree; - } - - private MethodReferenceTreeImpl convertMethodReference(CreationReference e) { - MethodReferenceTreeImpl t = new MethodReferenceTreeImpl( - convertType(e.getType()), - lastTokenIn(e, TerminalToken.TokenNameCOLON_COLON) - ); - IdentifierTreeImpl i = new IdentifierTreeImpl(lastTokenIn(e, TerminalToken.TokenNamenew)); - i.binding = e.resolveMethodBinding(); - usage(i.binding, i); - t.complete(convertTypeArguments(e.typeArguments()), i); - return t; - } - - private MethodReferenceTreeImpl convertMethodReference(ExpressionMethodReference e) { - MethodReferenceTreeImpl t = new MethodReferenceTreeImpl( - convertExpression(e.getExpression()), - firstTokenAfter(e.getExpression(), TerminalToken.TokenNameCOLON_COLON) - ); - IdentifierTreeImpl i = createSimpleName(e.getName()); - usage(i.binding, i); - t.complete(convertTypeArguments(e.typeArguments()), i); - return t; - } - - private MethodReferenceTreeImpl convertMethodReference(TypeMethodReference e) { - MethodReferenceTreeImpl t = new MethodReferenceTreeImpl( - convertType(e.getType()), - firstTokenAfter(e.getType(), TerminalToken.TokenNameCOLON_COLON) - ); - IdentifierTreeImpl i = createSimpleName(e.getName()); - usage(i.binding, i); - t.complete(convertTypeArguments(e.typeArguments()), i); - return t; - } - - private MethodReferenceTreeImpl convertMethodReference(SuperMethodReference e) { - MethodReferenceTreeImpl t; - if (e.getQualifier() != null) { - t = new MethodReferenceTreeImpl( - new MemberSelectExpressionTreeImpl( - convertExpression(e.getQualifier()), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - unqualifiedKeywordSuper(e) - ), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameCOLON_COLON) - ); - } else { - t = new MethodReferenceTreeImpl( - unqualifiedKeywordSuper(e), - firstTokenIn(e, TerminalToken.TokenNameCOLON_COLON) - ); - } - IdentifierTreeImpl i = createSimpleName(e.getName()); - usage(i.binding, i); - t.complete(convertTypeArguments(e.typeArguments()), i); - return t; - } - - private SwitchExpressionTreeImpl convertSwitchExpression(SwitchExpression e) { - Expression expr = e.getExpression(); - return new SwitchExpressionTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameswitch), - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - convertExpression(expr), - firstTokenAfter(expr, TerminalToken.TokenNameRPAREN), - firstTokenAfter(expr, TerminalToken.TokenNameLBRACE), - convertSwitchStatements(e.statements()), - lastTokenIn(e, TerminalToken.TokenNameRBRACE) - ); - } - - private LiteralTreeImpl convertLiteral(NullLiteral e) { - return new LiteralTreeImpl(Tree.Kind.NULL_LITERAL, firstTokenIn(e, TerminalToken.TokenNamenull)); - } - - private ExpressionTree convertLiteral(NumberLiteral e) { - int tokenIndex = tokenManager.findIndex(e.getStartPosition(), ANY_TOKEN, true); - TerminalToken tokenType = tokenManager.get(tokenIndex).tokenType; - boolean unaryMinus = tokenType == TerminalToken.TokenNameMINUS; - if (unaryMinus) { - tokenIndex++; - tokenType = tokenManager.get(tokenIndex).tokenType; - } - ExpressionTree result; - switch (tokenType) { - case TokenNameIntegerLiteral: - result = new LiteralTreeImpl(Tree.Kind.INT_LITERAL, createSyntaxToken(tokenIndex)); - break; - case TokenNameLongLiteral: - result = new LiteralTreeImpl(Tree.Kind.LONG_LITERAL, createSyntaxToken(tokenIndex)); - break; - case TokenNameFloatingPointLiteral: - result = new LiteralTreeImpl(Tree.Kind.FLOAT_LITERAL, createSyntaxToken(tokenIndex)); - break; - case TokenNameDoubleLiteral: - result = new LiteralTreeImpl(Tree.Kind.DOUBLE_LITERAL, createSyntaxToken(tokenIndex)); - break; - default: - throw new IllegalStateException(); - } - ((LiteralTreeImpl) result).typeBinding = e.resolveTypeBinding(); - if (unaryMinus) { - return new InternalPrefixUnaryExpression(Tree.Kind.UNARY_MINUS, createSyntaxToken(tokenIndex - 1), result); - } - return result; - } - - private LiteralTreeImpl convertLiteral(CharacterLiteral e) { - return new LiteralTreeImpl(Tree.Kind.CHAR_LITERAL, firstTokenIn(e, TerminalToken.TokenNameCharacterLiteral)); - } - - private LiteralTreeImpl convertLiteral(BooleanLiteral e) { - InternalSyntaxToken value = firstTokenIn(e, e.booleanValue() ? TerminalToken.TokenNametrue : TerminalToken.TokenNamefalse); - return new LiteralTreeImpl(Tree.Kind.BOOLEAN_LITERAL, value); - } - - private LiteralTreeImpl convertLiteral(StringLiteral e) { - return new LiteralTreeImpl(Tree.Kind.STRING_LITERAL, firstTokenIn(e, TerminalToken.TokenNameStringLiteral)); - } - - private LiteralTreeImpl convertTextBlock(TextBlock e) { - return new LiteralTreeImpl(Tree.Kind.TEXT_BLOCK, firstTokenIn(e, TerminalToken.TokenNameTextBlock)); - } - - private AnnotationTreeImpl convertAnnotation(Annotation e) { - ArgumentListTreeImpl arguments = ArgumentListTreeImpl.emptyList(); - if (e.getNodeType() == ASTNode.SINGLE_MEMBER_ANNOTATION) { - arguments.add(convertExpression(((SingleMemberAnnotation) e).getValue())); - arguments.complete( - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - lastTokenIn(e, TerminalToken.TokenNameRPAREN) - ); - } else if (e.getNodeType() == ASTNode.NORMAL_ANNOTATION) { - for (int i = 0; i < ((NormalAnnotation) e).values().size(); i++) { - MemberValuePair o = (MemberValuePair) ((NormalAnnotation) e).values().get(i); - arguments.add(new AssignmentExpressionTreeImpl( - Tree.Kind.ASSIGNMENT, - createSimpleName(o.getName()), - firstTokenAfter(o.getName(), TerminalToken.TokenNameEQUAL), - convertExpression(o.getValue()) - )); - if (i < ((NormalAnnotation) e).values().size() - 1) { - arguments.separators().add(firstTokenAfter(o, TerminalToken.TokenNameCOMMA)); - } - } - arguments.complete( - firstTokenIn(e, TerminalToken.TokenNameLPAREN), - lastTokenIn(e, TerminalToken.TokenNameRPAREN) - ); - } - return new AnnotationTreeImpl( - firstTokenIn(e, TerminalToken.TokenNameAT), - (TypeTree) convertExpression(e.getTypeName()), - arguments - ); - } - - private KeywordSuper unqualifiedKeywordSuper(ASTNode node) { - InternalSyntaxToken token = firstTokenIn(node, TerminalToken.TokenNamesuper); - do { - if (node instanceof AbstractTypeDeclaration abstractTypeDeclaration) { - return new KeywordSuper(token, abstractTypeDeclaration.resolveBinding()); - } - if (node instanceof AnonymousClassDeclaration anonymousClassDeclaration) { - return new KeywordSuper(token, anonymousClassDeclaration.resolveBinding()); - } - node = node.getParent(); - } while (true); - } - - private TypeTree convertType(Type node) { - switch (node.getNodeType()) { - case ASTNode.PRIMITIVE_TYPE: - return convertPrimitiveType((PrimitiveType) node); - case ASTNode.SIMPLE_TYPE: - return convertSimpleType((SimpleType) node); - case ASTNode.UNION_TYPE: - return convertUnionType((UnionType) node); - case ASTNode.ARRAY_TYPE: - return convertArrayType((ArrayType) node); - case ASTNode.PARAMETERIZED_TYPE: - return convertParameterizedType((ParameterizedType) node); - case ASTNode.QUALIFIED_TYPE: - return convertQualifiedType((QualifiedType) node); - case ASTNode.NAME_QUALIFIED_TYPE: - return convertNamedQualifiedType((NameQualifiedType) node); - case ASTNode.WILDCARD_TYPE: - return convertWildcardType((WildcardType) node); - default: - throw new IllegalStateException(ASTNode.nodeClassForType(node.getNodeType()).toString()); - } - } - - private JavaTree.PrimitiveTypeTreeImpl convertPrimitiveType(PrimitiveType e) { - final JavaTree.PrimitiveTypeTreeImpl t; - switch (e.getPrimitiveTypeCode().toString()) { - case "byte": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNamebyte)); - break; - case "short": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNameshort)); - break; - case "char": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNamechar)); - break; - case "int": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNameint)); - break; - case "long": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNamelong)); - break; - case "float": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNamefloat)); - break; - case "double": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNamedouble)); - break; - case "boolean": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNameboolean)); - break; - case "void": - t = new JavaTree.PrimitiveTypeTreeImpl(lastTokenIn(e, TerminalToken.TokenNamevoid)); - break; - default: - throw new IllegalStateException(e.getPrimitiveTypeCode().toString()); - } - t.complete(convertAnnotations(e.annotations())); - t.typeBinding = e.resolveBinding(); - return t; - } - - private JavaTree.AnnotatedTypeTree convertSimpleType(SimpleType e) { - List annotations = ((List) e.annotations()).stream() - .map(Annotation.class::cast) - .map(annotation -> (AnnotationTree) convertExpression(annotation)) - .toList(); - JavaTree.AnnotatedTypeTree t = e.isVar() ? convertVarType(e) : (JavaTree.AnnotatedTypeTree) convertExpression(e.getName()); - t.complete(annotations); - // typeBinding is assigned by convertVarType or convertExpression - return t; - } - - private JavaTree.UnionTypeTreeImpl convertUnionType(UnionType e) { - QualifiedIdentifierListTreeImpl alternatives = QualifiedIdentifierListTreeImpl.emptyList(); - for (int i = 0; i < e.types().size(); i++) { - Type o = (Type) e.types().get(i); - alternatives.add(convertType(o)); - if (i < e.types().size() - 1) { - alternatives.separators().add(firstTokenAfter(o, TerminalToken.TokenNameOR)); - } - } - JavaTree.UnionTypeTreeImpl t = new JavaTree.UnionTypeTreeImpl(alternatives); - t.typeBinding = e.resolveBinding(); - return t; - } - - private TypeTree convertArrayType(ArrayType e) { - @Nullable ITypeBinding elementTypeBinding = e.getElementType().resolveBinding(); - TypeTree t = convertType(e.getElementType()); - int tokenIndex = tokenManager.firstIndexAfter(e.getElementType(), TerminalToken.TokenNameLBRACKET); - for (int i = 0; i < e.dimensions().size(); i++) { - if (i > 0) { - tokenIndex = nextTokenIndex(tokenIndex, TerminalToken.TokenNameLBRACKET); - } - t = new JavaTree.ArrayTypeTreeImpl( - t, - (List) convertAnnotations(((Dimension) e.dimensions().get(i)).annotations()), - createSyntaxToken(tokenIndex), - createSyntaxToken(nextTokenIndex(tokenIndex, TerminalToken.TokenNameRBRACKET)) - ); - if (elementTypeBinding != null) { - ((JavaTree.ArrayTypeTreeImpl) t).typeBinding = elementTypeBinding.createArrayType(i + 1); - } - } - return t; - } - - private JavaTree.ParameterizedTypeTreeImpl convertParameterizedType(ParameterizedType e) { - LineColumnConverter.Pos pos = lineColumnConverter.toPos(e.getStartPosition() + e.getLength() - 1); - JavaTree.ParameterizedTypeTreeImpl t = new JavaTree.ParameterizedTypeTreeImpl( - convertType(e.getType()), - convertTypeArguments( - firstTokenAfter(e.getType(), TerminalToken.TokenNameLESS), - e.typeArguments(), - new InternalSyntaxToken( - pos.line(), - pos.columnOffset(), - ">", - /* TODO */ Collections.emptyList(), - false - ) - ) - ); - t.typeBinding = e.resolveBinding(); - return t; - } - - private MemberSelectExpressionTreeImpl convertQualifiedType(QualifiedType e) { - MemberSelectExpressionTreeImpl t = new MemberSelectExpressionTreeImpl( - (ExpressionTree) convertType(e.getQualifier()), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - createSimpleName(e.getName()) - ); - ((IdentifierTreeImpl) t.identifier()).complete(convertAnnotations(e.annotations())); - t.typeBinding = e.resolveBinding(); - return t; - } - - private MemberSelectExpressionTreeImpl convertNamedQualifiedType(NameQualifiedType e) { - MemberSelectExpressionTreeImpl t = new MemberSelectExpressionTreeImpl( - convertExpression(e.getQualifier()), - firstTokenAfter(e.getQualifier(), TerminalToken.TokenNameDOT), - createSimpleName(e.getName()) - ); - ((IdentifierTreeImpl) t.identifier()).complete(convertAnnotations(e.annotations())); - t.typeBinding = e.resolveBinding(); - return t; - } - - private JavaTree.WildcardTreeImpl convertWildcardType(WildcardType e) { - final InternalSyntaxToken questionToken = e.annotations().isEmpty() - ? firstTokenIn(e, TerminalToken.TokenNameQUESTION) - : firstTokenAfter((ASTNode) e.annotations().get(e.annotations().size() - 1), TerminalToken.TokenNameQUESTION); - JavaTree.WildcardTreeImpl t; - Type bound = e.getBound(); - if (bound == null) { - t = new JavaTree.WildcardTreeImpl(questionToken); - } else { - boolean isUpperBound = e.isUpperBound(); - t = new JavaTree.WildcardTreeImpl( - isUpperBound ? Tree.Kind.EXTENDS_WILDCARD : Tree.Kind.SUPER_WILDCARD, - firstTokenBefore(bound, isUpperBound ? TerminalToken.TokenNameextends : TerminalToken.TokenNamesuper), - convertType(bound) - ).complete(questionToken); - } - t.complete(convertAnnotations(e.annotations())); - t.typeBinding = e.resolveBinding(); - return t; - } - - @Nullable - private static IMethodBinding excludeRecovery(@Nullable IMethodBinding methodBinding, int arguments) { - if (methodBinding == null) { - return null; - } - if (methodBinding.isVarargs()) { - if (arguments + 1 < methodBinding.getParameterTypes().length) { - return null; - } - } else { - if (arguments != methodBinding.getParameterTypes().length) { - return null; - } - } - return methodBinding; - } - - @Nullable - private static IMethodBinding findConstructorForAnonymousClass(AST ast, @Nullable ITypeBinding typeBinding, @Nullable IMethodBinding methodBinding) { - if (typeBinding == null || methodBinding == null) { - return null; - } - if (typeBinding.isInterface()) { - typeBinding = ast.resolveWellKnownType("java.lang.Object"); - } - return Stream.of(typeBinding.getDeclaredMethods()) - .filter(methodBinding::isSubsignature) - .findFirst() - .orElse(null); - } - - private List convertAnnotations(List e) { - return e.stream() - .map(o -> (AnnotationTree) convertExpression((Annotation) o)) - .toList(); - } - - private ModifiersTreeImpl convertModifiers(List source) { - return new ModifiersTreeImpl(source.stream() - .map(o -> convertModifier((IExtendedModifier) o)) - .toList()); - } - - private ModifierTree convertModifier(IExtendedModifier node) { - switch (((ASTNode) node).getNodeType()) { - case ASTNode.NORMAL_ANNOTATION, - ASTNode.MARKER_ANNOTATION, - ASTNode.SINGLE_MEMBER_ANNOTATION: - return (AnnotationTree) convertExpression((Expression) node); - case ASTNode.MODIFIER: - return convertModifier((org.eclipse.jdt.core.dom.Modifier) node); - default: - throw new IllegalStateException(ASTNode.nodeClassForType(((ASTNode) node).getNodeType()).toString()); - } - } - - private ModifierTree convertModifier(org.eclipse.jdt.core.dom.Modifier node) { - switch (node.getKeyword().toString()) { - case "public": - return new ModifierKeywordTreeImpl(Modifier.PUBLIC, firstTokenIn(node, TerminalToken.TokenNamepublic)); - case "protected": - return new ModifierKeywordTreeImpl(Modifier.PROTECTED, firstTokenIn(node, TerminalToken.TokenNameprotected)); - case "private": - return new ModifierKeywordTreeImpl(Modifier.PRIVATE, firstTokenIn(node, TerminalToken.TokenNameprivate)); - case "static": - return new ModifierKeywordTreeImpl(Modifier.STATIC, firstTokenIn(node, TerminalToken.TokenNamestatic)); - case "abstract": - return new ModifierKeywordTreeImpl(Modifier.ABSTRACT, firstTokenIn(node, TerminalToken.TokenNameabstract)); - case "final": - return new ModifierKeywordTreeImpl(Modifier.FINAL, firstTokenIn(node, TerminalToken.TokenNamefinal)); - case "native": - return new ModifierKeywordTreeImpl(Modifier.NATIVE, firstTokenIn(node, TerminalToken.TokenNamenative)); - case "synchronized": - return new ModifierKeywordTreeImpl(Modifier.SYNCHRONIZED, firstTokenIn(node, TerminalToken.TokenNamesynchronized)); - case "transient": - return new ModifierKeywordTreeImpl(Modifier.TRANSIENT, firstTokenIn(node, TerminalToken.TokenNametransient)); - case "volatile": - return new ModifierKeywordTreeImpl(Modifier.VOLATILE, firstTokenIn(node, TerminalToken.TokenNamevolatile)); - case "strictfp": - return new ModifierKeywordTreeImpl(Modifier.STRICTFP, firstTokenIn(node, TerminalToken.TokenNamestrictfp)); - case "default": - return new ModifierKeywordTreeImpl(Modifier.DEFAULT, firstTokenIn(node, TerminalToken.TokenNamedefault)); - case "sealed": - return new ModifierKeywordTreeImpl(Modifier.SEALED, firstTokenIn(node, TerminalToken.TokenNameRestrictedIdentifiersealed)); - case "non-sealed": { - return new ModifierKeywordTreeImpl(Modifier.NON_SEALED, firstTokenIn(node, TerminalToken.TokenNamenon_sealed)); - } - default: - throw new IllegalStateException(node.getKeyword().toString()); - } - } - - private static final TerminalToken ANY_TOKEN = TerminalToken.TokenNameInvalid; - - private static final Map operators = new HashMap<>(); - - private static class Op { - final Tree.Kind kind; - - /** - * {@link TerminalToken} - */ - final TerminalToken tokenType; - - Op(Tree.Kind kind, TerminalToken tokenType) { - this.kind = kind; - this.tokenType = tokenType; - } - } - - static { - operators.put(PrefixExpression.Operator.PLUS, new Op(Tree.Kind.UNARY_PLUS, TerminalToken.TokenNamePLUS)); - operators.put(PrefixExpression.Operator.MINUS, new Op(Tree.Kind.UNARY_MINUS, TerminalToken.TokenNameMINUS)); - operators.put(PrefixExpression.Operator.NOT, new Op(Tree.Kind.LOGICAL_COMPLEMENT, TerminalToken.TokenNameNOT)); - operators.put(PrefixExpression.Operator.COMPLEMENT, new Op(Tree.Kind.BITWISE_COMPLEMENT, TerminalToken.TokenNameTWIDDLE)); - operators.put(PrefixExpression.Operator.DECREMENT, new Op(Tree.Kind.PREFIX_DECREMENT, TerminalToken.TokenNameMINUS_MINUS)); - operators.put(PrefixExpression.Operator.INCREMENT, new Op(Tree.Kind.PREFIX_INCREMENT, TerminalToken.TokenNamePLUS_PLUS)); - - operators.put(PostfixExpression.Operator.DECREMENT, new Op(Tree.Kind.POSTFIX_DECREMENT, TerminalToken.TokenNameMINUS_MINUS)); - operators.put(PostfixExpression.Operator.INCREMENT, new Op(Tree.Kind.POSTFIX_INCREMENT, TerminalToken.TokenNamePLUS_PLUS)); - - operators.put(InfixExpression.Operator.TIMES, new Op(Tree.Kind.MULTIPLY, TerminalToken.TokenNameMULTIPLY)); - operators.put(InfixExpression.Operator.DIVIDE, new Op(Tree.Kind.DIVIDE, TerminalToken.TokenNameDIVIDE)); - operators.put(InfixExpression.Operator.REMAINDER, new Op(Tree.Kind.REMAINDER, TerminalToken.TokenNameREMAINDER)); - operators.put(InfixExpression.Operator.PLUS, new Op(Tree.Kind.PLUS, TerminalToken.TokenNamePLUS)); - operators.put(InfixExpression.Operator.MINUS, new Op(Tree.Kind.MINUS, TerminalToken.TokenNameMINUS)); - operators.put(InfixExpression.Operator.LEFT_SHIFT, new Op(Tree.Kind.LEFT_SHIFT, TerminalToken.TokenNameLEFT_SHIFT)); - operators.put(InfixExpression.Operator.RIGHT_SHIFT_SIGNED, new Op(Tree.Kind.RIGHT_SHIFT, TerminalToken.TokenNameRIGHT_SHIFT)); - operators.put(InfixExpression.Operator.RIGHT_SHIFT_UNSIGNED, new Op(Tree.Kind.UNSIGNED_RIGHT_SHIFT, TerminalToken.TokenNameUNSIGNED_RIGHT_SHIFT)); - operators.put(InfixExpression.Operator.LESS, new Op(Tree.Kind.LESS_THAN, TerminalToken.TokenNameLESS)); - operators.put(InfixExpression.Operator.GREATER, new Op(Tree.Kind.GREATER_THAN, TerminalToken.TokenNameGREATER)); - operators.put(InfixExpression.Operator.LESS_EQUALS, new Op(Tree.Kind.LESS_THAN_OR_EQUAL_TO, TerminalToken.TokenNameLESS_EQUAL)); - operators.put(InfixExpression.Operator.GREATER_EQUALS, new Op(Tree.Kind.GREATER_THAN_OR_EQUAL_TO, TerminalToken.TokenNameGREATER_EQUAL)); - operators.put(InfixExpression.Operator.EQUALS, new Op(Tree.Kind.EQUAL_TO, TerminalToken.TokenNameEQUAL_EQUAL)); - operators.put(InfixExpression.Operator.NOT_EQUALS, new Op(Tree.Kind.NOT_EQUAL_TO, TerminalToken.TokenNameNOT_EQUAL)); - operators.put(InfixExpression.Operator.XOR, new Op(Tree.Kind.XOR, TerminalToken.TokenNameXOR)); - operators.put(InfixExpression.Operator.OR, new Op(Tree.Kind.OR, TerminalToken.TokenNameOR)); - operators.put(InfixExpression.Operator.AND, new Op(Tree.Kind.AND, TerminalToken.TokenNameAND)); - operators.put(InfixExpression.Operator.CONDITIONAL_OR, new Op(Tree.Kind.CONDITIONAL_OR, TerminalToken.TokenNameOR_OR)); - operators.put(InfixExpression.Operator.CONDITIONAL_AND, new Op(Tree.Kind.CONDITIONAL_AND, TerminalToken.TokenNameAND_AND)); - - operators.put(Assignment.Operator.ASSIGN, new Op(Tree.Kind.ASSIGNMENT, TerminalToken.TokenNameEQUAL)); - operators.put(Assignment.Operator.PLUS_ASSIGN, new Op(Tree.Kind.PLUS_ASSIGNMENT, TerminalToken.TokenNamePLUS_EQUAL)); - operators.put(Assignment.Operator.MINUS_ASSIGN, new Op(Tree.Kind.MINUS_ASSIGNMENT, TerminalToken.TokenNameMINUS_EQUAL)); - operators.put(Assignment.Operator.TIMES_ASSIGN, new Op(Tree.Kind.MULTIPLY_ASSIGNMENT, TerminalToken.TokenNameMULTIPLY_EQUAL)); - operators.put(Assignment.Operator.DIVIDE_ASSIGN, new Op(Tree.Kind.DIVIDE_ASSIGNMENT, TerminalToken.TokenNameDIVIDE_EQUAL)); - operators.put(Assignment.Operator.BIT_AND_ASSIGN, new Op(Tree.Kind.AND_ASSIGNMENT, TerminalToken.TokenNameAND_EQUAL)); - operators.put(Assignment.Operator.BIT_OR_ASSIGN, new Op(Tree.Kind.OR_ASSIGNMENT, TerminalToken.TokenNameOR_EQUAL)); - operators.put(Assignment.Operator.BIT_XOR_ASSIGN, new Op(Tree.Kind.XOR_ASSIGNMENT, TerminalToken.TokenNameXOR_EQUAL)); - operators.put(Assignment.Operator.REMAINDER_ASSIGN, new Op(Tree.Kind.REMAINDER_ASSIGNMENT, TerminalToken.TokenNameREMAINDER_EQUAL)); - operators.put(Assignment.Operator.LEFT_SHIFT_ASSIGN, new Op(Tree.Kind.LEFT_SHIFT_ASSIGNMENT, TerminalToken.TokenNameLEFT_SHIFT_EQUAL)); - operators.put(Assignment.Operator.RIGHT_SHIFT_SIGNED_ASSIGN, new Op(Tree.Kind.RIGHT_SHIFT_ASSIGNMENT, TerminalToken.TokenNameRIGHT_SHIFT_EQUAL)); - operators.put(Assignment.Operator.RIGHT_SHIFT_UNSIGNED_ASSIGN, new Op(Tree.Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT, TerminalToken.TokenNameUNSIGNED_RIGHT_SHIFT_EQUAL)); - } - } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java b/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java index 79cffd79a90..37aee277ec6 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java @@ -16,32 +16,22 @@ */ package org.sonar.java.model; +import com.google.gson.JsonObject; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; +import org.sonar.java.semsitter.SemsitterBridge; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; -import java.util.stream.StreamSupport; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.FileASTRequestor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; -import org.sonar.java.AnalysisException; import org.sonar.java.AnalysisProgress; import org.sonar.java.ExecutionTimeReport; -import org.sonar.java.ProgressMonitor; import org.sonar.java.annotations.VisibleForTesting; import org.sonar.plugins.java.api.JavaVersion; import org.sonarsource.analyzer.commons.ProgressReport; @@ -53,12 +43,10 @@ public abstract class JParserConfig { private static final Logger LOG = LoggerFactory.getLogger(JParserConfig.class); - private static final String MAXIMUM_ECJ_WARNINGS = "42000"; - private static final Set JRE_JARS = new HashSet<>(Arrays.asList("rt.jar", "jrt-fs.jar", "android.jar")); - final JavaVersion javaVersion; final List classpath; final boolean shouldIgnoreUnnamedModuleForSplitPackage; + private SemsitterBridge bridge; private JParserConfig(JavaVersion javaVersion, List classpath, boolean shouldIgnoreUnnamedModuleForSplitPackage) { this.javaVersion = javaVersion; @@ -113,36 +101,6 @@ public JavaTree.CompilationUnitTreeImpl get() throws Exception { } } - public ASTParser astParser() { - ASTParser astParser = ASTParser.newParser(AST.getJLSLatest()); - Map options = new HashMap<>(JavaCore.getOptions()); - JavaCore.setComplianceOptions(javaVersion.effectiveJavaVersionAsString(), options); - options.put(JavaCore.COMPILER_PB_MAX_PER_UNIT, MAXIMUM_ECJ_WARNINGS); - if (shouldIgnoreUnnamedModuleForSplitPackage) { - options.put(JavaCore.COMPILER_IGNORE_UNNAMED_MODULE_FOR_SPLIT_PACKAGE, "enabled"); - } - if (shouldEnablePreviewFlag(javaVersion)) { - options.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, "enabled"); - } - // enabling all supported compiler warnings - JProblem.Type.compilerOptions() - .forEach(option -> options.put(option, "warning")); - - astParser.setCompilerOptions(options); - - boolean includeRunningVMBootclasspath = classpath.stream() - .noneMatch(f -> JRE_JARS.contains(f.getName())); - - astParser.setEnvironment(classpath.stream() - .map(File::getAbsolutePath) - .toArray(String[]::new), new String[] {}, new String[] {}, includeRunningVMBootclasspath); - - astParser.setResolveBindings(true); - astParser.setBindingsRecovery(true); - - return astParser; - } - @VisibleForTesting static class Batch extends JParserConfig { @@ -153,70 +111,24 @@ static class Batch extends JParserConfig { @Override public void parse(Iterable inputFiles, BooleanSupplier isCanceled, AnalysisProgress analysisProgress, BiConsumer action) { - - List sourceFilePaths = new ArrayList<>(); - Set notYetAnalyzedFiles = new LinkedHashSet<>(); - List encodings = new ArrayList<>(); - Map inputs = new HashMap<>(); - for (InputFile inputFile : inputFiles) { - String sourceFilePath = inputFile.absolutePath(); - inputs.put(new File(sourceFilePath), inputFile); - sourceFilePaths.add(sourceFilePath); - encodings.add(inputFile.charset().name()); - notYetAnalyzedFiles.add(inputFile); - } - ExecutionTimeReport executionTimeReport = new ExecutionTimeReport(); - ProgressMonitor monitor = new ProgressMonitor(isCanceled, analysisProgress); PerformanceMeasure.Duration batchPerformance = PerformanceMeasure.start("ParseAsBatch"); + List files = new ArrayList<>(); + inputFiles.forEach(files::add); try { - astParser().createASTs(sourceFilePaths.toArray(new String[0]), encodings.toArray(new String[0]), new String[0], new FileASTRequestor() { - @Override - public void acceptAST(String sourceFilePath, CompilationUnit ast) { - PerformanceMeasure.Duration convertDuration = PerformanceMeasure.start("Convert"); - InputFile inputFile = inputs.get(new File(sourceFilePath)); - executionTimeReport.start(inputFile); - Result result; - try { - result = new Result(JParser.convert(javaVersion.effectiveJavaVersionAsString(), inputFile.filename(), inputFile.contents(), ast)); - } catch (Exception e) { - result = new Result(e); - } - convertDuration.stop(); - PerformanceMeasure.Duration analyzeDuration = PerformanceMeasure.start("Analyze"); - action.accept(inputFile, result); - - notYetAnalyzedFiles.remove(inputFile); - executionTimeReport.end(); - analyzeDuration.stop(); + openBridge(files); + for (InputFile inputFile : files) { + if (isCanceled.getAsBoolean()) { + break; } - }, monitor); - if (!notYetAnalyzedFiles.isEmpty()) { - String message = String.format("%d/%d files were not analyzed by the batch mode", notYetAnalyzedFiles.size(), sourceFilePaths.size()); - throw new AnalysisException(message); - } - } catch (OperationCanceledException e) { - throw e; - } catch (RuntimeException e) { - LOG.warn("Unexpected {}: {}", e.getClass().getSimpleName(), e.getMessage()); - if (!notYetAnalyzedFiles.isEmpty()) { - fallbackToFileByFileMode(notYetAnalyzedFiles.stream().toList(), isCanceled, action); + executionTimeReport.start(inputFile); + action.accept(inputFile, parseFile(inputFile)); + executionTimeReport.end(); } } finally { + closeBridge(); batchPerformance.stop(); - // ExecutionTimeReport will not include the parsing time by file when using batch mode. executionTimeReport.reportAsBatch(); - monitor.done(); - } - } - - private void fallbackToFileByFileMode(List inputFiles, BooleanSupplier isCanceled, BiConsumer action) { - LOG.warn("Fallback to file by file analysis for {} files", inputFiles.size()); - for (InputFile inputFile : inputFiles) { - if (isCanceled.getAsBoolean()) { - break; - } - FileByFile.parse(astParser(), inputFile, javaVersion, action); } } @@ -236,23 +148,25 @@ public void parse(Iterable inputFiles, BooleanSupplier isCa ExecutionTimeReport executionTimeReport = new ExecutionTimeReport(); ProgressReport progressReport = new ProgressReport("Report about progress of Java AST analyzer", TimeUnit.SECONDS.toMillis(10)); - List filesNames = StreamSupport.stream(inputFiles.spliterator(), false) - .map(InputFile::toString) - .toList(); + List files = new ArrayList<>(); + inputFiles.forEach(files::add); + List filesNames = files.stream().map(InputFile::toString).toList(); progressReport.start(filesNames); try { - for (InputFile inputFile : inputFiles) { + openBridge(files); + for (InputFile inputFile : files) { if (isCanceled.getAsBoolean()) { cancelled = true; break; } executionTimeReport.start(inputFile); - parse(astParser(), inputFile, javaVersion, action); + action.accept(inputFile, parseFile(inputFile)); executionTimeReport.end(); progressReport.nextFile(); } successfullyCompleted = !cancelled; } finally { + closeBridge(); if (successfullyCompleted) { progressReport.stop(); } else { @@ -261,18 +175,69 @@ public void parse(Iterable inputFiles, BooleanSupplier isCa executionTimeReport.report(); } } + } - private static void parse(ASTParser astParser, InputFile inputFile, JavaVersion javaVersion, BiConsumer action) { - Result result; - PerformanceMeasure.Duration parseDuration = PerformanceMeasure.start("JParser"); - try { - result = new Result(JParser.parse(astParser, javaVersion.effectiveJavaVersionAsString(), inputFile.filename(), inputFile.contents())); - } catch (Exception e) { - result = new Result(e); - } finally { - parseDuration.stop(); + final Result parseFile(InputFile inputFile) { + PerformanceMeasure.Duration parseDuration = PerformanceMeasure.start("JParser"); + try { + if (bridge == null) { + return new Result(JParser.parse(javaVersion.effectiveJavaVersionAsString(), inputFile.filename(), inputFile.contents())); + } + JsonObject document = bridge.cst(inputFile.path().toAbsolutePath().toString()); + JSema sema = new JSema(); + sema.ingestDocument(document); + sema.loadTypes(names -> { + if (bridge == null) { + return null; + } + try { + return bridge.types(names); + } catch (IOException failure) { + throw new UncheckedIOException(failure); + } + }); + return new Result(SemsitterTranslator.translate(sema, document)); + } catch (Exception e) { + return new Result(e); + } finally { + parseDuration.stop(); + } + } + + void openBridge(List files) { + try { + bridge = SemsitterBridge.start(JParser.bridgePath()); + if (!files.isEmpty()) { + Path root = files.get(0).path().toAbsolutePath().getParent(); + bridge.analyze( + root == null ? Path.of(".") : root, + files.stream().map(file -> file.path().toAbsolutePath()).toList(), + bridgeClasspath(classpath)); } - action.accept(inputFile, result); + } catch (Exception failure) { + closeBridge(); + LOG.warn("SemSitter bridge did not start ({})", failure.toString()); + } + } + + /** + * The project classpath preceded by the runtime image of the running JDK, which is the JDK ECJ resolved + * against, so JDK classes win over a library copy as on the boot classpath. + */ + static List bridgeClasspath(List classpath) { + List entries = new ArrayList<>(); + Path image = Path.of(System.getProperty("java.home"), "lib", "modules"); + if (image.toFile().isFile()) { + entries.add(image.toString()); + } + classpath.stream().map(File::getAbsolutePath).forEach(entries::add); + return entries; + } + + void closeBridge() { + if (bridge != null) { + bridge.close(); + bridge = null; } } @@ -280,6 +245,7 @@ private static void parse(ASTParser astParser, InputFile inputFile, JavaVersion static boolean shouldEnablePreviewFlag(JavaVersion currentVersion) { return currentVersion.arePreviewFeaturesEnabled(); } + @FunctionalInterface public interface ParserConfigConstructor { JParserConfig apply(JavaVersion version, List files, Boolean shouldIgnoreUnnamedModuleForSplitPackage); diff --git a/java-frontend/src/main/java/org/sonar/java/model/JProblem.java b/java-frontend/src/main/java/org/sonar/java/model/JProblem.java index 38abd459dad..e6102354e53 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JProblem.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JProblem.java @@ -21,8 +21,6 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Stream; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.compiler.IProblem; import org.sonar.plugins.java.api.tree.Tree; import org.sonarsource.analyzer.commons.collections.SetUtils; @@ -80,12 +78,12 @@ public int hashCode() { } public enum Type { - UNDEFINED_TYPE(IProblem.UndefinedType), - PREVIEW_FEATURE_USED(IProblem.PreviewFeatureUsed), - UNUSED_IMPORT(IProblem.UnusedImport, JavaCore.COMPILER_PB_UNUSED_IMPORT, Tree.Kind.IMPORT), - REDUNDANT_CAST(IProblem.UnnecessaryCast, JavaCore.COMPILER_PB_UNNECESSARY_TYPE_CHECK, Tree.Kind.TYPE_CAST, Tree.Kind.PARENTHESIZED_EXPRESSION), - ASSIGNMENT_HAS_NO_EFFECT(IProblem.AssignmentHasNoEffect, JavaCore.COMPILER_PB_NO_EFFECT_ASSIGNMENT, Tree.Kind.ASSIGNMENT), - MASKED_CATCH(IProblem.MaskedCatch, JavaCore.COMPILER_PB_HIDDEN_CATCH_BLOCK, Tree.Kind.IDENTIFIER, Tree.Kind.MEMBER_SELECT); + UNDEFINED_TYPE(16777218), + PREVIEW_FEATURE_USED(4195408), + UNUSED_IMPORT(268435844, "org.eclipse.jdt.core.compiler.problem.unusedImport", Tree.Kind.IMPORT), + REDUNDANT_CAST(553648309, "org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck", Tree.Kind.TYPE_CAST, Tree.Kind.PARENTHESIZED_EXPRESSION), + ASSIGNMENT_HAS_NO_EFFECT(536871090, "org.eclipse.jdt.core.compiler.problem.noEffectAssignment", Tree.Kind.ASSIGNMENT), + MASKED_CATCH(16777381, "org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock", Tree.Kind.IDENTIFIER, Tree.Kind.MEMBER_SELECT); private final int warningID; private final String compilerOptionKey; @@ -105,8 +103,8 @@ public enum Type { this.kinds = SetUtils.immutableSetOf(kinds); } - boolean matches(IProblem warning) { - return warning.getID() == warningID; + boolean matches(int problemId) { + return problemId == warningID; } public static Set compilerOptions() { diff --git a/java-frontend/src/main/java/org/sonar/java/model/JSema.java b/java-frontend/src/main/java/org/sonar/java/model/JSema.java index 4045f8d1db5..7973e64ec30 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JSema.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JSema.java @@ -16,178 +16,1860 @@ */ package org.sonar.java.model; -import java.util.Arrays; -import java.util.Collections; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTUtils; -import org.eclipse.jdt.core.dom.IAnnotationBinding; -import org.eclipse.jdt.core.dom.IBinding; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.IPackageBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; import org.sonar.plugins.java.api.semantic.Sema; import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.Tree; +/** + * Semantic model of one translated file. Symbols are created from the SemSitter dump and from the + * {@code types} response (project graph, classpath declarations including the JDK image, embedded JDK table). + */ public final class JSema implements Sema { + private static final String ANNOTATIONS = "annotations"; + private static final String STATIC = "static"; + private static final String ABSTRACT = "abstract"; + private static final String RECORD = "record"; + private static final String ARRAY = "array"; + - private final AST ast; - final Set undefinedTypes = new HashSet<>(); - final Map declarations = new HashMap<>(); - final Map> usages = new HashMap<>(); - private final Map types = new HashMap<>(); - private final Map symbols = new HashMap<>(); - private final Map initializerBlockSymbols = new HashMap<>(); - private final Map staticInitializerBlockSymbols = new HashMap<>(); - private final Map annotations = new HashMap<>(); - private final Map nameToTypeCache = new HashMap<>(); + private static final Set PRIMITIVES = Set.of("byte", "short", "int", "long", "char", "float", "double", "boolean", "void"); + private static final Set OBJECT_METHODS = Set.of("equals/1", "hashCode/0", "toString/0"); + private static final Pattern LAMBDA_SEGMENTS = Pattern.compile("^(\\$lambda\\$[^$()]*\\(\\))+"); - JSema(AST ast) { - this.ast = ast; + private final Map facts = new HashMap<>(); + private final Map dumpSymbols = new HashMap<>(); + private final Map typeInfos = new HashMap<>(); + private final Map> libraryAnnotations = new HashMap<>(); + private final Map symbols = new HashMap<>(); + private final Map typesByName = new HashMap<>(); + private final Map rawTypes = new HashMap<>(); + private final Map binaryNames = new HashMap<>(); + private final Map sourceNames = new HashMap<>(); + final Map declarations = new HashMap<>(); + final Map> usages = new HashMap<>(); + private final Set undefinedTypes = new HashSet<>(); + private final Set creating = new HashSet<>(); + private final Set requestedTypes = new HashSet<>(); + private Function, JsonObject> typeLoader = names -> new JsonObject(); + private boolean typesLoaded; + private final JSymbol unknownSymbol = new JVariableSymbol(this, null, "", "local"); + + public JSema() { + unknownSymbol.unknown(true); } - public JType type(ITypeBinding typeBinding) { - JType cached = types.get(typeBinding); - if (cached != null) { - return cached; + /** + * Reads the symbols and graph facts of one CST document. Type declarations already known by + * erased name are reused, so a later {@link #ingestTypes} call fills them in instead of + * creating a second symbol. + */ + public void ingestDocument(JsonObject document) { + JsonObject factObject = document.getAsJsonObject("facts"); + if (factObject != null) { + for (Map.Entry entry : factObject.entrySet()) { + facts.put(entry.getKey(), entry.getValue().getAsJsonObject()); + } + } + JsonArray symbolArray = document.getAsJsonArray("symbols"); + if (symbolArray != null) { + for (JsonElement element : symbolArray) { + JsonObject symbol = element.getAsJsonObject(); + String id = text(symbol, "id"); + if (id != null) { + dumpSymbols.put(id, symbol); + if ("type".equals(text(symbol, "kind")) && id.indexOf('<') < 0) { + binaryName(erase(id)); + } + } + } + } + for (String id : dumpSymbols.keySet()) { + createDumpSymbol(id); + } + for (String id : new ArrayList<>(dumpSymbols.keySet())) { + linkDumpSymbol(id); } - return types.computeIfAbsent(typeBinding, k -> new JType(this, JType.normalize(k))); } - List types(ITypeBinding[] typeBindings) { - if (typeBindings.length == 0) { - return Collections.emptyList(); + /** + * The binary name Java gives the type SemSitter names {@code id}: a local class {@code p.C#f()$Local} becomes + * {@code p.C$1Local}, numbered by the first index not yet taken in its enclosing class, in declaration order. + */ + private String binaryName(String id) { + String known = binaryNames.get(id); + if (known != null) { + return known; } - Type[] result = new Type[typeBindings.length]; - for (int i = 0; i < typeBindings.length; i++) { - result[i] = type(typeBindings[i]); + int hash = id.lastIndexOf('#'); + int open = id.indexOf('(', hash); + int close = open < 0 ? -1 : id.indexOf(')', open); + String rest = close < 0 ? "" : LAMBDA_SEGMENTS.matcher(id.substring(close + 1)).replaceFirst(""); + if (hash < 0 || !rest.startsWith("$") || rest.length() == 1) { + return id; } - return Arrays.asList(result); + int nested = rest.indexOf('$', 1); + String name = nested < 0 ? rest.substring(1) : rest.substring(1, nested); + String localId = id.substring(0, close + 1) + "$" + name; + String local = binaryNames.get(localId); + if (local == null) { + String enclosing = binaryName(id.substring(0, hash)); + int index = 1; + while (sourceNames.containsKey(enclosing + "$" + index + name)) { + index++; + } + local = enclosing + "$" + index + name; + binaryNames.put(localId, local); + sourceNames.put(local, localId); + } + String binary = nested < 0 ? local : local + rest.substring(nested); + binaryNames.put(id, binary); + sourceNames.put(binary, id); + return binary; } - public Symbol packageSymbol(@Nullable IPackageBinding packageBinding) { - if (packageBinding == null) { - return Symbol.ROOT_PACKAGE; + private String sourceName(String fqn) { + return sourceNames.getOrDefault(fqn, fqn); + } + + /** + * Loads the declarations of every type the document references, then keeps {@code loader} to + * fetch the types reached later, like the return type of a library method. + */ + public void loadTypes(Function, JsonObject> loader) { + typeLoader = loader; + Set names = referencedTypeNames(); + requestedTypes.addAll(names); + ingestTypes(loader.apply(names)); + } + + public void ingestTypes(JsonObject types) { + typesLoaded = true; + for (Map.Entry entry : types.entrySet()) { + JsonObject info = entry.getValue().getAsJsonObject(); + typeInfos.put(entry.getKey(), info); + JTypeSymbol symbol = typesByName.get(entry.getKey()); + if (symbol == null) { + symbol = new JTypeSymbol(this, entry.getKey(), simpleName(entry.getKey()), false); + symbol.owner(packageSymbol(packageName(entry.getKey()))); + symbol.type(rawNamed(entry.getKey())); + typesByName.put(entry.getKey(), symbol); + symbols.putIfAbsent(entry.getKey(), symbol); + } + applyTypeInfo(symbol, info); + } + } + + public Set referencedTypeNames() { + Set names = new HashSet<>(); + for (String id : facts.keySet()) { + addTypeName(names, id); } - JSymbol cached = symbols.get(packageBinding); - if (cached != null) { - return cached; + for (JsonObject symbol : dumpSymbols.values()) { + addTypeName(names, text(symbol, "id")); + JsonObject type = symbol.getAsJsonObject("facts"); + if (type != null && type.getAsJsonObject("type") != null) { + collectFactNames(names, type.getAsJsonObject("type")); + } } - return symbols.computeIfAbsent(packageBinding, k -> new JPackageSymbol(this, (IPackageBinding) k)); + return names; } - public JTypeSymbol typeSymbol(ITypeBinding typeBinding) { - JTypeSymbol cached = (JTypeSymbol) symbols.get(typeBinding); - if (cached != null) { - return cached; + private static void addTypeName(Set names, @Nullable String id) { + if (id == null || id.isEmpty()) { + return; + } + int hash = id.indexOf('#'); + String owner = hash < 0 ? id : id.substring(0, hash); + if (owner.indexOf('%') >= 0 || owner.indexOf('(') >= 0 || owner.indexOf('<') >= 0 && hash < 0) { + return; + } + String erased = erase(owner); + if (!erased.isEmpty() && !PRIMITIVES.contains(erased) && erased.indexOf(' ') < 0) { + names.add(erased); } - return (JTypeSymbol) symbols.computeIfAbsent(typeBinding, k -> new JTypeSymbol(this, JType.normalize((ITypeBinding) k))); } - public JMethodSymbol methodSymbol(IMethodBinding methodBinding) { - JMethodSymbol cached = (JMethodSymbol) symbols.get(methodBinding); - if (cached != null) { - return cached; + private static void collectFactNames(Set names, JsonObject fact) { + String id = text(fact, "id"); + if (id != null) { + addTypeName(names, id); } - return (JMethodSymbol) symbols.computeIfAbsent(methodBinding, k -> new JMethodSymbol(this, (IMethodBinding) k)); + if (fact.has("args")) { + for (JsonElement arg : fact.getAsJsonArray("args")) { + if (arg.isJsonObject()) { + collectFactNames(names, arg.getAsJsonObject()); + } + } + } + if (fact.has("elem") && fact.get("elem").isJsonObject()) { + collectFactNames(names, fact.getAsJsonObject("elem")); + } + } + + public void declare(String id, Tree tree) { + declarations.put(id, tree); } - public JInitializerBlockSymbol initializerBlockSymbol(JTypeSymbol owner) { - JInitializerBlockSymbol cached = initializerBlockSymbols.get(owner); - if (cached != null) { - return cached; + public void use(String id, IdentifierTree tree) { + usages.computeIfAbsent(id, key -> new ArrayList<>()).add(tree); + } + + @Override + public Type getClassType(String fullyQualifiedName) { + if (fullyQualifiedName == null || fullyQualifiedName.isBlank() || "!Unknown!".equals(fullyQualifiedName)) { + return Type.UNKNOWN; + } + if ("".equals(fullyQualifiedName)) { + return JType.nullType(this); + } + if (fullyQualifiedName.endsWith("[]")) { + return JType.array(this, asJType(getClassType(fullyQualifiedName.substring(0, fullyQualifiedName.length() - 2)))); + } + if (PRIMITIVES.contains(fullyQualifiedName)) { + return primitiveType(fullyQualifiedName); } - return initializerBlockSymbols.computeIfAbsent(owner, k -> new JInitializerBlockSymbol(k, false)); + typeSymbol(fullyQualifiedName); + return rawNamed(fullyQualifiedName); } - public JInitializerBlockSymbol staticInitializerBlockSymbol(JTypeSymbol owner) { - JInitializerBlockSymbol cached = staticInitializerBlockSymbols.get(owner); - if (cached != null) { - return cached; + public Runnable getEnvironmentCleaner() { + return () -> { + }; + } + + public Set undefinedTypes() { + return undefinedTypes; + } + + JSymbol resolve(@Nullable String id) { + if (id == null || id.isEmpty()) { + return unknownSymbol; + } + JSymbol found = symbols.get(id); + if (found != null) { + return found; + } + if (dumpSymbols.containsKey(id)) { + createDumpSymbol(id); + linkDumpSymbol(id); + return symbols.getOrDefault(id, unknownSymbol); } - return staticInitializerBlockSymbols.computeIfAbsent(owner, k -> new JInitializerBlockSymbol(k, true)); + if (id.indexOf('#') >= 0) { + return createMember(id); + } + if (id.indexOf('<') >= 0) { + return typeParameter(id); + } + return typeSymbol(erase(id)); } + Symbol.TypeSymbol resolveTypeSymbol(@Nullable String id) { + JSymbol symbol = resolve(id); + return symbol instanceof Symbol.TypeSymbol typeSymbol ? typeSymbol : Symbol.TypeSymbol.UNKNOWN_TYPE; + } - public JVariableSymbol variableSymbol(IVariableBinding variableBinding) { - JVariableSymbol cached = (JVariableSymbol) symbols.get(variableBinding); - if (cached != null) { - return cached; + JTypeSymbol typeSymbol(String name) { + String fqn = sourceName(name); + JTypeSymbol found = typesByName.get(fqn); + if (found != null) { + return found; } - return (JVariableSymbol) symbols.computeIfAbsent(variableBinding, k -> new JVariableSymbol(this, (IVariableBinding) k)); + JTypeSymbol created = new JTypeSymbol(this, fqn, simpleName(fqn), false); + created.owner(packageSymbol(packageName(fqn))); + created.type(rawNamed(fqn)); + typesByName.put(fqn, created); + symbols.putIfAbsent(fqn, created); + JsonObject info = typeInfos.get(fqn); + if (info == null && typesLoaded && requestedTypes.add(fqn)) { + JsonObject loaded = typeLoader.apply(Set.of(fqn)); + if (loaded == null) { + return created; + } + ingestTypes(loaded); + if (typeInfos.containsKey(fqn)) { + return created; + } + } + if (info != null) { + applyTypeInfo(created, info); + } else if (typesLoaded && !isWellKnown(fqn)) { + created.unknown(true); + undefinedTypes.add(new JProblem(fqn, JProblem.Type.UNDEFINED_TYPE)); + } + return created; } - JSymbolMetadata.JAnnotationInstance annotation(IAnnotationBinding annotationBinding) { - JSymbolMetadata.JAnnotationInstance cached = annotations.get(annotationBinding); - if (cached != null) { - return cached; + private static boolean isWellKnown(String fqn) { + return "java.lang.Object".equals(fqn) || "java.lang.Cloneable".equals(fqn) || "java.io.Serializable".equals(fqn); + } + + Symbol.TypeSymbol arraySymbol(JType element) { + String name = element.fullyQualifiedName() + "[]"; + JTypeSymbol found = typesByName.get(name); + if (found != null) { + return found; } - return annotations.computeIfAbsent(annotationBinding, k -> new JSymbolMetadata.JAnnotationInstance(this, k)); + JTypeSymbol symbol = new JTypeSymbol(this, name, element.name() + "[]", false); + symbol.elementFqn(element.fullyQualifiedName()); + symbol.owner(Symbol.ROOT_PACKAGE); + symbol.type(JType.array(this, element)); + typesByName.put(name, symbol); + return symbol; } - static IBinding declarationBinding(IBinding binding) { - switch (binding.getKind()) { - case IBinding.TYPE: - return ((ITypeBinding) binding).getTypeDeclaration(); - case IBinding.METHOD: - return ((IMethodBinding) binding).getMethodDeclaration(); - case IBinding.VARIABLE: - return ((IVariableBinding) binding).getVariableDeclaration(); - default: - return binding; + Symbol.TypeSymbol primitiveSymbol(String name) { + JTypeSymbol found = typesByName.get(name); + if (found != null) { + return found; } + JTypeSymbol symbol = new JTypeSymbol(this, name, name, false); + symbol.owner(Symbol.ROOT_PACKAGE); + symbol.type(primitiveType(name)); + typesByName.put(name, symbol); + return symbol; } - @Override - public Type getClassType(String fullyQualifiedName) { - Type cached = nameToTypeCache.get(fullyQualifiedName); - if (cached != null) { - return cached; + List annotationNames(@Nullable String id) { + return declaredAnnotations(id).stream().map(DeclaredAnnotation::name).toList(); + } + + List declaredAnnotations(@Nullable String id) { + if (id == null) { + return List.of(); + } + JsonObject fact = facts.get(id); + List fromValues = fact == null ? List.of() : annotationValues(fact); + if (!fromValues.isEmpty()) { + return fromValues; + } + List fromFact = fact == null ? List.of() : strings(fact, ANNOTATIONS); + if (!fromFact.isEmpty()) { + List library = libraryAnnotations.getOrDefault(id, List.of()); + if (!library.isEmpty()) { + return library; + } + return fromFact.stream().map(name -> new DeclaredAnnotation(erase(name), List.of())).toList(); + } + return libraryAnnotations.getOrDefault(id, List.of()); + } + + private List annotationValues(JsonObject fact) { + if (!fact.has("props") || !fact.get("props").isJsonObject()) { + return List.of(); + } + List annotations = new ArrayList<>(); + for (String encoded : strings(fact.getAsJsonObject("props"), "annotationValues")) { + JsonElement parsed = JsonParser.parseString(encoded); + if (!parsed.isJsonObject()) { + continue; + } + JsonObject annotation = parsed.getAsJsonObject(); + String name = text(annotation, "name"); + if (name != null) { + annotations.add(new DeclaredAnnotation(erase(name), readAnnotationValues(annotation.get("values")))); + } + } + return annotations; + } + + /** Annotations and deprecation a {@code types} answer gives a library type or member. */ + private void libraryAnnotations(JSymbol symbol, String id, JsonObject info) { + List annotations = readAnnotations(info); + if (annotations.isEmpty()) { + return; + } + libraryAnnotations.put(id, annotations); + if (annotations.stream().anyMatch(annotation -> "java.lang.Deprecated".equals(annotation.name()))) { + symbol.deprecated(true); + } + } + + private List readAnnotations(JsonObject info) { + if (!info.has(ANNOTATIONS) || !info.get(ANNOTATIONS).isJsonArray()) { + return List.of(); + } + List annotations = new ArrayList<>(); + for (JsonElement element : info.getAsJsonArray(ANNOTATIONS)) { + if (element.isJsonPrimitive()) { + annotations.add(new DeclaredAnnotation(element.getAsString(), List.of())); + } else if (element.isJsonObject()) { + JsonObject annotation = element.getAsJsonObject(); + String name = text(annotation, "name"); + if (name != null) { + annotations.add(new DeclaredAnnotation(name, readAnnotationValues(annotation.get("values")))); + } + } + } + return annotations; + } + + private List readAnnotationValues(@Nullable JsonElement element) { + if (!(element instanceof JsonArray array)) { + return List.of(); + } + List values = new ArrayList<>(); + for (JsonElement item : array) { + if (item.isJsonObject()) { + JsonObject value = item.getAsJsonObject(); + String name = text(value, "name"); + values.add(new AnnotationValueImpl(name == null ? "value" : name, readAnnotationElement(value))); + } } - return nameToTypeCache.computeIfAbsent(fullyQualifiedName, t -> { - ITypeBinding typeBinding = resolveType(t); - return typeBinding != null ? type(typeBinding) : Type.UNKNOWN; - }); + return values; } @Nullable - ITypeBinding resolveType(String name) { - int dimensions = 0; - int end = name.length() - 1; - while (name.charAt(end) == ']') { - end -= 2; - dimensions++; + private Object readAnnotationElement(JsonObject value) { + String kind = text(value, "kind"); + JsonElement raw = value.get("value"); + if (kind == null) { + return null; + } + return switch (kind) { + case "boolean" -> Boolean.valueOf(raw != null && raw.isJsonPrimitive() && raw.getAsBoolean()); + case "byte" -> raw == null || !raw.isJsonPrimitive() ? null : Byte.valueOf(raw.getAsByte()); + case "short" -> raw == null || !raw.isJsonPrimitive() ? null : Short.valueOf(raw.getAsShort()); + case "int" -> raw == null || !raw.isJsonPrimitive() ? null : Integer.valueOf(raw.getAsInt()); + case "long" -> raw == null || !raw.isJsonPrimitive() ? null : Long.valueOf(raw.getAsLong()); + case "float" -> raw == null || !raw.isJsonPrimitive() ? null : Float.valueOf(raw.getAsFloat()); + case "double" -> raw == null || !raw.isJsonPrimitive() ? null : Double.valueOf(raw.getAsDouble()); + case "char" -> raw == null || !raw.isJsonPrimitive() ? null : Character.valueOf((char) raw.getAsInt()); + case "string", "class" -> raw == null || !raw.isJsonPrimitive() ? null : raw.getAsString(); + case "enum" -> enumConstant(text(value, "type"), text(value, "const")); + case "array" -> readAnnotationArray(raw); + case "annotation" -> raw != null && raw.isJsonObject() ? text(raw.getAsJsonObject(), "name") : null; + default -> null; + }; + } + + private Object[] readAnnotationArray(@Nullable JsonElement element) { + if (!(element instanceof JsonArray array)) { + return new Object[0]; + } + List values = new ArrayList<>(); + for (JsonElement item : array) { + values.add(item.isJsonObject() ? readAnnotationElement(item.getAsJsonObject()) : null); + } + return values.toArray(); + } + + @Nullable + private Symbol.VariableSymbol enumConstant(@Nullable String typeName, @Nullable String constName) { + if (constName == null) { + return null; + } + String ownerName = typeName == null || typeName.isBlank() ? "" : erase(typeName); + String id = (ownerName.isEmpty() ? "enum" : ownerName) + "#" + constName; + if (symbols.get(id) instanceof Symbol.VariableSymbol variable) { + return variable; + } + JTypeSymbol type = ownerName.isEmpty() ? null : typeSymbol(ownerName); + JVariableSymbol created = new JVariableSymbol(this, id, constName, "enum_member"); + if (type != null) { + created.owner(type); + created.type(type.type()); + } + created.addModifiers(List.of("public", "static", "final")); + symbols.put(id, created); + return created; + } + + /** + * Return type of a call. A JDK member can name the class type parameter its erased return + * stands for ({@code List.get} returns {@code E}), which is taken from the receiver. + */ + Symbol receiverSymbol(Symbol.TypeSymbol type, String name, Type receiverType) { + String id = (type instanceof JSymbol symbol && symbol.id != null ? symbol.id : type.name()) + "#" + name; + JSymbol found = symbols.get(id); + if (found != null) { + return found; } - name = name.substring(0, end + 1); + JVariableSymbol variable = new JVariableSymbol(this, id, name, "field"); + variable.owner(type); + variable.type(receiverType); + variable.addModifiers(List.of("final")); + variable.receiver(true); + symbols.put(id, variable); + return variable; + } - ITypeBinding typeBinding = ast.resolveWellKnownType(name); - if (typeBinding == null) { - typeBinding = ASTUtils.resolveType(ast, name); - if (typeBinding == null) { - return null; + /** + * The type an invocation of {@code method} evaluates to. A self-typed method returns the receiver, or a + * capture of it when the receiver leaves its own type argument as a wildcard. + */ + Type invocationReturn(JMethodSymbol method, @Nullable Type receiver, List arguments) { + Integer index = method.returnTypeParameter(); + if (method.returnsSelf() && receiver instanceof JType returned && returned.isCapture()) { + return returned; + } + Type declaring = index == null || receiver == null ? null : declaringType(method, receiver); + if (declaring != null && declaring.isParameterized() && declaring.typeArguments().size() > index) { + Type argument = declaring.typeArguments().get(index); + if (method.returnsSelf() && argument instanceof JType captured && captured.isCapture() && receiver instanceof JType self && self.isClass()) { + return JType.capture(this, self); + } + if (!argument.isTypeVar()) { + return argument; } } - return dimensions == 0 ? typeBinding : typeBinding.createArrayType(dimensions); + if (method.returnsSelf() && receiver instanceof JType returned && returned.isClass()) { + boolean wildcard = returned.typeArguments().stream().anyMatch(argument -> argument instanceof JType type && type.isCapture()); + return wildcard ? JType.capture(this, returned) : returned; + } + JsonObject declaredReturn = declaredReturn(method); + if (declaredReturn != null) { + Map classSubstitution = receiver == null ? Map.of() : classSubstitution(method, receiver); + Map substitution = new HashMap<>(methodSubstitution(method, arguments)); + substitution.putAll(classSubstitution); + if (!substitution.isEmpty() || method.isStatic()) { + JType substituted = typeFromFact(declaredReturn, substitution, true); + boolean leaks = classSubstitution.isEmpty() ? mentionsTypeVariable(substituted) : mentionsMethodTypeVariable(method, substituted); + if (!substituted.isUnknown() && !leaks) { + return substituted; + } + } + } + Type declared = method.returnType().type(); + return declared == null ? Type.UNKNOWN : declared; } - IAnnotationBinding[] resolvePackageAnnotations(String packageName) { - return ASTUtils.resolvePackageAnnotations(ast, packageName); + /** Maps the type variables of {@code method} itself to the arguments they are inferred from. */ + private Map methodSubstitution(JMethodSymbol method, List arguments) { + JsonObject member = arguments.isEmpty() ? null : lookupGenericMember(method); + if (member == null || !(member.get("typeVars") instanceof JsonArray names)) { + return Map.of(); + } + List variables = parameterVariables(method); + Map substitution = new HashMap<>(); + for (int i = 0; i < names.size(); i++) { + Type inferred = methodArgument(variables, "m" + i, arguments); + if (inferred != null && !inferred.isUnknown()) { + substitution.put(names.get(i).getAsString(), inferred); + } + } + return substitution; } - public Runnable getEnvironmentCleaner() { - return ASTUtils.getEnvironmentCleaner(ast); + private static boolean mentionsTypeVariable(Type type) { + return mentionsTypeVariable(type, name -> true); } - public Set undefinedTypes() { - return Collections.unmodifiableSet(undefinedTypes); + private boolean mentionsMethodTypeVariable(JMethodSymbol method, Type type) { + JsonObject member = lookupGenericMember(method); + if (member == null || !(member.get("typeVars") instanceof JsonArray names) || names.isEmpty()) { + return false; + } + Set own = new HashSet<>(); + names.forEach(name -> own.add(name.getAsString())); + return mentionsTypeVariable(type, own::contains); + } + + private static boolean mentionsTypeVariable(Type type, Predicate variable) { + if (type.isTypeVar()) { + return variable.test(type.name()); + } + if (type.isArray()) { + return mentionsTypeVariable(((Type.ArrayType) type).elementType(), variable); + } + return type.isParameterized() && type.typeArguments().stream().anyMatch(argument -> mentionsTypeVariable(argument, variable)); + } + + /** The supertype of {@code receiver} that declares {@code method}, with the receiver's type arguments applied. */ + @Nullable + private Type declaringType(JMethodSymbol method, Type receiver) { + Type owner = method.owner().type(); + if (owner == null || owner.isUnknown()) { + return receiver; + } + Type found = asSuperType(receiver, owner.erasure().fullyQualifiedName(), new HashSet<>()); + return found == null ? receiver : found; + } + + /** + * The parameterization of {@code created} a diamond infers from {@code target}: each type parameter of the created + * class takes the target argument it is passed to through the supertype matching the target. + */ + Type inferDiamond(Type created, Type target) { + if (!(created.erasure() instanceof JType raw) || raw.isUnknown() || !(target instanceof JType expected) || !expected.isParameterized()) { + return created; + } + String targetFqn = expected.erasure().fullyQualifiedName(); + if (raw.fullyQualifiedName().equals(targetFqn)) { + return JType.named(this, raw.fullyQualifiedName(), expected.args); + } + Type generic = raw.declaringType(); + Type declared = generic.isParameterized() ? asSuperType(generic, targetFqn, new HashSet<>()) : null; + if (declared == null || declared.typeArguments().size() != expected.args.size()) { + return created; + } + Map inferred = new HashMap<>(); + for (int i = 0; i < expected.args.size(); i++) { + if (declared.typeArguments().get(i).isTypeVar()) { + inferred.put(declared.typeArguments().get(i).fullyQualifiedName(), expected.args.get(i)); + } + } + List arguments = new ArrayList<>(); + for (Type variable : generic.typeArguments()) { + JType argument = inferred.get(variable.fullyQualifiedName()); + if (argument == null) { + return created; + } + arguments.add(argument); + } + return JType.named(this, raw.fullyQualifiedName(), arguments); + } + + /** The type a lambda or method reference implements for a wildcard-parameterized target (JLS 9.9). */ + Type groundType(Type target) { + if (!(target instanceof JType type) || !type.isParameterized() || type.args.stream().noneMatch(JType::isWildcard)) { + return target; + } + return JType.named(this, type.fullyQualifiedName(), type.args.stream().map(JType::groundType).toList()); + } + + /** Capture conversion: a member is accessed through captures of the receiver's wildcard arguments. */ + private Type captureWildcards(Type receiver) { + if (!(receiver instanceof JType type) || !type.isParameterized() || type.args.stream().noneMatch(JType::isWildcard)) { + return receiver; + } + List captured = type.args.stream().map(argument -> argument.isWildcard() && argument.element != null ? JType.capture(this, argument.element) : argument).toList(); + return JType.named(this, type.fullyQualifiedName(), captured); + } + + /** The method as seen from {@code receiver}, so its owner keeps that parameterization. */ + JMethodSymbol invocationView(JMethodSymbol method, @Nullable Type receiver, List parameters, Type returned) { + JMethodSymbol view = method.withSignature(parameters, returned); + if (receiver != null && !method.isStatic() && method.owner().type() != null) { + Type declaring = asSuperType(captureWildcards(receiver), method.owner().type().erasure().fullyQualifiedName(), new HashSet<>()); + if (declaring != null && declaring.isParameterized()) { + view.owner(declaring.symbol()); + } + } + return view; + } + + /** + * The parameter types of {@code method} at one invocation: type variables of the owner are replaced by + * the receiver's type arguments, and type variables of the method by the boxed type of the first argument + * they are inferred from. + */ + List invocationParameters(JMethodSymbol method, @Nullable Type receiver, List arguments) { + List parameters = method.parameterTypes(); + List variables = parameterVariables(method); + if (variables.size() != parameters.size()) { + return parameters; + } + List result = new ArrayList<>(parameters); + for (int i = 0; i < variables.size(); i++) { + String variable = variables.get(i); + if (variable.isEmpty()) { + continue; + } + String base = variable.replace("[]", ""); + Type bound = base.charAt(0) == 'c' + ? classArgument(method, receiver, Integer.parseInt(base.substring(1))) + : methodArgument(variables, base, arguments); + if (bound != null) { + JType type = asJType(bound); + for (int dims = (variable.length() - base.length()) / 2; dims > 0; dims--) { + type = JType.array(this, type); + } + result.set(i, type); + } + } + substituteDeclaredParameters(method, receiver, variables, result); + return result; + } + + /** + * Fills in the parameters that are not a type variable on their own but are written in terms of one, + * such as {@code Consumer}. Their declared form is substituted for the arguments the receiver gives + * the declaring type, which may be a supertype of the receiver's own type. + */ + private void substituteDeclaredParameters(JMethodSymbol method, @Nullable Type receiver, List variables, List result) { + List declared = declaredParameters(method); + if (declared.size() != result.size() || receiver == null) { + return; + } + Map substitution = classSubstitution(method, receiver); + if (substitution.isEmpty()) { + return; + } + for (int i = 0; i < declared.size(); i++) { + if (variables.isEmpty() || variables.get(i).isEmpty()) { + result.set(i, typeFromFact(declared.get(i), substitution)); + } + } + } + + /** Maps the type variables of the type declaring {@code method} to the arguments {@code receiver} gives them. */ + private Map classSubstitution(JMethodSymbol method, Type receiver) { + String ownerFqn = method.owner().type().erasure().fullyQualifiedName(); + JsonObject info = typeInfos.get(sourceName(ownerFqn)); + List names = info == null ? List.of() : strings(info, "typeVars"); + if (names.isEmpty()) { + return Map.of(); + } + Type declaring = asSuperType(receiver, ownerFqn, new HashSet<>()); + if (declaring == null || !declaring.isParameterized()) { + return Map.of(); + } + List arguments = declaring.typeArguments(); + Map substitution = new HashMap<>(); + for (int i = 0; i < names.size() && i < arguments.size(); i++) { + substitution.put(names.get(i), arguments.get(i)); + } + return substitution; + } + + /** The instantiation of {@code targetFqn} reached from {@code from}, which is itself or one of its supertypes. */ + @Nullable + private Type asSuperType(Type from, String targetFqn, Set visited) { + if (from.erasure().fullyQualifiedName().equals(targetFqn)) { + return from; + } + Symbol.TypeSymbol symbol = from.symbol(); + if (symbol.isUnknown() || !visited.add(from.erasure().fullyQualifiedName())) { + return null; + } + List supertypes = new ArrayList<>(symbol.interfaces()); + Type superClass = symbol.superClass(); + if (superClass != null) { + supertypes.add(0, superClass); + } + for (Type supertype : supertypes) { + Type found = asSuperType(instantiateSupertype(from, supertype), targetFqn, visited); + if (found != null) { + return found; + } + } + return null; + } + + /** Applies {@code from}'s type arguments to a supertype written with those type variables. */ + private Type instantiateSupertype(Type from, Type supertype) { + if (!from.isParameterized() || !(from instanceof JType fromType) || !(supertype instanceof JType superType)) { + return supertype; + } + return superType.substitute(typeArgumentsOf(fromType)); + } + + private Map typeArgumentsOf(JType type) { + JsonObject info = typeInfos.get(sourceName(type.erasure().fullyQualifiedName())); + List names = info == null ? List.of() : strings(info, "typeVars"); + List arguments = type.typeArguments(); + Map substitution = new HashMap<>(); + for (int i = 0; i < names.size() && i < arguments.size(); i++) { + substitution.put(names.get(i), arguments.get(i)); + } + return substitution; + } + + @Nullable + private JsonObject declaredReturn(JMethodSymbol method) { + if (method.declaredReturnLoaded()) { + return method.declaredReturn(); + } + JsonObject member = lookupGenericMember(method); + JsonObject retType = member != null && member.get("retType") instanceof JsonObject fact ? fact : null; + method.declaredReturn(retType); + return retType; + } + + private List declaredParameters(JMethodSymbol method) { + List cached = method.declaredParameters(); + if (cached == null) { + JsonObject member = lookupGenericMember(method); + cached = member == null ? List.of() : objects(member, "paramTypes"); + method.declaredParameters(cached); + } + return cached; + } + + @Nullable + private static Type classArgument(JMethodSymbol method, @Nullable Type receiver, int index) { + if (receiver == null || !receiver.isParameterized() || receiver.typeArguments().size() <= index + || !receiver.erasure().equals(method.owner().type().erasure())) { + return null; + } + return receiver.typeArguments().get(index); + } + + @Nullable + private static Type methodArgument(List variables, String base, List arguments) { + for (int i = 0; i < variables.size() && i < arguments.size(); i++) { + String variable = variables.get(i); + if (!variable.replace("[]", "").equals(base)) { + continue; + } + Type argument = arguments.get(i); + if (!variable.equals(base)) { + boolean last = i == variables.size() - 1; + if (arguments.size() == variables.size() && argument.isArray()) { + argument = ((Type.ArrayType) argument).elementType(); + } else if (!last) { + continue; + } + } + if (!argument.isUnknown() && !argument.isNullType()) { + return argument.isPrimitive() ? argument.primitiveWrapperType() : argument; + } + } + return null; + } + + private List parameterVariables(JMethodSymbol method) { + List cached = method.parameterVariables(); + if (cached == null) { + cached = lookupParameterVariables(method); + method.parameterVariables(cached); + } + return cached; + } + + private List lookupParameterVariables(JMethodSymbol method) { + JsonObject member = lookupGenericMember(method); + if (member == null) { + return List.of(); + } + List variables = new ArrayList<>(); + member.getAsJsonArray("paramVars").forEach(v -> variables.add(v.getAsString())); + return variables; + } + + /** The loaded declaration of {@code method}, when its declaring type is known and carries a generic signature. */ + @Nullable + private JsonObject lookupGenericMember(JMethodSymbol method) { + String ownerFqn = method.owner().type().erasure().fullyQualifiedName(); + if (!declaresType(ownerFqn)) { + return null; + } + JsonArray members = typeInfos.get(sourceName(ownerFqn)).getAsJsonArray("members"); + if (members == null) { + return null; + } + List erased = method.parameterTypes().stream().map(t -> t.erasure().fullyQualifiedName()).toList(); + for (JsonElement element : members) { + JsonObject member = element.getAsJsonObject(); + String descriptor = text(member, "desc"); + if (method.name().equals(text(member, "name")) && descriptor != null && member.has("paramVars") && member.get("paramVars").isJsonArray() + && parameterTypesOf(descriptor).stream().map(t -> t.erasure().fullyQualifiedName()).toList().equals(erased)) { + return member; + } + } + return null; + } + + Symbol memberFromTypeInfo(JTypeSymbol owner, JsonObject member) { + if (member.has("id") && !member.get("id").isJsonNull()) { + return resolve(member.get("id").getAsString()); + } + String name = text(member, "name"); + String descriptor = text(member, "desc"); + String id = (owner.id == null ? owner.name() : owner.id) + "#" + name + (descriptor == null ? "" : descriptor); + JSymbol existing = symbols.get(id); + if (existing != null) { + return existing; + } + if ("f".equals(text(member, "k"))) { + JVariableSymbol field = new JVariableSymbol(this, id, name == null ? "" : name, "field"); + field.owner(owner); + if (descriptor != null) { + field.type(readDescriptor(descriptor, new int[] {0})); + } + applyMemberFlags(field, member); + libraryAnnotations(field, id, member); + if (truth(member, "enum")) { + field.enumSymbol(true); + } + String constant = text(member, "constant"); + if (constant != null) { + field.constantValue(constant); + } + symbols.put(id, field); + return field; + } + JMethodSymbol method = new JMethodSymbol(this, id, name == null ? "" : name); + method.owner(owner); + if (descriptor != null && descriptor.startsWith("(")) { + method.parameterTypes(parameterTypesOf(descriptor)); + method.returnTypeSymbol(readDescriptor(descriptor, new int[] {descriptor.indexOf(')') + 1}).symbol()); + } + applyMemberFlags(method, member); + if (owner.isInterface() && !truth(member, ABSTRACT) && !truth(member, STATIC) && !"private".equals(text(member, "vis"))) { + method.addModifiers(List.of("default")); + } + libraryAnnotations(method, id, member); + List thrown = strings(member, "throws"); + if (!thrown.isEmpty()) { + method.thrownTypes(thrown.stream().map(this::typeFromName).toList()); + } + if (member.has("varargs") && member.get("varargs").getAsBoolean()) { + method.varArgs(true); + } + if (member.has("retParam") && !member.get("retParam").isJsonNull()) { + method.returnTypeParameter(member.get("retParam").getAsInt()); + } + if (member.get("retType") instanceof JsonObject retType) { + method.declaredReturn(retType); + } + method.returnsSelf(member.has("retSelf") && member.get("retSelf").getAsBoolean()); + symbols.put(id, method); + return method; + } + + private void createDumpSymbol(String id) { + if (symbols.containsKey(id)) { + return; + } + JsonObject dump = dumpSymbols.get(id); + String kind = text(dump, "kind"); + String name = text(dump, "name"); + if (name == null) { + name = ""; + } + if ("label".equals(kind)) { + return; + } + JSymbol symbol; + if ("type".equals(kind) && id.indexOf('<') >= 0) { + symbol = new JTypeSymbol(this, id, name, true); + } else if ("type".equals(kind)) { + symbol = new JTypeSymbol(this, id, simpleName(erase(id)), false); + typesByName.putIfAbsent(erase(id), (JTypeSymbol) symbol); + } else if ("method".equals(kind) || "constructor".equals(kind)) { + symbol = new JMethodSymbol(this, id, "constructor".equals(kind) ? "" : name); + } else { + symbol = new JVariableSymbol(this, id, name, kind == null ? "field" : kind); + } + symbols.put(id, symbol); + } + + private void linkDumpSymbol(String id) { + JSymbol symbol = symbols.get(id); + JsonObject dump = dumpSymbols.get(id); + if (symbol == null || dump == null) { + return; + } + JsonObject fact = facts.get(id); + String ownerId = text(dump, "owner"); + String parent = text(fact, "parent"); + if ((ownerId == null && symbol instanceof JMethodSymbol) || (ownerId != null && parent != null && !parent.contains("::") && !facts.containsKey(ownerId) && !dumpSymbols.containsKey(ownerId))) { + ownerId = parent; + } + if (ownerId != null) { + symbol.owner(resolve(ownerId)); + } else if (symbol instanceof JTypeSymbol typeSymbol && !typeSymbol.isTypeParameter()) { + symbol.owner(packageSymbol(packageName(erase(id)))); + } + if (fact != null) { + applyFact(symbol, fact); + } + if ((symbol.isMethodSymbol() || symbol.isVariableSymbol()) && (fact == null || !"IN_PROJECT".equals(text(fact, "tier")))) { + applyLibraryMember(symbol, id); + } + JsonObject details = dump.getAsJsonObject("facts"); + if (symbol.isVariableSymbol() && details != null && details.getAsJsonObject("type") != null) { + symbol.type(typeFromFact(details.getAsJsonObject("type"))); + } + if (symbol instanceof JMethodSymbol method) { + fillMethod(method, fact, id); + } else if (symbol instanceof JTypeSymbol typeSymbol && typeSymbol.isTypeParameter()) { + symbol.type(JType.typeVar(this, symbol.name(), id)); + } else if (symbol.isTypeSymbol()) { + symbol.type(rawNamed(erase(id))); + } + } + + /** Flags, annotations and thrown types the {@code types} answer gives a library member the dump names. */ + private void applyLibraryMember(JSymbol symbol, String id) { + MemberId parsed = parseMember(id); + if (parsed == null || parsed.arityOnly || ARRAY.equals(parsed.owner)) { + return; + } + String ownerName = erase(parsed.owner); + typeSymbol(ownerName); + JsonObject member = findMember(ownerName, parsed, new HashSet<>(), Match.TYPED); + if (member == null || member.has("id")) { + return; + } + applyMemberFlags(symbol, member); + libraryAnnotations(symbol, id, member); + List thrown = strings(member, "throws"); + if (symbol instanceof JMethodSymbol method && !thrown.isEmpty()) { + method.thrownTypes(thrown.stream().map(this::typeFromName).toList()); + } + } + + private JSymbol typeParameter(String id) { + int open = id.lastIndexOf('<'); + String name = open >= 0 && id.endsWith(">") ? id.substring(open + 1, id.length() - 1) : id; + JTypeSymbol symbol = new JTypeSymbol(this, id, name, true); + symbol.type(JType.typeVar(this, name, id)); + symbols.put(id, symbol); + return symbol; + } + + private JSymbol createMember(String id) { + if (!creating.add(id)) { + return unknownSymbol; + } + try { + return createMemberOnce(id); + } finally { + creating.remove(id); + } + } + + private JSymbol createMemberOnce(String id) { + MemberId parsed = parseMember(id); + if (parsed == null) { + return typeSymbol(erase(id)); + } + if (ARRAY.equals(parsed.owner) && "length".equals(parsed.name)) { + JVariableSymbol length = new JVariableSymbol(this, id, "length", "field"); + length.addModifiers(List.of("public", "final")); + length.type(primitiveType("int")); + length.owner(Symbol.ROOT_PACKAGE); + symbols.put(id, length); + return length; + } + String ownerName = erase(parsed.owner); + JTypeSymbol owner = typeSymbol(ownerName); + JsonObject member = null; + if (parsed.params != null && !parsed.arityOnly) { + member = findMember(ownerName, parsed, new HashSet<>(), Match.TYPED); + } + if (member == null) { + member = findMember(ownerName, parsed, new HashSet<>(), Match.ARITY); + } + if (member == null && parsed.params != null) { + member = findMember(ownerName, parsed, new HashSet<>(), Match.VARARGS); + } + String memberId = member == null ? null : text(member, "id"); + if (member != null && (memberId == null || !creating.contains(memberId))) { + Symbol resolved = memberFromTypeInfo(owner, member); + if (resolved instanceof JSymbol jSymbol) { + symbols.put(id, jSymbol); + return jSymbol; + } + } + if (parsed.params == null && parsed.name.contains("$lambda$")) { + JMethodSymbol lambda = new JMethodSymbol(this, id, parsed.name); + lambda.owner(owner); + lambda.lambda(true); + symbols.put(id, lambda); + return lambda; + } + if (parsed.params == null && parsed.name.contains(")$")) { + return createTypeInMethodBody(id, parsed); + } + if (parsed.params == null) { + JVariableSymbol field = new JVariableSymbol(this, id, parsed.name, "field"); + field.owner(owner); + field.unknown(true); + symbols.put(id, field); + return field; + } + JMethodSymbol method = new JMethodSymbol(this, id, parsed.name); + method.owner(parsed.arityOnly ? Symbol.TypeSymbol.UNKNOWN_TYPE : owner); + method.unknown(parsed.arityOnly); + fillMethod(method, facts.get(id), id); + if ("".equals(parsed.name) && facts.get(id) == null && member == null) { + // The implicit default constructor has the access of its class. + method.addModifiers(List.of("public", "protected", "private").stream().filter(owner::hasModifier).toList()); + } + if (method.parameterTypes().isEmpty() && !parsed.arityOnly && parsed.params != null && !parsed.params.isEmpty()) { + List types = new ArrayList<>(); + for (String param : splitParams(parsed.params)) { + types.add(typeFromName(param)); + } + method.parameterTypes(types); + } + symbols.put(id, method); + return method; + } + + /** + * The method {@code name} of {@code ownerFqn} or of one of its supertypes that Java would pick for {@code arguments}: + * applicable by subtyping first, then with boxing, then as varargs, and the most specific of those. + */ + @Nullable + Symbol.MethodSymbol declaredMethod(String ownerFqn, String name, List arguments) { + if (!declaresType(ownerFqn)) { + return null; + } + List candidates = new ArrayList<>(); + collectMethods(ownerFqn, name, new HashSet<>(), candidates); + for (int phase = 0; phase < 3; phase++) { + int current = phase; + List applicable = candidates.stream().filter(method -> applicable(method, arguments, current)).toList(); + if (!applicable.isEmpty()) { + return applicable.stream().filter(method -> applicable.stream().allMatch(other -> moreSpecific(method, other))).findFirst().orElse(applicable.get(0)); + } + } + List sameArity = candidates.stream() + .filter(method -> method.isVarArgsMethod() ? arguments.size() >= method.parameterTypes().size() - 1 : method.parameterTypes().size() == arguments.size()) + .toList(); + if (sameArity.size() == 1) { + return sameArity.get(0); + } + return null; + } + + /** ECJ leaves a call unresolved when no declaration takes this many arguments, rather than guessing one. */ + static boolean arityMatches(Symbol.MethodSymbol method, int argumentCount) { + int parameters = method.parameterTypes().size(); + return method.isVarArgsMethod() ? argumentCount >= parameters - 1 : argumentCount == parameters; + } + + /** The single abstract method of {@code type} when it is a functional interface, else null. */ + @Nullable + JMethodSymbol functionalMethod(Type type) { + String fqn = type.erasure().fullyQualifiedName(); + if (!type.isClass() || !type.symbol().isInterface() || !declaresType(fqn)) { + return null; + } + Map found = new LinkedHashMap<>(); + collectAbstractMethods(fqn, new HashSet<>(), found); + return found.size() == 1 ? found.values().iterator().next() : null; + } + + private void collectAbstractMethods(String ownerFqn, Set seen, Map found) { + if (!seen.add(ownerFqn) || !declaresType(ownerFqn)) { + return; + } + JsonObject info = typeInfos.get(sourceName(ownerFqn)); + JsonArray members = info.getAsJsonArray("members"); + if (members != null) { + for (JsonElement element : members) { + if (memberFromTypeInfo(typeSymbol(ownerFqn), element.getAsJsonObject()) instanceof JMethodSymbol method && !method.isUnknown() && method.isAbstract() + && !OBJECT_METHODS.contains(method.name() + "/" + method.parameterTypes().size())) { + found.putIfAbsent(method.name() + "/" + method.parameterTypes().size(), method); + } + } + } + parents(info).forEach(parent -> collectAbstractMethods(erase(parent), seen, found)); + } + + /** The field {@code name} that {@code ownerFqn} or one of its supertypes declares, if any. */ + @Nullable + Symbol.VariableSymbol declaredField(String ownerFqn, String name) { + return declaredField(ownerFqn, name, new HashSet<>()); + } + + @Nullable + private Symbol.VariableSymbol declaredField(String ownerFqn, String name, Set seen) { + if (!seen.add(ownerFqn) || !declaresType(ownerFqn)) { + return null; + } + JsonObject info = typeInfos.get(sourceName(ownerFqn)); + JsonArray members = info.getAsJsonArray("members"); + if (members != null) { + for (JsonElement element : members) { + JsonObject member = element.getAsJsonObject(); + MemberId id = member.has("id") ? parseMember(member.get("id").getAsString()) : null; + boolean named = id == null ? (name.equals(text(member, "name")) && "f".equals(text(member, "k"))) : (name.equals(id.name) && id.params == null); + if (named && memberFromTypeInfo(typeSymbol(ownerFqn), member) instanceof Symbol.VariableSymbol field && !field.isUnknown()) { + return field; + } + } + } + for (String parent : parents(info)) { + Symbol.VariableSymbol found = declaredField(erase(parent), name, seen); + if (found != null) { + return found; + } + } + return null; + } + + private void collectMethods(String ownerFqn, String name, Set seen, List found) { + if (!seen.add(ownerFqn) || !declaresType(ownerFqn)) { + return; + } + JsonObject info = typeInfos.get(sourceName(ownerFqn)); + JsonArray members = info.getAsJsonArray("members"); + if (members != null) { + for (JsonElement element : members) { + JsonObject member = element.getAsJsonObject(); + MemberId id = member.has("id") ? parseMember(member.get("id").getAsString()) : null; + boolean named = id == null ? (name.equals(text(member, "name")) && "m".equals(text(member, "k"))) : (name.equals(id.name) && id.params != null); + if (named && memberFromTypeInfo(typeSymbol(ownerFqn), member) instanceof Symbol.MethodSymbol method && !method.isUnknown()) { + found.add(method); + } + } + } + if (!"".equals(name)) { + parents(info).forEach(parent -> collectMethods(erase(parent), name, seen, found)); + } + } + + /** The overload that matches {@code arguments} when {@code candidate} is more specific than {@code current}. */ + Symbol.MethodSymbol preferOverload(Symbol.MethodSymbol current, @Nullable Symbol.MethodSymbol candidate, List arguments) { + if (candidate == null || candidate == current || candidate.isUnknown()) { + return current; + } + for (int phase = 0; phase < 3; phase++) { + boolean currentOk = applicable(current, arguments, phase); + boolean candidateOk = applicable(candidate, arguments, phase); + if (currentOk && !candidateOk) { + return current; + } + if (candidateOk && !currentOk) { + return candidate; + } + if (currentOk && candidateOk) { + return moreSpecific(candidate, current) && !moreSpecific(current, candidate) ? candidate : current; + } + } + return current; + } + + private static boolean applicable(Symbol.MethodSymbol method, List arguments, int phase) { + List parameters = method.parameterTypes(); + int count = parameters.size(); + boolean varargs = phase == 2 && method.isVarArgsMethod(); + if (varargs ? arguments.size() < count - 1 : arguments.size() != count) { + return false; + } + for (int i = 0; i < arguments.size(); i++) { + Type parameter = varargs && i >= count - 1 ? ((Type.ArrayType) parameters.get(count - 1)).elementType() : parameters.get(i); + if (!convertible(arguments.get(i), parameter, phase > 0)) { + return false; + } + } + return true; + } + + private static boolean convertible(Type argument, Type parameter, boolean boxing) { + Type target = parameter.erasure(); + if (argument.isUnknown() || target.isUnknown() || target.isTypeVar() || argument.erasure().isSubtypeOf(target)) { + return true; + } + if (!boxing || argument.isPrimitive() == target.isPrimitive()) { + return false; + } + Type converted = argument.isPrimitive() ? argument.primitiveWrapperType() : argument.primitiveType(); + return converted != null && converted.isSubtypeOf(target); + } + + private static boolean moreSpecific(Symbol.MethodSymbol method, Symbol.MethodSymbol other) { + List mine = method.parameterTypes(); + List theirs = other.parameterTypes(); + if (mine.size() != theirs.size()) { + return true; + } + for (int i = 0; i < mine.size(); i++) { + Type target = theirs.get(i).erasure(); + if (!target.isTypeVar() && !mine.get(i).erasure().isSubtypeOf(target)) { + return false; + } + } + return true; + } + + @Nullable + private JsonObject findMember(String ownerFqn, MemberId parsed, Set seen, Match match) { + if (!seen.add(ownerFqn)) { + return null; + } + JsonObject info = typeInfos.get(sourceName(ownerFqn)); + JsonObject found = matchMembers(info == null ? null : info.getAsJsonArray("members"), parsed, match); + if (found != null) { + return found; + } + JTypeSymbol type = typesByName.get(sourceName(ownerFqn)); + if (type != null) { + for (JsonObject member : type.memberFacts()) { + if (matches(member, parsed, match)) { + return member; + } + } + } + if (info == null || "".equals(parsed.name)) { + return null; + } + for (String parent : parents(info)) { + found = findMember(erase(parent), parsed, seen, match); + if (found != null) { + return found; + } + } + return null; + } + + @Nullable + private JsonObject matchMembers(@Nullable JsonArray members, MemberId parsed, Match match) { + if (members == null) { + return null; + } + for (JsonElement element : members) { + JsonObject member = element.getAsJsonObject(); + if (matches(member, parsed, match)) { + return member; + } + } + return null; + } + + private boolean matches(JsonObject member, MemberId parsed, Match match) { + if (member.has("id")) { + MemberId other = parseMember(member.get("id").getAsString()); + return other != null && parsed.name.equals(other.name) && parsed.arity == other.arity + && (parsed.arityOnly || other.arityOnly || Objects.equals(parsed.params, other.params)); + } + if (!parsed.name.equals(text(member, "name"))) { + return false; + } + boolean method = parsed.params != null; + if (method != "m".equals(text(member, "k"))) { + return false; + } + if (!method) { + return true; + } + String descriptor = text(member, "desc"); + if (descriptor == null) { + return false; + } + List declaredParams = parameterTypesOf(descriptor); + int declaredCount = declaredParams.size(); + return switch (match) { + case TYPED -> declaredCount == parsed.arity && sameErasures(declaredParams, splitParams(parsed.params)); + case ARITY -> declaredCount == parsed.arity; + case VARARGS -> declaredCount > 0 && parsed.arity >= declaredCount - 1 && declaredParams.get(declaredCount - 1).isArray(); + }; + } + + private static boolean sameErasures(List declared, List names) { + for (int i = 0; i < declared.size(); i++) { + String expected = declared.get(i).erasure().fullyQualifiedName().replace('$', '.'); + if (!expected.equals(erasedName(names.get(i)))) { + return false; + } + } + return true; + } + + private static String erasedName(String name) { + StringBuilder out = new StringBuilder(name.length()); + int depth = 0; + for (char current : erase(name).toCharArray()) { + if (current == '<') { + depth++; + } else if (current == '>') { + depth--; + } else if (depth == 0) { + out.append(current == '$' ? '.' : current); + } + } + return out.toString().replace("...", "[]").trim(); + } + + private enum Match { + TYPED, ARITY, VARARGS + } + + private void fillMethod(JMethodSymbol method, @Nullable JsonObject fact, String id) { + MemberId parsed = parseMember(id); + if (fact != null && fact.has("returns")) { + List returns = flat(fact, "returns"); + if (!returns.isEmpty()) { + method.returnTypeSymbol(typeFromName(returns.get(0)).symbol()); + } + } + List factParams = fact == null ? List.of() : flat(fact, "params"); + if (!factParams.isEmpty() && (parsed == null || parsed.arityOnly || factParams.size() == parsed.arity)) { + List types = new ArrayList<>(); + for (String param : factParams) { + types.add(typeFromName(param)); + } + method.parameterTypes(types); + } else if (parsed != null && !parsed.arityOnly && parsed.params != null && !parsed.params.isEmpty()) { + List types = new ArrayList<>(); + for (String param : splitParams(parsed.params)) { + types.add(typeFromName(param)); + } + method.parameterTypes(types); + } + if ("".equals(method.name())) { + method.returnTypeSymbol(primitiveSymbol("void")); + } + method.parametrized(id.indexOf('<') >= 0); + if (fact != null && fact.get("props") instanceof JsonObject props) { + List thrown = strings(props, "thrownTypes"); + if (!thrown.isEmpty()) { + method.thrownTypes(thrown.stream().map(this::typeFromName).toList()); + } + } + } + + private void applyFact(JSymbol symbol, JsonObject fact) { + String kind = text(fact, "kind"); + if (symbol instanceof JTypeSymbol type) { + boolean interfaceKind = "interface".equals(kind) || "annotation".equals(kind); + type.interfaceSymbol(interfaceKind); + type.annotationSymbol("annotation".equals(kind)); + List extend = strings(fact, "extends"); + List implement = strings(fact, "implements"); + type.enumSymbol("enum".equals(kind) || extend.stream().map(JSema::erase).anyMatch("java.lang.Enum"::equals)); + if (interfaceKind) { + type.interfaceNames(extend.isEmpty() ? implement : extend); + } else { + type.superName(extend.isEmpty() ? null : extend.get(0)); + type.interfaceNames(implement); + } + } + JsonObject props = fact.getAsJsonObject("props"); + if (props != null) { + symbol.addModifiers(flat(props, "modifiers")); + } + if (symbol instanceof JTypeSymbol) { + symbol.addModifiers(implicitTypeModifiers(kind, text(fact, "parent"))); + } + for (String annotation : strings(fact, ANNOTATIONS)) { + if ("java.lang.Deprecated".equals(erase(annotation))) { + symbol.deprecated(true); + } + } + if (symbol.isVariableSymbol() && ("enum".equals(kind) || "enum_member".equals(kind))) { + symbol.enumSymbol(true); + symbol.addModifiers(List.of("public", STATIC, "final")); + } + } + + /** + * Modifiers Java implies: member interfaces, enums, records and annotations are static; enums and records are + * final (an enum constant with a body is not modelled). + */ + private List implicitTypeModifiers(@Nullable String kind, @Nullable String parent) { + List implied = new ArrayList<>(); + boolean member = parent != null && !parent.endsWith("::") && facts.get(parent) != null + && isTypeFact(facts.get(parent)); + if (member && ("interface".equals(kind) || "enum".equals(kind) || RECORD.equals(kind) || "annotation".equals(kind))) { + implied.add(STATIC); + } + if ("enum".equals(kind) || RECORD.equals(kind)) { + implied.add("final"); + } + return implied; + } + + private static boolean isTypeFact(JsonObject fact) { + String kind = text(fact, "kind"); + return "class".equals(kind) || "interface".equals(kind) || "enum".equals(kind) || RECORD.equals(kind) + || "annotation".equals(kind) || "exception".equals(kind); + } + + private void applyTypeInfo(JTypeSymbol symbol, JsonObject info) { + String kind = text(info, "kind"); + boolean interfaceKind = "interface".equals(kind) || "annotation".equals(kind); + symbol.interfaceSymbol(interfaceKind); + symbol.annotationSymbol("annotation".equals(kind)); + symbol.enumSymbol("enum".equals(kind) || "java.lang.Enum".equals(text(info, "super"))); + if (interfaceKind) { + List extend = strings(info, "super"); + symbol.interfaceNames(extend.isEmpty() ? strings(info, "interfaces") : extend); + symbol.superName(null); + } else { + symbol.superName(text(info, "super")); + symbol.interfaceNames(strings(info, "interfaces")); + } + symbol.genericSupertypes( + info.get("superType") instanceof JsonObject superType ? superType : null, + objects(info, "interfaceTypes")); + symbol.addModifiers(flat(info, "modifiers")); + libraryAnnotations(symbol, symbol.id == null ? symbol.name() : symbol.id, info); + if (info.has("typeParameters") && !info.get("typeParameters").isJsonNull()) { + symbol.typeParameterCount(info.get("typeParameters").getAsInt()); + } + List members = new ArrayList<>(); + if (info.has("members")) { + for (JsonElement member : info.getAsJsonArray("members")) { + members.add(member.getAsJsonObject()); + } + } + symbol.memberFacts(members); + symbol.unknown(false); + } + + private static void applyMemberFlags(JSymbol symbol, JsonObject member) { + List modifiers = new ArrayList<>(); + if (truth(member, STATIC)) { + modifiers.add(STATIC); + } + if (truth(member, "final")) { + modifiers.add("final"); + } + if (truth(member, ABSTRACT)) { + modifiers.add(ABSTRACT); + } + String visibility = text(member, "vis"); + if (visibility != null && !"package".equals(visibility)) { + modifiers.add(visibility); + } + symbol.addModifiers(modifiers); + } + + private static List parents(JsonObject info) { + List parents = new ArrayList<>(); + String superName = text(info, "super"); + if (superName != null) { + parents.add(superName); + } + parents.addAll(strings(info, "interfaces")); + parents.addAll(strings(info, "super")); + return parents; + } + + private JType typeFromFact(JsonObject fact) { + return typeFromFact(fact, Map.of()); + } + + private JType typeFromFact(JsonObject fact, Map substitution) { + return typeFromFact(fact, substitution, false); + } + + private JType typeFromFact(JsonObject fact, Map substitution, boolean captureWildcard) { + String kind = text(fact, "kind"); + if (ARRAY.equals(kind) && fact.getAsJsonObject("elem") != null) { + return JType.array(this, typeFromFact(fact.getAsJsonObject("elem"), substitution, captureWildcard)); + } + if ("wildcard".equals(kind)) { + return wildcardFromFact(fact, substitution, captureWildcard); + } + if ("typevar".equals(kind)) { + String id = text(fact, "id"); + Type substituted = substitution.get(id); + if (substituted != null) { + return asJType(substituted); + } + int open = id == null ? -1 : id.lastIndexOf('<'); + String name = open >= 0 && id.endsWith(">") ? id.substring(open + 1, id.length() - 1) : id; + return JType.typeVar(this, name == null ? "" : name, id); + } + if ("unknown".equals(kind)) { + return JType.unknown(this); + } + String id = text(fact, "id"); + if (id == null) { + return JType.unknown(this); + } + List args = new ArrayList<>(); + if (fact.has("args")) { + for (JsonElement arg : fact.getAsJsonArray("args")) { + if (arg.isJsonObject()) { + args.add(typeFromFact(arg.getAsJsonObject(), substitution, captureWildcard)); + } + } + } + String erased = erase(id); + if (PRIMITIVES.contains(erased)) { + return primitiveType(erased); + } + return JType.named(this, erased, args); + } + + private JType wildcardFromFact(JsonObject fact, Map substitution, boolean capture) { + JType object = asJType(getClassType("java.lang.Object")); + JType upper = fact.get("extends") instanceof JsonObject bound ? typeFromFact(bound, substitution) : object; + if (capture) { + return JType.capture(this, upper.isUnknown() ? object : upper); + } + if (fact.get("super") instanceof JsonObject bound) { + return JType.superWildcard(this, typeFromFact(bound, substitution)); + } + return JType.wildcard(this, upper); + } + + /** A supertype fact: the loaded raw type, parameterized when the fact has type arguments. */ + Type supertypeFromFact(JsonObject fact) { + Type raw = getClassType(erase(text(fact, "id"))); + JsonArray args = fact.getAsJsonArray("args"); + if (args == null || args.isEmpty() || !(raw instanceof JType rawType)) { + return raw; + } + List resolved = new ArrayList<>(); + for (JsonElement arg : args) { + if (arg.isJsonObject()) { + resolved.add(typeFromFact(arg.getAsJsonObject())); + } + } + return JType.named(this, rawType.fullyQualifiedName(), resolved); + } + + private static List objects(JsonObject object, String key) { + if (!object.has(key) || !object.get(key).isJsonArray()) { + return List.of(); + } + List values = new ArrayList<>(); + for (JsonElement item : object.getAsJsonArray(key)) { + if (item.isJsonObject()) { + values.add(item.getAsJsonObject()); + } + } + return values; + } + + private Type typeFromName(String name) { + String erased = erase(name); + if (PRIMITIVES.contains(erased)) { + return primitiveType(erased); + } + if (erased.endsWith("[]")) { + return JType.array(this, asJType(typeFromName(erased.substring(0, erased.length() - 2)))); + } + return getClassType(erased); + } + + private JType primitiveType(String name) { + return JType.primitive(this, name); + } + + private JType asJType(Type type) { + return type instanceof JType jType ? jType : JType.unknown(this); + } + + private JType rawNamed(String fqn) { + return rawTypes.computeIfAbsent(sourceName(fqn), name -> JType.named(this, binaryNames.getOrDefault(name, name), List.of())); + } + + private List parameterTypesOf(String descriptor) { + List types = new ArrayList<>(); + int[] index = {1}; + while (index[0] < descriptor.length() && descriptor.charAt(index[0]) != ')') { + types.add(readDescriptor(descriptor, index)); + } + return types; + } + + private Type readDescriptor(String descriptor, int[] index) { + int array = 0; + while (descriptor.charAt(index[0]) == '[') { + array++; + index[0]++; + } + Type type; + char kind = descriptor.charAt(index[0]); + if (kind == 'L') { + int semi = descriptor.indexOf(';', index[0]); + String binary = descriptor.substring(index[0] + 1, semi).replace('/', '.'); + index[0] = semi + 1; + type = getClassType(binary); + } else { + index[0]++; + type = primitiveType(switch (kind) { + case 'B' -> "byte"; + case 'C' -> "char"; + case 'D' -> "double"; + case 'F' -> "float"; + case 'I' -> "int"; + case 'J' -> "long"; + case 'S' -> "short"; + case 'Z' -> "boolean"; + default -> "void"; + }); + } + for (int i = 0; i < array; i++) { + type = JType.array(this, asJType(type)); + } + return type; + } + + /** Whether the project, the classpath or the JDK declares the type {@code fqn}. */ + boolean declaresType(String name) { + String fqn = sourceName(name); + if (!typeInfos.containsKey(fqn) && typesLoaded && requestedTypes.add(fqn)) { + ingestTypes(typeLoader.apply(Set.of(fqn))); + } + return typeInfos.containsKey(fqn); + } + + /** The unknown type a written name stands for when nothing declares it. */ + JTypeSymbol recoveredType(String name) { + JTypeSymbol recovered = typeSymbol(name); + if (!recovered.isUnknown() && !declaresType(name)) { + recovered.unknown(true); + } + return recovered; + } + + Symbol packageSymbol(String name) { + if (name == null || name.isEmpty()) { + return Symbol.ROOT_PACKAGE; + } + String key = "pkg:" + name; + JSymbol found = symbols.get(key); + if (found != null) { + return found; + } + JPackageSymbol pkg = new JPackageSymbol(this, name); + symbols.put(key, pkg); + pkg.owner(Symbol.ROOT_PACKAGE); + return pkg; + } + + @Nullable + /** + * Local and anonymous classes are identified as members of their enclosing type, with the declaring + * method embedded in the member name, as in {@code com.acme.Owner#create()$Local$1}. They carry no + * declaration of their own, so the symbol is rebuilt from the identifier and owned by whatever + * declares it, which is the method for a local class and the local class for a nested anonymous one. + */ + private JSymbol createTypeInMethodBody(String id, MemberId parsed) { + int lastSegment = parsed.name.lastIndexOf('$'); + String simpleName = parsed.name.substring(lastSegment + 1); + boolean anonymous = simpleName.chars().allMatch(Character::isDigit); + JTypeSymbol symbol = new JTypeSymbol(this, id, anonymous ? "" : simpleName, false); + symbols.put(id, symbol); + typesByName.putIfAbsent(id, symbol); + symbol.owner(resolve(parsed.owner + "#" + parsed.name.substring(0, lastSegment))); + symbol.type(rawNamed(id)); + JsonObject fact = facts.get(id); + if (fact != null) { + applyFact(symbol, fact); + } + return symbol; + } + + private static MemberId parseMember(String id) { + int hash = id.indexOf('#'); + if (hash < 0) { + return null; + } + String owner = id.substring(0, hash); + String rest = id.substring(hash + 1); + int paren = rest.endsWith(")") ? rest.lastIndexOf('(') : -1; + if (paren < 0) { + return new MemberId(owner, rest, null, false, -1); + } + String name = rest.substring(0, paren); + String params = rest.substring(paren + 1, rest.length() - 1); + boolean arityOnly = !params.isEmpty() && params.chars().allMatch(Character::isDigit); + int arity = arityOnly ? Integer.parseInt(params) : (params.isEmpty() ? 0 : splitParams(params).size()); + return new MemberId(owner, name, params, arityOnly, arity); + } + + private static List splitParams(String params) { + if (params.isEmpty()) { + return List.of(); + } + List result = new ArrayList<>(); + int depth = 0; + int start = 0; + for (int i = 0; i < params.length(); i++) { + char current = params.charAt(i); + if (current == '<') { + depth++; + } else if (current == '>') { + depth--; + } else if (current == ',' && depth == 0) { + result.add(params.substring(start, i)); + start = i + 1; + } + } + result.add(params.substring(start)); + return result; + } + + static String erase(String id) { + StringBuilder out = new StringBuilder(id.length()); + for (int i = 0; i < id.length(); i++) { + char current = id.charAt(i); + if (current == '`') { + while (i + 1 < id.length() && Character.isDigit(id.charAt(i + 1))) { + i++; + } + } else { + out.append(current); + } + } + return out.toString(); + } + + private static String simpleName(String fqn) { + int boundary = Math.max(fqn.lastIndexOf('.'), fqn.lastIndexOf('$')); + return boundary < 0 ? fqn : fqn.substring(boundary + 1); + } + + private static String packageName(String fqn) { + String top = fqn; + int nested = top.indexOf('$'); + if (nested >= 0) { + top = top.substring(0, nested); + } + int dot = top.lastIndexOf('.'); + return dot < 0 ? "" : top.substring(0, dot); + } + + @Nullable + private static String text(JsonObject object, String key) { + if (object == null || !object.has(key) || object.get(key).isJsonNull()) { + return null; + } + JsonElement element = object.get(key); + return element.isJsonPrimitive() ? element.getAsString() : null; + } + + private static boolean truth(JsonObject object, String key) { + return object.has(key) && object.get(key).isJsonPrimitive() && object.get(key).getAsBoolean(); + } + + private static List strings(JsonObject object, String key) { + if (object == null || !object.has(key) || object.get(key).isJsonNull()) { + return List.of(); + } + JsonElement element = object.get(key); + if (element.isJsonPrimitive()) { + return List.of(element.getAsString()); + } + if (!element.isJsonArray()) { + return List.of(); + } + List values = new ArrayList<>(); + for (JsonElement item : element.getAsJsonArray()) { + if (item.isJsonPrimitive()) { + values.add(item.getAsString()); + } + } + return values; + } + + private static List flat(JsonObject object, String key) { + List values = new ArrayList<>(); + for (String value : strings(object, key)) { + for (String part : value.split(",")) { + if (!part.isBlank()) { + values.add(part.trim()); + } + } + } + return values; + } + + private record MemberId(String owner, String name, @Nullable String params, boolean arityOnly, int arity) { + } + + record DeclaredAnnotation(String name, List values) { } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JSymbol.java b/java-frontend/src/main/java/org/sonar/java/model/JSymbol.java index dcd11673412..1e747e409bf 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JSymbol.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JSymbol.java @@ -16,468 +16,295 @@ */ package org.sonar.java.model; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.IBinding; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; -import org.eclipse.jdt.core.dom.Modifier; -import org.sonar.java.model.declaration.ClassTreeImpl; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.Tree; +/** + * A symbol keyed by its SemSitter id. Two symbols with the same id are equal across files, + * which is what a rule needs when the declaration and the use were translated separately. + */ abstract class JSymbol implements Symbol { - protected final JSema sema; - protected final IBinding binding; - - /** - * Cache for {@link #hashCode()}. - */ - private int hashCode; - - /** - * Cache for {@link #owner()}. - */ - private Symbol owner; + enum Kind { + TYPE, METHOD, VARIABLE, PACKAGE + } - /** - * Cache for {@link #metadata()}. - */ + final JSema sema; + final Kind kind; + @Nullable + final String id; + private final String name; + private Symbol owner = Symbol.UNKNOWN_SYMBOL; + private Type type = Type.UNKNOWN; + private boolean unknown; + private boolean enumSymbol; + private boolean interfaceSymbol; + private boolean annotationSymbol; + private boolean receiver; + private boolean deprecated; + private static final String PRIVATE = "private"; + private final Set modifiers = new HashSet<>(); private SymbolMetadata metadata; + private int hash; - JSymbol(JSema sema, IBinding binding) { + JSymbol(JSema sema, Kind kind, @Nullable String id, String name) { this.sema = Objects.requireNonNull(sema); - this.binding = Objects.requireNonNull(binding); + this.kind = kind; + this.id = id; + this.name = name == null ? "" : name; } - @Override - public final boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof JSymbol)) { - return false; - } - JSymbol other = (JSymbol) obj; - if (this.binding.getKind() != other.binding.getKind()) { - return false; - } - switch (this.binding.getKind()) { - case IBinding.TYPE: - return JType.areEqual( - (ITypeBinding) this.binding, - (ITypeBinding) other.binding - ); - case IBinding.VARIABLE: - return areEqualVariables( - this, - other - ); - case IBinding.METHOD: - return areEqualMethodSymbols((JMethodSymbol) this, (JMethodSymbol) other); - default: - return super.equals(obj); - } + void owner(Symbol owner) { + this.owner = owner == null ? Symbol.UNKNOWN_SYMBOL : owner; } - private static boolean areEqualVariables(JSymbol thisVariableSymbol, JSymbol otherVariableSymbol) { - IVariableBinding thisBinding = (IVariableBinding) thisVariableSymbol.binding; - IVariableBinding otherBinding = (IVariableBinding) otherVariableSymbol.binding; - return thisBinding.getVariableId() == otherBinding.getVariableId() - && thisVariableSymbol.owner().equals(otherVariableSymbol.owner()); - } - - /** - * Method symbols are considered equal if they have the same name, same owner, same parameter types, same type parameters and same type arguments. - * In this example: - *
-   * {@code   boolean foo(T t, String s);
-   *  foo(43, "bar");}
-   * 
- * The first symbol {@code foo} has type parameter {@code T}, parameter types {@code T} and {@code String}, and no type arguments. - * The symbol {@code foo} on the second line has no type parameter, parameter types are {@code Integer} and {@code String}, and has type argument {@code Integer}. - * So the two {@code foo} symbols are not equal, although they may refer to the same method. - */ - private static boolean areEqualMethodSymbols(JMethodSymbol thisMethodSymbol, JMethodSymbol otherMethodSymbol) { - IMethodBinding thisBinding = (IMethodBinding) thisMethodSymbol.binding; - IMethodBinding otherBinding = (IMethodBinding) otherMethodSymbol.binding; - // In the case of lambdas, ecj doesn't assign a name to the different symbols, unlike the Java compiler. - // We work around that by comparing keys provided by ecj. - if (thisMethodSymbol.isLambda() && otherMethodSymbol.isLambda()) { - return Objects.equals(thisBinding.getKey(), otherBinding.getKey()); - } - return thisMethodSymbol.name().equals(otherMethodSymbol.name()) - && thisMethodSymbol.owner().equals(otherMethodSymbol.owner()) - && Arrays.equals(thisBinding.getParameterTypes(), otherBinding.getParameterTypes()) - && Arrays.equals(thisBinding.getTypeParameters(), otherBinding.getTypeParameters()) - && Arrays.equals(thisBinding.getTypeArguments(), otherBinding.getTypeArguments()); + void type(Type type) { + this.type = type == null ? Type.UNKNOWN : type; } - @Override - public final int hashCode() { - if (hashCode == 0) { - Symbol resolvedOwner = owner(); - hashCode = resolvedOwner == null ? 0 : (resolvedOwner.hashCode() * 31); - hashCode += name().hashCode(); - } - return hashCode; + void unknown(boolean unknown) { + this.unknown = unknown; } - /** - * @see JType#name() - */ - @Override - public final String name() { - if (binding.getKind() == IBinding.METHOD && ((IMethodBinding) binding).isConstructor()) { - return ""; - } - if (binding.getKind() == IBinding.TYPE && ((ITypeBinding) binding).isParameterizedType()) { - // without names of parameters - return ((ITypeBinding) binding).getErasure().getName(); - } - return binding.getName(); + void enumSymbol(boolean enumSymbol) { + this.enumSymbol = enumSymbol; } - /** - * @see #enclosingClass() - */ - @Override - public final Symbol owner() { - if (isUnknown()) { - return Symbol.UNKNOWN_SYMBOL; - } - if (owner == null) { - owner = convertOwner(); - } - return owner; + void interfaceSymbol(boolean interfaceSymbol) { + this.interfaceSymbol = interfaceSymbol; } - private Symbol convertOwner() { - switch (binding.getKind()) { - case IBinding.PACKAGE: - return Symbol.ROOT_PACKAGE; - case IBinding.TYPE: - return typeOwner((ITypeBinding) binding); - case IBinding.METHOD: - return methodOwner((IMethodBinding) binding); - case IBinding.VARIABLE: - return variableOwner((IVariableBinding) binding); - default: - throw new IllegalStateException(unexpectedBinding()); - } + void annotationSymbol(boolean annotationSymbol) { + this.annotationSymbol = annotationSymbol; } - private String unexpectedBinding() { - return "Unexpected binding Kind: " + binding.getKind(); + boolean isAnnotationSymbol() { + return annotationSymbol; } - private Symbol typeOwner(ITypeBinding typeBinding) { - if (typeBinding.isPrimitive()) { - return Symbol.ROOT_PACKAGE; - } - IMethodBinding declaringMethod = typeBinding.getDeclaringMethod(); - if (declaringMethod != null) { - // local type - return sema.methodSymbol(declaringMethod); - } - ITypeBinding declaringClass = typeBinding.getDeclaringClass(); - if (declaringClass != null) { - // member type - return sema.typeSymbol(declaringClass); - } - // top-level type - return sema.packageSymbol(typeBinding.getPackage()); + /** {@code this} and {@code super}, which have no visibility. */ + void receiver(boolean receiver) { + this.receiver = receiver; } - private Symbol methodOwner(IMethodBinding methodBinding) { - return sema.typeSymbol(methodBinding.getDeclaringClass()); + void deprecated(boolean deprecated) { + this.deprecated = deprecated; } - private Symbol variableOwner(IVariableBinding variableBinding) { - if (!variableBinding.isRecordComponent()) { - IMethodBinding declaringMethod = variableBinding.getDeclaringMethod(); - if (declaringMethod != null) { - return sema.methodSymbol(declaringMethod); - } - ITypeBinding declaringClass = variableBinding.getDeclaringClass(); - if (declaringClass != null) { - // field - return sema.typeSymbol(declaringClass); + void copyStateFrom(JSymbol other) { + owner = other.owner; + unknown = other.unknown; + enumSymbol = other.enumSymbol; + interfaceSymbol = other.interfaceSymbol; + annotationSymbol = other.annotationSymbol; + deprecated = other.deprecated; + modifiers.addAll(other.modifiers); + } + + void addModifiers(Collection values) { + for (String value : values) { + for (String part : value.split(",")) { + if (!part.isBlank()) { + modifiers.add(part.trim()); + } } } - return ownerOfRecordComponentConstant(variableBinding); } - private Symbol ownerOfRecordComponentConstant(IVariableBinding variableBinding) { - Tree node = sema.declarations.get(variableBinding); - if (node == null) { - // array.length - return Symbol.UNKNOWN_SYMBOL; + boolean hasModifier(String modifier) { + return modifiers.contains(modifier); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; } - boolean initializerBlock = false; - boolean staticInitializerBlock = false; - while (true) { - node = node.parent(); - switch (node.kind()) { - case CLASS, - RECORD, - ENUM: - JTypeSymbol typeSymbol = sema.typeSymbol(((ClassTreeImpl) node).typeBinding); - if (initializerBlock) { - return sema.initializerBlockSymbol(typeSymbol); - } - if (staticInitializerBlock) { - return sema.staticInitializerBlockSymbol(typeSymbol); - } - // Record parameters - return typeSymbol; - case INITIALIZER: - initializerBlock = true; - break; - case STATIC_INITIALIZER: - staticInitializerBlock = true; - break; - case METHOD, - CONSTRUCTOR: - // local variable declaration in recovered method - // and recovered methods do not have bindings - return MethodSymbol.UNKNOWN_METHOD; - default: - // continue exploring parent - break; - } + if (!(obj instanceof JSymbol other) || kind != other.kind) { + return false; } + if (id != null && other.id != null) { + return id.equals(other.id); + } + return name.equals(other.name) && Objects.equals(owner, other.owner); } @Override - public final Type type() { - switch (binding.getKind()) { - case IBinding.TYPE: - return sema.type((ITypeBinding) binding); - case IBinding.VARIABLE: - ITypeBinding variableType = ((IVariableBinding) binding).getType(); - return variableType != null ? sema.type(variableType) : Type.UNKNOWN; - case IBinding.PACKAGE, - IBinding.METHOD: - return Type.UNKNOWN; - default: - throw new IllegalStateException(unexpectedBinding()); + public final int hashCode() { + if (hash == 0) { + hash = Objects.hash(kind, id != null ? id : name); + if (hash == 0) { + hash = 1; + } } + return hash; + } + + @Override + public final String name() { + return name; + } + + @Override + public final Symbol owner() { + return owner; + } + + @Override + public Type type() { + return type; } @Override public final boolean isVariableSymbol() { - return binding.getKind() == IBinding.VARIABLE; + return kind == Kind.VARIABLE; } @Override public final boolean isTypeSymbol() { - return !isUnknown() && binding.getKind() == IBinding.TYPE; + return kind == Kind.TYPE; } @Override public final boolean isMethodSymbol() { - return binding.getKind() == IBinding.METHOD; + return kind == Kind.METHOD; } @Override public final boolean isPackageSymbol() { - return binding.getKind() == IBinding.PACKAGE; + return kind == Kind.PACKAGE; } @Override public final boolean isStatic() { - return Modifier.isStatic(binding.getModifiers()); + return hasModifier("static") || (inInterface() && (isVariableSymbol() || isTypeSymbol())) || enumConstant(); } @Override public final boolean isFinal() { - return Modifier.isFinal(binding.getModifiers()); + return hasModifier("final") || (inInterface() && isVariableSymbol()) || enumConstant(); + } + + /** Whether this is a member of an interface or annotation type, which gives it implicit modifiers. */ + private boolean inInterface() { + return owner instanceof JSymbol ownerType && ownerType.isTypeSymbol() && ownerType.interfaceSymbol + && !(this instanceof JTypeSymbol typeSymbol && typeSymbol.isTypeParameter()); + } + + private boolean enumConstant() { + return isVariableSymbol() && enumSymbol; + } + + /** Enum constructors are private whatever they declare. */ + private boolean enumConstructor() { + return isMethodSymbol() && "".equals(name) && owner instanceof JSymbol ownerType && ownerType.isTypeSymbol() && ownerType.enumSymbol; } @Override public final boolean isEnum() { - switch (binding.getKind()) { - case IBinding.TYPE: - return ((ITypeBinding) binding).isEnum(); - case IBinding.VARIABLE: - return ((IVariableBinding) binding).isEnumConstant(); - default: - return false; - } + return enumSymbol; } @Override public final boolean isInterface() { - return binding.getKind() == IBinding.TYPE - && ((ITypeBinding) binding).isInterface(); + return interfaceSymbol; } @Override public final boolean isAbstract() { - return Modifier.isAbstract(binding.getModifiers()); + if (interfaceSymbol && isTypeSymbol()) { + return false; + } + return hasModifier("abstract") + || (inInterface() && isMethodSymbol() && !hasModifier("default") && !hasModifier("static") && !hasModifier(PRIVATE)); } @Override public final boolean isPublic() { - return Modifier.isPublic(binding.getModifiers()); + return hasModifier("public") || (inInterface() && !hasModifier(PRIVATE)) || enumConstant(); } @Override public final boolean isPrivate() { - return Modifier.isPrivate(binding.getModifiers()); + return hasModifier(PRIVATE) || enumConstructor(); } @Override public final boolean isProtected() { - return Modifier.isProtected(binding.getModifiers()); + return hasModifier("protected"); } @Override public final boolean isPackageVisibility() { - return !isPublic() && !isPrivate() && !isProtected(); + return !receiver && !isPublic() && !isPrivate() && !isProtected(); } @Override public final boolean isDeprecated() { - return binding.isDeprecated(); + return deprecated; } @Override public final boolean isVolatile() { - return Modifier.isVolatile(binding.getModifiers()); + return hasModifier("volatile"); } @Override - public final boolean isUnknown() { - return binding.isRecovered(); + public boolean isUnknown() { + return unknown; } @Override public final SymbolMetadata metadata() { if (metadata == null) { - try { - metadata = convertMetadata(); - } catch (RuntimeException e) { - // ECJ raises exception in rare occasions, when it is the case, we don't want to prevent the whole analysis of the file - // ECJ 3.41.0: no longer throws the exception on the code that was used in the test - metadata = Symbols.EMPTY_METADATA; - } + metadata = new JSymbolMetadata(this); } return metadata; } - private SymbolMetadata convertMetadata() { - switch (binding.getKind()) { - case IBinding.PACKAGE: - return new JSymbolMetadata(sema, this, sema.resolvePackageAnnotations(binding.getName())); - case IBinding.VARIABLE: - ITypeBinding type = ((IVariableBinding) binding).getType(); - if (type == null) { - return new JSymbolMetadata(sema, this, binding.getAnnotations()); - } - return convertMetadata(type); - case IBinding.METHOD: - ITypeBinding returnType = ((IMethodBinding) binding).getReturnType(); - // In rare circumstances, when the semantic information is incomplete, returnType can be null. - if (returnType == null) { - return Symbols.EMPTY_METADATA; - } - return convertMetadata(returnType); - default: - return new JSymbolMetadata(sema, this, binding.getAnnotations()); - } - } - - private SymbolMetadata convertMetadata(ITypeBinding type) { - return JSymbolMetadata.of(sema, this, type, binding.getAnnotations()); - } - - /** - * @see #owner() - */ @Nullable @Override - public final TypeSymbol enclosingClass() { - switch (binding.getKind()) { - case IBinding.PACKAGE: - return null; - case IBinding.TYPE: - return typeEnclosingClass((ITypeBinding) binding); - case IBinding.METHOD: - return methodEnclosingClass((IMethodBinding) binding); - case IBinding.VARIABLE: - return variableEnclosingClass((IVariableBinding) binding); - default: - throw new IllegalStateException(unexpectedBinding()); - } - } - - private TypeSymbol typeEnclosingClass(ITypeBinding typeBinding) { - ITypeBinding declaringClass = typeBinding.getDeclaringClass(); - if (declaringClass != null) { - // nested (member or local) type - return sema.typeSymbol(declaringClass); + public TypeSymbol enclosingClass() { + if (this instanceof JTypeSymbol typeSymbol && typeSymbol.isTypeParameter()) { + return owner() instanceof TypeSymbol ownerType ? ownerType : null; } - // top-level type - return (TypeSymbol) this; - } - - private TypeSymbol methodEnclosingClass(IMethodBinding methodBinding) { - ITypeBinding declaringClass = methodBinding.getDeclaringClass(); - return sema.typeSymbol(declaringClass); - } - - private TypeSymbol variableEnclosingClass(IVariableBinding variableBinding) { - ITypeBinding declaringClass = variableBinding.getDeclaringClass(); - if (declaringClass != null) { - // field - return sema.typeSymbol(declaringClass); - } - IMethodBinding declaringMethod = variableBinding.getDeclaringMethod(); - if (declaringMethod != null) { - // local variable - return sema.typeSymbol(declaringMethod.getDeclaringClass()); - } - Tree node = sema.declarations.get(variableBinding); - if (node == null) { - // array.length - return TypeSymbol.UNKNOWN_TYPE; - } - do { - node = node.parent(); - switch (node.kind()) { - case CLASS, - RECORD, - ENUM: - // variable declaration in a static or instance initializer - // or local variable declaration in recovered method - return sema.typeSymbol(((ClassTreeImpl) node).typeBinding); - default: - // continue exploring parent - break; + Symbol current = this; + while (current != null && !current.isTypeSymbol()) { + if (current.isPackageSymbol() || current.isUnknown()) { + return null; } - } while (true); + current = current.owner(); + } + return (TypeSymbol) current; } @Override public final List usages() { - List usages = sema.usages.get(JSema.declarationBinding(binding)); - return usages != null ? usages : Collections.emptyList(); + if (id == null) { + return Collections.emptyList(); + } + List found = sema.usages.get(id); + return found == null ? Collections.emptyList() : found; } @Nullable @Override public Tree declaration() { - return sema.declarations.get(JSema.declarationBinding(binding)); + return id == null ? null : sema.declarations.get(id); } + @Override + public String toString() { + return id == null ? name : id; + } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadata.java b/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadata.java index 2b9c5b6b862..2098879108a 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadata.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadata.java @@ -16,106 +16,50 @@ */ package org.sonar.java.model; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.IAnnotationBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; +import org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationInstance; +import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityData; +import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityLevel; +import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityTarget; +import org.sonar.plugins.java.api.semantic.SymbolMetadata.NullabilityType; +import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.NewArrayTree; import org.sonar.plugins.java.api.tree.Tree; -import static org.sonar.java.model.JSymbolMetadataNullabilityHelper.getNullabilityDataAtLevel; - +/** + * Annotations of one symbol, taken from its declaration tree when the file contains it and from + * the graph fact otherwise. Nullability walks those annotations and then overridden methods. + */ final class JSymbolMetadata implements SymbolMetadata { private static final NullabilityData[] NO_ANNOTATION_NULLABILITY = forEachLevel(level -> new JNullabilityData(NullabilityType.NO_ANNOTATION, level, null, null, false)); - private static final NullabilityData[] UNKNOWN_NULLABILITY = forEachLevel(level -> new JNullabilityData(NullabilityType.UNKNOWN, level, null, null, false)); - private final JSema sema; - private final Symbol symbol; - private final IAnnotationBinding[] symbolBindings; - private final SymbolMetadata[] parameterMetas; - private final IAnnotationBinding[] annotationBindings; - - /** - * Cache for {@link #annotations()}. - */ + private final JSymbol symbol; private List annotations; - - /** - * Cache for {@link #symbolAnnotations()}. - */ - private List symbolAnnotations; - private final Map nullabilityCache = new EnumMap<>(NullabilityTarget.class); - private final boolean isTypeArgMetadata; - - JSymbolMetadata(JSema sema, Symbol symbol, IAnnotationBinding[] annotationBindings) { - this.sema = Objects.requireNonNull(sema); + JSymbolMetadata(JSymbol symbol) { this.symbol = symbol; - this.symbolBindings = annotationBindings; - this.parameterMetas = new SymbolMetadata[0]; - this.isTypeArgMetadata = false; - - this.annotationBindings = annotationBindings; - } - - private JSymbolMetadata(JSema sema, Symbol symbol, IAnnotationBinding[] symbolAnnotations, JSymbolMetadata[] parameterMetaDatas, boolean isTypeArgMetadata) { - this.sema = Objects.requireNonNull(sema); - this.symbol = symbol; - this.symbolBindings = symbolAnnotations; - this.parameterMetas = parameterMetaDatas; - this.isTypeArgMetadata = isTypeArgMetadata; - - int total = symbolBindings.length; - for (JSymbolMetadata parameterMetaData : parameterMetaDatas) { - total += parameterMetaData.annotationBindings.length; - } - - this.annotationBindings = new IAnnotationBinding[total]; - int current = symbolAnnotations.length; - System.arraycopy(symbolAnnotations, 0, this.annotationBindings, 0, symbolAnnotations.length); - - for (JSymbolMetadata parameterMetaData : parameterMetaDatas) { - var paramBindings = parameterMetaData.annotationBindings; - System.arraycopy(paramBindings, 0, this.annotationBindings, current, paramBindings.length); - current += paramBindings.length; - } - } - - public static JSymbolMetadata of(JSema sema, Symbol owner, ITypeBinding type, IAnnotationBinding[] externalAnnotations) { - var symbolAnnotations = new IAnnotationBinding[externalAnnotations.length + type.getTypeAnnotations().length]; - System.arraycopy(externalAnnotations, 0, symbolAnnotations, 0, externalAnnotations.length); - System.arraycopy(type.getTypeAnnotations(), 0, symbolAnnotations, externalAnnotations.length, type.getTypeAnnotations().length); - - var parameterMetaDatas = Arrays.stream(type.getTypeArguments()) - .map(param -> ofTypeArg(sema, owner, param)) - .toArray(JSymbolMetadata[]::new); - - return new JSymbolMetadata(sema, owner, symbolAnnotations, parameterMetaDatas, false); - } - - private static JSymbolMetadata ofTypeArg(JSema sema, Symbol owner, ITypeBinding param) { - var symbolAnnotations = param.getTypeAnnotations(); - var parameterMetaDatas = Arrays.stream(param.getTypeArguments()) - .map(p -> ofTypeArg(sema, owner, p)) - .toArray(JSymbolMetadata[]::new); - return new JSymbolMetadata(sema, owner, symbolAnnotations, parameterMetaDatas, true); } private static NullabilityData[] forEachLevel(Function initializer) { @@ -133,27 +77,36 @@ public static NullabilityData unknownNullabilityAt(NullabilityLevel level) { @Override public List annotations() { if (annotations == null) { - annotations = Arrays.stream(annotationBindings) - .map(sema::annotation) - .collect(Collectors.toList()); + annotations = loadAnnotations(); } return annotations; } @Override public List symbolAnnotations() { - if (symbolAnnotations == null) { - symbolAnnotations = Arrays.stream(symbolBindings) - .map(sema::annotation) - .collect(Collectors.toList()); + return annotations(); + } + + private List loadAnnotations() { + Tree declaration = symbol.declaration(); + if (declaration != null) { + List fromTree = new ArrayList<>(); + for (AnnotationTree annotation : ModifiersUtils.getAnnotations(declaration)) { + fromTree.add(new TreeAnnotation(symbol.sema, annotation)); + } + return fromTree; } - return symbolAnnotations; + List fromFacts = new ArrayList<>(); + for (JSema.DeclaredAnnotation annotation : symbol.sema.declaredAnnotations(symbol.id)) { + fromFacts.add(new FactAnnotation(symbol.sema, JSema.erase(annotation.name()), annotation.values())); + } + return fromFacts; } @Override - public final boolean isAnnotatedWith(String fullyQualifiedName) { - for (AnnotationInstance a : annotations()) { - if (a.symbol().type().is(fullyQualifiedName)) { + public boolean isAnnotatedWith(String fullyQualifiedName) { + for (AnnotationInstance annotation : annotations()) { + if (annotation.symbol().type().is(fullyQualifiedName)) { return true; } } @@ -163,10 +116,9 @@ public final boolean isAnnotatedWith(String fullyQualifiedName) { @Nullable @Override public List valuesForAnnotation(String fullyQualifiedNameOfAnnotation) { - for (AnnotationInstance a : annotations()) { - if (a.symbol().type().is(fullyQualifiedNameOfAnnotation)) { - // TODO what about repeating annotations? - return a.values(); + for (AnnotationInstance annotation : annotations()) { + if (annotation.symbol().type().is(fullyQualifiedNameOfAnnotation)) { + return annotation.values(); } } return null; @@ -174,7 +126,7 @@ public List valuesForAnnotation(String fullyQualifiedNameOfAnno @Override public NullabilityData nullabilityData() { - NullabilityTarget target = getTarget(symbol); + NullabilityTarget target = target(symbol); if (target == null) { return unknownNullabilityAt(NullabilityLevel.UNKNOWN); } @@ -190,128 +142,101 @@ public NullabilityData nullabilityData(NullabilityTarget target) { @Override public AnnotationTree findAnnotationTree(AnnotationInstance annotationInstance) { Tree declaration = symbol.declaration(); - if (declaration != null) { - return findAnnotationTree(ModifiersUtils.getAnnotations(declaration), annotationInstance); + if (declaration == null) { + return null; + } + for (AnnotationTree annotation : ModifiersUtils.getAnnotations(declaration)) { + if (annotation.annotationType().symbolType().symbol().equals(annotationInstance.symbol()) + && annotation.arguments().size() == annotationInstance.values().size()) { + return annotation; + } } return null; } @Override public SymbolMetadata[] parametersMetadata() { - return parameterMetas; - } - - @Nullable - private static AnnotationTree findAnnotationTree(List annotations, AnnotationInstance annotationInstance) { - for (AnnotationTree annotationTree : annotations) { - if (annotationTree.symbolType().symbol() == annotationInstance.symbol() && - annotationTree.arguments().size() == annotationInstance.values().size()) { - return annotationTree; - } - } - return null; + return new SymbolMetadata[0]; } private NullabilityData resolveNullability(NullabilityTarget target) { - NullabilityLevel currentLevel = getLevel(symbol); - NullabilityData nullabilityDataAtLevel = getNullabilityDataAtLevel(this, target, currentLevel); - if (nullabilityDataAtLevel.type() != NullabilityType.NO_ANNOTATION) { - return nullabilityDataAtLevel; - } - - // Check nullability from the inheritance hierarchy. - // Skipped for type argument metadata: type arg nullability must not inherit from the enclosing - // method's return type annotations, which belong to the outer type, not the type argument. - if (symbol.isMethodSymbol() && !isTypeArgMetadata) { - NullabilityData nullabilityDataFromInheritance = getNullabilityDataFromInheritance((Symbol.MethodSymbol) symbol, target); - if (nullabilityDataFromInheritance.type() != NullabilityType.NO_ANNOTATION) { - return nullabilityDataFromInheritance; + NullabilityLevel level = level(symbol); + NullabilityData atLevel = JSymbolMetadataNullabilityHelper.getNullabilityDataAtLevel(this, target, level); + if (atLevel.type() != NullabilityType.NO_ANNOTATION) { + return atLevel; + } + if (symbol.isMethodSymbol()) { + NullabilityData inherited = inheritedNullability((Symbol.MethodSymbol) symbol, target); + if (inherited.type() != NullabilityType.NO_ANNOTATION) { + return inherited; } } - - // Not annotated or meta annotated, check upper level in the ownership hierarchy... if (symbol.isPackageSymbol()) { - return NO_ANNOTATION_NULLABILITY[currentLevel.ordinal()]; + return NO_ANNOTATION_NULLABILITY[level.ordinal()]; } - Symbol owner = getEffectiveOwner(symbol, currentLevel); - if (owner == null) return unknownNullabilityAt(currentLevel); - var metadata = owner.metadata(); - return metadata.nullabilityData(target); - } - - private static NullabilityData getNullabilityDataFromInheritance(Symbol.MethodSymbol methodSymbol, NullabilityTarget target) { - List overriddenSymbols = methodSymbol.overriddenSymbols(); - NullabilityLevel level = NullabilityLevel.METHOD; - for (Symbol.MethodSymbol overriddenSymbol : overriddenSymbols) { - SymbolMetadata metadata = overriddenSymbol.metadata(); - NullabilityData nullabilityData = getNullabilityDataAtLevel(metadata, target, level); - if (nullabilityData.type() != NullabilityType.NO_ANNOTATION && !nullabilityData.equals(unknownNullabilityAt(level))) { - return nullabilityData; - } + Symbol owner = symbol.owner(); + if (owner == null || owner.isUnknown()) { + return unknownNullabilityAt(level); } - return NO_ANNOTATION_NULLABILITY[level.ordinal()]; + return owner.metadata().nullabilityData(target); } - @CheckForNull - private static Symbol getEffectiveOwner(Symbol symbol, NullabilityLevel currentLevel) { - Symbol owner = symbol.owner(); - if (owner != null && owner.isMethodSymbol() && currentLevel == NullabilityLevel.CLASS) { - // Owner of anonymous class can be a method symbol, but the nullability of the method is unrelated to the ones from the anonymous classes. - return getEffectiveOwner(owner, currentLevel); + private static NullabilityData inheritedNullability(Symbol.MethodSymbol method, NullabilityTarget target) { + for (Symbol.MethodSymbol overridden : method.overriddenSymbols()) { + NullabilityData data = JSymbolMetadataNullabilityHelper.getNullabilityDataAtLevel(overridden.metadata(), target, NullabilityLevel.METHOD); + if (data.type() != NullabilityType.NO_ANNOTATION && !data.equals(unknownNullabilityAt(NullabilityLevel.METHOD))) { + return data; + } } - return owner; + return NO_ANNOTATION_NULLABILITY[NullabilityLevel.METHOD.ordinal()]; } - private static NullabilityLevel getLevel(Symbol symbol) { + private static NullabilityLevel level(Symbol symbol) { if (symbol.isVariableSymbol()) { return NullabilityLevel.VARIABLE; - } else if (symbol.isMethodSymbol()) { + } + if (symbol.isMethodSymbol()) { return NullabilityLevel.METHOD; - } else if (symbol.isTypeSymbol()) { + } + if (symbol.isTypeSymbol()) { return NullabilityLevel.CLASS; - } else if (symbol.isPackageSymbol()) { + } + if (symbol.isPackageSymbol()) { return NullabilityLevel.PACKAGE; } return NullabilityLevel.UNKNOWN; } @CheckForNull - private static NullabilityTarget getTarget(Symbol symbol) { + private static NullabilityTarget target(Symbol symbol) { if (symbol.isMethodSymbol()) { return NullabilityTarget.METHOD; - } else if (symbol.isVariableSymbol()) { - Symbol owner = symbol.owner(); - if (owner != null && owner.isTypeSymbol()) { - return NullabilityTarget.FIELD; - } - Tree variableTree = symbol.declaration(); - if (variableTree == null) { - return NullabilityTarget.PARAMETER; - } - Tree variableTreeParent = variableTree.parent(); - if (variableTreeParent == null || variableTreeParent instanceof MethodTree) { - return NullabilityTarget.PARAMETER; - } - // If variableTreeParent is not a lambda, we consider that the target is a local variable. - if (!variableTreeParent.is(Tree.Kind.LAMBDA_EXPRESSION)) { - return NullabilityTarget.LOCAL_VARIABLE; - } - // When variableTreeParent is an instance of LambdaExpressionTree we intentionally return null - // because parameters of lambdas are not supported. } - return null; + if (!symbol.isVariableSymbol()) { + return null; + } + Symbol owner = symbol.owner(); + if (owner != null && owner.isTypeSymbol()) { + return NullabilityTarget.FIELD; + } + Tree declaration = symbol.declaration(); + if (declaration == null || declaration.parent() == null || declaration.parent() instanceof MethodTree) { + return NullabilityTarget.PARAMETER; + } + if (declaration.parent().is(Tree.Kind.LAMBDA_EXPRESSION)) { + return null; + } + return NullabilityTarget.LOCAL_VARIABLE; } static final class JNullabilityData implements NullabilityData { private final NullabilityType type; private final NullabilityLevel level; - @Nullable private final AnnotationInstance annotation; @Nullable private final AnnotationTree annotationTree; - private final boolean metaAnnotation; public JNullabilityData(NullabilityType type, NullabilityLevel level, @Nullable AnnotationInstance annotation, @@ -340,29 +265,28 @@ public boolean metaAnnotation() { @Override public boolean isNonNull(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue) { - return testNullabilityType(minLevel, ignoreMetaAnnotation, defaultValue, t -> t == NullabilityType.NON_NULL); + return test(minLevel, ignoreMetaAnnotation, defaultValue, candidate -> candidate == NullabilityType.NON_NULL); } @Override public boolean isNullable(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue) { - return testNullabilityType(minLevel, ignoreMetaAnnotation, defaultValue, - t -> t == NullabilityType.STRONG_NULLABLE || t == NullabilityType.WEAK_NULLABLE); + return test(minLevel, ignoreMetaAnnotation, defaultValue, + candidate -> candidate == NullabilityType.STRONG_NULLABLE || candidate == NullabilityType.WEAK_NULLABLE); } @Override public boolean isStrongNullable(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue) { - return testNullabilityType(minLevel, ignoreMetaAnnotation, defaultValue, t -> t == NullabilityType.STRONG_NULLABLE); + return test(minLevel, ignoreMetaAnnotation, defaultValue, candidate -> candidate == NullabilityType.STRONG_NULLABLE); } - private boolean testNullabilityType(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue, Predicate typePredicate) { + private boolean test(NullabilityLevel minLevel, boolean ignoreMetaAnnotation, boolean defaultValue, Predicate typePredicate) { if (type == NullabilityType.NO_ANNOTATION) { return false; - } else if (type == NullabilityType.UNKNOWN || (ignoreMetaAnnotation && metaAnnotation)) { + } + if (type == NullabilityType.UNKNOWN || (ignoreMetaAnnotation && metaAnnotation)) { return defaultValue; - } else if (typePredicate.test(type)) { - return minLevel.ordinal() <= level.ordinal(); } - return false; + return typePredicate.test(type) && minLevel.ordinal() <= level.ordinal(); } @Nullable @@ -378,50 +302,96 @@ public Tree declaration() { } } - static final class JAnnotationInstance implements AnnotationInstance { - private final JSema sema; - private final IAnnotationBinding annotationBinding; + private static final class TreeAnnotation implements AnnotationInstance { - /** - * Cache for {@link #values()}. - */ - private List values; + private final JSema sema; + private final AnnotationTree tree; - JAnnotationInstance(JSema sema, IAnnotationBinding annotationBinding) { + private TreeAnnotation(JSema sema, AnnotationTree tree) { this.sema = sema; - this.annotationBinding = annotationBinding; + this.tree = tree; } @Override public Symbol symbol() { - return sema.typeSymbol(annotationBinding.getAnnotationType()); + Type type = tree.annotationType().symbolType(); + if (type.isUnknown() && tree.symbolType().isUnknown() && !"!Unknown!".equals(tree.symbolType().fullyQualifiedName())) { + // an unresolved annotation still carries its written name + type = tree.symbolType(); + } + return type.symbol(); } @Override public List values() { - if (values == null) { - values = Arrays.stream(annotationBinding.getDeclaredMemberValuePairs()) - .map(p -> new AnnotationValueImpl(p.getName(), convertAnnotationValue(p.getValue()))) - .collect(Collectors.toList()); + List values = new ArrayList<>(); + for (ExpressionTree argument : tree.arguments()) { + if (argument instanceof AssignmentExpressionTree assignment) { + String name = assignment.variable().toString(); + values.add(new AnnotationValueImpl(name, value(name, assignment.expression()))); + } else { + values.add(new AnnotationValueImpl("value", value("value", argument))); + } } return values; } - private Object convertAnnotationValue(Object value) { - return switch (value) { - case IVariableBinding iVariableBinding -> sema.variableSymbol(iVariableBinding); - case ITypeBinding iTypeBinding -> sema.typeSymbol(iTypeBinding); - case IAnnotationBinding iAnnotationBinding -> sema.annotation(iAnnotationBinding); - case Object[] a -> { - Object[] result = new Object[a.length]; - for (int i = 0; i < a.length; i++) { - result[i] = convertAnnotationValue(a[i]); - } - yield result; - } - case null, default -> value; - }; + /** + * The value of the member {@code name}, wrapped in an array when the member is array typed, as the value of an + * array typed member is always an array no matter how many elements the annotation spells out. + */ + private Object value(String name, ExpressionTree expression) { + Object value = literal(expression); + return value instanceof Object[] || !isArrayTyped(name) ? value : new Object[] {value}; + } + + private boolean isArrayTyped(String name) { + return symbol() instanceof Symbol.TypeSymbol annotation + && annotation.lookupSymbols(name).stream() + .anyMatch(member -> member instanceof Symbol.MethodSymbol method && method.returnType().type().isArray()); + } + + private Object literal(ExpressionTree expression) { + if (expression instanceof LiteralTree literal) { + return LiteralUtils.constantValue(literal); + } + if (expression instanceof AnnotationTree annotation) { + return new TreeAnnotation(sema, annotation); + } + if (expression instanceof NewArrayTree array) { + return array.initializers().stream().map(this::literal).toArray(); + } + if (expression instanceof MemberSelectExpressionTree select && "class".equals(select.identifier().name())) { + return select.expression().symbolType().symbol(); + } + Symbol symbol = expression instanceof MemberSelectExpressionTree select ? select.identifier().symbol() + : (expression instanceof IdentifierTree identifier ? identifier.symbol() : null); + if (symbol instanceof Symbol.VariableSymbol variable && !variable.isUnknown()) { + return variable.constantValue().orElse(variable); + } + return expression.toString(); } + } + private static final class FactAnnotation implements AnnotationInstance { + + private final Symbol symbol; + private final List values; + + private FactAnnotation(JSema sema, String fqn, List values) { + this.symbol = sema.typeSymbol(fqn); + this.values = values; + } + + @Override + public Symbol symbol() { + return symbol; + } + + @Override + public List values() { + return values; + } + } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JType.java b/java-frontend/src/main/java/org/sonar/java/model/JType.java index ee13c72eebe..475ed1da8a6 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JType.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JType.java @@ -16,51 +16,158 @@ */ package org.sonar.java.model; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; -import java.util.TreeSet; +import java.util.Set; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; +/** + * A type recovered from a SemSitter symbol fact or a JVM descriptor. + * Fully-qualified names are erased binary names ({@code java.util.Map$Entry}). + */ final class JType implements Type, Type.ArrayType { - private static final Logger LOG = LoggerFactory.getLogger(JType.class); + enum Kind { + NAMED, PRIMITIVE, VOID, NULL, ARRAY, TYPEVAR, UNKNOWN, INTERSECTION, CAPTURE, WILDCARD + } + + private static final Set NUMERIC = Set.of("byte", "char", "short", "int", "long", "float", "double"); final JSema sema; - final ITypeBinding typeBinding; + final Kind kind; + final String fqn; + final List args; + @Nullable + final JType element; + @Nullable + final String typeVarId; + private final List intersection; + @Nullable + private final JType lower; + + private JType(JSema sema, Kind kind, String fqn, List args, @Nullable JType element, @Nullable String typeVarId, List intersection) { + this(sema, kind, fqn, args, element, typeVarId, intersection, null); + } + + private JType(JSema sema, Kind kind, String fqn, List args, @Nullable JType element, @Nullable String typeVarId, List intersection, + @Nullable JType lower) { + this.sema = sema; + this.kind = kind; + this.fqn = fqn; + this.args = args; + this.element = element; + this.typeVarId = typeVarId; + this.intersection = intersection; + this.lower = lower; + } - private final String fullyQualifiedName; + static JType named(JSema sema, String fqn, List args) { + return new JType(sema, Kind.NAMED, fqn, List.copyOf(args), null, null, List.of()); + } - /** - * Cache for {@link #primitiveWrapperType()}. - */ - private Type primitiveWrapperType; + static JType primitive(JSema sema, String name) { + return new JType(sema, "void".equals(name) ? Kind.VOID : Kind.PRIMITIVE, name, List.of(), null, null, List.of()); + } - /** - * Cache for {@link #primitiveType()}. - */ - private Type primitiveType; + static JType nullType(JSema sema) { + return new JType(sema, Kind.NULL, "", List.of(), null, null, List.of()); + } - /** - * Cache for {@link #declaringType()}. - */ - private Type declaringType; + static JType unknown(JSema sema) { + return new JType(sema, Kind.UNKNOWN, "!Unknown!", List.of(), null, null, List.of()); + } - /** - * Cache for {@link #typeArguments()}. - */ - private List typeArguments; + static JType array(JSema sema, JType element) { + return new JType(sema, Kind.ARRAY, element.fullyQualifiedName() + "[]", List.of(), element, null, List.of()); + } - JType(JSema sema, ITypeBinding typeBinding) { - this.sema = Objects.requireNonNull(sema); - this.typeBinding = Objects.requireNonNull(typeBinding); - this.fullyQualifiedName = fullyQualifiedName(typeBinding); + static JType typeVar(JSema sema, String name, String id) { + return new JType(sema, Kind.TYPEVAR, name, List.of(), null, id, List.of()); + } + + /** Replaces type variables named in {@code substitution} inside this type. */ + JType substitute(Map substitution) { + if (substitution.isEmpty()) { + return this; + } + if (kind == Kind.TYPEVAR) { + Type found = typeVarId == null ? null : substitution.get(typeVarId); + if (found == null) { + found = substitution.get(fqn); + } + if (found instanceof JType replaced) { + return replaced; + } + } + if (kind == Kind.CAPTURE && element != null) { + JType replaced = element.substitute(substitution); + return replaced == element ? this : capture(sema, replaced); + } + if (kind == Kind.WILDCARD && lower != null) { + JType replaced = lower.substitute(substitution); + return replaced == lower ? this : superWildcard(sema, replaced); + } + if (kind == Kind.WILDCARD && element != null) { + JType replaced = element.substitute(substitution); + return replaced == element ? this : wildcard(sema, replaced); + } + if (kind == Kind.ARRAY && element != null) { + JType replaced = element.substitute(substitution); + return replaced == element ? this : array(sema, replaced); + } + if (kind == Kind.NAMED && !args.isEmpty()) { + List replaced = new ArrayList<>(); + boolean changed = false; + for (JType argument : args) { + JType next = argument.substitute(substitution); + changed |= next != argument; + replaced.add(next); + } + return changed ? named(sema, fqn, replaced) : this; + } + return this; + } + + static JType capture(JSema sema, JType upper) { + return new JType(sema, Kind.CAPTURE, "!capture!", List.of(), upper, null, List.of()); + } + + /** A wildcard type argument, named after the erasure of its upper bound as ECJ names it. */ + static JType wildcard(JSema sema, JType upper) { + if (upper.isUnknown()) { + return upper; + } + return new JType(sema, Kind.WILDCARD, upper.erasure().fullyQualifiedName(), List.of(), upper, null, List.of()); + } + + /** A {@code ? super lower} type argument, which ECJ names after its implicit upper bound {@code Object}. */ + static JType superWildcard(JSema sema, JType lower) { + JType object = sema.getClassType("java.lang.Object") instanceof JType type ? type : unknown(sema); + return new JType(sema, Kind.WILDCARD, object.fullyQualifiedName(), List.of(), object, null, List.of(), lower); + } + + boolean isWildcard() { + return kind == Kind.WILDCARD; + } + + /** The type a lambda sees for this type argument: the bound a wildcard is written with, or the type itself. */ + JType groundType() { + if (kind != Kind.WILDCARD) { + return this; + } + return lower != null ? lower : element; + } + + static JType intersection(JSema sema, List bounds) { + String name = bounds.stream().map(JType::fullyQualifiedName).sorted().reduce((a, b) -> a + " & " + b).orElse("!Unknown!"); + return new JType(sema, Kind.INTERSECTION, name, List.of(), null, null, List.copyOf(bounds)); } @Override @@ -75,52 +182,114 @@ public boolean isSubtypeOf(String fullyQualifiedName) { @Override public boolean isSubtypeOf(Type superType) { - return !superType.isUnknown() - && isSubtype(this.typeBinding, ((JType) superType).typeBinding); + return isSubtypeOf(superType, new HashSet<>()); } - private static boolean isSubtype(ITypeBinding left, ITypeBinding right) { - if (left.isRecovered()) { + private boolean isSubtypeOf(Type superType, Set visited) { + if ((kind == Kind.CAPTURE || kind == Kind.WILDCARD) && element != null) { + return element.isSubtypeOf(superType, visited); + } + if (superType == null || superType.isUnknown() || isUnknown()) { + return false; + } + if (isNullType()) { + return !superType.isPrimitive(); + } + if (!visited.add(fullyQualifiedName() + ">" + superType.fullyQualifiedName())) { + return false; + } + if (sameErasure(superType) && argumentsMatch(superType)) { + return true; + } + if (isPrimitive() && superType.isPrimitive()) { + return widensTo(fullyQualifiedName(), superType.fullyQualifiedName()); + } + if (isArray()) { + if (superType.isArray()) { + return elementType().isSubtypeOf(((Type.ArrayType) superType).elementType()); + } + return superType.is("java.lang.Object") || superType.is("java.lang.Cloneable") || superType.is("java.io.Serializable"); + } + if (isPrimitive() || superType.isPrimitive() || isTypeVar()) { return false; } - if (left.isNullType()) { - return !right.isPrimitive(); + Symbol.TypeSymbol owner = declaredSymbol(); + if (owner.isUnknown()) { + return false; + } + // Supertypes are walked erased: their arguments name the owner's type variables, not this type's arguments. + Type superClass = owner.superClass(); + if (superClass != null && ((JType) superClass.erasure()).isSubtypeOf(superType, visited)) { + return true; } - try { - return left.isSubTypeCompatible(right); - } catch (NullPointerException ex) { - // In rare circumstances, ECJ may produce a NPE while calling isSubTypeCompatible(), see SONARJAVA-4390 - LOG.debug("NullPointerException while resolving isSubTypeCompatible()", ex); + for (Type iface : owner.interfaces()) { + if (((JType) iface.erasure()).isSubtypeOf(superType, visited)) { + return true; + } + } + return false; + } + + private boolean sameErasure(Type superType) { + return fullyQualifiedName().equals(superType.erasure().fullyQualifiedName()); + } + + private boolean argumentsMatch(Type superType) { + if (!isParameterized() || !superType.isParameterized()) { + return true; + } + List theirs = superType.typeArguments(); + if (args.size() != theirs.size()) { return false; } + for (int i = 0; i < args.size(); i++) { + if (!args.get(i).is(theirs.get(i).fullyQualifiedName())) { + return false; + } + } + return true; + } + + private static boolean widensTo(String from, String to) { + if (from.equals(to)) { + return true; + } + return switch (from) { + case "byte" -> Set.of("short", "int", "long", "float", "double").contains(to); + case "short", "char" -> Set.of("int", "long", "float", "double").contains(to); + case "int" -> Set.of("long", "float", "double").contains(to); + case "long" -> Set.of("float", "double").contains(to); + case "float" -> "double".equals(to); + default -> false; + }; } @Override public boolean isArray() { - return typeBinding.isArray(); + return kind == Kind.ARRAY; + } + + boolean isCapture() { + return kind == Kind.CAPTURE; } @Override public boolean isClass() { - return typeBinding.isClass() - || typeBinding.isInterface() - || typeBinding.isEnum(); + return kind == Kind.NAMED && !isUnknown(); } @Override public boolean isVoid() { - return "void".equals(fullyQualifiedName()); + return kind == Kind.VOID; } @Override public boolean isPrimitive() { - return typeBinding.isPrimitive() - && !isVoid(); + return kind == Kind.PRIMITIVE; } @Override public boolean isPrimitive(Primitives primitive) { - // TODO suboptimal return primitive.name().toLowerCase(Locale.ROOT).equals(fullyQualifiedName()); } @@ -132,152 +301,88 @@ public boolean isPrimitiveWrapper() { @Nullable @Override public Type primitiveWrapperType() { - if (primitiveWrapperType == null) { - String name = JUtils.PRIMITIVE_TO_WRAPPER.get(fullyQualifiedName()); - if (name == null) { - return null; - } - ITypeBinding resolvedType = sema.resolveType(name); - if (resolvedType == null) { - return null; - } - primitiveWrapperType = sema.type(resolvedType); - } - return primitiveWrapperType; + String name = JUtils.PRIMITIVE_TO_WRAPPER.get(fullyQualifiedName()); + return name == null ? null : sema.getClassType(name); } @Nullable @Override public Type primitiveType() { - if (primitiveType == null) { - String name = JUtils.WRAPPER_TO_PRIMITIVE.get(fullyQualifiedName()); - if (name == null) { - return null; - } - ITypeBinding resolvedType = sema.resolveType(name); - if (resolvedType == null) { - return null; - } - primitiveType = sema.type(resolvedType); - } - return primitiveType; + String name = JUtils.WRAPPER_TO_PRIMITIVE.get(fullyQualifiedName()); + return name == null ? null : sema.getClassType(name); } @Override public boolean isNullType() { - return !isUnknown() && typeBinding.isNullType(); + return kind == Kind.NULL; } @Override public boolean isTypeVar() { - return !isUnknown() && typeBinding.isTypeVariable(); + return kind == Kind.TYPEVAR; } @Override public boolean isRawType() { - if (isUnknown()) { + if (kind != Kind.NAMED || isUnknown()) { return false; } - return typeBinding.isRawType(); + Symbol.TypeSymbol owner = declaredSymbol(); + return owner instanceof JTypeSymbol typeSymbol && typeSymbol.typeParameterCount() > 0 && args.isEmpty(); } @Override public Type declaringType() { - if (declaringType == null) { - if (isUnknown()) { - return this; + if (isUnknown() || kind != Kind.NAMED) { + return this; + } + if (isRawType()) { + Symbol.TypeSymbol owner = declaredSymbol(); + int count = owner instanceof JTypeSymbol typeSymbol ? typeSymbol.typeParameterCount() : 0; + List variables = new java.util.ArrayList<>(); + for (int i = 0; i < count; i++) { + variables.add(typeVar(sema, "T" + i, fqn + "<" + i + ">")); } - declaringType = sema.type(typeBinding.getTypeDeclaration()); + return named(sema, fqn, variables); } - return declaringType; + return sema.getClassType(fqn); } @Override public boolean isUnknown() { - return typeBinding.isRecovered(); - } - - @Override - public boolean isNumerical() { - switch (fullyQualifiedName()) { - case "byte", - "char", - "short", - "int", - "long", - "float", - "double": - return true; - default: - return false; + if (kind == Kind.UNKNOWN) { + return true; } - } - - @Override - public String fullyQualifiedName() { - return fullyQualifiedName; - } - - private static String fullyQualifiedName(ITypeBinding typeBinding) { - String qualifiedName; - if (typeBinding.isNullType()) { - qualifiedName = ""; - } else if (typeBinding.isPrimitive()) { - qualifiedName = typeBinding.getName(); - } else if (typeBinding.isArray()) { - qualifiedName = fullyQualifiedName(typeBinding.getComponentType()) + "[]"; - } else if (typeBinding.isCapture()) { - qualifiedName = "!capture!"; - } else if (typeBinding.isTypeVariable()) { - qualifiedName = typeBinding.getName(); - } else { - qualifiedName = typeBinding.getBinaryName(); - if (qualifiedName == null) { - // e.g. anonymous class in unreachable code - qualifiedName = typeBinding.getKey(); - } + if (kind == Kind.ARRAY && element != null) { + return element.isUnknown(); } - if (typeBinding.isIntersectionType()) { - TreeSet intersectionTypes = new TreeSet<>(); - intersectionTypes.add(qualifiedName); - for (ITypeBinding typeBound : typeBinding.getTypeBounds()) { - intersectionTypes.add(fullyQualifiedName(typeBound)); - } - qualifiedName = String.join(" & ", intersectionTypes); + if (kind == Kind.NAMED) { + Symbol.TypeSymbol owner = declaredSymbol(); + return owner.isUnknown(); } - return qualifiedName; + return false; } @Override - public boolean isIntersectionType() { - return typeBinding.isIntersectionType(); + public boolean isNumerical() { + return NUMERIC.contains(fullyQualifiedName()); } @Override - public Type[] getIntersectionTypes() { - if (!isIntersectionType()) { - return new Type[] { this }; - } - ITypeBinding[] bounds = typeBinding.getTypeBounds(); - Type[] types = new Type[bounds.length]; - for (int i = 0; i < bounds.length; i++) { - types[i] = sema.type(bounds[i]); - } - return types; + public String fullyQualifiedName() { + return fqn; } - /** - * @see JSymbol#name() - */ @Override public String name() { - if (typeBinding.isNullType()) { + if (kind == Kind.NULL) { return ""; - } else if (typeBinding.isParameterizedType()) { - // without names of parameters - return typeBinding.getErasure().getName(); } - return typeBinding.getName(); + if (kind == Kind.ARRAY) { + return element.name() + "[]"; + } + int boundary = Math.max(fqn.lastIndexOf('.'), fqn.lastIndexOf('$')); + return boundary < 0 ? fqn : fqn.substring(boundary + 1); } @Override @@ -287,80 +392,76 @@ public String toString() { @Override public Symbol.TypeSymbol symbol() { - return sema.typeSymbol(typeBinding); - } - - @Override - public Type erasure() { - return sema.type(typeBinding.getErasure()); + Symbol.TypeSymbol declared = declaredSymbol(); + return !args.isEmpty() && declared instanceof JTypeSymbol generic ? generic.parameterizedAs(this) : declared; } - @Override - public Type elementType() { - return sema.type(typeBinding.getComponentType()); + private Symbol.TypeSymbol declaredSymbol() { + return switch (kind) { + case ARRAY -> sema.arraySymbol(element); + case PRIMITIVE, VOID -> sema.primitiveSymbol(fqn); + case TYPEVAR -> sema.resolveTypeSymbol(typeVarId); + case NULL, UNKNOWN, INTERSECTION -> Symbol.TypeSymbol.UNKNOWN_TYPE; + case CAPTURE, WILDCARD -> element == null ? Symbol.TypeSymbol.UNKNOWN_TYPE : element.symbol(); + case NAMED -> sema.typeSymbol(fqn); + }; } @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; + public Type erasure() { + if ((kind == Kind.CAPTURE || kind == Kind.WILDCARD) && element != null) { + return element.erasure(); } - if (obj instanceof JType other) { - return areEqual(this.typeBinding, other.typeBinding); + if (kind == Kind.ARRAY) { + return JType.array(sema, (JType) element.erasure()); } - return false; + if (kind == Kind.NAMED && !args.isEmpty()) { + return sema.getClassType(fqn); + } + return this; } @Override - public int hashCode() { - return fullyQualifiedName().hashCode(); + public boolean isParameterized() { + return kind == Kind.NAMED && !args.isEmpty(); } - static boolean areEqual(@Nullable ITypeBinding binding1, @Nullable ITypeBinding binding2) { - if (binding1 == null || binding2 == null) { - return binding1 == binding2; - } - if (binding1.isWildcardType()) { - return binding2.isWildcardType() - && binding1.isUpperbound() == binding2.isUpperbound() - && areEqual(binding1.getBound(), binding2.getBound()); - } - // TODO compare declaring class, method, member - return binding1.getTypeDeclaration().equals(binding2.getTypeDeclaration()) - && isParameterizedOrGeneric(binding1) == isParameterizedOrGeneric(binding2) - && binding1.isRawType() == binding2.isRawType() - && Arrays.equals(binding1.getTypeParameters(), binding2.getTypeParameters()) - && Arrays.equals(binding1.getTypeArguments(), binding2.getTypeArguments()); + @Override + public List typeArguments() { + return Collections.unmodifiableList(args); } - static ITypeBinding normalize(ITypeBinding typeBinding) { - ITypeBinding typeDeclaration = typeBinding.getTypeDeclaration(); - if (typeBinding.isLocal()) { - return typeDeclaration; - } - return typeBinding.isParameterizedType() && Arrays.equals(typeDeclaration.getTypeParameters(), typeBinding.getTypeArguments()) - ? typeDeclaration - : typeBinding; + @Override + public Type elementType() { + return element == null ? Type.UNKNOWN : element; } - private static boolean isParameterizedOrGeneric(ITypeBinding typeBinding) { - return typeBinding.isParameterizedType() || typeBinding.isGenericType(); + @Override + public boolean isIntersectionType() { + return kind == Kind.INTERSECTION; } @Override - public boolean isParameterized() { - return typeBinding.isParameterizedType() - // when diamond operator is not fully resolved by ECJ, there is 0 typeArguments, while ECJ - // knows it is a Parameterized Type - && !typeArguments().isEmpty(); + public Type[] getIntersectionTypes() { + if (intersection.isEmpty()) { + return new Type[] {this}; + } + return intersection.toArray(Type[]::new); } @Override - public List typeArguments() { - if (typeArguments == null) { - typeArguments = sema.types(typeBinding.getTypeArguments()); + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof JType other)) { + return false; } - return typeArguments; + return kind == other.kind && fqn.equals(other.fqn) && args.equals(other.args) && Objects.equals(typeVarId, other.typeVarId); } + @Override + public int hashCode() { + return fqn.hashCode(); + } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JTypeSymbol.java b/java-frontend/src/main/java/org/sonar/java/model/JTypeSymbol.java index 0d41214e309..dbb2592a0ff 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JTypeSymbol.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JTypeSymbol.java @@ -16,230 +16,299 @@ */ package org.sonar.java.model; +import com.google.gson.JsonObject; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.IdentifierTree; -import org.sonar.plugins.java.api.tree.VariableTree; final class JTypeSymbol extends JSymbol implements Symbol.TypeSymbol { - /** - * Cache for {@link #superClass()}. - */ - private Type superclass = Type.UNKNOWN; + private static final String ENUM = "java.lang.Enum"; - /** - * Cache for {@link #interfaces()}. - */ - private List interfaces; + private final boolean typeParameter; + private int typeParameterCount; + @Nullable + private String superName; + private List interfaceNames = List.of(); + @Nullable + private JsonObject superTypeFact; + private List interfaceTypeFacts = List.of(); + private List memberFacts = List.of(); + @Nullable + private String elementFqn; + private Collection members; + @Nullable + private Type declaredSuperClass; + private List declaredInterfaces = List.of(); + @Nullable + private JTypeSymbol generic; - /** - * Cache for {@link #memberSymbols()}. - */ - private Collection memberSymbols; + JTypeSymbol(JSema sema, @Nullable String id, String name, boolean typeParameter) { + super(sema, Kind.TYPE, id, name); + this.typeParameter = typeParameter; + } /** - * Cache for {@link #superTypes()}. + * A view of this generic type symbol whose {@link #type()} is the given parameterization, + * so that {@code type.symbol().type()} keeps the type arguments. */ - private Set superTypes; + JTypeSymbol parameterizedAs(JType parameterized) { + JTypeSymbol view = new JTypeSymbol(sema, id, name(), typeParameter); + view.copyStateFrom(this); + view.type(parameterized); + view.typeParameterCount = typeParameterCount; + view.superName = superName; + view.interfaceNames = interfaceNames; + view.superTypeFact = superTypeFact; + view.interfaceTypeFacts = interfaceTypeFacts; + view.memberFacts = memberFacts; + view.elementFqn = elementFqn; + view.declaredSuperClass = declaredSuperClass; + view.declaredInterfaces = declaredInterfaces; + view.generic = this; + return view; + } - final SpecialField superSymbol = new SpecialField() { - @Override - public String name() { - return "super"; - } + void declaredSupertypes(@Nullable Type superClass, List interfaces) { + this.declaredSuperClass = superClass; + this.declaredInterfaces = List.copyOf(interfaces); + } - @Override - public Type type() { - if (typeBinding().isInterface()) { - // JLS § 15.12.1: - // for "T.super.foo()", if T is an interface, 'super' keyword is used to access method of the interface itself - return JTypeSymbol.this.type(); - } - Type superClass = JTypeSymbol.this.superClass(); - return superClass == null ? Type.UNKNOWN : superClass; - } - }; + boolean isTypeParameter() { + return typeParameter; + } - final SpecialField thisSymbol = new SpecialField() { - @Override - public String name() { - return "this"; - } + int typeParameterCount() { + return typeParameterCount; + } - @Override - public Type type() { - return JTypeSymbol.this.type(); - } - }; + void typeParameterCount(int typeParameterCount) { + this.typeParameterCount = typeParameterCount; + } + + void superName(@Nullable String superName) { + this.superName = superName; + } - JTypeSymbol(JSema sema, ITypeBinding typeBinding) { - super(sema, typeBinding); + void interfaceNames(List interfaceNames) { + this.interfaceNames = interfaceNames; } - ITypeBinding typeBinding() { - return (ITypeBinding) binding; + /** Supertypes with their type arguments, used when they name the same classes as the erased ones. */ + void genericSupertypes(@Nullable JsonObject superType, List interfaceTypes) { + this.superTypeFact = superType; + this.interfaceTypeFacts = interfaceTypes; + } + + void memberFacts(List memberFacts) { + this.memberFacts = memberFacts; + this.members = null; + } + + List memberFacts() { + return memberFacts; + } + + void elementFqn(@Nullable String elementFqn) { + this.elementFqn = elementFqn; } @CheckForNull @Override public Type superClass() { - if (superclass == Type.UNKNOWN) { - superclass = convertSuperClass(); + if (typeParameter || elementFqn != null) { + return sema.getClassType("java.lang.Object"); + } + if (isInterface() || isAnnotation()) { + return sema.getClassType("java.lang.Object"); + } + String erased = JSema.erase(id == null ? name() : id); + if ("java.lang.Object".equals(erased)) { + return null; + } + boolean noSuper = superName == null || superName.isEmpty(); + if (isEnum() && superTypeFact == null && (noSuper || ENUM.equals(JSema.erase(superName))) && type() instanceof JType self) { + sema.getClassType(ENUM); + return JType.named(sema, ENUM, List.of(self)); + } + if (noSuper || inheritsFrom(erased) || interfaceDeclaredAsSuper() != null) { + return sema.getClassType("java.lang.Object"); + } + if (namesSameClass(superTypeFact, superName)) { + return sema.supertypeFromFact(superTypeFact); } - return superclass; + return withDeclaredArguments(sema.getClassType(JSema.erase(superName)), declaredSuperClass == null ? List.of() : List.of(declaredSuperClass)); } - @CheckForNull - private Type convertSuperClass() { - if (typeBinding().isInterface() || typeBinding().isArray()) { - ITypeBinding objectBinding = sema.resolveType("java.lang.Object"); - return objectBinding != null ? sema.type(objectBinding) : Type.UNKNOWN; - } else if (typeBinding().getSuperclass() == null) { - // java.lang.Object - return null; - } else { - return sema.type(typeBinding().getSuperclass()); + private static Type withDeclaredArguments(Type erased, List declared) { + return declared.stream().filter(type -> type.isParameterized() && type.erasure().equals(erased)).findFirst().orElse(erased); + } + + private boolean inheritsFrom(String erasedSelf) { + Set visited = new HashSet<>(); + String current = JSema.erase(superName); + while (visited.add(current)) { + if (current.equals(erasedSelf)) { + return true; + } + if (!(sema.getClassType(current).symbol() instanceof JTypeSymbol parent) || parent.superName == null || parent.superName.isEmpty()) { + return false; + } + current = JSema.erase(parent.superName); } + return false; } @Override public List interfaces() { - if (interfaces == null) { - interfaces = sema.types(typeBinding().getInterfaces()); + if (elementFqn != null) { + return List.of(sema.getClassType("java.lang.Cloneable"), sema.getClassType("java.io.Serializable")); + } + if (interfaceNames.isEmpty()) { + return withInterfaceSuper(isAnnotation() ? List.of(sema.getClassType("java.lang.annotation.Annotation")) : List.of()); + } + boolean generic = interfaceTypeFacts.size() == interfaceNames.size(); + List types = new ArrayList<>(); + for (int i = 0; i < interfaceNames.size(); i++) { + String name = interfaceNames.get(i); + JsonObject fact = generic ? interfaceTypeFacts.get(i) : null; + Type erased = sema.getClassType(JSema.erase(name)); + types.add(namesSameClass(fact, name) ? sema.supertypeFromFact(fact) : withDeclaredArguments(erased, declaredInterfaces)); + } + return withInterfaceSuper(types); + } + + /** + * An anonymous class whose named supertype is an interface is recorded with that interface as its superclass. + * Java implements the interface and extends Object. + */ + @Nullable + private Type interfaceDeclaredAsSuper() { + if (superName == null || superName.isEmpty() || isInterface() || isAnnotation() || typeParameter || elementFqn != null) { + return null; + } + String erased = JSema.erase(superName); + JTypeSymbol parent = sema.typeSymbol(erased); + if (!parent.isInterface() || parent.isUnknown()) { + return null; + } + return sema.getClassType(erased); + } + + private List withInterfaceSuper(List interfaces) { + Type superInterface = interfaceDeclaredAsSuper(); + if (superInterface == null || interfaces.stream().anyMatch(type -> type.erasure().equals(superInterface.erasure()))) { + return interfaces; } - return interfaces; + List combined = new ArrayList<>(interfaces.size() + 1); + combined.add(superInterface); + combined.addAll(interfaces); + return combined; + } + + private static boolean namesSameClass(@Nullable JsonObject fact, String name) { + return fact != null && fact.has("id") && fact.get("id").isJsonPrimitive() + && JSema.erase(fact.get("id").getAsString()).equals(JSema.erase(name)); } @Override public Collection memberSymbols() { - if (memberSymbols == null) { - memberSymbols = convertMemberSymbols(); + if (generic != null) { + return generic.memberSymbols(); + } + if (members == null) { + List resolved = new ArrayList<>(); + for (JsonObject member : memberFacts) { + resolved.add(sema.memberFromTypeInfo(this, member)); + } + addImplicitConstructor(resolved); + members = resolved; } - return memberSymbols; + return members; } - private Collection convertMemberSymbols() { - return Stream.concat( - Stream.concat( - Arrays.stream(typeBinding().getDeclaredTypes()).map(sema::typeSymbol), - Arrays.stream(typeBinding().getDeclaredFields()).map(sema::variableSymbol)), - Arrays.stream(typeBinding().getDeclaredMethods()).map(sema::methodSymbol)) - .collect(Collectors.toCollection(ArrayList::new)); + /** A class of this file that declares no constructor still has the default one as a member. */ + private void addImplicitConstructor(List resolved) { + if (id == null || isUnknown() || isTypeParameter() || isInterface() || declaration() == null + || resolved.stream().anyMatch(member -> member.isMethodSymbol() && "".equals(member.name()))) { + return; + } + Symbol constructor = sema.resolve(id + "#()"); + if (!constructor.isUnknown()) { + resolved.add(constructor); + } } @Override public Collection lookupSymbols(String name) { - return memberSymbols().stream() - .filter(m -> name.equals(m.name())) - .collect(Collectors.toSet()); + Set found = new HashSet<>(); + for (Symbol member : memberSymbols()) { + if (name.equals(member.name())) { + found.add(member); + } + } + return found; } @Nullable @Override public ClassTree declaration() { - return (ClassTree) super.declaration(); + return super.declaration() instanceof ClassTree classTree ? classTree : null; } @Override public Set superTypes() { - if (superTypes == null) { - if (isUnknown()) { - superTypes = Collections.emptySet(); - } else { - superTypes = new HashSet<>(); - JUtils.collectSuperTypes(superTypes, sema, typeBinding()); + if (isUnknown()) { + return Collections.emptySet(); + } + Set collected = new HashSet<>(); + collectSuperTypes(collected, new HashSet<>()); + return collected; + } + + private void collectSuperTypes(Set collected, Set visited) { + if (!visited.add(JSema.erase(id == null ? name() : id))) { + return; + } + Type superClass = superClass(); + if (superClass != null && !superClass.isUnknown()) { + collected.add(superClass); + if (superClass.symbol() instanceof JTypeSymbol parent) { + parent.collectSuperTypes(collected, visited); + } + } + for (Type iface : interfaces()) { + if (!iface.isUnknown()) { + collected.add(iface); + if (iface.symbol() instanceof JTypeSymbol parent) { + parent.collectSuperTypes(collected, visited); + } } } - return superTypes; } - @Override @Nullable + @Override public TypeSymbol outermostClass() { - Symbol symbol = this; - Symbol result = null; - while (symbol != null && !symbol.isPackageSymbol()) { - result = symbol; - symbol = symbol.owner(); + Symbol current = this; + Symbol result = this; + while (current != null && !current.isPackageSymbol()) { + result = current; + current = current.owner(); } - return (Symbol.TypeSymbol) result; + return result instanceof TypeSymbol typeSymbol ? typeSymbol : this; } @Override public boolean isAnnotation() { - return !isUnknown() && typeBinding().isAnnotation(); + return !isUnknown() && isAnnotationSymbol(); } - - abstract class SpecialField extends Symbols.DefaultSymbol implements Symbol.VariableSymbol { - @Override - public final Symbol owner() { - return JTypeSymbol.this; - } - - @Override - public final boolean isVariableSymbol() { - return true; - } - - @Override - public final boolean isFinal() { - return true; - } - - @Override - public final boolean isUnknown() { - return false; - } - - @Override - public final TypeSymbol enclosingClass() { - return JTypeSymbol.this; - } - - @Override - public final List usages() { - return Collections.emptyList(); - } - - @Nullable - @Override - public final VariableTree declaration() { - return null; - } - - @Override - public final boolean isEffectivelyFinal(){ - return false; - } - @Override - public final Optional constantValue() { - return Optional.empty(); - } - - @Override - public final boolean isLocalVariable() { - return false; - } - - @Override - public final boolean isParameter() { - return false; - } - } - } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JUtils.java b/java-frontend/src/main/java/org/sonar/java/model/JUtils.java index 6018e221558..1767701abe2 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JUtils.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JUtils.java @@ -16,7 +16,6 @@ */ package org.sonar.java.model; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -26,7 +25,6 @@ import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type; @@ -67,19 +65,7 @@ public static Type wrapTypeIfPrimitive(Type type) { } public static boolean isIntersectionType(Type type) { - return !type.isUnknown() && ((JType) type).typeBinding.isIntersectionType(); - } - - public static void collectSuperTypes(Set result, JSema sema, ITypeBinding typeBinding) { - ITypeBinding s = typeBinding.getSuperclass(); - if (s != null) { - result.add(sema.type(s)); - collectSuperTypes(result, sema, s); - } - Arrays.stream(typeBinding.getInterfaces()).forEach(i -> { - result.add(sema.type(i)); - collectSuperTypes(result, sema, i); - }); + return type.isIntersectionType(); } public static Symbol getPackage(Symbol symbol) { @@ -91,10 +77,7 @@ public static Symbol getPackage(Symbol symbol) { @Nullable public static Object defaultValue(Symbol.MethodSymbol method) { - if (method.isUnknown()) { - return null; - } - return ((JMethodSymbol) method).methodBinding().getDefaultValue(); + return null; } /** @@ -162,13 +145,13 @@ public static Set directSuperTypes(Type type) { return Collections.emptySet(); } Set result = new HashSet<>(); - JType t = (JType) type; - ITypeBinding superclass = t.typeBinding.getSuperclass(); - if (superclass != null) { - result.add(t.sema.type(superclass)); + Symbol.TypeSymbol symbol = type.symbol(); + Type superClass = symbol.superClass(); + if (superClass != null && !superClass.isUnknown()) { + result.add(superClass); } - Arrays.stream(t.typeBinding.getInterfaces()) - .map(t.sema::type) + symbol.interfaces().stream() + .filter(iface -> !iface.isUnknown()) .forEach(result::add); return result; } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JVariableSymbol.java b/java-frontend/src/main/java/org/sonar/java/model/JVariableSymbol.java index ae6458f39de..bb82895682c 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JVariableSymbol.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JVariableSymbol.java @@ -20,23 +20,37 @@ import java.util.List; import java.util.Optional; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.VariableTree; final class JVariableSymbol extends JSymbol implements Symbol.VariableSymbol { - // cache for this.constantValue() - private boolean constantValueComputed = false; - private Optional constantValue; + private final boolean parameter; + private final boolean local; + private boolean effectivelyFinal; + @Nullable + private Optional constant; + + JVariableSymbol(JSema sema, @Nullable String id, String name, String dumpKind) { + super(sema, Kind.VARIABLE, id, name); + this.parameter = "param".equals(dumpKind); + this.local = "local".equals(dumpKind); + this.effectivelyFinal = "constant".equals(dumpKind) || "enum_member".equals(dumpKind); + enumSymbol("enum_member".equals(dumpKind)); + } + + void effectivelyFinal(boolean effectivelyFinal) { + this.effectivelyFinal = effectivelyFinal; + } - JVariableSymbol(JSema sema, IVariableBinding variableBinding) { - super(sema, variableBinding); + void constantValue(@Nullable Object value) { + this.constant = Optional.ofNullable(value); } @Nullable @@ -47,53 +61,84 @@ public VariableTree declaration() { @Override public boolean isEffectivelyFinal() { - return ((IVariableBinding)binding).isEffectivelyFinal(); + return isFinal() || effectivelyFinal; } @Override public Optional constantValue() { - if (!constantValueComputed) { - constantValueComputed = true; - if (!isFinal() || !isStatic()) { - constantValue = Optional.empty(); - } else { - Object c = ((IVariableBinding) binding).getConstantValue(); - if (c instanceof Short shortValue) { - c = Integer.valueOf(shortValue); - } else if (c instanceof Byte byteValue) { - c = Integer.valueOf(byteValue); - } else if (c instanceof Character characterValue) { - c = Integer.valueOf(characterValue); - } - constantValue = Optional.ofNullable(c); + if (constant == null) { + constant = Optional.ofNullable(declaredConstant()); + } + return constant; + } + + /** A constant is a static final field with a compile time value, typed as its declaration. */ + @Nullable + private Object declaredConstant() { + VariableTree declaration = declaration(); + if (declaration == null || declaration.initializer() == null || declaration.type() == null || !isStaticFinalOrInterfaceMember()) { + return null; + } + ExpressionTree initializer = declaration.initializer(); + Object value = initializer.is(Tree.Kind.LONG_LITERAL, Tree.Kind.FLOAT_LITERAL, Tree.Kind.DOUBLE_LITERAL) + ? literalNumber((LiteralTree) initializer) + : ExpressionUtils.resolveAsConstant(initializer); + return convert(value, declaration.type().symbolType()); + } + + private boolean isStaticFinalOrInterfaceMember() { + boolean constantKind = isFinal() || owner().isInterface() || isEnum(); + return constantKind && (isStatic() || owner().isInterface()); + } + + @Nullable + private static Object literalNumber(LiteralTree literal) { + String text = literal.value().replace("_", ""); + try { + return switch (literal.kind()) { + case LONG_LITERAL -> LiteralUtils.longLiteralValue(literal); + case FLOAT_LITERAL -> Float.valueOf(text); + default -> Double.valueOf(text); + }; + } catch (NumberFormatException e) { + return null; + } + } + + @Nullable + private static Object convert(@Nullable Object value, Type type) { + if (value instanceof Number number) { + if (type.is("long")) { + return number.longValue(); + } else if (type.is("double")) { + return number.doubleValue(); + } else if (type.is("float")) { + return number.floatValue(); } } - return constantValue; + return value; } @Override public boolean isLocalVariable() { - Symbol owner = owner(); - return owner != null && owner.isMethodSymbol(); + return local || (owner().isMethodSymbol() && !parameter); } @Override public boolean isParameter() { - return ((IVariableBinding) binding).isParameter(); + return parameter; } - static class ParameterPlaceholderSymbol extends Symbols.DefaultSymbol implements Symbol.VariableSymbol { + private final String name; private final Symbol owner; private final Type type; - private final SymbolMetadata metadata; - ParameterPlaceholderSymbol(int index, JSema sema, IMethodBinding owner, ITypeBinding typeBinding) { + ParameterPlaceholderSymbol(int index, Symbol owner, Type type) { this.name = "arg" + index; - this.owner = sema.methodSymbol(owner); - this.type = sema.type(typeBinding); - this.metadata = JSymbolMetadata.of(sema, this, typeBinding, owner.getParameterAnnotations(index)); + this.owner = owner; + this.type = type == null ? Type.UNKNOWN : type; } @Override @@ -126,6 +171,7 @@ public List usages() { return Collections.emptyList(); } + @Nullable @Override public VariableTree declaration() { return null; @@ -133,7 +179,7 @@ public VariableTree declaration() { @Override public boolean isEffectivelyFinal() { - return false; + return true; } @Override @@ -153,7 +199,7 @@ public boolean isParameter() { @Override public SymbolMetadata metadata() { - return metadata; + return Symbols.EMPTY_METADATA; } @Override @@ -163,9 +209,7 @@ public boolean isVariableSymbol() { @Override public boolean isFinal() { - // From a caller perspective, it is not useful (and even not possible) to know if the parameter is final. return false; } - } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JWarning.java b/java-frontend/src/main/java/org/sonar/java/model/JWarning.java index 8a95e692df6..c123b4326f0 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JWarning.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JWarning.java @@ -28,9 +28,6 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.CheckForNull; -import org.eclipse.jdt.core.compiler.IProblem; -import org.eclipse.jdt.core.dom.CompilationUnit; import org.sonar.java.annotations.VisibleForTesting; import org.sonar.java.ast.visitors.SubscriptionVisitor; import org.sonar.java.model.location.InternalPosition; @@ -52,6 +49,13 @@ public final class JWarning extends JProblem { this.end = InternalPosition.atOffset(endLine, endColumnOffset); } + JWarning(String message, Type type, Tree syntaxTree) { + super(message, type); + this.start = startOf(syntaxTree); + this.end = endOf(syntaxTree); + this.syntaxTree = syntaxTree; + } + public Tree syntaxTree() { return syntaxTree; } @@ -97,30 +101,7 @@ public static class Mapper extends SubscriptionVisitor { private final PriorityQueue warnings = new PriorityQueue<>(Comparator.comparing(JWarning::start).thenComparing(JWarning::end)); - private Mapper(CompilationUnit ast, LineColumnConverter lineColumnConverter) { - Stream.of(ast.getProblems()) - .map(problem -> convert(problem, ast, lineColumnConverter)) - .filter(Objects::nonNull) - .forEach(warnings::add); - } - - @CheckForNull - private static JWarning convert(IProblem problem, CompilationUnit root, LineColumnConverter lineColumnConverter) { - for (Type type : Type.values()) { - if (type.matches(problem)) { - LineColumnConverter.Pos start = lineColumnConverter.toPos(problem.getSourceStart()); - LineColumnConverter.Pos end = lineColumnConverter.toPos(problem.getSourceStart()); - return new JWarning(problem.getMessage(), - type, - start.line(), start.columnOffset(), - end.line(), end.columnOffset()); - } - } - return null; - } - - public static Mapper warningsFor(CompilationUnit ast, LineColumnConverter lineColumnConverter) { - return new Mapper(ast, lineColumnConverter); + private Mapper() { } public void mappedInto(JavaTree.CompilationUnitTreeImpl cut) { diff --git a/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java b/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java index bf41aee95c2..2fd0af8ff32 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java @@ -27,10 +27,6 @@ import java.util.Objects; import java.util.Set; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.IBinding; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; import org.sonar.java.Preconditions; import org.sonar.java.annotations.Beta; import org.sonar.java.ast.parser.QualifiedIdentifierListTreeImpl; @@ -326,7 +322,8 @@ public static class ImportTreeImpl extends JavaTree implements ImportTree { @Nullable private final SyntaxToken moduleToken; - public IBinding binding; + @Nullable + public Symbol symbol; public ImportTreeImpl(InternalSyntaxToken importToken, @Nullable InternalSyntaxToken staticToken, @Nullable InternalSyntaxToken moduleToken, @@ -342,17 +339,7 @@ public ImportTreeImpl(InternalSyntaxToken importToken, @Nullable InternalSyntaxT @Nullable public Symbol symbol() { - if (binding != null) { - switch (binding.getKind()) { - case IBinding.TYPE: - return root.sema.typeSymbol((ITypeBinding) binding); - case IBinding.METHOD: - return root.sema.methodSymbol((IMethodBinding) binding); - case IBinding.VARIABLE: - return root.sema.variableSymbol((IVariableBinding) binding); - } - } - return null; + return symbol; } @Override 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 91ec5318ce1..6dc23da9572 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 @@ -16,18 +16,14 @@ */ package org.sonar.java.model; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.java.model.expression.IdentifierTreeImpl; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; -import javax.annotation.Nullable; - final class KeywordSuper extends IdentifierTreeImpl { - KeywordSuper(InternalSyntaxToken token, @Nullable ITypeBinding typeBinding) { + KeywordSuper(InternalSyntaxToken token) { super(token); - this.typeBinding = typeBinding; } @Override @@ -37,10 +33,7 @@ public Type symbolType() { @Override public Symbol symbol() { - if (typeBinding == null) { - return Symbol.UNKNOWN_SYMBOL; - } - return root.sema.typeSymbol(typeBinding).superSymbol; + return symbol != null ? symbol : Symbol.UNKNOWN_SYMBOL; } } 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 8d28995961b..bedc5faabfe 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 @@ -16,25 +16,18 @@ */ package org.sonar.java.model; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.java.model.expression.IdentifierTreeImpl; import org.sonar.plugins.java.api.semantic.Symbol; -import javax.annotation.Nullable; - final class KeywordThis extends IdentifierTreeImpl { - KeywordThis(InternalSyntaxToken token, @Nullable ITypeBinding typeBinding) { + KeywordThis(InternalSyntaxToken token) { super(token); - this.typeBinding = typeBinding; } @Override public Symbol symbol() { - if (typeBinding == null) { - return Symbol.UNKNOWN_SYMBOL; - } - return root.sema.typeSymbol(typeBinding).thisSymbol; + return symbol != null ? symbol : Symbol.UNKNOWN_SYMBOL; } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/LiteralUtils.java b/java-frontend/src/main/java/org/sonar/java/model/LiteralUtils.java index 5632dcfbad1..ddc343328f7 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/LiteralUtils.java +++ b/java-frontend/src/main/java/org/sonar/java/model/LiteralUtils.java @@ -133,6 +133,23 @@ public static String trimQuotes(String value) { return value.substring(delimiterLength, value.length() - delimiterLength); } + /** + * The compile time value of {@code literal}, typed as the literal is: a {@link String} for text, a {@link Boolean} + * for booleans and an {@link Integer} for an int that fits. Anything else keeps its source spelling. + */ + public static Object constantValue(LiteralTree literal) { + String value = literal.value(); + return switch (literal.kind()) { + case STRING_LITERAL, TEXT_BLOCK -> trimQuotes(value); + case BOOLEAN_LITERAL -> Boolean.valueOf(value); + case INT_LITERAL -> { + Integer integer = intLiteralValue(literal); + yield integer == null ? value : integer; + } + default -> value; + }; + } + public static boolean isTextBlock(String value) { return value.startsWith("\"\"\""); } diff --git a/java-frontend/src/main/java/org/sonar/java/model/SemsitterTranslator.java b/java-frontend/src/main/java/org/sonar/java/model/SemsitterTranslator.java new file mode 100644 index 00000000000..a37f9095650 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/model/SemsitterTranslator.java @@ -0,0 +1,3312 @@ +/* + * 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 com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sonar.sslr.api.RecognitionException; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +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.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.AbstractPatternTree; +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.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.ArrayDimensionTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.CaseGroupTree; +import org.sonar.plugins.java.api.tree.CaseLabelTree; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.ExpressionStatementTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.ImportClauseTree; +import org.sonar.plugins.java.api.tree.LambdaExpressionTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodTree; +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.ParameterizedTypeTree; +import org.sonar.plugins.java.api.tree.PatternTree; +import org.sonar.plugins.java.api.tree.StatementTree; +import org.sonar.plugins.java.api.tree.SwitchExpressionTree; +import org.sonar.plugins.java.api.tree.SyntaxToken; +import org.sonar.plugins.java.api.tree.SyntaxTrivia; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeParameterTree; +import org.sonar.plugins.java.api.tree.TypeTree; +import org.sonar.plugins.java.api.tree.VariableTree; +import org.sonar.plugins.java.api.tree.WildcardTree; +import org.sonar.plugins.java.api.tree.YieldStatementTree; + +/** + * Builds the SonarJava AST from one SemSitter CST document. + * Positions come from the CST. Symbols and types come from {@link JSema}. + */ +final class SemsitterTranslator { + + private static final Pattern ARITY_ONLY = Pattern.compile("[^#]+#[^#(]+\\(\\d+\\)"); + private final JSema sema; + private final Map tokens = new IdentityHashMap<>(); + private final Deque labels = new ArrayDeque<>(); + private final Deque types = new ArrayDeque<>(); + private InternalSyntaxToken eof = new InternalSyntaxToken(1, 0, "", List.of(), true); + private boolean switchExpression; + private static final List FINAL_MODIFIER = List.of("final"); + private String implicitClassName = ""; + private String packageName = ""; + private boolean naming; + private final Map singleImports = new HashMap<>(); + private final List onDemandImports = new ArrayList<>(); + private final Map staticImports = new HashMap<>(); + private final List staticOnDemandImports = new ArrayList<>(); + private final Deque> scopes = new ArrayDeque<>(); + private final Deque returnTargets = new ArrayDeque<>(); + + private SemsitterTranslator(JSema sema) { + this.sema = sema; + } + + static JavaTree.CompilationUnitTreeImpl translate(JSema sema, JsonObject document) { + boolean broken = document.has("hasError") && document.get("hasError").getAsBoolean(); + SemsitterTranslator translator = new SemsitterTranslator(sema); + if (document.has("path") && document.get("path").isJsonPrimitive()) { + String fileName = Path.of(document.get("path").getAsString()).getFileName().toString(); + translator.implicitClassName = fileName.endsWith(".java") ? fileName.substring(0, fileName.length() - 5) : fileName; + } + Node root = new Node(document.getAsJsonObject("tree")); + translator.indexTokens(root); + translator.declareGenericReturns(root); + JavaTree.CompilationUnitTreeImpl unit = translator.compilationUnit(root); + unit.sema = sema; + setParents(unit); + CompilerWarnings.addTo(unit); + EffectivelyFinalVariables.mark(unit); + if (broken && unit.types().isEmpty() && unit.imports().isEmpty() && unit.moduleDeclaration() == null) { + throw new RecognitionException(1, "SemSitter: syntax error"); + } + return unit; + } + + private void indexTokens(Node root) { + List leaves = new ArrayList<>(); + collectLeaves(root, leaves); + List pending = new ArrayList<>(); + for (Node leaf : leaves) { + if (isComment(leaf)) { + pending.add(trivia(leaf)); + } else { + tokens.put(leaf.json, new InternalSyntaxToken(leaf.line(), leaf.col(), leaf.text(), pending, false)); + pending = new ArrayList<>(); + } + } + int line = 1; + int column = 0; + if (root.json.has("p")) { + line = root.json.getAsJsonArray("p").get(2).getAsInt(); + column = root.json.getAsJsonArray("p").get(3).getAsInt(); + } else if (!leaves.isEmpty()) { + Node last = leaves.get(leaves.size() - 1); + line = last.json.getAsJsonArray("p").get(2).getAsInt(); + column = last.json.getAsJsonArray("p").get(3).getAsInt(); + } + eof = new InternalSyntaxToken(line, column, "", pending, true); + } + + private static void collectLeaves(Node node, List leaves) { + if (node.missing()) { + return; + } + if (isComment(node) || node.allChildren().isEmpty() || isLiteral(node.k())) { + if (!node.text().isEmpty() || isComment(node)) { + leaves.add(node); + } + return; + } + for (Node child : node.allChildren()) { + collectLeaves(child, leaves); + } + } + + private JavaTree.CompilationUnitTreeImpl compilationUnit(Node program) { + JavaTree.PackageDeclarationTreeImpl packageDeclaration = null; + List imports = new ArrayList<>(); + List types = new ArrayList<>(); + List compactMembers = new ArrayList<>(); + ModuleDeclarationTree module = null; + boolean[] compact = {false}; + for (Node child : program.children()) { + TopLevel collected = addTopLevel(child, imports, types, compactMembers, compact[0]); + packageDeclaration = collected.packageDeclaration == null ? packageDeclaration : collected.packageDeclaration; + module = collected.module == null ? module : collected.module; + compact[0] = collected.compact; + } + if (compact[0]) { + ClassTreeImpl implicitClass = new ClassTreeImpl(Tree.Kind.IMPLICIT_CLASS, null, compactMembers, null); + bindImplicitClass(implicitClass, compactMembers); + types.add(implicitClass); + } + return new JavaTree.CompilationUnitTreeImpl(packageDeclaration, imports, types, module, eof); + } + + /** One top-level construct. Children of an error node are kept, so a syntax error does not drop the rest of the file. */ + private TopLevel addTopLevel(Node child, List imports, List types, List compactMembers, boolean compact) { + if ("ERROR".equals(child.k())) { + JavaTree.PackageDeclarationTreeImpl packageDeclaration = null; + ModuleDeclarationTree module = null; + for (Node nested : child.children()) { + TopLevel collected = addTopLevel(nested, imports, types, compactMembers, compact); + packageDeclaration = collected.packageDeclaration == null ? packageDeclaration : collected.packageDeclaration; + module = collected.module == null ? module : collected.module; + compact = collected.compact; + } + return new TopLevel(packageDeclaration, module, compact); + } + return switch (child.k()) { + case "package_declaration" -> new TopLevel(packageDeclaration(child), null, compact); + case "module_declaration" -> new TopLevel(null, moduleDeclaration(child), compact); + case "import_declaration" -> { + imports.add(importDeclaration(child)); + yield new TopLevel(null, null, compact); + } + case ";" -> { + EmptyStatementTreeImpl empty = new EmptyStatementTreeImpl(token(child)); + if (compact) { + compactMembers.add(empty); + } else if (types.isEmpty()) { + imports.add(empty); + } else { + types.add(empty); + } + yield new TopLevel(null, null, compact); + } + case "class_declaration", "interface_declaration", "enum_declaration", "record_declaration", "annotation_type_declaration" -> { + addMember(compact ? compactMembers : types, child); + yield new TopLevel(null, null, compact); + } + case "method_declaration", "constructor_declaration", "field_declaration", "constant_declaration", "local_variable_declaration", "static_initializer", "block" -> { + if (!compact) { + compact = true; + compactMembers.addAll(types); + types.clear(); + } + addMember(compactMembers, child); + yield new TopLevel(null, null, compact); + } + default -> new TopLevel(null, null, compact); + }; + } + + private record TopLevel(@Nullable JavaTree.PackageDeclarationTreeImpl packageDeclaration, @Nullable ModuleDeclarationTree module, boolean compact) { + } + + private ModuleDeclarationTreeImpl moduleDeclaration(Node node) { + Node body = requiredField(node, "body", childKind(node, "module_body")); + List directives = new ArrayList<>(); + for (Node child : body.children()) { + if (child.k().endsWith("_module_directive")) { + directives.add(moduleDirective(child)); + } + } + return new ModuleDeclarationTreeImpl( + node.children().stream().filter(SemsitterTranslator::isAnnotation).map(this::annotation).toList(), + optionalToken(node, "open"), + token(required(node, "module")), + moduleName(requiredField(node, "name", firstNamed(node))), + token(required(body, "{")), + directives, + token(required(body, "}"))); + } + + private ModuleDirectiveTree moduleDirective(Node node) { + InternalSyntaxToken keyword = token(node.children().get(0)); + InternalSyntaxToken semicolon = token(required(node, ";")); + return switch (node.k()) { + case "requires_module_directive" -> new RequiresDirectiveTreeImpl(keyword, requiresModifiers(node), moduleName(requiredField(node, "module", lastNamed(node))), semicolon); + case "exports_module_directive" -> new ExportsDirectiveTreeImpl(keyword, expression(requiredField(node, "package", firstNamed(node))), optionalToken(node, "to"), moduleNames(node), semicolon); + case "opens_module_directive" -> new OpensDirectiveTreeImpl(keyword, expression(requiredField(node, "package", firstNamed(node))), optionalToken(node, "to"), moduleNames(node), semicolon); + case "uses_module_directive" -> new UsesDirectiveTreeImpl(keyword, (TypeTree) expression(requiredField(node, "type", firstNamed(node))), semicolon); + default -> new ProvidesDirectiveTreeImpl(keyword, (TypeTree) expression(requiredField(node, "provided", firstNamed(node))), token(required(node, "with")), providers(node), semicolon); + }; + } + + private ModifiersTreeImpl requiresModifiers(Node node) { + List modifiers = new ArrayList<>(); + for (Node child : node.children()) { + if ("requires_modifier".equals(child.k())) { + Node keyword = child.children().get(0); + modifiers.add(new ModifierKeywordTreeImpl("static".equals(keyword.text()) ? Modifier.STATIC : Modifier.TRANSITIVE, token(keyword))); + } + } + return new ModifiersTreeImpl(modifiers); + } + + private ModuleNameListTreeImpl moduleNames(Node node) { + ModuleNameListTreeImpl names = ModuleNameListTreeImpl.emptyList(); + for (Node child : node.children()) { + if ("modules".equals(child.f())) { + names.add(moduleName(child)); + } else if (",".equals(child.text())) { + names.separators().add(token(child)); + } + } + return names; + } + + private QualifiedIdentifierListTreeImpl providers(Node node) { + QualifiedIdentifierListTreeImpl providers = QualifiedIdentifierListTreeImpl.emptyList(); + boolean afterWith = false; + for (Node child : node.children()) { + if ("with".equals(child.text())) { + afterWith = true; + } else if (",".equals(child.text())) { + providers.separators().add(token(child)); + } else if (afterWith && !";".equals(child.text())) { + providers.add((TypeTree) expression(child)); + } + } + return providers; + } + + private ModuleNameTreeImpl moduleName(Node node) { + ModuleNameTreeImpl name = ModuleNameTreeImpl.emptyList(); + addModuleName(name, node); + return name; + } + + private void addModuleName(ModuleNameTreeImpl name, Node node) { + if ("scoped_identifier".equals(node.k())) { + addModuleName(name, requiredField(node, "scope", firstNamed(node))); + name.add(new IdentifierTreeImpl(token(requiredField(node, "name", lastNamed(node))))); + } else { + name.add(new IdentifierTreeImpl(token(node))); + } + } + + private JavaTree.PackageDeclarationTreeImpl packageDeclaration(Node node) { + naming = true; + ExpressionTree name = expression(requiredField(node, "name", firstOf(node, "scoped_identifier", "identifier"))); + naming = false; + packageName = dottedName(name); + bindPackage(name, packageName); + return new JavaTree.PackageDeclarationTreeImpl(annotations(node), token(required(node, "package")), name, token(required(node, ";"))); + } + + private JavaTree.ImportTreeImpl importDeclaration(Node node) { + InternalSyntaxToken staticToken = optionalToken(node, "static"); + naming = true; + ExpressionTree name = expression(requiredField(node, "name", firstNamed(node))); + naming = false; + if (keyword(node, "*") != null || keyword(node, "asterisk") != null) { + Node star = keyword(node, "*"); + if (star == null) { + star = keyword(node, "asterisk"); + } + (staticToken == null ? onDemandImports : staticOnDemandImports).add(dottedName(name)); + name = new MemberSelectExpressionTreeImpl(name, token(lastKeyword(node, ".")), new IdentifierTreeImpl(token(star))); + } else if (name instanceof MemberSelectExpressionTree select) { + if (staticToken == null) { + singleImports.putIfAbsent(select.identifier().name(), dottedName(name)); + } else { + staticImports.putIfAbsent(select.identifier().name(), dottedName(select.expression())); + } + } + JavaTree.ImportTreeImpl imported = new JavaTree.ImportTreeImpl(token(required(node, "import")), staticToken, optionalToken(node, "module"), name, token(required(node, ";"))); + if (staticToken == null && name instanceof MemberSelectExpressionTree select && !"*".equals(select.identifier().name())) { + String fqn = dottedName(name); + if (sema.declaresType(fqn) && sema.resolve(fqn) instanceof Symbol.TypeSymbol type) { + imported.symbol = type; + } + } + return imported; + } + + private void addMember(List members, Node node) { + switch (node.k()) { + case "field_declaration", "constant_declaration", "local_variable_declaration" -> members.addAll(variables(node)); + case "method_declaration", "constructor_declaration", "compact_constructor_declaration" -> members.add(method(node)); + case "annotation_type_element_declaration" -> members.add(annotationElement(node)); + case "class_declaration", "interface_declaration", "enum_declaration", "record_declaration", "annotation_type_declaration" -> members.add(typeDeclaration(node)); + case "static_initializer" -> members.add(staticInitializer(node)); + case "block" -> members.add(initializer(node)); + case "enum_constant" -> members.add(enumConstant(node, null)); + case ";" -> members.add(new EmptyStatementTreeImpl(token(node))); + case "{", "}" -> { + // punctuation of the body + } + case "ERROR" -> { + for (Node nested : node.children()) { + addMember(members, nested); + } + } + default -> { + // skipped construct + } + } + } + + private ClassTreeImpl typeDeclaration(Node node) { + Tree.Kind kind = switch (node.k()) { + case "interface_declaration" -> Tree.Kind.INTERFACE; + case "enum_declaration" -> Tree.Kind.ENUM; + case "record_declaration" -> Tree.Kind.RECORD; + case "annotation_type_declaration" -> Tree.Kind.ANNOTATION_TYPE; + default -> Tree.Kind.CLASS; + }; + Node body = field(node, "body"); + Node declaredName = nameNode(node); + if (declaredName != null && declaredName.decl() != null && sema.resolve(declaredName.decl()) instanceof Symbol.TypeSymbol typeSymbol) { + types.push(typeSymbol); + } else { + types.push(Symbol.TypeSymbol.UNKNOWN_TYPE); + } + List members; + try { + members = body == null ? new ArrayList<>() : membersOf(body); + } finally { + types.pop(); + } + InternalSyntaxToken open = body == null ? synthetic("{") : token(required(body, "{")); + InternalSyntaxToken close = body == null ? synthetic("}") : token(required(body, "}")); + ClassTreeImpl tree = new ClassTreeImpl(kind, open, members, close); + String keyword = switch (kind) { + case INTERFACE, ANNOTATION_TYPE -> "interface"; + case ENUM -> "enum"; + case RECORD -> "record"; + default -> "class"; + }; + IdentifierTreeImpl name = identifier(requiredField(node, "name", firstOf(node, "identifier", "type_identifier"))); + tree.complete(modifiers(node), token(required(node, keyword)), name); + Node typeParameters = field(node, "type_parameters"); + if (typeParameters != null) { + tree.completeTypeParameters(typeParameters(typeParameters)); + } + if (kind == Tree.Kind.ANNOTATION_TYPE) { + Node at = keyword(node, "@"); + if (at != null) { + tree.completeAtToken(token(at)); + } + } + if (kind == Tree.Kind.RECORD) { + Node parameters = field(node, "parameters"); + tree.completeRecordComponents( + parameters == null ? synthetic("(") : token(required(parameters, "(")), + parameters == null ? List.of() : parameters(parameters), + parameters == null ? synthetic(")") : token(required(parameters, ")"))); + } + Node superclass = childKind(node, "superclass"); + if (superclass != null) { + tree.completeSuperclass(token(required(superclass, "extends")), type(firstType(superclass))); + } + Node interfaces = childKind(node, "super_interfaces"); + if (interfaces == null) { + interfaces = childKind(node, "extends_interfaces"); + } + if (interfaces != null) { + tree.completeInterfaces(firstKeyword(interfaces), separatedTypes(interfaces)); + } + Node permits = childKind(node, "permits"); + if (permits != null) { + tree.completePermittedTypes(token(required(permits, "permits")), separatedTypes(permits)); + } + bindDeclaration(tree, nameNode(node), name); + if (tree.symbol() instanceof JTypeSymbol declared) { + if (tree.superClass() != null + && tree.superClass().symbolType().symbol() instanceof JTypeSymbol parent && parent.id != null && !parent.equals(declared)) { + declared.superName(parent.id); + } else if (kind == Tree.Kind.RECORD) { + declared.superName("java.lang.Record"); + } else if (kind == Tree.Kind.ENUM) { + declared.superName("java.lang.Enum"); + } + declared.declaredSupertypes(tree.superClass() == null ? null : tree.superClass().symbolType(), + tree.superInterfaces().stream().map(TypeTree::symbolType).toList()); + } + return tree; + } + + private List membersOf(Node body) { + List members = new ArrayList<>(); + List children = body.children(); + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if ("enum_constant".equals(child.k())) { + members.add(enumConstant(child, enumSeparator(children, i))); + } else if ("enum_body_declarations".equals(child.k())) { + List declarations = child.children(); + int j = 0; + while (childKind(body, "enum_constant") != null && j < declarations.size() && ";".equals(declarations.get(j).text())) { + j++; + } + for (; j < declarations.size(); j++) { + addMember(members, declarations.get(j)); + } + } else if (i + 1 < children.size() && brokenCompactConstructor(child, children.get(i + 1))) { + members.add(recoveredRecordConstructor(child, children.get(i + 1))); + i++; + } else { + addMember(members, child); + } + } + return members; + } + + /** Tree-sitter splits {@code Record throws Ex} into a field named {@code throws} and a compact constructor named {@code Ex}. */ + private static boolean brokenCompactConstructor(Node field, Node next) { + if (!"field_declaration".equals(field.k()) || !"compact_constructor_declaration".equals(next.k())) { + return false; + } + for (Node child : field.children()) { + if ("variable_declarator".equals(child.k())) { + Node name = field(child, "name"); + return name != null && "throws".equals(name.text()); + } + } + return false; + } + + private MethodTreeImpl recoveredRecordConstructor(Node field, Node compact) { + Node typeNode = field(field, "type"); + IdentifierTreeImpl name = identifier(typeNode == null ? field : typeNode); + Node throwsName = null; + for (Node child : field.children()) { + if ("variable_declarator".equals(child.k())) { + throwsName = field(child, "name"); + } + } + Node thrownName = nameNode(compact); + QualifiedIdentifierListTreeImpl thrown = QualifiedIdentifierListTreeImpl.emptyList(); + if (thrownName != null && !"throws".equals(thrownName.text())) { + thrown.add(type(thrownName)); + } + Node body = field(compact, "body"); + BlockTreeImpl block = body == null ? null : block(body); + MethodTreeImpl tree = new MethodTreeImpl( + null, + name, + new FormalParametersListTreeImpl(null, null), + throwsName == null ? null : token(throwsName), + thrown, + block, + null) + .completeWithModifiers(modifiers(field)); + bindDeclaration(tree, nameNode(compact), name); + return tree; + } + + @Nullable + private static Node enumSeparator(List siblings, int index) { + if (index + 1 >= siblings.size()) { + return null; + } + Node next = siblings.get(index + 1); + if (",".equals(next.text())) { + return next; + } + if ("enum_body_declarations".equals(next.k())) { + List declarations = next.children(); + if (!declarations.isEmpty() && ";".equals(declarations.get(0).text())) { + return declarations.get(0); + } + } + return null; + } + + private EnumConstantTreeImpl enumConstant(Node node, @Nullable Node separatorNode) { + IdentifierTreeImpl name = identifier(requiredField(node, "name", firstOf(node, "identifier"))); + Node arguments = field(node, "arguments"); + ArgumentListTreeImpl args = arguments == null ? ArgumentListTreeImpl.emptyList() : arguments(arguments); + Node body = field(node, "body"); + ClassTreeImpl classBody = null; + if (body != null) { + classBody = new ClassTreeImpl(Tree.Kind.CLASS, token(required(body, "{")), membersOf(body), token(required(body, "}"))); + bindType(classBody, body); + } + InternalSyntaxToken separator = separatorNode == null ? null : token(separatorNode); + EnumConstantTreeImpl tree = new EnumConstantTreeImpl(modifiers(node), name, new NewClassTreeImpl(name, args, classBody), separator); + bindDeclaration(tree, nameNode(node), name); + Symbol.TypeSymbol enumType = types.peek(); + if (enumType != null && !enumType.isUnknown()) { + String owner = enumType.type().fullyQualifiedName(); + Symbol.MethodSymbol constructor = sema.declaredMethod(owner, "", argumentTypes(args)); + name.symbol = constructor != null || !args.isEmpty() ? constructor : sema.resolve(owner + "#()"); + if (name.symbol instanceof Symbol.MethodSymbol method && !method.isUnknown()) { + recordUse(method, name); + } + ascribe(name, enumType.type()); + if (tree.symbol() instanceof JSymbol field && field.type().isUnknown()) { + field.type(enumType.type()); + } + } + return tree; + } + + private MethodTreeImpl annotationElement(Node node) { + FormalParametersListTreeImpl formals = new FormalParametersListTreeImpl(token(required(node, "(")), token(required(node, ")"))); + Node value = field(node, "value"); + IdentifierTreeImpl name = identifier(requiredField(node, "name", firstOf(node, "identifier"))); + MethodTreeImpl tree = new MethodTreeImpl(formals, value == null ? null : token(required(node, "default")), value == null ? null : expression(value)) + .complete(arrayType(type(requiredField(node, "type", firstType(node))), field(node, "dimensions")), name, token(required(node, ";"))) + .completeWithModifiers(modifiers(node)); + bindDeclaration(tree, nameNode(node), name); + return tree; + } + + private MethodTreeImpl method(Node node) { + scopes.addFirst(new HashMap<>()); + Node typeNode = field(node, "type"); + returnTargets.push(typeNode == null || field(node, "dimensions") != null ? Type.UNKNOWN : syntacticType(typeNode)); + try { + return methodInScope(node); + } finally { + scopes.removeFirst(); + returnTargets.pop(); + } + } + + private MethodTreeImpl methodInScope(Node node) { + Node parameters = field(node, "parameters"); + FormalParametersListTreeImpl formals = new FormalParametersListTreeImpl( + parameters == null ? null : token(required(parameters, "(")), + parameters == null ? null : token(required(parameters, ")"))); + if (parameters != null) { + VariableTree previous = null; + for (Node child : parameters.children()) { + if ("formal_parameter".equals(child.k()) || "spread_parameter".equals(child.k()) || "receiver_parameter".equals(child.k())) { + VariableTreeImpl parameter = variable(child, false); + formals.add(parameter); + previous = parameter; + } else if (",".equals(child.text()) && previous != null) { + ((VariableTreeImpl) previous).setEndToken(token(child)); + } + } + } + Node body = field(node, "body"); + BlockTreeImpl block = body == null ? null : block(body); + Node typeNode = field(node, "type"); + TypeTree returnType = typeNode == null || "constructor_declaration".equals(node.k()) ? null : arrayType(recoverQualifiedType(type(typeNode)), field(node, "dimensions")); + IdentifierTreeImpl name = identifier(requiredField(node, "name", firstOf(node, "identifier"))); + Node thrown = childKind(node, "throws"); + MethodTreeImpl tree = new MethodTreeImpl( + returnType, + name, + formals, + thrown == null ? null : token(required(thrown, "throws")), + thrown == null ? QualifiedIdentifierListTreeImpl.emptyList() : separatedTypes(thrown), + block, + body == null ? token(required(node, ";")) : null) + .completeWithModifiers(modifiers(node)); + Node typeParameters = field(node, "type_parameters"); + if (typeParameters != null) { + tree.completeWithTypeParameters(typeParameters(typeParameters)); + } + bindDeclaration(tree, nameNode(node), name); + return tree; + } + + private List parameters(Node parameters) { + List trees = new ArrayList<>(); + VariableTreeImpl previous = null; + for (Node child : parameters.children()) { + if ("formal_parameter".equals(child.k()) || "spread_parameter".equals(child.k())) { + previous = variable(child, false); + trees.add(previous); + } else if (",".equals(child.text()) && previous != null) { + previous.setEndToken(token(child)); + } + } + return trees; + } + + private List variables(Node declaration) { + ModifiersTreeImpl modifiers = modifiers(declaration); + Node typeNode = field(declaration, "type"); + TypeTree type; + if (typeNode != null) { + type = type(typeNode); + if ("field_declaration".equals(declaration.k()) || "constant_declaration".equals(declaration.k())) { + type = recoverQualifiedType(type); + } + } else if ("spread_parameter".equals(declaration.k())) { + type = spreadType(declaration); + } else { + Node catchTypeNode = childKind(declaration, "catch_type"); + type = catchTypeNode == null ? new VarTypeTreeImpl(synthetic("var")) : catchType(catchTypeNode); + } + List trees = new ArrayList<>(); + VariableTreeImpl previous = null; + for (Node child : declaration.children()) { + if ("variable_declarator".equals(child.k()) || "formal_parameter".equals(child.k())) { + previous = declarator(child, modifiers, type); + trees.add(previous); + } else if ("ERROR".equals(child.k()) && previous != null && previous.initializer() == null && containsNew(child)) { + InternalSyntaxToken equals = null; + for (Node part : child.children()) { + if ("=".equals(part.text())) { + equals = token(part); + } + } + previous.completeTypeAndInitializer(previous.type(), equals == null ? synthetic("=") : equals, arrayFromError(child)); + } else if ((",".equals(child.text()) || ";".equals(child.text())) && previous != null) { + previous.setEndToken(token(child)); + } + } + if (trees.isEmpty() && field(declaration, "name") != null) { + trees.add(declarator(declaration, modifiers, type)); + } + return trees; + } + + private static boolean containsNew(Node node) { + for (Node child : node.children()) { + if ("new".equals(child.text()) || containsNew(child)) { + return true; + } + } + return false; + } + + /** {@code new byte[]} left inside an error node beside the declarator. */ + private NewArrayTreeImpl arrayFromError(Node error) { + InternalSyntaxToken newKeyword = synthetic("new"); + Node typeNode = null; + List dims = new ArrayList<>(); + for (Node child : error.children()) { + if ("new".equals(child.text())) { + newKeyword = token(child); + } else if (isTypeKind(child.k())) { + typeNode = child; + } else if ("dimensions".equals(child.k()) || "dimensions_expr".equals(child.k())) { + dims.addAll(dimensions(child)); + } + } + if (dims.isEmpty()) { + dims.addAll(dimensions(error)); + } + NewArrayTreeImpl tree = new NewArrayTreeImpl(dims, InitializerListTreeImpl.emptyList()) + .complete(type(typeNode == null ? error : typeNode)) + .completeWithNewKeyword(newKeyword); + ascribe(tree, arrayTypeOf(tree.type(), Math.max(dims.size(), 1))); + return tree; + } + + private TypeTree spreadType(Node node) { + TypeTree element = type(firstType(node)); + JavaTree.ArrayTypeTreeImpl array = new JavaTree.ArrayTypeTreeImpl(element, List.of(), token(required(node, "..."))); + ascribe(array, JType.array(sema, element.symbolType() instanceof JType elementType ? elementType : JType.unknown(sema))); + return array; + } + + private VariableTreeImpl variable(Node parameter, boolean sharedModifiers) { + return variables(parameter).get(0); + } + + private VariableTreeImpl declarator(Node node, ModifiersTreeImpl modifiers, TypeTree type) { + IdentifierTreeImpl name = identifier(requiredField(node, "name", firstOf(node, "identifier"))); + VariableTreeImpl tree = new VariableTreeImpl(name).completeModifiersAndType(modifiers, applyDimensions(type, node)); + Node value = field(node, "value"); + if (value != null) { + ExpressionTree initializer = expression(value); + ascribeTarget(initializer, tree.type().symbolType()); + tree.completeTypeAndInitializer(tree.type(), token(required(node, "=")), initializer); + } + bindDeclaration(tree, nameNode(node) == null ? node : nameNode(node), name); + Type declared = tree.type().symbolType(); + if (declared.isParameterized() && tree.symbol() instanceof JSymbol symbol) { + symbol.type(declared); + } else if (tree.symbol().type() != null && !tree.symbol().type().isUnknown()) { + ascribe(tree.type(), tree.symbol().type()); + } + return tree; + } + + private TypeTree applyDimensions(TypeTree type, Node node) { + Node dimensions = field(node, "dimensions"); + if (dimensions == null) { + return type; + } + return arrayType(type, dimensions); + } + + private BlockTreeImpl staticInitializer(Node node) { + Node body = field(node, "body"); + if (body == null) { + body = node; + } + BlockTreeImpl block = block(body); + return new StaticInitializerTreeImpl(token(required(node, "static")), (InternalSyntaxToken) block.openBraceToken(), block.body(), (InternalSyntaxToken) block.closeBraceToken()); + } + + private BlockTreeImpl initializer(Node node) { + BlockTreeImpl block = block(node); + return new BlockTreeImpl(Tree.Kind.INITIALIZER, (InternalSyntaxToken) block.openBraceToken(), block.body(), (InternalSyntaxToken) block.closeBraceToken()); + } + + private BlockTreeImpl block(Node node) { + scopes.addFirst(new HashMap<>()); + try { + return blockInScope(node); + } finally { + scopes.removeFirst(); + } + } + + private BlockTreeImpl blockInScope(Node node) { + if (!"block".equals(node.k())) { + Node nested = childKind(node, "block"); + if (nested != null) { + node = nested; + } + } + InternalSyntaxToken open = token(required(node, "{")); + InternalSyntaxToken close = token(required(node, "}")); + List statements = new ArrayList<>(); + for (Node child : node.children()) { + if ("{".equals(child.text()) || "}".equals(child.text())) { + continue; + } + addStatement(statements, child); + } + return new BlockTreeImpl(open, statements, close); + } + + private void addStatement(List statements, Node node) { + if ("ERROR".equals(node.k())) { + for (Node nested : node.children()) { + if (nested.children().isEmpty() && nested.text().equals(nested.k())) { + continue; + } + addStatement(statements, nested); + } + return; + } + if ("local_variable_declaration".equals(node.k())) { + statements.addAll(variables(node)); + return; + } + if (";".equals(node.text())) { + statements.add(new EmptyStatementTreeImpl(token(node))); + return; + } + Node switchNode = childKind(node, "switch_expression"); + if ("expression_statement".equals(node.k()) && switchNode != null) { + statements.add(switchStatement(switchNode)); + statements.add(new EmptyStatementTreeImpl(token(required(node, ";")))); + return; + } + statements.add(statement(node)); + } + + private StatementTree statement(Node node) { + return switch (node.k()) { + case "block" -> block(node); + case "expression_statement" -> expressionStatement(node); + case "explicit_constructor_invocation" -> constructorInvocation(node); + case "if_statement" -> ifStatement(node); + case "while_statement" -> whileStatement(node); + case "do_statement" -> doStatement(node); + case "for_statement" -> forStatement(node); + case "enhanced_for_statement" -> forEach(node); + case "break_statement" -> breakStatement(node); + case "continue_statement" -> continueStatement(node); + case "return_statement" -> returnStatement(node); + case "yield_statement" -> yieldStatement(node); + case "throw_statement" -> throwStatement(node); + case "assert_statement" -> assertStatement(node); + case "labeled_statement" -> labeled(node); + case "try_statement", "try_with_resources_statement" -> tryStatement(node); + case "synchronized_statement" -> synchronizedStatement(node); + case "switch_statement", "switch_expression" -> switchStatement(node); + case "local_variable_declaration" -> variables(node).get(0); + case "class_declaration", "interface_declaration", "enum_declaration", "record_declaration" -> typeDeclaration(node); + case "ERROR" -> recoveredStatement(node); + default -> new EmptyStatementTreeImpl(";".equals(node.text()) ? token(node) : token(required(node, ";"))); + }; + } + + private ExpressionStatementTreeImpl expressionStatement(Node node) { + ExpressionTree expression = null; + InternalSyntaxToken semicolon = null; + for (Node child : node.children()) { + if (";".equals(child.text())) { + semicolon = token(child); + } else if (expression == null) { + expression = expression(child); + } + } + return new ExpressionStatementTreeImpl(expression == null ? literal(Tree.Kind.NULL_LITERAL, synthetic("null"), sema.getClassType("")) : expression, semicolon); + } + + private ExpressionStatementTreeImpl constructorInvocation(Node node) { + Node constructor = requiredField(node, "constructor", firstOf(node, "super", "this")); + IdentifierTreeImpl identifier = identifier(constructor); + bindUse(identifier, constructor); + Node object = field(node, "object"); + ExpressionTree select = object == null + ? identifier + : new MemberSelectExpressionTreeImpl(expression(object), token(required(node, ".")), identifier); + Node typeArguments = field(node, "type_arguments"); + MethodInvocationTreeImpl invocation = new MethodInvocationTreeImpl(select, typeArguments == null ? null : typeArguments(typeArguments), + arguments(requiredField(node, "arguments", childKind(node, "argument_list")))); + if (identifier.symbol instanceof Symbol.MethodSymbol method) { + invocation.methodSymbol = method; + ascribe(invocation, method.owner().type()); + ascribe(identifier, Type.UNKNOWN); + } + return new ExpressionStatementTreeImpl(invocation, token(required(node, ";"))); + } + + private IfStatementTreeImpl ifStatement(Node node) { + Parens condition = parens(requiredField(node, "condition", childKind(node, "parenthesized_expression"))); + Node consequence = field(node, "consequence"); + Node alternative = field(node, "alternative"); + return new IfStatementTreeImpl( + token(required(node, "if")), + condition.open, + condition.expr, + condition.close, + consequence == null ? new EmptyStatementTreeImpl(synthetic(";")) : statement(consequence), + alternative == null ? null : token(required(node, "else")), + alternative == null ? null : statement(alternative)); + } + + private WhileStatementTreeImpl whileStatement(Node node) { + Parens condition = parens(requiredField(node, "condition", childKind(node, "parenthesized_expression"))); + return new WhileStatementTreeImpl(token(required(node, "while")), condition.open, condition.expr, condition.close, statement(requiredField(node, "body", lastNamed(node)))); + } + + private DoWhileStatementTreeImpl doStatement(Node node) { + Node body = field(node, "body"); + Parens condition = parens(requiredField(node, "condition", childKind(node, "parenthesized_expression"))); + return new DoWhileStatementTreeImpl( + token(required(node, "do")), + body == null ? new EmptyStatementTreeImpl(synthetic(";")) : statement(body), + token(required(node, "while")), + condition.open, + condition.expr, + condition.close, + token(lastKeyword(node, ";"))); + } + + private ForStatementTreeImpl forStatement(Node node) { + scopes.addFirst(new HashMap<>()); + try { + return forStatementInScope(node); + } finally { + scopes.removeFirst(); + } + } + + private ForStatementTreeImpl forStatementInScope(Node node) { + StatementListTreeImpl init = StatementListTreeImpl.emptyList(); + StatementListTreeImpl update = StatementListTreeImpl.emptyList(); + InternalSyntaxToken firstSemicolon = null; + InternalSyntaxToken secondSemicolon = null; + ExpressionTree condition = null; + int semicolons = 0; + for (Node child : node.children()) { + if (")".equals(child.text())) { + break; + } + if ("for".equals(child.text()) || "(".equals(child.text())) { + continue; + } + if (";".equals(child.text())) { + semicolons++; + if (semicolons == 1) { + firstSemicolon = token(child); + } else { + secondSemicolon = token(child); + } + continue; + } + if (",".equals(child.text())) { + (semicolons == 0 ? init : update).separators().add(token(child)); + continue; + } + if ("local_variable_declaration".equals(child.k()) && semicolons == 0) { + init.addAll(variables(child)); + InternalSyntaxToken semi = optionalToken(child, ";"); + if (semi != null) { + firstSemicolon = semi; + semicolons = 1; + } + } else if (semicolons == 0) { + init.add(new ExpressionStatementTreeImpl(expression(child), null)); + } else if (semicolons == 1 && condition == null && !"block".equals(child.k()) && !same(field(node, "body"), child)) { + condition = expression(child); + } else if (semicolons >= 1 && same(child, field(node, "body"))) { + break; + } else if (semicolons >= 2 || same(field(node, "update"), child)) { + update.add(new ExpressionStatementTreeImpl(expression(child), null)); + } + } + Node body = field(node, "body"); + return new ForStatementTreeImpl( + token(required(node, "for")), + token(required(node, "(")), + init, + firstSemicolon == null ? synthetic(";") : firstSemicolon, + condition, + secondSemicolon == null ? synthetic(";") : secondSemicolon, + update, + token(required(node, ")")), + body == null ? new EmptyStatementTreeImpl(synthetic(";")) : statement(body)); + } + + private ForEachStatementImpl forEach(Node node) { + scopes.addFirst(new HashMap<>()); + try { + return forEachInScope(node); + } finally { + scopes.removeFirst(); + } + } + + private ForEachStatementImpl forEachInScope(Node node) { + TypeTree type = arrayType(type(requiredField(node, "type", firstType(node))), field(node, "dimensions")); + IdentifierTreeImpl name = identifier(requiredField(node, "name", firstOf(node, "identifier"))); + VariableTreeImpl variable = new VariableTreeImpl(modifiers(node), type, name); + bindDeclaration(variable, requiredField(node, "name", nameNode(node)), name); + return new ForEachStatementImpl( + token(required(node, "for")), + token(required(node, "(")), + variable, + token(required(node, ":")), + expression(requiredField(node, "value", firstNamed(node))), + token(required(node, ")")), + statement(requiredField(node, "body", lastNamed(node)))); + } + + private BreakStatementTreeImpl breakStatement(Node node) { + IdentifierTreeImpl label = labelUse(node); + return new BreakStatementTreeImpl(token(required(node, "break")), label, token(required(node, ";"))); + } + + private ContinueStatementTreeImpl continueStatement(Node node) { + return new ContinueStatementTreeImpl(token(required(node, "continue")), labelUse(node), token(required(node, ";"))); + } + + private ReturnStatementTreeImpl returnStatement(Node node) { + ExpressionTree expression = null; + for (Node child : node.children()) { + if (!"return".equals(child.text()) && !";".equals(child.text())) { + expression = expression(child); + } + } + if (expression instanceof NewClassTreeImpl newClass && !returnTargets.isEmpty()) { + inferDiamond(newClass, returnTargets.peek()); + } + return new ReturnStatementTreeImpl(token(required(node, "return")), expression, token(required(node, ";"))); + } + + private YieldStatementTreeImpl yieldStatement(Node node) { + ExpressionTree expression = null; + for (Node child : node.children()) { + if (!"yield".equals(child.text()) && !";".equals(child.text())) { + expression = expression(child); + } + } + Node keyword = keyword(node, "yield"); + return new YieldStatementTreeImpl(keyword == null ? null : token(keyword), expression, token(required(node, ";"))); + } + + private ThrowStatementTreeImpl throwStatement(Node node) { + return new ThrowStatementTreeImpl(token(required(node, "throw")), expression(firstNamed(node)), token(required(node, ";"))); + } + + private AssertStatementTreeImpl assertStatement(Node node) { + List expressions = new ArrayList<>(); + InternalSyntaxToken colon = null; + for (Node child : node.children()) { + if (":".equals(child.text())) { + colon = token(child); + } else if (!"assert".equals(child.text()) && !";".equals(child.text())) { + expressions.add(expression(child)); + } + } + AssertStatementTreeImpl tree = new AssertStatementTreeImpl(token(required(node, "assert")), expressions.get(0), token(required(node, ";"))); + if (expressions.size() > 1) { + tree.complete(colon == null ? synthetic(":") : colon, expressions.get(1)); + } + return tree; + } + + private LabeledStatementTreeImpl labeled(Node node) { + IdentifierTreeImpl name = identifier(firstOf(node, "identifier")); + JLabelSymbol symbol = new JLabelSymbol(name.name()); + labels.push(symbol); + List children = node.children(); + LabeledStatementTreeImpl tree = new LabeledStatementTreeImpl(name, token(required(node, ":")), statement(children.get(children.size() - 1))); + labels.pop(); + symbol.declaration = tree; + tree.labelSymbol = symbol; + name.labelSymbol = symbol; + String id = nameNode(node) == null ? null : firstOf(node, "identifier").decl(); + if (id != null) { + sema.declare(id, tree); + } + return tree; + } + + private TryStatementTreeImpl tryStatement(Node node) { + ResourceListTreeImpl resources = ResourceListTreeImpl.emptyList(); + Node resourceNode = field(node, "resources"); + if (resourceNode != null) { + for (Node child : resourceNode.children()) { + if ("resource".equals(child.k()) && field(child, "name") == null) { + resources.add(expression(firstNamed(child))); + } else if ("resource".equals(child.k()) || "local_variable_declaration".equals(child.k())) { + for (VariableTreeImpl resource : variables(child)) { + // A resource variable is implicitly final. + if (resource.symbol() instanceof JSymbol symbol) { + symbol.addModifiers(FINAL_MODIFIER); + } + resources.add(resource); + } + } else if (";".equals(child.text())) { + resources.separators().add(token(child)); + } else if (!"(".equals(child.text()) && !")".equals(child.text())) { + resources.add(expression(child)); + } + } + } + List catches = new ArrayList<>(); + for (Node child : node.children()) { + if ("catch_clause".equals(child.k())) { + scopes.addFirst(new HashMap<>()); + try { + Node parameter = field(child, "parameter"); + if (parameter == null) { + parameter = childKind(child, "formal_parameter") == null ? childKind(child, "catch_formal_parameter") : childKind(child, "formal_parameter"); + } + VariableTreeImpl variable = parameter == null ? variables(child).get(0) : variable(parameter, false); + catches.add(new CatchTreeImpl(token(required(child, "catch")), token(required(child, "(")), variable, token(required(child, ")")), block(requiredField(child, "body", childKind(child, "block"))))); + } finally { + scopes.removeFirst(); + } + } + } + Node body = field(node, "body"); + Node finallyBlock = childKind(node, "finally_clause"); + if (finallyBlock == null) { + finallyBlock = field(node, "finally"); + } + return new TryStatementTreeImpl( + token(required(node, "try")), + resources.isEmpty() ? null : token(required(node, "(")), + resources, + resources.isEmpty() ? null : token(required(node, ")")), + body == null ? block(node) : block(body), + catches, + finallyBlock == null ? null : token(required(finallyBlock, "finally")), + finallyBlock == null ? null : block(childKind(finallyBlock, "block") == null ? finallyBlock : childKind(finallyBlock, "block"))); + } + + private SynchronizedStatementTreeImpl synchronizedStatement(Node node) { + Parens parens = parens(requiredField(node, "condition", childKind(node, "parenthesized_expression"))); + return new SynchronizedStatementTreeImpl(token(required(node, "synchronized")), parens.open, parens.expr, parens.close, block(requiredField(node, "body", childKind(node, "block")))); + } + + private SwitchStatementTreeImpl switchStatement(Node node) { + Parens parens = parens(requiredField(node, "condition", childKind(node, "parenthesized_expression"))); + Node body = field(node, "body"); + return new SwitchStatementTreeImpl( + token(required(node, "switch")), + parens.open, + parens.expr, + parens.close, + body == null ? synthetic("{") : token(required(body, "{")), + body == null ? List.of() : switchGroups(body, false), + body == null ? synthetic("}") : token(required(body, "}"))); + } + + private SwitchExpressionTreeImpl switchExpression(Node node) { + boolean previous = switchExpression; + switchExpression = true; + Parens parens = parens(requiredField(node, "condition", childKind(node, "parenthesized_expression"))); + Node body = field(node, "body"); + SwitchExpressionTreeImpl tree = new SwitchExpressionTreeImpl( + token(required(node, "switch")), + parens.open, + parens.expr, + parens.close, + body == null ? synthetic("{") : token(required(body, "{")), + body == null ? List.of() : switchGroups(body, true), + body == null ? synthetic("}") : token(required(body, "}"))); + switchExpression = previous; + ascribe(tree, switchResultType(tree)); + return tree; + } + + private static Type switchResultType(SwitchExpressionTreeImpl tree) { + List results = new ArrayList<>(); + for (CaseGroupTree group : tree.cases()) { + List body = group.body(); + if (group.labels().stream().noneMatch(CaseLabelTree::isFallThrough) && body.size() == 1 && body.get(0) instanceof ExpressionStatementTree statement) { + results.add(statement.expression().symbolType()); + } + body.forEach(statement -> statement.accept(new BaseTreeVisitor() { + @Override + public void visitYieldStatement(YieldStatementTree yield) { + results.add(yield.expression().symbolType()); + } + + @Override + public void visitSwitchExpression(SwitchExpressionTree nested) { + // nested switch expressions yield their own results + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree lambda) { + // lambdas cannot yield to the enclosing switch + } + + @Override + public void visitClass(ClassTree type) { + // classes cannot yield to the enclosing switch + } + })); + } + if (results.isEmpty() || results.stream().anyMatch(Type::isUnknown) || results.stream().distinct().count() != 1) { + return Type.UNKNOWN; + } + return results.get(0); + } + + private List switchGroups(Node body, boolean expression) { + List groups = new ArrayList<>(); + List labels = new ArrayList<>(); + List carried = new ArrayList<>(); + StatementListTreeImpl statements = StatementListTreeImpl.emptyList(); + List children = body.children(); + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if ("{".equals(child.text()) || "}".equals(child.text())) { + continue; + } + if ("switch_label".equals(child.k()) || "switch_rule".equals(child.k()) || "switch_block_statement_group".equals(child.k())) { + if ("switch_label".equals(child.k())) { + if (!statements.isEmpty()) { + groups.add(new CaseGroupTreeImpl(labels, statements)); + labels = new ArrayList<>(); + statements = StatementListTreeImpl.emptyList(); + } + Node next = i + 1 < children.size() ? children.get(i + 1) : null; + boolean separator = next != null && ":".equals(next.text()); + labels.add(caseLabel(child, separator ? token(next) : null)); + if (separator) { + i++; + } + } else { + CaseGroupTreeImpl group = switchArm(child, expression, carried); + if (group.body().isEmpty() && "switch_block_statement_group".equals(child.k()) && nextIsGroup(children, i)) { + carried = group.labels().stream().map(CaseLabelTreeImpl.class::cast).collect(Collectors.toCollection(ArrayList::new)); + } else { + groups.add(group); + carried = new ArrayList<>(); + } + } + } else if (!labels.isEmpty()) { + addStatement(statements, child); + } + } + if (!labels.isEmpty()) { + groups.add(new CaseGroupTreeImpl(labels, statements)); + } + return groups; + } + + private static boolean nextIsGroup(List siblings, int index) { + return index + 1 < siblings.size() && "switch_block_statement_group".equals(siblings.get(index + 1).k()); + } + + private CaseGroupTreeImpl switchArm(Node node, boolean expression, List carried) { + List labels = new ArrayList<>(carried); + StatementListTreeImpl body = StatementListTreeImpl.emptyList(); + List children = node.children(); + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if ("switch_label".equals(child.k())) { + Node next = i + 1 < children.size() ? children.get(i + 1) : null; + boolean separator = next != null && (":".equals(next.text()) || "->".equals(next.text())); + labels.add(caseLabel(child, separator ? token(next) : null)); + if (separator) { + i++; + } + continue; + } + if ("->".equals(child.text()) || ":".equals(child.text())) { + continue; + } + if ("expression_statement".equals(child.k()) && "switch_rule".equals(node.k())) { + ExpressionStatementTreeImpl statement = expressionStatement(child); + body.add(new YieldStatementTreeImpl(null, statement.expression(), (InternalSyntaxToken) statement.semicolonToken())); + } else if (expression && !isStatementKind(child.k())) { + ExpressionTree value = expression(child); + body.add(new YieldStatementTreeImpl(null, value, optionalToken(node, ";"))); + } else { + addStatement(body, child); + } + } + return new CaseGroupTreeImpl(labels, body); + } + + private CaseLabelTreeImpl caseLabel(Node node, @Nullable InternalSyntaxToken separator) { + InternalSyntaxToken keyword = keyword(node, "case") != null ? token(keyword(node, "case")) : token(required(node, "default")); + List expressions = new ArrayList<>(); + InternalSyntaxToken end = separator; + boolean caseLabel = keyword(node, "case") != null; + for (Node child : node.children()) { + if ("case".equals(child.text())) { + continue; + } + if ("default".equals(child.text())) { + if (caseLabel) { + expressions.add(new DefaultPatternTreeImpl(token(child))); + } + } else if (":".equals(child.text()) || "->".equals(child.text())) { + end = token(child); + } else if (",".equals(child.text())) { + continue; + } else if ("pattern".equals(child.k()) || "record_pattern".equals(child.k()) || "type_pattern".equals(child.k())) { + expressions.add(pattern(child)); + } else if ("guard".equals(child.k()) && !expressions.isEmpty() && expressions.get(expressions.size() - 1) instanceof PatternTree guarded) { + GuardedPatternTreeImpl pattern = new GuardedPatternTreeImpl(guarded, token(required(child, "when")), expression(firstNamed(child))); + ascribe(pattern, guarded.symbolType()); + expressions.set(expressions.size() - 1, pattern); + } else if ("null_literal".equals(child.k())) { + expressions.add(new NullPatternTreeImpl((LiteralTreeImpl) expression(child))); + } else { + expressions.add(expression(child)); + } + } + return new CaseLabelTreeImpl(keyword, expressions, end == null ? synthetic(":") : end); + } + + private ExpressionTree expression(Node node) { + if (node == null) { + return literal(Tree.Kind.NULL_LITERAL, synthetic("null"), JType.nullType(sema)); + } + ExpressionTree tree = switch (node.k()) { + case "identifier", "type_identifier", "this", "super", "true", "false", "null_literal" -> primary(node); + case "decimal_integer_literal", "hex_integer_literal", "octal_integer_literal", "binary_integer_literal" -> integerLiteral(node); + case "decimal_floating_point_literal", "hex_floating_point_literal" -> floatingLiteral(node); + case "string_literal", "string_fragment" -> literal(node.text().startsWith("\"\"\"") ? Tree.Kind.TEXT_BLOCK : Tree.Kind.STRING_LITERAL, token(node), sema.getClassType("java.lang.String")); + case "character_literal" -> literal(Tree.Kind.CHAR_LITERAL, token(node), sema.getClassType("char")); + case "text_block" -> literal(Tree.Kind.TEXT_BLOCK, token(node), sema.getClassType("java.lang.String")); + case "scoped_identifier", "scoped_type_identifier", "field_access" -> memberSelect(node); + case "method_invocation" -> invocation(node); + case "object_creation_expression" -> creation(node); + case "array_creation_expression" -> arrayCreation(node); + case "array_initializer", "element_value_array_initializer" -> arrayInitializer(node); + case "element_value_pair" -> elementValuePair(node); + case "class_literal" -> classLiteral(node); + case "annotation", "marker_annotation" -> annotation(node); + case "array_access" -> arrayAccess(node); + case "assignment_expression" -> assignment(node); + case "binary_expression" -> binary(node); + case "unary_expression" -> unary(node, true); + case "update_expression" -> update(node); + case "parenthesized_expression" -> parenthesized(node); + case "ternary_expression" -> ternary(node); + case "cast_expression" -> cast(node); + case "instanceof_expression" -> instanceOf(node); + case "lambda_expression" -> lambda(node); + case "method_reference" -> methodReference(node); + case "switch_expression" -> switchExpression(node); + case "generic_type", "array_type", "integral_type", "floating_point_type", "boolean_type", "void_type", "wildcard", "annotated_type" -> typeExpression(node); + case "ERROR" -> recoveredExpression(node); + default -> primary(node); + }; + return tree; + } + + /** The first recognizable expression inside a syntax error, so the surrounding code keeps a tree. */ + private ExpressionTree recoveredExpression(Node node) { + for (Node child : node.children()) { + String kind = child.k(); + if ("ERROR".equals(kind) || kind.endsWith("_expression") || kind.endsWith("_literal") || kind.endsWith("_identifier") + || "identifier".equals(kind) || "array_creation_expression".equals(kind) || "object_creation_expression".equals(kind) + || "method_invocation".equals(kind) || "field_access".equals(kind) || "array_access".equals(kind) + || "lambda_expression".equals(kind) || "method_reference".equals(kind) || "class_literal".equals(kind) + || "array_initializer".equals(kind) || "this".equals(kind) || "super".equals(kind)) { + return expression(child); + } + } + return literal(Tree.Kind.NULL_LITERAL, synthetic("null"), JType.nullType(sema)); + } + + /** The first recognizable statement inside a syntax error. */ + private StatementTree recoveredStatement(Node node) { + for (Node child : node.children()) { + String kind = child.k(); + if ("ERROR".equals(kind) || kind.endsWith("_statement") || "block".equals(kind) || kind.endsWith("_declaration")) { + return statement(child); + } + } + return new EmptyStatementTreeImpl(synthetic(";")); + } + + private ExpressionTree primary(Node node) { + if ("this".equals(node.k()) || "this".equals(node.text())) { + KeywordThis keyword = new KeywordThis(token(node)); + Symbol.TypeSymbol enclosing = types.peek(); + if (enclosing != null) { + keyword.symbol = sema.receiverSymbol(enclosing, "this", enclosing.type()); + ascribe(keyword, enclosing.type()); + } + return keyword; + } + if ("super".equals(node.k()) || "super".equals(node.text())) { + KeywordSuper keyword = new KeywordSuper(token(node)); + Symbol.TypeSymbol enclosing = types.peek(); + if (enclosing != null) { + Type superType = enclosing.superClass() == null ? Type.UNKNOWN : enclosing.superClass(); + keyword.symbol = sema.receiverSymbol(enclosing, "super", superType); + ascribe(keyword, superType); + } + return keyword; + } + if ("true".equals(node.k()) || "false".equals(node.k()) || "true".equals(node.text()) || "false".equals(node.text())) { + return literal(Tree.Kind.BOOLEAN_LITERAL, token(node), sema.getClassType("boolean")); + } + if ("null_literal".equals(node.k()) || "null".equals(node.text())) { + return literal(Tree.Kind.NULL_LITERAL, token(node), JType.nullType(sema)); + } + IdentifierTreeImpl identifier = identifier(node); + bindUse(identifier, node); + if (!naming && identifier.symbol().isUnknown() && node.unresolved()) { + String type = importedType(identifier.name()); + if (type != null) { + bindType(identifier, type); + } + } + if (!naming && "type_identifier".equals(node.k())) { + preferSingleImport(identifier); + recoverType(identifier); + } + return identifier; + } + + /** An unresolved qualified type name keeps its written name where the compiler's recovery does. */ + private TypeTree recoverQualifiedType(TypeTree type) { + if (!naming && type instanceof MemberSelectExpressionTreeImpl select && select.symbolType().isUnknown() + && select.identifier() instanceof IdentifierTreeImpl identifier && identifier.symbol().isUnknown()) { + String name = dottedName(select); + if (!name.isEmpty()) { + JTypeSymbol recovered = sema.recoveredType(name); + identifier.symbol = recovered; + ascribe(identifier, recovered.type()); + ascribe(select, recovered.type()); + } + } + return type; + } + + /** The type of an annotation: an unresolved one keeps its written, possibly qualified, name. */ + private Type annotationType(TypeTree type) { + String name = type instanceof ExpressionTree expression ? dottedName(expression) : ""; + if (type.symbolType().isUnknown() && !naming && !name.isEmpty()) { + return sema.recoveredType(name).type(); + } + return type.symbolType(); + } + + /** An unresolved type name keeps its written name, as a recovered binding does, so checks can match on the simple name. */ + private void recoverType(IdentifierTreeImpl identifier) { + if (identifier.symbol().isUnknown()) { + JTypeSymbol recovered = sema.recoveredType(identifier.name()); + identifier.symbol = recovered; + ascribe(identifier, recovered.type()); + } + } + + /** A duplicate single-type import is an error; recovery keeps the first, which is the type this simple name denotes. */ + private void preferSingleImport(IdentifierTreeImpl identifier) { + String imported = singleImports.get(identifier.name()); + if (imported == null || !sema.declaresType(imported)) { + return; + } + Symbol symbol = identifier.symbol(); + boolean constructor = symbol.isMethodSymbol() && "".equals(symbol.name()); + if (!symbol.isTypeSymbol() && !symbol.isUnknown() && !constructor) { + return; + } + String resolved = identifier.symbolType().erasure().fullyQualifiedName(); + if (!imported.equals(resolved)) { + bindType(identifier, imported); + } + } + + /** The type a simple name stands for when SemSitter left it unresolved: imported, in the package, or in java.lang. */ + @Nullable + private String importedType(String name) { + List candidates = new ArrayList<>(); + if (singleImports.containsKey(name)) { + candidates.add(singleImports.get(name)); + } + candidates.add(packageName.isEmpty() ? name : (packageName + "." + name)); + onDemandImports.forEach(prefix -> candidates.add(prefix + "." + name)); + candidates.add("java.lang." + name); + return candidates.stream().filter(sema::declaresType).findFirst().orElse(null); + } + + private void resolveQualifiedType(ExpressionTree qualifier, IdentifierTreeImpl identifier) { + Symbol owner = qualifier instanceof MemberSelectExpressionTree select ? select.identifier().symbol() : (qualifier instanceof IdentifierTree name ? name.symbol() : Symbol.UNKNOWN_SYMBOL); + String candidate; + if (owner.isTypeSymbol() && !owner.isUnknown()) { + candidate = ((Symbol.TypeSymbol) owner).type().fullyQualifiedName() + "$" + identifier.name(); + } else if (owner.isUnknown() || owner.isPackageSymbol()) { + String prefix = dottedName(qualifier); + candidate = prefix.isEmpty() ? null : (prefix + "." + identifier.name()); + } else { + candidate = null; + } + if (candidate != null && sema.declaresType(candidate)) { + bindType(identifier, candidate); + } + } + + @Nullable + private String fieldOwner(ExpressionTree qualifier) { + Type receiver = qualifier.symbolType().erasure(); + if (receiver.isClass()) { + return receiver.fullyQualifiedName(); + } + return qualifier instanceof IdentifierTree name ? importedType(name.name()) : null; + } + + /** The method SemSitter left unresolved, looked up in the receiver type, or in the enclosing types and static imports of an unqualified call. */ + @Nullable + private Symbol.MethodSymbol libraryMethod(@Nullable ExpressionTree qualifier, String name, List arguments) { + List owners = new ArrayList<>(); + if (qualifier != null) { + Type receiver = qualifier.symbolType().erasure(); + if (receiver.isClass()) { + owners.add(receiver.fullyQualifiedName()); + } else if (receiver.isArray()) { + owners.add("java.lang.Object"); + } else if (qualifier instanceof IdentifierTree typeName) { + String imported = importedType(typeName.name()); + if (imported != null) { + owners.add(imported); + } + } + } else { + types.stream().filter(type -> !type.isUnknown()).forEach(type -> owners.add(type.type().fullyQualifiedName())); + if (staticImports.containsKey(name)) { + owners.add(staticImports.get(name)); + } + owners.addAll(staticOnDemandImports); + } + Symbol.MethodSymbol found = owners.stream().map(owner -> sema.declaredMethod(owner, name, arguments)).filter(Objects::nonNull).findFirst().orElse(null); + if (found == null && qualifier != null && !owners.contains("java.lang.Object")) { + found = sema.declaredMethod("java.lang.Object", name, arguments); + } + return found; + } + + /** Types a lambda, a method reference or an array initializer from the type its context expects. */ + private void ascribeTarget(ExpressionTree expression, Type target) { + boolean targetTyped = expression.is(Tree.Kind.LAMBDA_EXPRESSION, Tree.Kind.METHOD_REFERENCE) || (expression instanceof NewArrayTreeImpl array && array.type() == null); + if (targetTyped && expression.symbolType().isUnknown() && !target.isUnknown()) { + ascribe(expression, expression instanceof NewArrayTreeImpl ? target : sema.groundType(target)); + } + if (expression instanceof NewClassTreeImpl newClass) { + inferDiamond(newClass, target); + } + if (expression instanceof LambdaExpressionTreeImpl lambda) { + bindLambdaParameterOwner(lambda, lambda.symbolType()); + } + } + + /** A diamond takes the type arguments its target gives the created class, and its constructor is seen from that type. */ + private void inferDiamond(NewClassTreeImpl tree, Type target) { + if (!(tree.identifier() instanceof JavaTree.ParameterizedTypeTreeImpl generic) || !generic.typeArguments().isEmpty() || !target.isParameterized()) { + return; + } + Type inferred = sema.inferDiamond(tree.symbolType(), sema.groundType(target)); + if (!inferred.isParameterized()) { + return; + } + ascribe(tree, inferred); + ascribe(generic, inferred); + if (tree.getConstructorIdentifier() instanceof IdentifierTreeImpl identifier) { + ascribe(identifier, inferred); + if (identifier.symbol() instanceof JMethodSymbol constructor && !constructor.isUnknown()) { + List parameters = sema.invocationParameters(constructor, inferred, argumentTypes(tree.arguments())); + identifier.symbol = sema.invocationView(constructor, inferred, parameters, constructor.returnType().type()); + } + } + } + + private void bindLambdaParameterOwner(LambdaExpressionTreeImpl lambda, Type target) { + JMethodSymbol functional = sema.functionalMethod(target); + if (functional == null || functional.parameterTypes().size() != lambda.parameters().size()) { + return; + } + List substituted = sema.invocationParameters(functional, target, List.of()); + List types = new ArrayList<>(); + for (int i = 0; i < lambda.parameters().size(); i++) { + VariableTree parameter = lambda.parameters().get(i); + Type type = parameter.symbol().type(); + if ((type == null || type.isUnknown()) && parameter.symbol() instanceof JVariableSymbol symbol && !substituted.get(i).isUnknown()) { + type = substituted.get(i); + symbol.type(type); + ascribe((IdentifierTreeImpl) parameter.simpleName(), type); + } + types.add(type == null || type.isUnknown() ? functional.parameterTypes().get(i) : type); + } + JMethodSymbol view = sema.invocationView(functional, target, types, sema.invocationReturn(functional, target, List.of())); + for (VariableTree parameter : lambda.parameters()) { + if (parameter.symbol() instanceof JVariableSymbol symbol) { + symbol.owner(view); + } + } + lambda.body().accept(new BaseTreeVisitor() { + @Override + public void visitVariable(VariableTree tree) { + if (tree.symbol() instanceof JVariableSymbol symbol && symbol.owner() instanceof JSymbol owner && owner.id != null && owner.id.contains("$lambda$")) { + symbol.owner(view); + } + super.visitVariable(tree); + } + + @Override + public void visitLambdaExpression(LambdaExpressionTree tree) { + // nested lambdas own their variables + } + + @Override + public void visitClass(ClassTree tree) { + // local and anonymous classes own their variables + } + }); + } + + private void ascribeArguments(Symbol.MethodSymbol method, List arguments) { + ascribeArguments(method, method.parameterTypes(), arguments); + } + + /** Types the arguments from the declared parameters, substituted for the receiver's type arguments when the call is generic. */ + private void ascribeArguments(Symbol.MethodSymbol method, List parameters, List arguments) { + for (int i = 0; i < arguments.size() && !parameters.isEmpty(); i++) { + Type parameter = parameters.get(Math.min(i, parameters.size() - 1)); + if (method.isVarArgsMethod() && i >= parameters.size() - 1 && parameter.isArray() && arguments.size() != parameters.size()) { + parameter = ((Type.ArrayType) parameter).elementType(); + } + if (i < parameters.size() || method.isVarArgsMethod()) { + ascribeTarget(arguments.get(i), parameter); + } + } + } + + private static List argumentTypes(List arguments) { + return arguments.stream().map(ExpressionTree::symbolType).toList(); + } + + private void bindType(IdentifierTreeImpl identifier, String fqn) { + Symbol symbol = sema.resolve(fqn); + identifier.symbol = symbol; + ascribe(identifier, symbol.type()); + sema.use(fqn, identifier); + } + + private void bindQualifierPackages(ExpressionTree qualifier, IdentifierTree identifier) { + if (identifier.symbol() instanceof Symbol.TypeSymbol type && !type.isUnknown()) { + Symbol owner = type.owner(); + while (owner != null && !owner.isPackageSymbol()) { + owner = owner.owner(); + } + if (owner != null && dottedName(qualifier).equals(owner.name())) { + bindPackage(qualifier, owner.name()); + } + } + } + + private void bindPackage(ExpressionTree tree, String name) { + ExpressionTree last = tree; + if (tree instanceof MemberSelectExpressionTree select) { + int dot = name.lastIndexOf('.'); + if (dot < 0) { + return; + } + bindPackage(select.expression(), name.substring(0, dot)); + last = select.identifier(); + } + if (last instanceof IdentifierTreeImpl identifier && identifier.symbol().isUnknown()) { + identifier.symbol = sema.packageSymbol(name); + } + } + + private static String dottedName(ExpressionTree tree) { + if (tree instanceof MemberSelectExpressionTree select) { + String qualifier = dottedName(select.expression()); + return qualifier.isEmpty() ? "" : (qualifier + "." + select.identifier().name()); + } + return tree instanceof IdentifierTree identifier ? identifier.name() : ""; + } + + private IdentifierTreeImpl identifier(Node node) { + String text = node.text().isEmpty() ? node.k() : node.text(); + return new IdentifierTreeImpl(tokens.getOrDefault(node.json, new InternalSyntaxToken(node.line(), node.col(), text, List.of(), false)), "_".equals(text)); + } + + private LiteralTreeImpl integerLiteral(Node node) { + String text = node.text(); + boolean isLong = text.endsWith("l") || text.endsWith("L"); + return literal(isLong ? Tree.Kind.LONG_LITERAL : Tree.Kind.INT_LITERAL, token(node), sema.getClassType(isLong ? "long" : "int")); + } + + private LiteralTreeImpl floatingLiteral(Node node) { + String text = node.text(); + boolean isFloat = text.endsWith("f") || text.endsWith("F"); + return literal(isFloat ? Tree.Kind.FLOAT_LITERAL : Tree.Kind.DOUBLE_LITERAL, token(node), sema.getClassType(isFloat ? "float" : "double")); + } + + private LiteralTreeImpl literal(Tree.Kind kind, InternalSyntaxToken token, Type type) { + LiteralTreeImpl tree = new LiteralTreeImpl(kind, token); + ascribe(tree, type); + return tree; + } + + private MemberSelectExpressionTreeImpl memberSelect(Node node) { + Node object = field(node, "object"); + if (object == null) { + object = field(node, "scope"); + } + Node name = field(node, "name"); + if (name == null) { + name = field(node, "field"); + } + Node scope = object == null ? firstNamed(node) : object; + ExpressionTree qualifier = object == null && compoundName(scope) ? typeExpression(scope) : (object == null ? primary(scope) : superQualifier(node, object)); + Node nameNode = name == null ? lastNamed(node) : name; + IdentifierTreeImpl identifier = identifier(nameNode); + bindUse(identifier, name != null || nameNode.json.has("refs") ? nameNode : node, true); + if (!naming) { + bindQualifiedReceiver(qualifier, identifier); + if (identifier.symbol().isUnknown()) { + resolveQualifiedType(qualifier, identifier); + } + if (identifier.symbol().isUnknown()) { + String owner = fieldOwner(qualifier); + Symbol.VariableSymbol field = owner == null ? null : sema.declaredField(owner, identifier.name()); + if (field != null) { + identifier.symbol = field; + ascribe(identifier, field.type()); + } + } + bindQualifierPackages(qualifier, identifier); + } + List annotations = node.children().stream().filter(SemsitterTranslator::isAnnotation).map(this::annotation).toList(); + if (!annotations.isEmpty()) { + identifier.complete(annotations); + } + MemberSelectExpressionTreeImpl tree = new MemberSelectExpressionTreeImpl(qualifier, token(lastKeyword(node, ".")), identifier); + ascribe(tree, identifier.symbolType()); + return tree; + } + + private ExpressionTree superQualifier(Node node, Node object) { + ExpressionTree qualifier = expression(object); + Node superNode = keyword(node, "super"); + if (superNode == null || same(superNode, object)) { + return qualifier; + } + IdentifierTreeImpl identifier = new IdentifierTreeImpl(token(superNode)); + bindQualifiedReceiver(qualifier, identifier); + return new MemberSelectExpressionTreeImpl(qualifier, token(required(node, ".")), identifier); + } + + private void bindQualifiedReceiver(ExpressionTree qualifier, IdentifierTreeImpl identifier) { + boolean isThis = "this".equals(identifier.name()); + if (!identifier.symbol().isUnknown() || (!isThis && !"super".equals(identifier.name())) + || !(qualifier.symbolType().symbol() instanceof Symbol.TypeSymbol owner) || owner.isUnknown()) { + return; + } + Type type = isThis || owner.isInterface() || owner.superClass() == null ? owner.type() : owner.superClass(); + identifier.symbol = sema.receiverSymbol(owner, identifier.name(), type); + ascribe(identifier, type); + } + + private static boolean compoundName(Node node) { + return switch (node.k()) { + case "scoped_type_identifier", "scoped_identifier", "generic_type", "field_access" -> true; + default -> false; + }; + } + + private MethodInvocationTreeImpl invocation(Node node) { + Node name = field(node, "name"); + Node object = field(node, "object"); + IdentifierTreeImpl identifier = identifier(name == null ? firstOf(node, "identifier") : name); + bindUse(identifier, name == null ? node : name, object != null); + ExpressionTree select; + ExpressionTree qualifier = null; + if (object == null) { + select = identifier; + } else { + qualifier = superQualifier(node, object); + select = new MemberSelectExpressionTreeImpl(qualifier, token(lastKeyword(node, ".")), identifier); + } + Node typeArguments = field(node, "type_arguments"); + MethodInvocationTreeImpl tree = new MethodInvocationTreeImpl(select, typeArguments == null ? null : typeArguments(typeArguments), arguments(requiredField(node, "arguments", childKind(node, "argument_list")))); + Type receiver = qualifier == null ? currentType() : qualifier.symbolType(); + String target = (name == null ? node : name).valueTarget(); + boolean arityOnly = target != null && ARITY_ONLY.matcher(target).matches(); + List arguments = argumentTypes(tree.arguments()); + if (qualifier == null && staticImports.containsKey(identifier.name())) { + boolean declaredHere = types.stream().filter(type -> !type.isUnknown()) + .anyMatch(type -> sema.declaredMethod(type.type().fullyQualifiedName(), identifier.name(), arguments) != null); + if (!declaredHere) { + Symbol.MethodSymbol imported = sema.declaredMethod(staticImports.get(identifier.name()), identifier.name(), arguments); + if (imported != null) { + identifier.symbol = imported; + } + } + } + if (identifier.symbol().isUnknown() || arityOnly) { + Symbol.MethodSymbol method = arityOnly + ? sema.declaredMethod(JSema.erase(target.substring(0, target.indexOf('#'))), identifier.name(), arguments) + : libraryMethod(qualifier, identifier.name(), arguments); + if (method != null) { + identifier.symbol = method; + } + } else if (identifier.symbol() instanceof Symbol.MethodSymbol bound) { + identifier.symbol = sema.preferOverload(bound, libraryMethod(qualifier, identifier.name(), arguments), arguments); + } + if (identifier.symbol instanceof JMethodSymbol method && !method.isUnknown() && !JSema.arityMatches(method, arguments.size())) { + identifier.symbol = Symbol.MethodSymbol.UNKNOWN_METHOD; + } + if (identifier.symbol instanceof JMethodSymbol method) { + List parameters = sema.invocationParameters(method, receiver, arguments); + ascribeArguments(method, parameters, tree.arguments()); + Type returned = sema.invocationReturn(method, receiver, arguments); + JMethodSymbol view = sema.invocationView(method, receiver, parameters, returned); + tree.methodSymbol = view; + identifier.symbol = view; + ascribe(identifier, returned); + ascribe(tree, returned); + } + return tree; + } + + private ExpressionTree typeExpression(Node node) { + TypeTree type = type(node); + return type instanceof ExpressionTree expression ? expression : literal(Tree.Kind.NULL_LITERAL, synthetic("null"), Type.UNKNOWN); + } + + private NewClassTreeImpl creation(Node node) { + Node typeNode = field(node, "type"); + TypeTree type = annotate(recoverQualifiedType(type(Objects.requireNonNull(typeNode == null ? firstType(node) : typeNode))), node); + Node arguments = field(node, "arguments"); + ArgumentListTreeImpl args = arguments == null ? ArgumentListTreeImpl.emptyList().complete(synthetic("("), synthetic(")")) : arguments(arguments); + Node body = field(node, "body"); + if (body == null) { + body = childKind(node, "class_body"); + } + ClassTreeImpl classBody = null; + if (body != null) { + classBody = new ClassTreeImpl(Tree.Kind.CLASS, token(required(body, "{")), membersOf(body), token(required(body, "}"))); + bindType(classBody, body); + } + NewClassTreeImpl tree = new NewClassTreeImpl(type, args, classBody).completeWithNewKeyword(token(required(node, "new"))); + Node enclosing = node.children().get(0); + if (!"new".equals(enclosing.text())) { + tree.completeWithEnclosingExpression(expression(enclosing)); + tree.completeWithDotToken(token(required(node, "."))); + } + Node typeArguments = field(node, "type_arguments"); + if (typeArguments != null) { + tree.completeWithTypeArguments(typeArguments(typeArguments)); + } + bindConstructor(tree, typeNode); + ascribe(tree, classBody != null && !classBody.symbol().isUnknown() ? classBody.symbol().type() : type.symbolType()); + return tree; + } + + private void bindConstructor(NewClassTreeImpl tree, @Nullable Node typeNode) { + if (!(tree.getConstructorIdentifier() instanceof IdentifierTreeImpl identifier)) { + return; + } + TypeTree written = tree.identifier() instanceof ParameterizedTypeTree parameterized ? parameterized.type() : tree.identifier(); + String imported = written instanceof MemberSelectExpressionTree ? null : singleImports.get(identifier.name()); + String constructor = typeNode == null ? null : typeNode.valueTarget(); + if (constructor != null) { + Symbol resolved = sema.resolve(constructor); + String owner = resolved.owner() == null || resolved.owner().type() == null ? "" : resolved.owner().type().erasure().fullyQualifiedName(); + boolean importedElsewhere = imported != null && sema.declaresType(imported) && !imported.equals(owner); + if (resolved instanceof Symbol.MethodSymbol method && !importedElsewhere && JSema.arityMatches(method, tree.arguments().size())) { + identifier.symbol = resolved; + recordUse(resolved, identifier); + ascribeArguments(method, tree.arguments()); + return; + } + } + Type type = tree.identifier().symbolType().erasure(); + if (imported != null && sema.declaresType(imported) && !imported.equals(type.fullyQualifiedName())) { + type = sema.getClassType(imported); + bindType(identifier, imported); + } + String owner = tree.classBody() != null && type.symbol().isInterface() ? "java.lang.Object" : type.fullyQualifiedName(); + Symbol.MethodSymbol declared = type.isClass() ? sema.declaredMethod(owner, "", argumentTypes(tree.arguments())) : null; + if (declared != null && JSema.arityMatches(declared, tree.arguments().size())) { + identifier.symbol = declared; + recordUse(declared, identifier); + ascribeArguments(declared, tree.arguments()); + } else if (!(identifier.symbol() instanceof Symbol.MethodSymbol)) { + identifier.symbol = Symbol.MethodSymbol.UNKNOWN_METHOD; + } + } + + private NewArrayTreeImpl arrayCreation(Node node) { + Node initializer = field(node, "value"); + String dimensionsKind = initializer == null ? "dimensions_expr" : "dimensions"; + List dims = new ArrayList<>(); + for (Node child : node.children()) { + if (dimensionsKind.equals(child.k())) { + dims.addAll(dimensions(child)); + } + } + InitializerListTreeImpl initializers = initializer == null ? InitializerListTreeImpl.emptyList() : initializers(initializer); + NewArrayTreeImpl tree = new NewArrayTreeImpl(dims, initializers) + .complete(annotate(type(requiredField(node, "type", firstType(node))), node)) + .completeWithNewKeyword(token(required(node, "new"))); + if (initializer != null) { + tree.completeWithCurlyBraces(token(required(initializer, "{")), token(required(initializer, "}"))); + } + int rank = dims.size(); + if (rank == 0 && initializer != null) { + rank = 1; + } + ascribe(tree, arrayTypeOf(tree.type(), rank)); + return tree; + } + + private NewArrayTreeImpl arrayInitializer(Node node) { + NewArrayTreeImpl tree = new NewArrayTreeImpl(List.of(), initializers(node)).completeWithCurlyBraces(token(required(node, "{")), token(required(node, "}"))); + return tree; + } + + private AssignmentExpressionTreeImpl elementValuePair(Node node) { + Node key = requiredField(node, "key", firstNamed(node)); + IdentifierTreeImpl name = identifier(key); + bindUse(name, key); + ExpressionTree value = expression(requiredField(node, "value", lastNamed(node))); + AssignmentExpressionTreeImpl tree = new AssignmentExpressionTreeImpl(Tree.Kind.ASSIGNMENT, name, token(required(node, "=")), value); + ascribe(tree, value.symbolType()); + return tree; + } + + private MemberSelectExpressionTreeImpl classLiteral(Node node) { + ExpressionTree type = (ExpressionTree) type(firstNamed(node)); + IdentifierTreeImpl className = new IdentifierTreeImpl(token(required(node, "class"))); + MemberSelectExpressionTreeImpl tree = new MemberSelectExpressionTreeImpl(type, token(required(node, ".")), className); + Type literal = type.symbolType().isPrimitive() ? type.symbolType().primitiveWrapperType() : type.symbolType(); + JType argument = literal instanceof JType jType ? jType : JType.unknown(sema); + ascribe(tree, JType.named(sema, "java.lang.Class", List.of(argument))); + return tree; + } + + private InitializerListTreeImpl initializers(Node node) { + InitializerListTreeImpl list = InitializerListTreeImpl.emptyList(); + for (Node child : node.children()) { + if ("{".equals(child.text()) || "}".equals(child.text())) { + continue; + } + if (",".equals(child.text())) { + list.separators().add(token(child)); + } else { + list.add(expression(child)); + } + } + return list; + } + + private ArrayAccessExpressionTreeImpl arrayAccess(Node node) { + Node index = field(node, "index"); + ExpressionTree array = expression(requiredField(node, "array", firstNamed(node))); + ArrayDimensionTreeImpl dimension = new ArrayDimensionTreeImpl(token(required(node, "[")), index == null ? null : expression(index), token(required(node, "]"))); + ArrayAccessExpressionTreeImpl tree = new ArrayAccessExpressionTreeImpl(array, dimension); + Type arrayType = array.symbolType(); + ascribe(tree, arrayType.isArray() ? ((Type.ArrayType) arrayType).elementType() : Type.UNKNOWN); + return tree; + } + + private AssignmentExpressionTreeImpl assignment(Node node) { + Node operator = field(node, "operator"); + String text = operator == null ? "=" : operator.text(); + AssignmentExpressionTreeImpl tree = new AssignmentExpressionTreeImpl( + assignmentKind(text), + expression(requiredField(node, "left", firstNamed(node))), + operator == null ? token(required(node, text)) : token(operator), + expression(requiredField(node, "right", lastNamed(node)))); + ascribe(tree, tree.variable().symbolType()); + if (tree.is(Tree.Kind.ASSIGNMENT) && tree.expression() instanceof NewClassTreeImpl newClass) { + inferDiamond(newClass, tree.variable().symbolType()); + } + return tree; + } + + private BinaryExpressionTreeImpl binary(Node node) { + Node operator = field(node, "operator"); + String text = operator == null ? "+" : operator.text(); + ExpressionTree left = expression(requiredField(node, "left", firstNamed(node))); + ExpressionTree right = expression(requiredField(node, "right", lastNamed(node))); + BinaryExpressionTreeImpl tree = new BinaryExpressionTreeImpl(binaryKind(text), left, operator == null ? synthetic(text) : token(operator), right); + ascribe(tree, binaryType(text, left.symbolType(), right.symbolType())); + return tree; + } + + private ExpressionTree unary(Node node, boolean prefix) { + Node operator = field(node, "operator"); + Node operand = field(node, "operand"); + if (operand == null) { + operand = firstNamed(node); + } + String text = operator == null ? "!" : operator.text(); + ExpressionTree value = expression(operand); + InternalPrefixUnaryExpression tree = new InternalPrefixUnaryExpression(unaryKind(text, true), operator == null ? synthetic(text) : token(operator), value); + ascribe(tree, "!".equals(text) ? sema.getClassType("boolean") : value.symbolType()); + return tree; + } + + private ExpressionTree update(Node node) { + Node operator = field(node, "operator"); + List children = node.children(); + if (operator == null) { + operator = children.stream().filter(child -> "++".equals(child.text()) || "--".equals(child.text())).findFirst().orElse(null); + } + boolean prefix = !children.isEmpty() && ("++".equals(children.get(0).text()) || "--".equals(children.get(0).text())); + ExpressionTree operand = expression(firstNamed(node)); + String text = operator == null ? "++" : operator.text(); + InternalSyntaxToken token = operator == null ? synthetic(text) : token(operator); + ExpressionTree tree = prefix + ? new InternalPrefixUnaryExpression(unaryKind(text, true), token, operand) + : new InternalPostfixUnaryExpression(unaryKind(text, false), operand, token); + ascribe((AbstractTypedTree) tree, operand.symbolType()); + return tree; + } + + private ParenthesizedTreeImpl parenthesized(Node node) { + Parens parens = parens(node); + ParenthesizedTreeImpl tree = new ParenthesizedTreeImpl(parens.open, parens.expr, parens.close); + ascribe(tree, parens.expr.symbolType()); + return tree; + } + + private ConditionalExpressionTreeImpl ternary(Node node) { + ExpressionTree condition = expression(requiredField(node, "condition", firstNamed(node))); + ExpressionTree then = expression(requiredField(node, "consequence", firstNamed(node))); + ExpressionTree otherwise = expression(requiredField(node, "alternative", lastNamed(node))); + ConditionalExpressionTreeImpl tree = new ConditionalExpressionTreeImpl(condition, token(required(node, "?")), then, token(required(node, ":")), otherwise); + ascribe(tree, then.symbolType().isUnknown() ? otherwise.symbolType() : then.symbolType()); + return tree; + } + + private TypeCastExpressionTreeImpl cast(Node node) { + TypeTree type = type(requiredField(node, "type", firstType(node))); + ExpressionTree value = expression(requiredField(node, "value", lastNamed(node))); + TypeCastExpressionTreeImpl tree = new TypeCastExpressionTreeImpl(token(required(node, "(")), type, token(required(node, ")")), value); + ascribe(tree, type.symbolType()); + return tree; + } + + private InstanceOfTreeImpl instanceOf(Node node) { + ExpressionTree left = expression(requiredField(node, "left", firstNamed(node))); + Node name = field(node, "name"); + Node right = field(node, "right"); + Node patternNode = field(node, "pattern"); + InstanceOfTreeImpl tree; + if (patternNode != null) { + tree = new InstanceOfTreeImpl(left, token(required(node, "instanceof")), pattern(patternNode)); + } else if (name != null && right != null) { + VariableTreeImpl variable = new VariableTreeImpl(modifiers(node), type(right), identifier(name)); + bindDeclaration(variable, name, identifier(name)); + TypePatternTreeImpl pattern = new TypePatternTreeImpl(variable); + ascribe(pattern, variable.symbol().type().isUnknown() ? variable.type().symbolType() : variable.symbol().type()); + tree = new InstanceOfTreeImpl(left, token(required(node, "instanceof")), pattern); + } else { + tree = new InstanceOfTreeImpl(left, token(required(node, "instanceof")), type(right == null ? firstType(node) : right)); + } + ascribe(tree, sema.getClassType("boolean")); + return tree; + } + + private PatternTree pattern(Node node) { + if ("record_pattern".equals(node.k())) { + return recordPattern(node); + } + Node nested = firstOf(node, "record_pattern", "type_pattern", "pattern", "underscore_pattern"); + if (nested != null && !"type_pattern".equals(node.k())) { + return pattern(nested); + } + if ("underscore_pattern".equals(node.k())) { + return new TypePatternTreeImpl(new VariableTreeImpl(new IdentifierTreeImpl(token(node), true))); + } + Node name = lastOf(node, "identifier"); + VariableTreeImpl variable = new VariableTreeImpl(modifiers(node), type(firstType(node)), identifier(name == null ? node : name)); + bindDeclaration(variable, name, (IdentifierTreeImpl) variable.simpleName()); + if (variable.symbol().type() != null && !variable.symbol().type().isUnknown()) { + ascribe(variable.type(), variable.symbol().type()); + } + TypePatternTreeImpl tree = new TypePatternTreeImpl(variable); + ascribe(tree, variable.symbol().type().isUnknown() ? variable.type().symbolType() : variable.symbol().type()); + return tree; + } + + private RecordPatternTreeImpl recordPattern(Node node) { + Node body = required(node, "record_pattern_body"); + List patterns = new ArrayList<>(); + for (Node child : body.children()) { + if ("record_pattern_component".equals(child.k()) || "record_pattern".equals(child.k())) { + patterns.add(pattern(child)); + } + } + return new RecordPatternTreeImpl(type(firstType(node)), token(required(body, "(")), patterns, token(required(body, ")"))); + } + + private LambdaExpressionTreeImpl lambda(Node node) { + scopes.addFirst(new HashMap<>()); + returnTargets.push(Type.UNKNOWN); + try { + return lambdaInScope(node); + } finally { + scopes.removeFirst(); + returnTargets.pop(); + } + } + + private LambdaExpressionTreeImpl lambdaInScope(Node node) { + List parameters = new ArrayList<>(); + Node formals = field(node, "parameters"); + InternalSyntaxToken open = null; + InternalSyntaxToken close = null; + if (formals != null && ("formal_parameters".equals(formals.k()) || "inferred_parameters".equals(formals.k()))) { + open = optionalToken(formals, "("); + close = optionalToken(formals, ")"); + if ("formal_parameters".equals(formals.k())) { + parameters.addAll(parameters(formals)); + } else { + VariableTreeImpl previous = null; + for (Node child : formals.children()) { + if ("identifier".equals(child.k())) { + IdentifierTreeImpl name = identifier(child); + previous = new VariableTreeImpl(name); + bindDeclaration(previous, child, name); + parameters.add(previous); + } else if (",".equals(child.text()) && previous != null) { + previous.setEndToken(token(child)); + } + } + } + } else if (formals != null) { + IdentifierTreeImpl name = identifier(formals); + VariableTreeImpl parameter = new VariableTreeImpl(name); + bindDeclaration(parameter, formals, name); + parameters.add(parameter); + } + Node body = field(node, "body"); + Tree lambdaBody = body == null ? block(node) : ("block".equals(body.k()) ? block(body) : expression(body)); + LambdaExpressionTreeImpl tree = new LambdaExpressionTreeImpl(open, parameters, close, token(required(node, "->")), lambdaBody); + for (VariableTree parameter : parameters) { + if (parameter.symbol().owner() instanceof JMethodSymbol method) { + method.lambda(true); + tree.methodSymbol = method; + break; + } + } + return tree; + } + + private MethodReferenceTreeImpl methodReference(Node node) { + ExpressionTree qualifier = expression(requiredField(node, "type", node.children().get(0))); + Node nameNode = field(node, "name"); + if (nameNode == null) { + List children = node.children(); + Node last = children.isEmpty() ? null : children.get(children.size() - 1); + nameNode = last != null && "new".equals(last.text()) ? last : lastOf(node, "identifier"); + } + IdentifierTreeImpl name = identifier(nameNode == null ? node : nameNode); + bindUse(name, nameNode == null ? node : nameNode, true); + MethodReferenceTreeImpl tree = new MethodReferenceTreeImpl(qualifier, token(required(node, "::"))); + Node typeArguments = field(node, "type_arguments"); + tree.complete(typeArguments == null ? null : typeArguments(typeArguments), name); + ascribe(tree, Type.UNKNOWN); + return tree; + } + + private TypeTree type(Node node) { + return switch (node.k()) { + case "integral_type", "floating_point_type", "boolean_type", "void_type" -> primitive(node); + case "generic_type" -> generic(node); + case "array_type" -> arrayType(type(requiredField(node, "element", firstType(node))), field(node, "dimensions")); + case "scoped_identifier", "scoped_type_identifier", "field_access" -> memberSelect(node); + case "wildcard" -> wildcard(node); + case "annotated_type" -> annotatedType(node); + case "type_identifier" -> "var".equals(node.text()) ? new VarTypeTreeImpl(token(node)) : (TypeTree) primary(node); + default -> { + ExpressionTree expression = "identifier".equals(node.k()) || "type_identifier".equals(node.k()) ? primary(node) : expression(node); + yield expression instanceof TypeTree typeTree ? typeTree : new VarTypeTreeImpl(synthetic("var")); + } + }; + } + + private TypeTree annotatedType(Node node) { + return annotate(type(firstType(node)), node); + } + + private TypeTree annotate(TypeTree type, Node node) { + List annotations = node.children().stream().filter(SemsitterTranslator::isAnnotation).map(this::annotation).toList(); + if (!annotations.isEmpty() && type instanceof JavaTree.AnnotatedTypeTree annotated) { + annotated.complete(annotations); + } + return type; + } + + private JavaTree.PrimitiveTypeTreeImpl primitive(Node node) { + Node keyword = firstTokenChild(node); + String text = keyword == null ? node.text() : keyword.text(); + if (text.isEmpty()) { + text = node.k().replace("_type", ""); + } + JavaTree.PrimitiveTypeTreeImpl tree = new JavaTree.PrimitiveTypeTreeImpl(keyword == null ? token(node) : token(keyword)); + tree.complete(List.of()); + ascribe(tree, sema.getClassType(text)); + return tree; + } + + private JavaTree.ParameterizedTypeTreeImpl generic(Node node) { + TypeTree raw = type(firstType(node)); + Node arguments = field(node, "type_arguments"); + if (arguments == null) { + arguments = childKind(node, "type_arguments"); + } + JavaTree.ParameterizedTypeTreeImpl tree = new JavaTree.ParameterizedTypeTreeImpl(raw, arguments == null ? new TypeArgumentListTreeImpl(synthetic("<"), synthetic(">")) : typeArguments(arguments)); + List args = new ArrayList<>(); + for (TypeTree argument : tree.typeArguments()) { + args.add(argument instanceof WildcardTree wildcard ? wildcardType(wildcard) : argument.symbolType()); + } + if (raw.symbolType() instanceof JType) { + List typeArguments = new ArrayList<>(); + for (Type argument : args) { + typeArguments.add(argument instanceof JType jType ? jType : JType.unknown(sema)); + } + JType parameterized = JType.named(sema, raw.symbolType().erasure().fullyQualifiedName(), typeArguments); + ascribe(tree, parameterized); + if (raw instanceof AbstractTypedTree typed) { + ascribe(typed, parameterized); + } + if (raw instanceof IdentifierTreeImpl identifier) { + identifier.symbol = parameterized.symbol(); + } + } + return tree; + } + + private TypeTree arrayType(TypeTree element, @Nullable Node dimensions) { + if (dimensions == null) { + return element; + } + TypeTree current = element; + List annotations = new ArrayList<>(); + List children = dimensions.children(); + for (int i = 0; i < children.size(); i++) { + if ("[".equals(children.get(i).text())) { + InternalSyntaxToken close = i + 1 < children.size() ? token(children.get(i + 1)) : synthetic("]"); + JType component = current.symbolType() instanceof JType jType ? jType : JType.unknown(sema); + current = new JavaTree.ArrayTypeTreeImpl(current, annotations, token(children.get(i)), close); + annotations = new ArrayList<>(); + ascribe((AbstractTypedTree) current, JType.array(sema, component)); + } else if (isAnnotation(children.get(i))) { + annotations.add(annotation(children.get(i))); + } + } + return current; + } + + private Type wildcardType(WildcardTree wildcard) { + if (wildcard.is(Tree.Kind.SUPER_WILDCARD) && wildcard.bound() != null && wildcard.bound().symbolType() instanceof JType lower) { + return JType.superWildcard(sema, lower); + } + Type upper = wildcard.is(Tree.Kind.EXTENDS_WILDCARD) && wildcard.bound() != null ? wildcard.bound().symbolType() : sema.getClassType("java.lang.Object"); + return upper instanceof JType bound ? JType.wildcard(sema, bound) : Type.UNKNOWN; + } + + private JavaTree.WildcardTreeImpl wildcard(Node node) { + Node bound = firstType(node); + if (bound == null || same(bound, keyword(node, "?"))) { + JavaTree.WildcardTreeImpl tree = new JavaTree.WildcardTreeImpl(token(required(node, "?"))); + tree.complete(List.of()); + return tree; + } + boolean upper = keyword(node, "extends") != null; + JavaTree.WildcardTreeImpl tree = new JavaTree.WildcardTreeImpl( + upper ? Tree.Kind.EXTENDS_WILDCARD : Tree.Kind.SUPER_WILDCARD, + token(required(node, upper ? "extends" : "super")), + type(bound)).complete(token(required(node, "?"))); + tree.complete(List.of()); + return tree; + } + + private TypeArgumentListTreeImpl typeArguments(Node node) { + TypeArgumentListTreeImpl list = new TypeArgumentListTreeImpl(token(required(node, "<")), closingAngle(node)); + for (Node child : node.children()) { + if ("<".equals(child.text()) || ">".equals(child.text()) || ">>".equals(child.text()) || ">>>".equals(child.text())) { + continue; + } + if (",".equals(child.text())) { + list.separators().add(token(child)); + } else { + list.add(type(child)); + } + } + return list; + } + + private TypeParameterListTreeImpl typeParameters(Node node) { + TypeParameterListTreeImpl list = new TypeParameterListTreeImpl(token(required(node, "<")), closingAngle(node)); + for (Node child : node.children()) { + if ("<".equals(child.text()) || child.text().startsWith(">")) { + continue; + } + if (",".equals(child.text())) { + list.separators().add(token(child)); + } else if ("type_parameter".equals(child.k())) { + list.add(typeParameter(child)); + } + } + return list; + } + + private TypeParameterTree typeParameter(Node node) { + Node nameNode = firstOf(node, "type_identifier", "identifier"); + IdentifierTreeImpl name = identifier(nameNode); + bindUse(name, nameNode); + Node bound = childKind(node, "type_bound"); + TypeParameterTreeImpl tree; + if (bound == null) { + tree = new TypeParameterTreeImpl(name); + } else { + tree = new TypeParameterTreeImpl(name, token(required(bound, "extends")), separatedTypes(bound)); + } + if (name.symbol().isUnknown()) { + bindDeclaration(tree, nameNode, name); + } + if (name.symbol instanceof Symbol.TypeSymbol typeSymbol) { + tree.typeSymbol = typeSymbol; + } + return tree; + } + + private TypeTree catchType(Node node) { + int alternatives = 0; + for (Node child : node.children()) { + if (isTypeKind(child.k())) { + alternatives++; + } + } + if (alternatives <= 1) { + return type(firstType(node)); + } + QualifiedIdentifierListTreeImpl list = QualifiedIdentifierListTreeImpl.emptyList(); + for (Node child : node.children()) { + if (isTypeKind(child.k())) { + list.add(type(child)); + } else if ("|".equals(child.text())) { + list.separators().add(token(child)); + } + } + return new JavaTree.UnionTypeTreeImpl(list); + } + + private QualifiedIdentifierListTreeImpl separatedTypes(Node node) { + QualifiedIdentifierListTreeImpl list = QualifiedIdentifierListTreeImpl.emptyList(); + addSeparatedTypes(list, node); + return list; + } + + private void addSeparatedTypes(QualifiedIdentifierListTreeImpl list, Node node) { + for (Node child : node.children()) { + if ("type_list".equals(child.k())) { + addSeparatedTypes(list, child); + } else if (isTypeKind(child.k())) { + list.add(type(child)); + } else if (",".equals(child.text()) || "&".equals(child.text())) { + list.separators().add(token(child)); + } + } + } + + private ArgumentListTreeImpl arguments(Node node) { + if (node == null) { + return ArgumentListTreeImpl.emptyList(); + } + ArgumentListTreeImpl list = ArgumentListTreeImpl.emptyList(); + InternalSyntaxToken open = null; + InternalSyntaxToken close = null; + for (Node child : node.children()) { + if ("(".equals(child.text())) { + open = token(child); + } else if (")".equals(child.text())) { + close = token(child); + } else if (",".equals(child.text())) { + list.separators().add(token(child)); + } else { + list.add(expression(child)); + } + } + return list.complete(open, close); + } + + private List dimensions(Node node) { + List dims = new ArrayList<>(); + List annotations = new ArrayList<>(); + ExpressionTree index = null; + InternalSyntaxToken open = null; + for (Node child : node.children()) { + if ("[".equals(child.text())) { + open = token(child); + index = null; + } else if ("]".equals(child.text()) && open != null) { + dims.add(new ArrayDimensionTreeImpl(open, index, token(child)).completeAnnotations(annotations)); + annotations = new ArrayList<>(); + open = null; + } else if (open != null) { + index = expression(child); + } else if (isAnnotation(child)) { + annotations.add(annotation(child)); + } + } + return dims; + } + + private ModifiersTreeImpl modifiers(Node node) { + Node modifiers = childKind(node, "modifiers"); + List trees = new ArrayList<>(); + if (modifiers != null) { + for (Node child : modifiers.children()) { + ModifierTree modifier = modifier(child); + if (modifier != null) { + trees.add(modifier); + } + } + } + return new ModifiersTreeImpl(trees); + } + + @Nullable + private ModifierTree modifier(Node node) { + if (isAnnotation(node)) { + return annotation(node); + } + Modifier modifier = modifierKeyword(node.text()); + return modifier == null ? null : new ModifierKeywordTreeImpl(modifier, token(node)); + } + + private AnnotationTreeImpl annotation(Node node) { + Node name = field(node, "name"); + TypeTree type = name == null ? (TypeTree) primary(firstOf(node, "identifier", "scoped_identifier", "type_identifier")) : (TypeTree) expression(name); + if (type instanceof IdentifierTreeImpl identifier && !naming) { + recoverType(identifier); + } + ArgumentListTreeImpl arguments = ArgumentListTreeImpl.emptyList(); + Node args = field(node, "arguments"); + if (args != null) { + arguments = arguments(args); + } + AnnotationTreeImpl tree = new AnnotationTreeImpl(token(required(node, "@")), type, arguments); + ascribe(tree, annotationType(type)); + for (ExpressionTree argument : arguments) { + if (argument instanceof AssignmentExpressionTree assignment && assignment.variable() instanceof IdentifierTreeImpl element && element.symbol().isUnknown() + && type.symbolType().isClass()) { + Symbol.MethodSymbol method = sema.declaredMethod(type.symbolType().fullyQualifiedName(), element.name(), List.of()); + if (method != null) { + element.symbol = method; + ascribe(element, method.returnType().type()); + } + } + } + return tree; + } + + private List annotations(Node node) { + Node modifiers = childKind(node, "modifiers"); + List annotations = new ArrayList<>(); + for (Node child : (modifiers == null ? node : modifiers).children()) { + if (isAnnotation(child)) { + annotations.add(annotation(child)); + } + } + return annotations; + } + + private void bindDeclaration(Tree tree, @Nullable Node name, IdentifierTreeImpl identifier) { + if (name == null) { + return; + } + String id = name.decl(); + if (id == null) { + return; + } + if (id.contains("#(")) { + id = name.decls().stream().filter(extra -> !extra.contains("(")).findFirst().orElse(id); + } + Symbol symbol = sema.resolve(id); + sema.declare(id, tree); + for (String extra : name.decls()) { + if (!extra.equals(id)) { + sema.declare(extra, tree); + } + } + // The scan records no modifiers for locals and parameters. + if (tree instanceof VariableTreeImpl declared && symbol instanceof JSymbol declaredSymbol && symbol.isVariableSymbol() + && ModifiersUtils.hasModifier(declared.modifiers(), Modifier.FINAL)) { + declaredSymbol.addModifiers(FINAL_MODIFIER); + } + if (tree instanceof VariableTreeImpl variable && symbol instanceof JSymbol variableSymbol && symbol.isVariableSymbol() && variable.type() != null) { + Type declared = variable.type().symbolType(); + if (symbol.type().isUnknown() ? isKnownOrRecovered(declared) : declared.isParameterized() && sameErasure(declared, symbol.type())) { + variableSymbol.type(declared); + } + } + if (tree instanceof MethodTreeImpl method && symbol instanceof JMethodSymbol methodSymbol && method.returnType() != null + && methodSymbol.returnType().type().isUnknown() && isKnownOrRecovered(method.returnType().symbolType())) { + methodSymbol.returnTypeSymbol(method.returnType().symbolType().symbol()); + } + identifier.symbol = symbol; + ascribe(identifier, identifierType(symbol)); + if (!scopes.isEmpty() && symbol.isVariableSymbol() && symbol.owner() != null && symbol.owner().isMethodSymbol()) { + scopes.peekFirst().put(symbol.name(), symbol); + } + if (tree instanceof ClassTreeImpl classTree && symbol instanceof Symbol.TypeSymbol typeSymbol) { + classTree.typeSymbol = typeSymbol; + if (symbol instanceof JSymbol nested && ModifiersUtils.hasModifier(classTree.modifiers(), Modifier.STATIC) && !nested.isStatic()) { + nested.addModifiers(List.of("static")); + } + } else if (tree instanceof MethodTreeImpl method && symbol instanceof Symbol.MethodSymbol methodSymbol) { + method.methodSymbol = methodSymbol; + if (methodSymbol instanceof JMethodSymbol declared) { + declaredParameterTypes(declared, method.parameters()); + } + } else if (tree instanceof VariableTreeImpl variable && symbol instanceof Symbol.VariableSymbol variableSymbol) { + variable.variableSymbol = variableSymbol; + } + } + + /** + * The scan records erased return types, so the type arguments of each declared return type are read from the + * syntax before any body is translated: invocations may precede the declaration they call. + */ + private void declareGenericReturns(Node node) { + if ("method_declaration".equals(node.k()) && field(node, "dimensions") == null) { + Node typeNode = field(node, "type"); + Node name = nameNode(node); + String id = name == null ? null : name.decl(); + if (typeNode != null && id != null && sema.resolve(id) instanceof JMethodSymbol method) { + Type declared = syntacticType(typeNode); + if (declared.isParameterized() && !mentionsUnknown(declared) && sameErasure(declared, method.returnType().type())) { + method.returnTypeSymbol(declared.symbol()); + } + } + } + for (Node child : node.children()) { + declareGenericReturns(child); + } + } + + /** The type a type node denotes, from the references the scan bound, without translating it. */ + private Type syntacticType(Node node) { + switch (node.k()) { + case "type_identifier", "scoped_type_identifier" -> { + String target = node.typeTarget(); + return target != null && sema.resolve(target) instanceof Symbol.TypeSymbol type && !type.isUnknown() ? type.type() : Type.UNKNOWN; + } + case "generic_type" -> { + Node rawNode = firstType(node); + Type raw = rawNode == null ? Type.UNKNOWN : syntacticType(rawNode); + Node arguments = childKind(node, "type_arguments"); + if (raw.isUnknown() || arguments == null) { + return raw; + } + List types = new ArrayList<>(); + for (Node argument : arguments.children()) { + if (isTypeKind(argument.k())) { + types.add(syntacticType(argument) instanceof JType type ? type : JType.unknown(sema)); + } + } + return JType.named(sema, raw.erasure().fullyQualifiedName(), types); + } + case "wildcard" -> { + Node bound = firstType(node); + if (bound == null) { + return JType.wildcard(sema, (JType) sema.getClassType("java.lang.Object")); + } + JType type = syntacticType(bound) instanceof JType known ? known : JType.unknown(sema); + return keyword(node, "super") != null ? JType.superWildcard(sema, type) : JType.wildcard(sema, type); + } + default -> { + return Type.UNKNOWN; + } + } + } + + private static boolean mentionsUnknown(Type type) { + return type.isUnknown() || (type.isParameterized() && type.typeArguments().stream().anyMatch(SemsitterTranslator::mentionsUnknown)); + } + + private static boolean sameErasure(Type left, Type right) { + return left.erasure().fullyQualifiedName().equals(right.erasure().fullyQualifiedName()); + } + + /** The scan records erased parameter types, while the declared parameters keep their type arguments. */ + private static void declaredParameterTypes(JMethodSymbol method, List parameters) { + List erased = method.parameterTypes(); + if (erased.size() != parameters.size()) { + return; + } + List declared = new ArrayList<>(); + for (int i = 0; i < parameters.size(); i++) { + Type type = parameters.get(i).type().symbolType(); + if (type.isUnknown() || !sameErasure(type, erased.get(i))) { + return; + } + declared.add(type); + } + method.parameterTypes(declared); + } + + private void bindImplicitClass(ClassTreeImpl tree, List members) { + JTypeSymbol implicitClass = null; + for (Tree member : members) { + if (member instanceof MethodTree method && method.symbol().owner() instanceof JTypeSymbol owner && !owner.isUnknown()) { + implicitClass = owner; + break; + } + } + if (implicitClass == null && !implicitClassName.isEmpty() && sema.resolve(implicitClassName) instanceof JTypeSymbol named) { + implicitClass = named; + } + if (implicitClass == null) { + return; + } + tree.typeSymbol = implicitClass; + sema.declare(implicitClass.id, tree); + for (Tree member : members) { + if (member instanceof MethodTree method && method.symbol() instanceof JMethodSymbol symbol && (symbol.owner() == null || symbol.owner().isUnknown())) { + symbol.owner(implicitClass); + } + } + } + + private void bindType(ClassTreeImpl tree, Node node) { + String id = node.decl(); + if (id != null && sema.resolve(id) instanceof Symbol.TypeSymbol typeSymbol) { + tree.typeSymbol = typeSymbol; + sema.declare(id, tree); + return; + } + for (Tree member : tree.members()) { + Symbol symbol = member instanceof MethodTree method ? method.symbol() : (member instanceof VariableTree variable ? variable.symbol() : null); + if (symbol != null && !symbol.isUnknown() && symbol.owner() instanceof Symbol.TypeSymbol owner) { + tree.typeSymbol = owner; + return; + } + } + } + + private void bindUse(IdentifierTreeImpl identifier, Node node) { + bindUse(identifier, node, false); + } + + private void bindUse(IdentifierTreeImpl identifier, Node node, boolean qualified) { + String target = node.valueTarget(); + if (target == null) { + target = node.typeTarget(); + } + if (target == null) { + if (!qualified) { + Symbol local = scopedVariable(identifier.name()); + if (local != null) { + identifier.symbol = local; + ascribe(identifier, identifierType(local)); + sema.use(usageId(local, local.name()), identifier); + } + } + return; + } + Symbol symbol = sema.resolve(target); + String typeTarget = node.typeTarget(); + if (symbol.isMethodSymbol() && "".equals(symbol.name()) && typeTarget != null && !typeTarget.equals(target)) { + target = typeTarget; + symbol = sema.resolve(target); + } + if (symbol.isMethodSymbol() && symbol.isUnknown()) { + symbol = Symbol.MethodSymbol.UNKNOWN_METHOD; + } + symbol = typedField(symbol); + if (!qualified && (symbol.isUnknown() || symbol.isVariableSymbol())) { + Symbol local = scopedVariable(identifier.name()); + if (local != null) { + symbol = local; + } + } + identifier.symbol = symbol; + ascribe(identifier, identifierType(symbol)); + sema.use(usageId(symbol, target), identifier); + } + + @Nullable + private Symbol scopedVariable(String name) { + for (Map scope : scopes) { + Symbol found = scope.get(name); + if (found != null) { + return found; + } + } + return null; + } + + /** A field the graph typed as void keeps the type its class file declares. */ + private Symbol typedField(Symbol symbol) { + if (!(symbol instanceof JSymbol field) || !field.isVariableSymbol() || field.type() == null || !field.type().isVoid()) { + return symbol; + } + String owner = field.owner().type() != null && !field.owner().type().isUnknown() + ? field.owner().type().erasure().fullyQualifiedName() + : (field.id != null && field.id.indexOf('#') > 0 ? field.id.substring(0, field.id.indexOf('#')) : null); + if (owner == null) { + return symbol; + } + Symbol.VariableSymbol declared = sema.declaredField(JSema.erase(owner), field.name()); + if (declared != null && declared.type() != null && !declared.type().isVoid() && !declared.type().isUnknown()) { + return declared; + } + return symbol; + } + + /** Usages are stored under the symbol's own id, which can differ from the fact key that resolved it. */ + private static String usageId(Symbol symbol, String target) { + if (symbol instanceof JSymbol jSymbol && !jSymbol.isUnknown()) { + String id = jSymbol.toString(); + if (id != null && !id.equals(jSymbol.name())) { + return id; + } + } + return target; + } + + private void recordUse(Symbol symbol, IdentifierTree identifier) { + String id = usageId(symbol, ""); + if (!id.isEmpty()) { + sema.use(id, identifier); + } + } + + @Nullable + private IdentifierTreeImpl labelUse(Node node) { + Node name = firstOf(node, "identifier"); + if (name == null || "break".equals(name.text()) || "continue".equals(name.text())) { + return null; + } + IdentifierTreeImpl identifier = identifier(name); + for (JLabelSymbol label : labels) { + if (label.name().equals(identifier.name())) { + label.usages.add(identifier); + identifier.labelSymbol = label; + } + } + return identifier; + } + + private Parens parens(@Nullable Node node) { + if (node == null) { + return new Parens(synthetic("("), literal(Tree.Kind.BOOLEAN_LITERAL, synthetic("true"), sema.getClassType("boolean")), synthetic(")")); + } + if (!"parenthesized_expression".equals(node.k())) { + return new Parens(synthetic("("), expression(node), synthetic(")")); + } + InternalSyntaxToken open = token(required(node, "(")); + InternalSyntaxToken close = token(required(node, ")")); + ExpressionTree expression = null; + for (Node child : node.children()) { + if (!"(".equals(child.text()) && !")".equals(child.text())) { + expression = expression(child); + } + } + return new Parens(open, expression == null ? literal(Tree.Kind.BOOLEAN_LITERAL, synthetic("true"), sema.getClassType("boolean")) : expression, close); + } + + @Nullable + private Type currentType() { + Symbol.TypeSymbol enclosing = types.isEmpty() ? null : types.peek(); + return enclosing == null ? Type.UNKNOWN : enclosing.type(); + } + + private Type arrayTypeOf(TypeTree element, int dimensions) { + Type type = element.symbolType(); + if (!(type instanceof JType jType)) { + return Type.UNKNOWN; + } + for (int i = 0; i < Math.max(dimensions, 1); i++) { + jType = JType.array(sema, jType); + } + return jType; + } + + private static Type binaryType(String operator, Type left, Type right) { + if (operator.equals("&&") || operator.equals("||") || operator.equals("==") || operator.equals("!=") + || operator.equals("<") || operator.equals(">") || operator.equals("<=") || operator.equals(">=") || operator.equals("instanceof")) { + return left instanceof JType j ? j.sema.getClassType("boolean") : Type.UNKNOWN; + } + if ("+".equals(operator) && (left.is("java.lang.String") || right.is("java.lang.String"))) { + return left instanceof JType j ? j.sema.getClassType("java.lang.String") : Type.UNKNOWN; + } + if (left.isUnknown()) { + return right; + } + return left; + } + + private static Tree.Kind binaryKind(String operator) { + return switch (operator) { + case "+" -> Tree.Kind.PLUS; + case "-" -> Tree.Kind.MINUS; + case "*" -> Tree.Kind.MULTIPLY; + case "/" -> Tree.Kind.DIVIDE; + case "%" -> Tree.Kind.REMAINDER; + case "<<" -> Tree.Kind.LEFT_SHIFT; + case ">>" -> Tree.Kind.RIGHT_SHIFT; + case ">>>" -> Tree.Kind.UNSIGNED_RIGHT_SHIFT; + case "<" -> Tree.Kind.LESS_THAN; + case ">" -> Tree.Kind.GREATER_THAN; + case "<=" -> Tree.Kind.LESS_THAN_OR_EQUAL_TO; + case ">=" -> Tree.Kind.GREATER_THAN_OR_EQUAL_TO; + case "==" -> Tree.Kind.EQUAL_TO; + case "!=" -> Tree.Kind.NOT_EQUAL_TO; + case "&" -> Tree.Kind.AND; + case "|" -> Tree.Kind.OR; + case "^" -> Tree.Kind.XOR; + case "&&" -> Tree.Kind.CONDITIONAL_AND; + case "||" -> Tree.Kind.CONDITIONAL_OR; + default -> Tree.Kind.PLUS; + }; + } + + private static Tree.Kind assignmentKind(String operator) { + return switch (operator) { + case "+=" -> Tree.Kind.PLUS_ASSIGNMENT; + case "-=" -> Tree.Kind.MINUS_ASSIGNMENT; + case "*=" -> Tree.Kind.MULTIPLY_ASSIGNMENT; + case "/=" -> Tree.Kind.DIVIDE_ASSIGNMENT; + case "%=" -> Tree.Kind.REMAINDER_ASSIGNMENT; + case "&=" -> Tree.Kind.AND_ASSIGNMENT; + case "|=" -> Tree.Kind.OR_ASSIGNMENT; + case "^=" -> Tree.Kind.XOR_ASSIGNMENT; + case "<<=" -> Tree.Kind.LEFT_SHIFT_ASSIGNMENT; + case ">>=" -> Tree.Kind.RIGHT_SHIFT_ASSIGNMENT; + case ">>>=" -> Tree.Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT; + default -> Tree.Kind.ASSIGNMENT; + }; + } + + private static Tree.Kind unaryKind(String operator, boolean prefix) { + return switch (operator) { + case "++" -> prefix ? Tree.Kind.PREFIX_INCREMENT : Tree.Kind.POSTFIX_INCREMENT; + case "--" -> prefix ? Tree.Kind.PREFIX_DECREMENT : Tree.Kind.POSTFIX_DECREMENT; + case "-" -> Tree.Kind.UNARY_MINUS; + case "+" -> Tree.Kind.UNARY_PLUS; + case "~" -> Tree.Kind.BITWISE_COMPLEMENT; + default -> Tree.Kind.LOGICAL_COMPLEMENT; + }; + } + + @Nullable + private static Modifier modifierKeyword(String text) { + return switch (text) { + case "public" -> Modifier.PUBLIC; + case "protected" -> Modifier.PROTECTED; + case "private" -> Modifier.PRIVATE; + case "static" -> Modifier.STATIC; + case "abstract" -> Modifier.ABSTRACT; + case "final" -> Modifier.FINAL; + case "native" -> Modifier.NATIVE; + case "synchronized" -> Modifier.SYNCHRONIZED; + case "transient" -> Modifier.TRANSIENT; + case "volatile" -> Modifier.VOLATILE; + case "strictfp" -> Modifier.STRICTFP; + case "default" -> Modifier.DEFAULT; + case "sealed" -> Modifier.SEALED; + case "non-sealed" -> Modifier.NON_SEALED; + case "transitive" -> Modifier.TRANSITIVE; + default -> null; + }; + } + + private static boolean isTypeKind(String kind) { + return switch (kind) { + case "type_identifier", "identifier", "scoped_identifier", "scoped_type_identifier", "generic_type", "array_type", "integral_type", "floating_point_type", "boolean_type", "void_type", "wildcard", "annotated_type" -> true; + default -> false; + }; + } + + private static boolean isStatementKind(String kind) { + return kind.endsWith("_statement") || "block".equals(kind) || "local_variable_declaration".equals(kind) || "switch_expression".equals(kind); + } + + private static boolean isLiteral(String kind) { + return switch (kind) { + case "string_literal", "character_literal", "text_block", "decimal_integer_literal", "hex_integer_literal", "octal_integer_literal", "binary_integer_literal", "decimal_floating_point_literal", "hex_floating_point_literal" -> true; + default -> false; + }; + } + + private static boolean isComment(Node node) { + return "line_comment".equals(node.k()) || "block_comment".equals(node.k()); + } + + private static SyntaxTrivia trivia(Node node) { + String text = node.text(); + if (text.startsWith("//") && text.endsWith("\r")) { + text = text.substring(0, text.length() - 1); + } + SyntaxTrivia.CommentKind kind; + if (text.startsWith("///")) { + kind = SyntaxTrivia.CommentKind.MARKDOWN; + } else if (text.startsWith("/**") && !"/**/".equals(text)) { + kind = SyntaxTrivia.CommentKind.JAVADOC; + } else if (text.startsWith("//")) { + kind = SyntaxTrivia.CommentKind.LINE; + } else { + kind = SyntaxTrivia.CommentKind.BLOCK; + } + return new InternalSyntaxTrivia(kind, text, node.line(), node.col()); + } + + private InternalSyntaxToken token(Node node) { + InternalSyntaxToken found = tokens.get(node.json); + if (found != null) { + return found; + } + if ("@interface".equals(node.text())) { + return new InternalSyntaxToken(node.line(), node.col(), "@", List.of(), false); + } + if (!node.children().isEmpty()) { + for (Node child : node.children()) { + if (child.children().isEmpty() && !child.text().isEmpty() && !isComment(child)) { + return token(child); + } + } + } + String text = node.text().isEmpty() ? node.k() : node.text(); + if (text.indexOf('_') >= 0) { + text = ""; + } + return new InternalSyntaxToken(node.line(), node.col(), text, List.of(), false); + } + + private InternalSyntaxToken closingAngle(Node node) { + for (int i = node.children().size() - 1; i >= 0; i--) { + Node child = node.children().get(i); + if (">".equals(child.text()) || ">>".equals(child.text()) || ">>>".equals(child.text())) { + return new InternalSyntaxToken(child.line(), child.col() + child.text().length() - 1, ">", List.of(), false); + } + } + return synthetic(">"); + } + + private InternalSyntaxToken synthetic(String text) { + return new InternalSyntaxToken(1, 0, text, List.of(), false); + } + + @Nullable + private InternalSyntaxToken optionalToken(Node node, String text) { + Node found = keyword(node, text); + return found == null ? null : token(found); + } + + private static Type identifierType(Symbol symbol) { + return symbol instanceof Symbol.MethodSymbol method && !method.isUnknown() ? method.returnType().type() : symbol.type(); + } + + /** A resolved type, or an unresolved one that still carries its written name. */ + private static boolean isKnownOrRecovered(Type type) { + return !type.isUnknown() || !"!Unknown!".equals(type.fullyQualifiedName()); + } + + private static void ascribe(Tree tree, Type type) { + if (tree instanceof AbstractTypedTree typed && type != null) { + typed.type = type; + } else if (tree instanceof AbstractPatternTree pattern && type != null) { + pattern.type = type; + } + } + + @Nullable + private static Node keyword(Node node, String text) { + for (Node child : node.children()) { + if (text.equals(child.text()) || text.equals(child.k())) { + return child; + } + } + return null; + } + + private static Node lastKeyword(Node node, String text) { + List children = node.children(); + for (int i = children.size() - 1; i >= 0; i--) { + if (text.equals(children.get(i).text())) { + return children.get(i); + } + } + return node; + } + + private static boolean same(@Nullable Node left, @Nullable Node right) { + return left != null && right != null && left.json == right.json; + } + + private static Node required(Node node, String text) { + Node found = keyword(node, text); + return found == null ? node : found; + } + + @Nullable + private static Node field(Node node, String name) { + for (Node child : node.children()) { + if (name.equals(child.f())) { + return child; + } + } + return null; + } + + private static Node requiredField(Node node, String name, @Nullable Node fallback) { + Node found = field(node, name); + return found != null ? found : (fallback == null ? node : fallback); + } + + @Nullable + private static Node childKind(Node node, String kind) { + for (Node child : node.children()) { + if (kind.equals(child.k())) { + return child; + } + } + return null; + } + + @Nullable + private static Node firstOf(Node node, String... kinds) { + for (Node child : node.children()) { + for (String kind : kinds) { + if (kind.equals(child.k())) { + return child; + } + } + } + return null; + } + + @Nullable + private static Node lastOf(Node node, String kind) { + Node found = null; + for (Node child : node.children()) { + if (kind.equals(child.k())) { + found = child; + } + } + return found; + } + + @Nullable + private static Node nameNode(Node node) { + Node named = field(node, "name"); + return named != null ? named : firstOf(node, "identifier", "type_identifier"); + } + + @Nullable + private static Node firstNamed(Node node) { + for (Node child : node.children()) { + if (!child.text().equals(child.k()) || child.k().contains("_")) { + if (!child.children().isEmpty() || child.k().contains("_") || "identifier".equals(child.k()) || "type_identifier".equals(child.k())) { + return child; + } + } + } + return node.children().isEmpty() ? node : node.children().get(0); + } + + @Nullable + private static boolean isAnnotation(Node node) { + return "annotation".equals(node.k()) || "marker_annotation".equals(node.k()); + } + + private static Node lastNamed(Node node) { + List children = node.children(); + for (int i = children.size() - 1; i >= 0; i--) { + Node child = children.get(i); + if (child.k().contains("_") || "identifier".equals(child.k())) { + return child; + } + } + return node; + } + + @Nullable + private static Node firstType(Node node) { + for (Node child : node.children()) { + if (isTypeKind(child.k())) { + return child; + } + } + return null; + } + + @Nullable + private static Node firstTokenChild(Node node) { + for (Node child : node.children()) { + if (child.children().isEmpty() && !child.text().isEmpty()) { + return child; + } + } + return null; + } + + private static InternalSyntaxToken firstKeyword(Node node) { + for (Node child : node.children()) { + if (child.children().isEmpty() && !child.text().isEmpty() && !",".equals(child.text())) { + return new InternalSyntaxToken(child.line(), child.col(), child.text(), List.of(), false); + } + } + return new InternalSyntaxToken(node.line(), node.col(), "implements", List.of(), false); + } + + private static void setParents(Tree node) { + if (node instanceof JavaTree javaTree && node.kind() != Tree.Kind.INFERED_TYPE && node.kind() != Tree.Kind.TOKEN) { + for (Tree child : javaTree.getChildren()) { + if (child instanceof JavaTree javaChild) { + javaChild.setParent(node); + setParents(child); + } + } + } + } + + private record Parens(InternalSyntaxToken open, ExpressionTree expr, InternalSyntaxToken close) { + } + + private static final class Node { + private final JsonObject json; + + private Node(JsonObject json) { + this.json = json; + } + + private String k() { + return json.get("k").getAsString(); + } + + private String f() { + return json.has("f") ? json.get("f").getAsString() : ""; + } + + private String text() { + return json.has("t") ? json.get("t").getAsString() : ""; + } + + private boolean missing() { + return json.has("m") && json.get("m").getAsBoolean(); + } + + private int line() { + return json.has("p") ? json.getAsJsonArray("p").get(0).getAsInt() : 1; + } + + private int col() { + return json.has("p") ? json.getAsJsonArray("p").get(1).getAsInt() : 0; + } + + private List children() { + List children = new ArrayList<>(); + for (Node child : allChildren()) { + if (!isComment(child)) { + children.add(child); + } + } + return children; + } + + private List allChildren() { + if (!json.has("c")) { + return List.of(); + } + List children = new ArrayList<>(); + for (JsonElement element : json.getAsJsonArray("c")) { + Node child = new Node(element.getAsJsonObject()); + if (!child.missing()) { + children.add(child); + } + } + return children; + } + + @Nullable + private String decl() { + List decls = decls(); + return decls.isEmpty() ? null : decls.get(0); + } + + private List decls() { + if (!json.has("decls")) { + return List.of(); + } + List ids = new ArrayList<>(); + for (JsonElement element : json.getAsJsonArray("decls")) { + ids.add(element.getAsString()); + } + return ids; + } + + private boolean unresolved() { + if (!json.has("refs")) { + return "type_identifier".equals(k()); + } + for (JsonElement element : json.getAsJsonArray("refs")) { + if ("bound".equals(element.getAsJsonObject().get("outcome").getAsString())) { + return false; + } + } + return true; + } + + @Nullable + private String valueTarget() { + return target(true); + } + + @Nullable + private String typeTarget() { + return target(false); + } + + @Nullable + private String target(boolean value) { + if (!json.has("refs")) { + return null; + } + String fallback = null; + for (JsonElement element : json.getAsJsonArray("refs")) { + JsonObject ref = element.getAsJsonObject(); + if (!ref.has("target") || ref.get("target").isJsonNull() || !"bound".equals(ref.get("outcome").getAsString())) { + continue; + } + String access = ref.has("access") ? ref.get("access").getAsString() : ""; + boolean isValue = "r".equals(access) || "w".equals(access) || "rw".equals(access); + if (isValue == value) { + return ref.get("target").getAsString(); + } + fallback = ref.get("target").getAsString(); + } + return value ? null : fallback; + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/model/TypeParameterTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/TypeParameterTreeImpl.java index d7814d9242b..94ea0b4ab2f 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/TypeParameterTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/TypeParameterTreeImpl.java @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.List; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.java.ast.parser.QualifiedIdentifierListTreeImpl; import org.sonar.java.model.expression.IdentifierTreeImpl; import org.sonar.plugins.java.api.semantic.Symbol; @@ -40,7 +39,7 @@ public class TypeParameterTreeImpl extends JavaTree implements TypeParameterTree private final QualifiedIdentifierListTreeImpl bounds; @Nullable - ITypeBinding typeBinding; + public Symbol.TypeSymbol typeSymbol; public TypeParameterTreeImpl(IdentifierTreeImpl identifier) { this(identifier, null, QualifiedIdentifierListTreeImpl.emptyList()); @@ -90,8 +89,6 @@ public List children() { } public Symbol symbol() { - return typeBinding != null - ? root.sema.typeSymbol(typeBinding) - : Symbol.UNKNOWN_SYMBOL; + return typeSymbol != null ? typeSymbol : Symbol.UNKNOWN_SYMBOL; } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java b/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java index bed0a6217a9..a7ad3bb7d43 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java +++ b/java-frontend/src/main/java/org/sonar/java/model/VisitorsBridge.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonar.api.utils.AnnotationUtils; import org.sonar.check.Rule; import org.sonar.java.AnalysisException; @@ -354,7 +355,11 @@ private void createSonarSymbolTable(CompilationUnitTree tree) { && !sonarComponents.isSonarLintContext() // don't provide semantic data (symbol highlighting) to SQ for generated files (jsp) && !(currentFile instanceof GeneratedFile)) { - SonarSymbolTableVisitor symVisitor = new SonarSymbolTableVisitor(sonarComponents.symbolizableFor(currentFile)); + NewSymbolTable symbols = sonarComponents.symbolizableFor(currentFile); + if (symbols == null) { + return; + } + SonarSymbolTableVisitor symVisitor = new SonarSymbolTableVisitor(symbols); symVisitor.visitCompilationUnit(tree); } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java index aa7ac772899..9d881067627 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java @@ -16,7 +16,6 @@ */ package org.sonar.java.model.declaration; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.java.ast.parser.QualifiedIdentifierListTreeImpl; import org.sonar.java.ast.parser.TypeParameterListTreeImpl; import org.sonarsource.analyzer.commons.collections.ListUtils; @@ -69,7 +68,7 @@ public class ClassTreeImpl extends JavaTree implements ClassTree { private SyntaxToken permitsKeyword; private ListTree permittedTypes; @Nullable - public ITypeBinding typeBinding; + public Symbol.TypeSymbol typeSymbol; public ClassTreeImpl(Kind kind, @Nullable SyntaxToken openBraceToken, List members, @Nullable SyntaxToken closeBraceToken) { this.kind = kind; @@ -218,9 +217,7 @@ public SyntaxToken closeBraceToken() { @Override public Symbol.TypeSymbol symbol() { - return typeBinding != null - ? root.sema.typeSymbol(typeBinding) - : Symbol.TypeSymbol.UNKNOWN_TYPE; + return typeSymbol != null ? typeSymbol : Symbol.TypeSymbol.UNKNOWN_TYPE; } @Nullable diff --git a/java-frontend/src/main/java/org/sonar/java/model/declaration/MethodTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/declaration/MethodTreeImpl.java index 188637e08ba..632098ab9a1 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/declaration/MethodTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/declaration/MethodTreeImpl.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Objects; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.IMethodBinding; import org.sonar.java.Preconditions; import org.sonar.java.ast.parser.FormalParametersListTreeImpl; import org.sonar.java.ast.parser.QualifiedIdentifierListTreeImpl; @@ -74,7 +73,7 @@ public class MethodTreeImpl extends JavaTree implements MethodTree { private CFG cfg; @Nullable - public IMethodBinding methodBinding; + public Symbol.MethodSymbol methodSymbol; /** * Cache for {@link #isOverriding()}. @@ -218,9 +217,7 @@ public ExpressionTree defaultValue() { @Override public Symbol.MethodSymbol symbol() { - return methodBinding != null - ? root.sema.methodSymbol(methodBinding) - : Symbol.MethodSymbol.UNKNOWN_METHOD; + return methodSymbol != null ? methodSymbol : Symbol.MethodSymbol.UNKNOWN_METHOD; } @Override diff --git a/java-frontend/src/main/java/org/sonar/java/model/declaration/VariableTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/declaration/VariableTreeImpl.java index e46d74b113a..13ec96b1a31 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/declaration/VariableTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/declaration/VariableTreeImpl.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.List; -import org.eclipse.jdt.core.dom.IVariableBinding; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonarsource.analyzer.commons.collections.ListUtils; import org.sonar.java.model.InternalSyntaxToken; @@ -49,8 +48,8 @@ public class VariableTreeImpl extends JavaTree implements VariableTree { private ExpressionTree initializer; @Nullable private SyntaxToken endToken; - - public IVariableBinding variableBinding; + @Nullable + public Symbol.VariableSymbol variableSymbol; public VariableTreeImpl( ModifiersTree modifiers, @@ -131,9 +130,7 @@ public SyntaxToken equalToken() { @Override public Symbol symbol() { - return variableBinding != null - ? root.sema.variableSymbol(variableBinding) - : Symbol.UNKNOWN_SYMBOL; + return variableSymbol != null ? variableSymbol : Symbol.UNKNOWN_SYMBOL; } @Override diff --git a/java-frontend/src/main/java/org/sonar/java/model/expression/IdentifierTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/expression/IdentifierTreeImpl.java index 166f6275421..517677e62e5 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/expression/IdentifierTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/expression/IdentifierTreeImpl.java @@ -19,11 +19,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import org.eclipse.jdt.core.dom.IBinding; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.IPackageBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; +import javax.annotation.Nullable; import org.sonar.java.model.InternalSyntaxToken; import org.sonar.java.model.JLabelSymbol; import org.sonar.java.model.JavaTree; @@ -41,9 +37,11 @@ public class IdentifierTreeImpl extends AssessableExpressionTree implements Iden private final boolean isUnnamedVariable; private List annotations; - public IBinding binding; public JLabelSymbol labelSymbol; + @Nullable + public Symbol symbol; + public IdentifierTreeImpl(InternalSyntaxToken nameToken) { this(nameToken, false); } @@ -81,17 +79,8 @@ public boolean isUnnamedVariable() { @Override public Symbol symbol() { - if (binding != null && !isUnnamedVariable) { - switch (binding.getKind()) { - case IBinding.TYPE: - return root.sema.typeSymbol((ITypeBinding) binding); - case IBinding.METHOD: - return root.sema.methodSymbol((IMethodBinding) binding); - case IBinding.VARIABLE: - return root.sema.variableSymbol((IVariableBinding) binding); - case IBinding.PACKAGE: - return root.sema.packageSymbol((IPackageBinding) binding); - } + if (symbol != null && !isUnnamedVariable) { + return symbol; } if (labelSymbol != null) { return labelSymbol; diff --git a/java-frontend/src/main/java/org/sonar/java/model/expression/LambdaExpressionTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/expression/LambdaExpressionTreeImpl.java index d737562cf49..caf0b99132c 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/expression/LambdaExpressionTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/expression/LambdaExpressionTreeImpl.java @@ -17,7 +17,6 @@ package org.sonar.java.model.expression; import java.util.Arrays; -import org.eclipse.jdt.core.dom.IMethodBinding; import org.sonar.java.cfg.CFG; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonarsource.analyzer.commons.collections.ListUtils; @@ -46,7 +45,7 @@ public class LambdaExpressionTreeImpl extends AssessableExpressionTree implement private CFG cfg; @Nullable - public IMethodBinding methodBinding; + public Symbol.MethodSymbol methodSymbol; public LambdaExpressionTreeImpl(@Nullable InternalSyntaxToken openParenToken, List parameters, @Nullable InternalSyntaxToken closeParenToken, InternalSyntaxToken arrowToken, Tree body) { @@ -91,9 +90,7 @@ public Tree body() { @Override public Symbol.MethodSymbol symbol() { - return methodBinding != null - ? root.sema.methodSymbol(methodBinding) - : Symbol.MethodSymbol.UNKNOWN_METHOD; + return methodSymbol != null ? methodSymbol : Symbol.MethodSymbol.UNKNOWN_METHOD; } @Override diff --git a/java-frontend/src/main/java/org/sonar/java/model/expression/MethodInvocationTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/expression/MethodInvocationTreeImpl.java index 6a52cb0d654..4e7b2a38c1c 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/expression/MethodInvocationTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/expression/MethodInvocationTreeImpl.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.List; -import org.eclipse.jdt.core.dom.IMethodBinding; import org.sonar.java.ast.parser.ArgumentListTreeImpl; import org.sonarsource.analyzer.commons.collections.ListUtils; import org.sonar.plugins.java.api.semantic.Symbol; @@ -43,7 +42,7 @@ public class MethodInvocationTreeImpl extends AssessableExpressionTree implement private TypeArguments typeArguments; @Nullable - public IMethodBinding methodBinding; + public Symbol.MethodSymbol methodSymbol; public MethodInvocationTreeImpl(ExpressionTree methodSelect, @Nullable TypeArguments typeArguments, ArgumentListTreeImpl arguments) { this.methodSelect = Objects.requireNonNull(methodSelect); @@ -91,9 +90,7 @@ public Symbol symbol() { @Override public Symbol.MethodSymbol methodSymbol() { - return methodBinding != null - ? root.sema.methodSymbol(methodBinding) - : Symbol.MethodSymbol.UNKNOWN_METHOD; + return methodSymbol != null ? methodSymbol : Symbol.MethodSymbol.UNKNOWN_METHOD; } @Override diff --git a/java-frontend/src/main/java/org/sonar/java/model/expression/NewClassTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/expression/NewClassTreeImpl.java index cbd2a572b70..8ebfebb66e7 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/expression/NewClassTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/expression/NewClassTreeImpl.java @@ -165,9 +165,10 @@ public Symbol constructorSymbol() { @Override public Symbol.MethodSymbol methodSymbol() { Symbol constructorSymbol = this.getConstructorIdentifier().symbol(); - return constructorSymbol.isUnknown() ? - Symbol.MethodSymbol.UNKNOWN_METHOD : - (Symbol.MethodSymbol) constructorSymbol; + if (constructorSymbol instanceof Symbol.MethodSymbol methodSymbol) { + return methodSymbol; + } + return Symbol.MethodSymbol.UNKNOWN_METHOD; } private static void addIfNotNull(List list, Tree... trees) { diff --git a/java-frontend/src/main/java/org/sonar/java/model/pattern/AbstractPatternTree.java b/java-frontend/src/main/java/org/sonar/java/model/pattern/AbstractPatternTree.java index 183d239479f..99771372361 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/pattern/AbstractPatternTree.java +++ b/java-frontend/src/main/java/org/sonar/java/model/pattern/AbstractPatternTree.java @@ -18,7 +18,6 @@ import java.util.Optional; import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.java.model.JavaTree; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.PatternTree; @@ -32,18 +31,15 @@ public abstract class AbstractPatternTree extends JavaTree implements PatternTre private final Tree.Kind kind; @Nullable - public ITypeBinding typeBinding; + public Type type; - AbstractPatternTree(Tree.Kind kind, @Nullable ITypeBinding typeBinding) { + AbstractPatternTree(Tree.Kind kind) { this.kind = kind; - this.typeBinding = typeBinding; } @Override public Type symbolType() { - return typeBinding != null - ? root.sema.type(typeBinding) - : Type.UNKNOWN; + return type != null ? type : Type.UNKNOWN; } @Override diff --git a/java-frontend/src/main/java/org/sonar/java/model/pattern/DefaultPatternTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/pattern/DefaultPatternTreeImpl.java index 797b890d3f3..6335db83326 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/pattern/DefaultPatternTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/pattern/DefaultPatternTreeImpl.java @@ -18,8 +18,6 @@ import java.util.Collections; import java.util.List; -import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.plugins.java.api.tree.DefaultPatternTree; import org.sonar.plugins.java.api.tree.SyntaxToken; import org.sonar.plugins.java.api.tree.Tree; @@ -32,8 +30,8 @@ public class DefaultPatternTreeImpl extends AbstractPatternTree implements Defau private final SyntaxToken defaultToken; - public DefaultPatternTreeImpl(SyntaxToken defaultToken, @Nullable ITypeBinding typeBinding) { - super(Tree.Kind.DEFAULT_PATTERN, typeBinding); + public DefaultPatternTreeImpl(SyntaxToken defaultToken) { + super(Tree.Kind.DEFAULT_PATTERN); this.defaultToken = defaultToken; } diff --git a/java-frontend/src/main/java/org/sonar/java/model/pattern/GuardedPatternTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/pattern/GuardedPatternTreeImpl.java index af42ddfcdbf..29dc286ce23 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/pattern/GuardedPatternTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/pattern/GuardedPatternTreeImpl.java @@ -18,8 +18,6 @@ import java.util.Arrays; import java.util.List; -import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.GuardedPatternTree; import org.sonar.plugins.java.api.tree.PatternTree; @@ -36,8 +34,8 @@ public class GuardedPatternTreeImpl extends AbstractPatternTree implements Guard private final SyntaxToken whenOperator; private final ExpressionTree expression; - public GuardedPatternTreeImpl(PatternTree pattern, SyntaxToken whenOperator, ExpressionTree expression, @Nullable ITypeBinding typeBinding) { - super(Tree.Kind.GUARDED_PATTERN, typeBinding); + public GuardedPatternTreeImpl(PatternTree pattern, SyntaxToken whenOperator, ExpressionTree expression) { + super(Tree.Kind.GUARDED_PATTERN); this.pattern = pattern; this.whenOperator = whenOperator; this.expression = expression; diff --git a/java-frontend/src/main/java/org/sonar/java/model/pattern/NullPatternTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/pattern/NullPatternTreeImpl.java index 665eb9e0d2b..328f96ca56c 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/pattern/NullPatternTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/pattern/NullPatternTreeImpl.java @@ -33,7 +33,7 @@ public class NullPatternTreeImpl extends AbstractPatternTree implements NullPatt private final LiteralTree nullLiteral; public NullPatternTreeImpl(LiteralTree nullLiteral) { - super(Tree.Kind.NULL_PATTERN, null); + super(Tree.Kind.NULL_PATTERN); this.nullLiteral = nullLiteral; } diff --git a/java-frontend/src/main/java/org/sonar/java/model/pattern/RecordPatternTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/pattern/RecordPatternTreeImpl.java index 93e10579aa2..bbd858dd401 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/pattern/RecordPatternTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/pattern/RecordPatternTreeImpl.java @@ -38,7 +38,7 @@ public class RecordPatternTreeImpl extends AbstractPatternTree implements Record private final InternalSyntaxToken closeParenToken; public RecordPatternTreeImpl(TypeTree type, InternalSyntaxToken openParenToken, List patterns, InternalSyntaxToken closeParenToken) { - super(Kind.RECORD_PATTERN, null); + super(Kind.RECORD_PATTERN); this.type = type; this.openParenToken = openParenToken; this.patterns = patterns; diff --git a/java-frontend/src/main/java/org/sonar/java/model/pattern/TypePatternTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/pattern/TypePatternTreeImpl.java index afc45f2b891..1a53a36bcf3 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/pattern/TypePatternTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/pattern/TypePatternTreeImpl.java @@ -18,8 +18,6 @@ import java.util.Collections; import java.util.List; -import javax.annotation.Nullable; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TreeVisitor; import org.sonar.plugins.java.api.tree.TypePatternTree; @@ -32,8 +30,8 @@ public class TypePatternTreeImpl extends AbstractPatternTree implements TypePatt private final VariableTree patternVariable; - public TypePatternTreeImpl(VariableTree patternVariable, @Nullable ITypeBinding typeBinding) { - super(Tree.Kind.TYPE_PATTERN, typeBinding); + public TypePatternTreeImpl(VariableTree patternVariable) { + super(Tree.Kind.TYPE_PATTERN); this.patternVariable = patternVariable; } diff --git a/java-frontend/src/main/java/org/sonar/java/semsitter/BridgeExtractor.java b/java-frontend/src/main/java/org/sonar/java/semsitter/BridgeExtractor.java new file mode 100644 index 00000000000..a31a1462ac5 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/semsitter/BridgeExtractor.java @@ -0,0 +1,143 @@ +/* + * 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.semsitter; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFileAttributeView; +import java.util.Set; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.sonar.java.semsitter.PlatformDetection.Platform; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public final class BridgeExtractor { + + private BridgeExtractor() { + } + + public static Path extract(Path targetDir, Platform platform) throws IOException { + if (platform == Platform.UNSUPPORTED) { + throw new IllegalStateException("Cannot extract bridge for unsupported platform"); + } + String bridgeName = bridgeName(platform); + Path bridgePath = targetDir.resolve(bridgeName); + String archiveName = archiveName(platform); + Path prebuilt = findPrebuilt(archiveName); + if (Files.exists(bridgePath) && !prebuiltIsNewer(prebuilt, bridgePath)) { + return bridgePath; + } + String resourcePath = "/bridge/" + archiveName; + InputStream resource = prebuilt != null ? Files.newInputStream(prebuilt) : BridgeExtractor.class.getResourceAsStream(resourcePath); + if (resource == null) { + throw new IllegalStateException("Bridge resource not found"); + } + try (InputStream stream = resource) { + extractArchive(targetDir, bridgePath, stream, resourcePath, bridgeName); + } + return bridgePath; + } + + private static boolean prebuiltIsNewer(Path prebuilt, Path bridgePath) throws IOException { + return prebuilt != null && Files.getLastModifiedTime(prebuilt).compareTo(Files.getLastModifiedTime(bridgePath)) > 0; + } + + private static Path findPrebuilt(String archiveName) { + Path found = walk(Path.of("").toAbsolutePath(), archiveName); + if (found != null) { + return found; + } + Path origin = codeOrigin(); + return origin == null ? null : walk(origin, archiveName); + } + + private static Path codeOrigin() { + try { + var source = BridgeExtractor.class.getProtectionDomain().getCodeSource(); + if (source == null || source.getLocation() == null) { + return null; + } + return Path.of(source.getLocation().toURI()); + } catch (Exception failure) { + return null; + } + } + + private static Path walk(Path dir, String archiveName) { + for (int i = 0; i < 8 && dir != null; i++) { + Path archive = dir.resolve("semsitter-bridge").resolve("prebuilt").resolve(archiveName); + if (Files.isRegularFile(archive)) { + return archive; + } + dir = dir.getParent(); + } + return null; + } + + private static String archiveName(Platform platform) { + String suffix = switch (platform) { + case ALPINE_AARCH64 -> Platform.LINUX_AARCH64.toString(); + case ALPINE_X64 -> Platform.LINUX_X64.toString(); + default -> platform.toString(); + }; + return "bridge-" + suffix + ".xz"; + } + + private static String bridgeName(Platform platform) { + return platform == Platform.WINDOWS_X64 ? "bridge.exe" : "bridge"; + } + + private static void extractArchive(Path target, Path bridgePath, InputStream archive, String resourcePath, String bridgeName) throws IOException { + Path stagingPath = target.resolve(bridgeName + ".ext"); + try { + try (var xzStream = new XZCompressorInputStream(archive); + var tarStream = new TarArchiveInputStream(xzStream)) { + TarArchiveEntry entry = tarStream.getNextEntry(); + while (entry != null && (!bridgeName.equals(entry.getName()) || entry.isDirectory())) { + entry = tarStream.getNextEntry(); + } + if (entry == null) { + throw new IllegalStateException("Bridge binary not found in archive: " + resourcePath); + } + Files.createDirectories(target); + Files.copy(tarStream, stagingPath, StandardCopyOption.REPLACE_EXISTING); + } + Files.deleteIfExists(bridgePath); + Files.move(stagingPath, bridgePath, StandardCopyOption.ATOMIC_MOVE); + PosixFileAttributeView view = Files.getFileAttributeView(bridgePath, PosixFileAttributeView.class); + if (view != null) { + view.setPermissions(Set.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE)); + } + } catch (Exception failure) { + Files.deleteIfExists(stagingPath); + if (failure instanceof IOException ioException) { + throw ioException; + } + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(failure); + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/semsitter/BridgeFrames.java b/java-frontend/src/main/java/org/sonar/java/semsitter/BridgeFrames.java new file mode 100644 index 00000000000..a1020ed3efb --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/semsitter/BridgeFrames.java @@ -0,0 +1,391 @@ +/* + * 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.semsitter; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Protobuf codec of {@code semsitter-bridge/proto/bridge.proto}. Field numbers live here and in + * the Rust encoder so the CST framing can move into SemSitter without a second schema. + */ +final class BridgeFrames { + + private BridgeFrames() { + } + + static byte[] analyze(int id, String root, Iterable files, Iterable classpath) { + ByteSink analyze = new ByteSink(); + analyze.string(1, root); + for (String file : files) { + analyze.string(2, file); + } + for (String entry : classpath) { + analyze.string(3, entry); + } + ByteSink out = id(id); + out.message(2, analyze.bytes()); + return out.bytes(); + } + + static byte[] cst(int id, String path) { + ByteSink body = new ByteSink(); + body.string(1, path); + ByteSink out = id(id); + out.message(3, body.bytes()); + return out.bytes(); + } + + static byte[] types(int id, Iterable names) { + ByteSink body = new ByteSink(); + for (String name : names) { + body.string(1, name); + } + ByteSink out = id(id); + out.message(4, body.bytes()); + return out.bytes(); + } + + static byte[] parse(int id, String source) { + ByteSink body = new ByteSink(); + body.string(1, source); + ByteSink out = id(id); + out.message(5, body.bytes()); + return out.bytes(); + } + + static byte[] quit(int id) { + ByteSink out = id(id); + out.varint(6, 1); + return out.bytes(); + } + + static JsonObject response(byte[] bytes) { + Cursor cursor = new Cursor(bytes); + JsonObject object = new JsonObject(); + object.addProperty("ok", false); + while (cursor.remaining()) { + long key = cursor.varint(); + int field = (int) (key >> 3); + int wire = (int) (key & 7); + switch (field) { + case 1 -> object.addProperty("id", (int) cursor.varint()); + case 2 -> object.addProperty("ok", cursor.varint() != 0); + case 3 -> object.addProperty("error", cursor.text()); + case 4 -> analyzeResult(object, cursor.bytes()); + case 5 -> object.add("cst", document(cursor.bytes())); + case 6 -> object.add("types", JsonParser.parseString(cursor.text())); + default -> cursor.skip(wire); + } + } + return object; + } + + private static void analyzeResult(JsonObject object, byte[] bytes) { + Cursor cursor = new Cursor(bytes); + JsonArray files = new JsonArray(); + while (cursor.remaining()) { + long key = cursor.varint(); + int field = (int) (key >> 3); + switch (field) { + case 1 -> object.addProperty("root", cursor.text()); + case 2 -> files.add(cursor.text()); + default -> cursor.skip((int) (key & 7)); + } + } + object.add("files", files); + } + + private static JsonObject document(byte[] bytes) { + Cursor cursor = new Cursor(bytes); + JsonObject object = new JsonObject(); + object.addProperty("hasError", false); + object.addProperty("semantic", false); + object.add("symbols", new JsonArray()); + object.add("facts", new JsonObject()); + while (cursor.remaining()) { + long key = cursor.varint(); + int field = (int) (key >> 3); + switch (field) { + case 1 -> object.addProperty("hasError", cursor.varint() != 0); + case 2 -> object.add("tree", node(cursor.bytes())); + case 3 -> object.addProperty("path", cursor.text()); + case 4 -> object.addProperty("semantic", cursor.varint() != 0); + case 5 -> object.add("symbols", JsonParser.parseString(cursor.text())); + case 6 -> factEntry(object.getAsJsonObject("facts"), cursor.bytes()); + default -> cursor.skip((int) (key & 7)); + } + } + return object; + } + + private static void factEntry(JsonObject facts, byte[] bytes) { + Cursor cursor = new Cursor(bytes); + String key = ""; + JsonObject fact = new JsonObject(); + while (cursor.remaining()) { + long header = cursor.varint(); + int field = (int) (header >> 3); + switch (field) { + case 1 -> key = cursor.text(); + case 2 -> fact = fact(cursor.bytes()); + default -> cursor.skip((int) (header & 7)); + } + } + facts.add(key, fact); + } + + private static JsonObject fact(byte[] bytes) { + Cursor cursor = new Cursor(bytes); + JsonObject object = new JsonObject(); + while (cursor.remaining()) { + long header = cursor.varint(); + int field = (int) (header >> 3); + switch (field) { + case 1 -> object.addProperty("kind", cursor.text()); + case 2 -> object.addProperty("tier", cursor.text()); + case 3 -> object.addProperty("parent", cursor.text()); + case 4 -> prop(object, cursor.bytes()); + case 5 -> addString(object, "extends", cursor.text()); + case 6 -> addString(object, "implements", cursor.text()); + case 7 -> addString(object, "permits", cursor.text()); + case 8 -> addString(object, "returns", cursor.text()); + case 9 -> addString(object, "type", cursor.text()); + case 10 -> addString(object, "params", cursor.text()); + case 11 -> addString(object, "overrides", cursor.text()); + case 12 -> addString(object, "annotations", cursor.text()); + default -> cursor.skip((int) (header & 7)); + } + } + return object; + } + + private static void prop(JsonObject fact, byte[] bytes) { + Cursor cursor = new Cursor(bytes); + String name = ""; + JsonArray values = new JsonArray(); + while (cursor.remaining()) { + long header = cursor.varint(); + int field = (int) (header >> 3); + switch (field) { + case 1 -> name = cursor.text(); + case 2 -> values.add(cursor.text()); + default -> cursor.skip((int) (header & 7)); + } + } + if (!fact.has("props")) { + fact.add("props", new JsonObject()); + } + fact.getAsJsonObject("props").add(name, values); + } + + private static JsonObject node(byte[] bytes) { + Cursor cursor = new Cursor(bytes); + JsonObject object = new JsonObject(); + long[] position = new long[4]; + long[] span = new long[2]; + boolean sawPosition = false; + boolean sawSpan = false; + while (cursor.remaining()) { + long header = cursor.varint(); + int field = (int) (header >> 3); + switch (field) { + case 1 -> object.addProperty("k", cursor.text()); + case 2 -> { + if (cursor.varint() != 0) { + object.addProperty("n", true); + } + } + case 3 -> object.addProperty("f", cursor.text()); + case 4 -> { + if (cursor.varint() != 0) { + object.addProperty("m", true); + } + } + case 5, 6, 7, 8 -> { + position[field - 5] = cursor.varint(); + sawPosition = true; + } + case 9, 10 -> { + span[field - 9] = cursor.varint(); + sawSpan = true; + } + case 11 -> object.addProperty("t", cursor.text()); + case 12 -> add(object, "c", node(cursor.bytes())); + case 13 -> addString(object, "decls", cursor.text()); + case 14 -> add(object, "refs", ref(cursor.bytes())); + default -> cursor.skip((int) (header & 7)); + } + } + if (sawPosition) { + JsonArray array = new JsonArray(); + for (long value : position) { + array.add(value); + } + object.add("p", array); + } + if (sawSpan) { + JsonArray array = new JsonArray(); + for (long value : span) { + array.add(value); + } + object.add("b", array); + } + return object; + } + + private static JsonObject ref(byte[] bytes) { + Cursor cursor = new Cursor(bytes); + JsonObject object = new JsonObject(); + while (cursor.remaining()) { + long header = cursor.varint(); + int field = (int) (header >> 3); + switch (field) { + case 1 -> object.addProperty("access", cursor.text()); + case 2 -> object.addProperty("outcome", cursor.text()); + case 3 -> object.addProperty("target", cursor.text()); + case 4 -> object.addProperty("reason", cursor.text()); + case 5 -> { + if (cursor.varint() != 0) { + object.addProperty("bounded", true); + } + } + case 6 -> object.add("facts", JsonParser.parseString(cursor.text())); + default -> cursor.skip((int) (header & 7)); + } + } + return object; + } + + private static void add(JsonObject object, String key, JsonElement element) { + if (!object.has(key)) { + object.add(key, new JsonArray()); + } + object.getAsJsonArray(key).add(element); + } + + private static void addString(JsonObject object, String key, String value) { + if (!object.has(key)) { + object.add(key, new JsonArray()); + } + object.getAsJsonArray(key).add(value); + } + + private static ByteSink id(int id) { + ByteSink out = new ByteSink(); + out.varint(1, id); + return out; + } + + private static final class ByteSink { + private final List bytes = new ArrayList<>(); + + void varint(int field, long value) { + rawVarint(((long) field) << 3); + rawVarint(value); + } + + void string(int field, String value) { + message(field, value.getBytes(StandardCharsets.UTF_8)); + } + + void message(int field, byte[] value) { + rawVarint((((long) field) << 3) | 2); + rawVarint(value.length); + for (byte b : value) { + bytes.add(b); + } + } + + byte[] bytes() { + byte[] out = new byte[bytes.size()]; + for (int i = 0; i < bytes.size(); i++) { + out[i] = bytes.get(i); + } + return out; + } + + private void rawVarint(long value) { + long rest = value; + while (true) { + int bite = (int) (rest & 0x7f); + rest >>>= 7; + if (rest != 0) { + bite |= 0x80; + } + bytes.add((byte) bite); + if (rest == 0) { + return; + } + } + } + } + + private static final class Cursor { + private final byte[] bytes; + private int at; + + private Cursor(byte[] bytes) { + this.bytes = bytes; + } + + private boolean remaining() { + return at < bytes.length; + } + + private long varint() { + long value = 0; + int shift = 0; + while (at < bytes.length && shift < 64) { + int bite = bytes[at++] & 0xff; + value |= (long) (bite & 0x7f) << shift; + if ((bite & 0x80) == 0) { + return value; + } + shift += 7; + } + throw new IllegalArgumentException("truncated varint"); + } + + private byte[] bytes() { + int len = (int) varint(); + byte[] out = new byte[len]; + System.arraycopy(bytes, at, out, 0, len); + at += len; + return out; + } + + private String text() { + return new String(bytes(), StandardCharsets.UTF_8); + } + + private void skip(int wire) { + switch (wire) { + case 0 -> varint(); + case 1 -> at += 8; + case 2 -> at += (int) varint(); + case 5 -> at += 4; + default -> throw new IllegalArgumentException("unsupported protobuf wire type " + wire); + } + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/semsitter/PlatformDetection.java b/java-frontend/src/main/java/org/sonar/java/semsitter/PlatformDetection.java new file mode 100644 index 00000000000..be7db6214e3 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/semsitter/PlatformDetection.java @@ -0,0 +1,98 @@ +/* + * 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.semsitter; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Optional; + +public class PlatformDetection { + + private final String rawOsName; + private final String rawOsArch; + + public enum Platform { + LINUX_X64("linux-x64"), + LINUX_AARCH64("linux-aarch64"), + ALPINE_X64("alpine-x64"), + ALPINE_AARCH64("alpine-aarch64"), + MACOS_X64("macos-x64"), + MACOS_AARCH64("macos-aarch64"), + WINDOWS_X64("windows-x64"), + UNSUPPORTED("unsupported"); + + private final String name; + + Platform(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + public PlatformDetection() { + this(System.getProperty("os.name"), System.getProperty("os.arch")); + } + + PlatformDetection(String rawOsName, String rawOsArch) { + this.rawOsName = rawOsName; + this.rawOsArch = rawOsArch; + } + + public Platform getPlatform() { + String platformString = osName() + "-" + osArch(); + for (Platform platform : Platform.values()) { + if (platform.toString().equalsIgnoreCase(platformString)) { + return platform; + } + } + return Platform.UNSUPPORTED; + } + + private String osName() { + String osName = Optional.ofNullable(rawOsName).orElse("generic").toLowerCase(Locale.ROOT); + if (osName.contains("win")) { + return "windows"; + } else if (osName.contains("mac")) { + return "macos"; + } else if (osName.contains("nux")) { + if (isAlpineLinux()) { + return "alpine"; + } + return "Linux"; + } + return osName.replaceAll("\\s+", ""); + } + + private String osArch() { + String osArch = Optional.ofNullable(rawOsArch).orElse("unknown").toLowerCase(Locale.ROOT); + return switch (osArch) { + case "amd64", "x86_64" -> "x64"; + case "aarch64", "arm64" -> "aarch64"; + case "x86", "i386", "i686" -> "x86"; + default -> osArch; + }; + } + + private static boolean isAlpineLinux() { + return Files.exists(Path.of("/etc/alpine-release")); + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/semsitter/SemsitterBridge.java b/java-frontend/src/main/java/org/sonar/java/semsitter/SemsitterBridge.java new file mode 100644 index 00000000000..756a1c53361 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/semsitter/SemsitterBridge.java @@ -0,0 +1,307 @@ +/* + * 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.semsitter; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Client for the {@code semsitter-bridge} executable. + * + *

A current bridge is spoken to over shared memory: protobuf payloads, one ping byte on the + * pipe. {@code -Dsonar.java.semsitter.dump=

} also asks it to write {@code udg.json}, the + * semantic-dump JSON and the annotated CST JSON under that directory. + * + *

{@link #analyze} scans a batch of files with the classpath, then {@link #cst} returns the + * CST of one of them with its SemSitter semantics: {@code tree} (nodes with kind {@code k}, + * field {@code f}, position {@code p} as 1-based lines and 0-based UTF-16 columns, text {@code t}, + * children {@code c}, declared symbol ids {@code decls}, resolved occurrences {@code refs}), + * {@code symbols} (the semantic dump symbols) and {@code facts} (graph facts by id). + */ +public final class SemsitterBridge implements AutoCloseable { + + private static final int STDERR_LIMIT = 8192; + + private final Session session; + private int nextId = 1; + private boolean closed; + + private SemsitterBridge(Session session) { + this.session = session; + } + + public static SemsitterBridge start(Path executable) throws IOException { + boolean shm = speaksShm(executable); + List command = new ArrayList<>(); + command.add(executable.toAbsolutePath().toString()); + command.add("serve"); + if (shm) { + command.add("--shm"); + } + String dump = System.getProperty("sonar.java.semsitter.dump"); + if (shm && dump != null && !dump.isBlank()) { + command.add("--dump-json"); + command.add(dump); + } + Process process = new ProcessBuilder(command).start(); + return new SemsitterBridge(shm ? new ShmSession(process) : new JsonlSession(process)); + } + + /** + * Scans {@code files} as one project rooted at {@code root}. + * @return the response, whose {@code files} are the project-relative paths accepted by {@link #cst} + */ + public synchronized JsonObject analyze(Path root, Collection files, Collection classpath) throws IOException { + return session.exchange(BridgeFrames.analyze(nextId++, root.toAbsolutePath().toString(), absolute(files), classpath), jsonAnalyze(root, files, classpath)); + } + + public synchronized JsonObject cst(String path) throws IOException { + JsonObject request = new JsonObject(); + request.addProperty("op", "cst"); + request.addProperty("path", path); + return session.exchange(BridgeFrames.cst(nextId++, path), request).getAsJsonObject("cst"); + } + + /** + * @return declarations (kind, supertypes, members) of the types named by erased binary names, and of their supertypes + */ + public synchronized JsonObject types(Collection names) throws IOException { + return session.exchange(BridgeFrames.types(nextId++, names), jsonTypes(names)).getAsJsonObject("types"); + } + + public synchronized JsonObject parse(String source) throws IOException { + JsonObject request = new JsonObject(); + request.addProperty("op", "parse"); + request.addProperty("source", source); + return session.exchange(BridgeFrames.parse(nextId++, source), request).getAsJsonObject("cst"); + } + + @Override + public synchronized void close() { + if (closed) { + return; + } + closed = true; + session.close(BridgeFrames.quit(nextId++)); + } + + private static boolean speaksShm(Path executable) throws IOException { + Process help = new ProcessBuilder(executable.toAbsolutePath().toString(), "-h").start(); + String text = new String(help.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + try { + help.waitFor(2, TimeUnit.SECONDS); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + help.destroyForcibly(); + return text.contains("--shm"); + } + + private static JsonObject jsonAnalyze(Path root, Collection files, Collection classpath) { + JsonObject request = new JsonObject(); + request.addProperty("op", "analyze"); + request.addProperty("root", root.toAbsolutePath().toString()); + JsonArray fileArray = new JsonArray(); + absolute(files).forEach(fileArray::add); + request.add("files", fileArray); + JsonArray classpathArray = new JsonArray(); + classpath.forEach(classpathArray::add); + request.add("classpath", classpathArray); + return request; + } + + private static JsonObject jsonTypes(Collection names) { + JsonObject request = new JsonObject(); + request.addProperty("op", "types"); + JsonArray nameArray = new JsonArray(); + names.forEach(nameArray::add); + request.add("names", nameArray); + return request; + } + + private static List absolute(Collection files) { + List paths = new ArrayList<>(); + files.forEach(file -> paths.add(file.toAbsolutePath().toString())); + return paths; + } + + private interface Session { + JsonObject exchange(byte[] protobuf, JsonObject jsonl) throws IOException; + + void close(byte[] quit); + } + + private static final class ShmSession implements Session { + private final Process process; + private final OutputStream toBridge; + private final InputStream fromBridge; + private final SharedSlots slots; + private final StringBuilder stderr = new StringBuilder(); + + private ShmSession(Process process) throws IOException { + this.process = process; + this.toBridge = process.getOutputStream(); + this.fromBridge = process.getInputStream(); + Thread drain = new Thread(() -> drain(process.getErrorStream(), stderr), "semsitter-bridge-stderr"); + drain.setDaemon(true); + drain.start(); + int ready = fromBridge.read(); + if (ready != SharedSlots.READY) { + throw new IOException("semsitter bridge did not open shared memory" + suffix(stderr)); + } + this.slots = SharedSlots.open(process.pid()); + toBridge.write(SharedSlots.ATTACHED); + toBridge.flush(); + } + + @Override + public JsonObject exchange(byte[] protobuf, JsonObject jsonl) throws IOException { + slots.writeRequest(protobuf, toBridge, fromBridge); + JsonObject response = BridgeFrames.response(slots.readResponse(fromBridge, toBridge)); + if (!response.has("ok") || !response.get("ok").getAsBoolean()) { + String error = response.has("error") ? response.get("error").getAsString() : "bridge request failed"; + throw new IOException("semsitter bridge: " + error + suffix(stderr)); + } + return response; + } + + @Override + public void close(byte[] quit) { + try { + exchange(quit, new JsonObject()); + slots.close(); + toBridge.close(); + } catch (IOException ignored) { + process.destroy(); + } + waitFor(process); + } + } + + private static final class JsonlSession implements Session { + private final Process process; + private final BufferedWriter toBridge; + private final BufferedReader fromBridge; + private final StringBuilder stderr = new StringBuilder(); + private int nextId = 1; + + private JsonlSession(Process process) { + this.process = process; + this.toBridge = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)); + this.fromBridge = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); + Thread drain = new Thread(() -> drain(process.getErrorStream(), stderr), "semsitter-bridge-stderr"); + drain.setDaemon(true); + drain.start(); + } + + @Override + public JsonObject exchange(byte[] protobuf, JsonObject jsonl) throws IOException { + int id = nextId++; + jsonl.addProperty("id", id); + toBridge.write(jsonl.toString()); + toBridge.write('\n'); + toBridge.flush(); + String responseLine = fromBridge.readLine(); + if (responseLine == null) { + throw new IOException("semsitter bridge closed stdout" + suffix(stderr)); + } + JsonElement parsed; + try { + JsonReader reader = new JsonReader(new StringReader(responseLine)); + reader.setNestingLimit(Integer.MAX_VALUE); + parsed = JsonParser.parseReader(reader); + } catch (RuntimeException invalid) { + throw new IOException("semsitter bridge returned invalid JSON" + suffix(stderr), invalid); + } + if (!parsed.isJsonObject()) { + throw new IOException("semsitter bridge returned a non-object" + suffix(stderr)); + } + JsonObject response = parsed.getAsJsonObject(); + if (!response.has("id") || response.get("id").getAsInt() != id) { + throw new IOException("semsitter bridge response id mismatch, expected " + id); + } + if (!response.has("ok") || !response.get("ok").getAsBoolean()) { + String error = response.has("error") ? response.get("error").getAsString() : responseLine; + throw new IOException("semsitter bridge: " + error + suffix(stderr)); + } + return response; + } + + @Override + public void close(byte[] quit) { + try { + JsonObject request = new JsonObject(); + request.addProperty("op", "quit"); + exchange(quit, request); + toBridge.close(); + } catch (IOException ignored) { + process.destroy(); + } + waitFor(process); + } + } + + private static void waitFor(Process process) { + try { + if (!process.waitFor(2, TimeUnit.SECONDS)) { + process.destroyForcibly(); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + process.destroyForcibly(); + } + } + + private static String suffix(StringBuilder stderr) { + synchronized (stderr) { + return stderr.isEmpty() ? "" : (" stderr: " + stderr); + } + } + + private static void drain(InputStream stream, StringBuilder buffer) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + synchronized (buffer) { + buffer.append(line).append('\n'); + if (buffer.length() > STDERR_LIMIT) { + buffer.delete(0, buffer.length() - STDERR_LIMIT); + } + } + } + } catch (IOException ignored) { + // stderr closed with the process + } + } +} diff --git a/java-frontend/src/main/java/org/sonar/java/semsitter/SharedSlots.java b/java-frontend/src/main/java/org/sonar/java/semsitter/SharedSlots.java new file mode 100644 index 00000000000..dd89e42e921 --- /dev/null +++ b/java-frontend/src/main/java/org/sonar/java/semsitter/SharedSlots.java @@ -0,0 +1,165 @@ +/* + * 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.semsitter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Pair of shared mappings the bridge creates for one process. Payloads stay in the mappings. + * The pipe only carries a one-byte ping: {@code 1} data is ready, {@code 2} the mapping grew. + */ +final class SharedSlots implements AutoCloseable { + + static final byte READY = 'R'; + static final byte ATTACHED = 'A'; + static final byte DATA = 1; + static final byte GROW = 2; + + private static final int HEADER = 24; + private static final int MAGIC = 0x31424A53; + private static final int MAX = 512 * 1024 * 1024; + + private final RandomAccessFile requestFile; + private final RandomAccessFile responseFile; + private MappedByteBuffer request; + private MappedByteBuffer response; + + private SharedSlots(RandomAccessFile requestFile, MappedByteBuffer request, RandomAccessFile responseFile, MappedByteBuffer response) { + this.requestFile = requestFile; + this.request = request; + this.responseFile = responseFile; + this.response = response; + } + + static SharedSlots open(long pid) throws IOException { + Path dir = Files.isDirectory(Path.of("/dev/shm")) ? Path.of("/dev/shm") : Path.of(System.getProperty("java.io.tmpdir")); + RandomAccessFile requestFile = new RandomAccessFile(dir.resolve("sjb-" + pid + "-q").toFile(), "rw"); + RandomAccessFile responseFile = new RandomAccessFile(dir.resolve("sjb-" + pid + "-a").toFile(), "rw"); + return new SharedSlots(requestFile, map(requestFile), responseFile, map(responseFile)); + } + + void writeRequest(byte[] bytes, OutputStream toBridge, InputStream fromBridge) throws IOException { + request = write(requestFile, request, bytes, toBridge, fromBridge); + } + + byte[] readResponse(InputStream fromBridge, OutputStream toBridge) throws IOException { + Read read = read(responseFile, response, fromBridge, toBridge); + response = read.buffer; + return read.payload; + } + + @Override + public void close() throws IOException { + requestFile.close(); + responseFile.close(); + } + + private static MappedByteBuffer write(RandomAccessFile file, MappedByteBuffer buffer, byte[] bytes, OutputStream toPeer, InputStream fromPeer) throws IOException { + int need = HEADER + bytes.length; + if (need > buffer.capacity()) { + int cap = buffer.capacity(); + while (cap < need) { + if (cap >= MAX) { + throw new IOException("protobuf message of " + bytes.length + " bytes exceeds the shared mapping"); + } + cap = Math.min(cap * 2, MAX); + } + file.setLength(cap); + header(buffer, GROW, cap, cap); + ping(toPeer, GROW); + if (readPing(fromPeer) != GROW) { + throw new IOException("shared mapping grow was not acknowledged"); + } + buffer = map(file); + } + buffer.position(HEADER); + buffer.put(bytes); + header(buffer, (byte) 0, bytes.length, buffer.capacity()); + ping(toPeer, DATA); + return buffer; + } + + private static Read read(RandomAccessFile file, MappedByteBuffer buffer, InputStream fromPeer, OutputStream toPeer) throws IOException { + while (true) { + switch (readPing(fromPeer)) { + case GROW -> { + int cap = (int) len(buffer); + file.setLength(cap); + buffer = map(file); + ping(toPeer, GROW); + } + case DATA -> { + int size = (int) len(buffer); + if (HEADER + size > buffer.capacity()) { + throw new IOException("payload of " + size + " bytes does not fit the shared mapping"); + } + byte[] payload = new byte[size]; + buffer.position(HEADER); + buffer.get(payload); + return new Read(buffer, payload); + } + default -> throw new IOException("unexpected shared-memory ping"); + } + } + } + + private static void header(MappedByteBuffer buffer, int kind, long len, long cap) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(0, MAGIC); + buffer.putInt(4, kind); + buffer.putLong(8, len); + buffer.putLong(16, cap); + } + + private static long len(MappedByteBuffer buffer) throws IOException { + buffer.order(ByteOrder.LITTLE_ENDIAN); + if (buffer.getInt(0) != MAGIC) { + throw new IOException("shared mapping has no bridge header"); + } + return buffer.getLong(8); + } + + private static MappedByteBuffer map(RandomAccessFile file) throws IOException { + MappedByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, file.length()); + buffer.order(ByteOrder.LITTLE_ENDIAN); + return buffer; + } + + private static void ping(OutputStream toPeer, byte value) throws IOException { + toPeer.write(value); + toPeer.flush(); + } + + private static byte readPing(InputStream fromPeer) throws IOException { + int value = fromPeer.read(); + if (value < 0) { + throw new IOException("semsitter bridge closed the ping pipe"); + } + return (byte) value; + } + + private record Read(MappedByteBuffer buffer, byte[] payload) { + } +} diff --git a/java-frontend/src/test/java/org/eclipse/jdt/core/dom/ASTUtilsTest.java b/java-frontend/src/test/java/org/eclipse/jdt/core/dom/ASTUtilsTest.java deleted file mode 100644 index 7e985fc8e8a..00000000000 --- a/java-frontend/src/test/java/org/eclipse/jdt/core/dom/ASTUtilsTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.eclipse.jdt.core.dom; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNull; - -class ASTUtilsTest { - - @Test - void type_resolution_should_recover() { - assertNull(ASTUtils.resolveType(/* causes NPE */null, "Unknown")); - } -} diff --git a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java index 368f9ed81aa..0e283de6e2b 100644 --- a/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java +++ b/java-frontend/src/test/java/org/sonar/java/JavaFrontendTest.java @@ -133,11 +133,12 @@ void number_of_visitors_in_sonarLint_context_LTS() throws Exception { } @Test - void parsing_errors_should_be_reported_to_sonarlint() throws Exception { + void parsing_errors_are_recovered() throws Exception { scan(SONARLINT_RUNTIME, "class A {"); - assertThat(sensorContext.allAnalysisErrors()).hasSize(1); - assertThat(sensorContext.allAnalysisErrors().iterator().next().message()).startsWith("Parse error at line 1 column 9"); + assertThat(sensorContext.allAnalysisErrors()).isEmpty(); + assertThat(mainCodeIssueScannerAndFilter.scanFileInvocationCount).isEqualTo(1); + assertThat(mainCodeIssueScannerAndFilter.lastScannedTree).isInstanceOf(CompilationUnitTree.class); } @Test @@ -616,8 +617,7 @@ void should_handle_compilation_error_in_batch_mode(String propertyName, String p MapSettings settings = new MapSettings(); settings.setProperty(propertyName, propertyValue); scan(settings, SONARQUBE_RUNTIME, "class A {}", "class B {", "class C {}"); - String allLogs = String.join("\n", logTester.logs()); - assertThat(allLogs).contains("Unable to parse source file : 'B.java'"); + assertThat(sensorContext.allAnalysisErrors()).isEmpty(); assertThat(mainCodeIssueScannerAndFilter.scanFileInvocationCount).isEqualTo(3); assertThat(mainCodeIssueScannerAndFilter.endOfAnalysisInvocationCount).isEqualTo(1); } diff --git a/java-frontend/src/test/java/org/sonar/java/semsitter/SemsitterBridgeTest.java b/java-frontend/src/test/java/org/sonar/java/semsitter/SemsitterBridgeTest.java new file mode 100644 index 00000000000..cb681c218b1 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/semsitter/SemsitterBridgeTest.java @@ -0,0 +1,63 @@ +/* + * 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.semsitter; + +import com.google.gson.JsonObject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class SemsitterBridgeTest { + + private Path executable; + + @BeforeEach + void locateBridge() { + String configured = System.getenv("SEMSITTER_BRIDGE"); + assumeTrue(configured != null && Files.isExecutable(Path.of(configured))); + executable = Path.of(configured); + } + + @Test + void analyze_then_cst(@TempDir Path root) throws IOException { + Path file = Files.writeString(root.resolve("A.java"), "class A { int f; int g() { return f; } }"); + try (SemsitterBridge bridge = SemsitterBridge.start(executable)) { + JsonObject analysis = bridge.analyze(root, List.of(file), List.of()); + assertThat(analysis.getAsJsonArray("files").get(0).getAsString()).isEqualTo("A.java"); + JsonObject cst = bridge.cst(file.toString()); + assertThat(cst.getAsJsonObject("tree").get("k").getAsString()).isEqualTo("program"); + assertThat(cst.getAsJsonArray("symbols")).isNotEmpty(); + assertThat(cst.getAsJsonObject("facts").has("A#g()")).isTrue(); + } + } + + @Test + void errors_are_reported_and_the_bridge_survives() throws IOException { + try (SemsitterBridge bridge = SemsitterBridge.start(executable)) { + assertThatThrownBy(() -> bridge.cst("A.java")).isInstanceOf(IOException.class).hasMessageContaining("no analyze request yet"); + assertThat(bridge.parse("class B {}").get("hasError").getAsBoolean()).isFalse(); + } + } +} diff --git a/semsitter-bridge/.gitignore b/semsitter-bridge/.gitignore new file mode 100644 index 00000000000..b83d22266ac --- /dev/null +++ b/semsitter-bridge/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/semsitter-bridge/Cargo.lock b/semsitter-bridge/Cargo.lock new file mode 100644 index 00000000000..5ade5443c91 --- /dev/null +++ b/semsitter-bridge/Cargo.lock @@ -0,0 +1,1757 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fswatch" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "ignore", + "notify-debouncer-full", + "tracing", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasm-bindgen", +] + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "notify" +version = "8.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f812702fbc75d35416d3f884e04925be7e9498f4eceea2e24ba34559284674" +dependencies = [ + "bitflags", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "objc2-core-foundation", + "objc2-core-services", + "walkdir", + "windows-sys 0.61.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.9", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semsitter-bridge" +version = "0.1.0" +dependencies = [ + "anyhow", + "memmap2", + "serde_json", + "tempfile", + "tree-sitter", + "tree-sitter-java", + "udg-family-jvm", + "udg-gen", + "udg-lang-sdk", + "udg-semantic", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tree-sitter" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5129de2ccc97808c4583d48f1fb44576d015cf241e432800572bda11ef30c3b" +dependencies = [ + "cc", + "regex", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0af592be68c579aa78a16846bd19422978c3c52e438523d45ff5d1bff1f9d4a" + +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "udg-boundary" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "flate2", + "serde", + "serde_json", + "sha2", + "tracing", + "udg-engine", + "udg-model", +] + +[[package]] +name = "udg-buffer" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "rustc-hash", + "tracing", + "udg-lang-sdk", + "udg-model", +] + +[[package]] +name = "udg-embed" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "clap", + "rustc-hash", + "serde", + "serde_json", + "tracing", + "udg-engine", + "udg-lang-sdk", + "udg-model", + "udg-sdk-tree-sitter", + "udg-store", +] + +[[package]] +name = "udg-engine" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "ignore", + "pulldown-cmark", + "rayon", + "regex", + "rustc-hash", + "serde", + "serde_json", + "tracing", + "tree-sitter", + "udg-buffer", + "udg-lang-sdk", + "udg-model", + "udg-sdk-tree-sitter", +] + +[[package]] +name = "udg-engine-plugins" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "rayon", + "tracing", + "tree-sitter", + "udg-engine", + "udg-family-jvm", + "udg-lang-java", + "udg-lang-sdk", + "udg-model", + "udg-sdk-tree-sitter", +] + +[[package]] +name = "udg-family-jvm" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "encoding_rs", + "globset", + "quick-xml", + "rustc-hash", + "serde", + "serde_json", + "toml", + "tracing", + "udg-lang-sdk", + "udg-model", + "udg-packed-table", + "zip", +] + +[[package]] +name = "udg-gen" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "clap", + "mimalloc", + "prost", + "rustc-hash", + "serde", + "serde_json", + "toml", + "tracing", + "tracing-subscriber", + "tree-sitter", + "tree-sitter-java", + "udg-boundary", + "udg-embed", + "udg-engine", + "udg-engine-plugins", + "udg-family-jvm", + "udg-lang-java", + "udg-lang-sdk", + "udg-live", + "udg-live-serve", + "udg-model", + "udg-semantic", + "udg-semantic-session", + "udg-store", + "udg-ucfg", +] + +[[package]] +name = "udg-lang-java" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "rustc-hash", + "serde", + "serde_json", + "tracing", + "tree-sitter", + "tree-sitter-java", + "udg-family-jvm", + "udg-lang-sdk", + "udg-model", + "udg-packed-table", + "udg-sdk-tree-sitter", + "udg-semantic-core", +] + +[[package]] +name = "udg-lang-sdk" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "encoding_rs", + "rustc-hash", + "serde", + "udg-model", +] + +[[package]] +name = "udg-live" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "tracing", + "udg-embed", + "udg-engine", + "udg-lang-sdk", + "udg-model", + "udg-store", +] + +[[package]] +name = "udg-live-serve" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "fswatch", + "ignore", + "prost", + "schemars", + "serde", + "serde_json", + "tracing", + "udg-embed", + "udg-engine", + "udg-lang-sdk", + "udg-live", + "udg-model", + "udg-store", +] + +[[package]] +name = "udg-model" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "clap", + "rustc-hash", + "schemars", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "udg-packed-table" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "flate2", +] + +[[package]] +name = "udg-sdk-tree-sitter" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "rustc-hash", + "serde_json", + "tree-sitter", + "udg-lang-sdk", + "udg-model", +] + +[[package]] +name = "udg-semantic" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "udg-lang-sdk", + "udg-model", + "udg-semantic-core", + "udg-store", +] + +[[package]] +name = "udg-semantic-core" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "serde_json", + "udg-lang-sdk", + "udg-model", +] + +[[package]] +name = "udg-semantic-session" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "rayon", + "tracing", + "tree-sitter", + "udg-engine", + "udg-lang-java", + "udg-lang-sdk", + "udg-model", + "udg-semantic", +] + +[[package]] +name = "udg-store" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "clap", + "ignore", + "memmap2", + "prost", + "rayon", + "regex", + "rustc-hash", + "schemars", + "serde", + "serde_json", + "tracing", + "udg-model", +] + +[[package]] +name = "udg-ucfg" +version = "0.1.0" +source = "git+ssh://git@github.com/SonarSource/semsitter.git?rev=b2a0ab3ab12d11ba2a77333765fecf189410fe4a#b2a0ab3ab12d11ba2a77333765fecf189410fe4a" +dependencies = [ + "anyhow", + "clap", + "prost", + "regex", + "serde", + "serde_json", + "sha2", + "tracing", + "tree-sitter", + "tree-sitter-java", + "udg-engine", + "udg-engine-plugins", + "udg-family-jvm", + "udg-lang-java", + "udg-lang-sdk", + "udg-model", + "udg-sdk-tree-sitter", + "udg-semantic", + "zip", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zip" +version = "9.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e3161de27091f18a831d9494e9c53378a34833af5c1dda35ad85849da18d6" +dependencies = [ + "crc32fast", + "flate2", + "getrandom", + "indexmap", + "memchr", + "nix", + "typed-path", + "windows-sys 0.61.2", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/semsitter-bridge/Cargo.toml b/semsitter-bridge/Cargo.toml new file mode 100644 index 00000000000..d48af9b7e8c --- /dev/null +++ b/semsitter-bridge/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "semsitter-bridge" +version = "0.1.0" +edition = "2024" +publish = false +description = "Java CST with SemSitter semantics, served over JSONL to the SonarJava frontend." + +[[bin]] +name = "semsitter-bridge" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +memmap2 = "0.9" +serde_json = "1" +tree-sitter = "0.27" +tree-sitter-java = "0.23.5" +udg-gen = { git = "ssh://git@github.com/SonarSource/semsitter.git", rev = "b2a0ab3ab12d11ba2a77333765fecf189410fe4a", default-features = false, features = ["lang-java"] } +udg-family-jvm = { git = "ssh://git@github.com/SonarSource/semsitter.git", rev = "b2a0ab3ab12d11ba2a77333765fecf189410fe4a" } +udg-lang-sdk = { git = "ssh://git@github.com/SonarSource/semsitter.git", rev = "b2a0ab3ab12d11ba2a77333765fecf189410fe4a", features = ["unstable-sdk"] } +udg-semantic = { git = "ssh://git@github.com/SonarSource/semsitter.git", rev = "b2a0ab3ab12d11ba2a77333765fecf189410fe4a" } + +[dev-dependencies] +tempfile = "3" + +[profile.release] +lto = "thin" +codegen-units = 4 +strip = true diff --git a/semsitter-bridge/archive-bridge.sh b/semsitter-bridge/archive-bridge.sh new file mode 100644 index 00000000000..95b33bce1c9 --- /dev/null +++ b/semsitter-bridge/archive-bridge.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Regenerates prebuilt/bridge--.xz, packaged in the plugin JAR so CI needs neither cargo nor +# access to the private semsitter repository. Run on an Apple silicon Mac with Docker and SSH access to GitHub. +set -euo pipefail + +cd "$(dirname "$0")" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=url.ssh://git@github.com/.insteadOf GIT_CONFIG_VALUE_0=https://github.com/ +export CARGO_NET_GIT_FETCH_WITH_CLI=true +target_dir="${CARGO_TARGET_DIR:-$PWD/target}" + +cargo vendor --locked "$work/vendor" | sed "s#$work/vendor#/vendor#" > "$work/config.toml" +mkdir -p "$work/out" + +docker_build() { + local platform=$1 image=$2 setup=$3 target=$4 out=$5 + docker run --rm --platform "$platform" \ + -v "$PWD:/src:ro" -v "$work/vendor:/vendor:ro" -v "$work/config.toml:/usr/local/cargo/config.toml:ro" -v "$work/out:/out" \ + -e CARGO_TARGET_DIR=/tmp/target "$image" \ + sh -c "$setup && cd /src && cargo build --release --locked --offline $target && cp \$(ls /tmp/target/${target:+${target#--target }/}release/semsitter-bridge /tmp/target/${target:+${target#--target }/}release/semsitter-bridge.exe 2>/dev/null) /out/$out" +} + +docker_build linux/amd64 rust:1-alpine "apk add --no-cache musl-dev" "" linux-x64 +docker_build linux/arm64 rust:1-alpine "apk add --no-cache musl-dev" "" linux-aarch64 +docker_build linux/arm64 rust:1 "apt-get update -qq && apt-get install -y -qq gcc-mingw-w64-x86-64 && rustup target add x86_64-pc-windows-gnu" \ + "--target x86_64-pc-windows-gnu" windows-x64 + +rustup target add x86_64-apple-darwin +cargo build --release --locked +cargo build --release --locked --target x86_64-apple-darwin +cp "$target_dir/release/semsitter-bridge" "$work/out/macos-aarch64" +cp "$target_dir/x86_64-apple-darwin/release/semsitter-bridge" "$work/out/macos-x64" + +mkdir -p prebuilt +for os in linux-x64 linux-aarch64 macos-x64 macos-aarch64 windows-x64; do + bin=bridge + [[ $os == windows-x64 ]] && bin=bridge.exe + mkdir -p "$work/$os" + install -m 755 "$work/out/$os" "$work/$os/$bin" + tar -cJf "prebuilt/bridge-$os.xz" -C "$work/$os" "$bin" +done diff --git a/semsitter-bridge/prebuilt/bridge-linux-aarch64.xz b/semsitter-bridge/prebuilt/bridge-linux-aarch64.xz new file mode 100644 index 00000000000..37e90fef4ac Binary files /dev/null and b/semsitter-bridge/prebuilt/bridge-linux-aarch64.xz differ diff --git a/semsitter-bridge/prebuilt/bridge-linux-x64.xz b/semsitter-bridge/prebuilt/bridge-linux-x64.xz new file mode 100644 index 00000000000..18ec1aa0282 Binary files /dev/null and b/semsitter-bridge/prebuilt/bridge-linux-x64.xz differ diff --git a/semsitter-bridge/prebuilt/bridge-macos-aarch64.xz b/semsitter-bridge/prebuilt/bridge-macos-aarch64.xz new file mode 100644 index 00000000000..ebab8710160 Binary files /dev/null and b/semsitter-bridge/prebuilt/bridge-macos-aarch64.xz differ diff --git a/semsitter-bridge/prebuilt/bridge-macos-x64.xz b/semsitter-bridge/prebuilt/bridge-macos-x64.xz new file mode 100644 index 00000000000..72181768dfb Binary files /dev/null and b/semsitter-bridge/prebuilt/bridge-macos-x64.xz differ diff --git a/semsitter-bridge/prebuilt/bridge-windows-x64.xz b/semsitter-bridge/prebuilt/bridge-windows-x64.xz new file mode 100644 index 00000000000..6c961e6a013 Binary files /dev/null and b/semsitter-bridge/prebuilt/bridge-windows-x64.xz differ diff --git a/semsitter-bridge/proto/bridge.proto b/semsitter-bridge/proto/bridge.proto new file mode 100644 index 00000000000..d1831481b06 --- /dev/null +++ b/semsitter-bridge/proto/bridge.proto @@ -0,0 +1,110 @@ +syntax = "proto3"; + +// Contract between the Rust bridge and the Java frontend. Field numbers are the wire format +// implemented by hand in src/wire.rs and BridgeFrames.java, so this file can move into +// SemSitter with the CST builder without a protocol change. +// +// The CST is protobuf. Semantic-dump symbols and the open `facts` object on an occurrence stay +// JSON bytes: that schema is SemSitter's semantic-dump/1. Graph facts projected for SonarJava +// are protobuf. Type declarations are one JSON object, same reason. + +message Request { + uint32 id = 1; + AnalyzeRequest analyze = 2; + CstRequest cst = 3; + TypesRequest types = 4; + ParseRequest parse = 5; + bool quit = 6; +} + +message AnalyzeRequest { + string root = 1; + repeated string files = 2; + repeated string classpath = 3; +} + +message CstRequest { + string path = 1; +} + +message TypesRequest { + repeated string names = 1; +} + +message ParseRequest { + string source = 1; +} + +message Response { + uint32 id = 1; + bool ok = 2; + string error = 3; + AnalyzeResult analyze = 4; + Document document = 5; + string types_json = 6; +} + +message AnalyzeResult { + string root = 1; + repeated string files = 2; +} + +message Document { + bool has_error = 1; + Node tree = 2; + string path = 3; + bool semantic = 4; + string symbols_json = 5; + repeated FactEntry facts = 6; +} + +message FactEntry { + string key = 1; + Fact value = 2; +} + +message Fact { + string kind = 1; + string tier = 2; + string parent = 3; + repeated Prop props = 4; + repeated string extends = 5; + repeated string implements = 6; + repeated string permits = 7; + repeated string returns = 8; + repeated string type = 9; + repeated string params = 10; + repeated string overrides = 11; + repeated string annotations = 12; +} + +message Prop { + string name = 1; + repeated string values = 2; +} + +message Node { + string kind = 1; + bool named = 2; + string field = 3; + bool missing = 4; + uint32 start_line = 5; + uint32 start_col = 6; + uint32 end_line = 7; + uint32 end_col = 8; + uint32 start_byte = 9; + uint32 end_byte = 10; + string text = 11; + repeated Node children = 12; + repeated string decls = 13; + repeated Ref refs = 14; +} + +message Ref { + string access = 1; + string outcome = 2; + string target = 3; + string reason = 4; + bool bounded = 5; + string facts_json = 6; +} diff --git a/semsitter-bridge/src/lib.rs b/semsitter-bridge/src/lib.rs new file mode 100644 index 00000000000..dca30250c9e --- /dev/null +++ b/semsitter-bridge/src/lib.rs @@ -0,0 +1,641 @@ +/* + * 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/ + */ + +//! Java CST annotated with SemSitter semantics. +//! +//! [`analyze`] runs one SemSitter scan over a batch of Java files (with the project classpath) +//! and keeps each file's `semantic-dump/1` in memory, plus the graph facts SonarJava needs on +//! top of it (return types, supertypes, overrides, item kinds and modifiers). +//! [`Analysis::cst`] then parses a file with tree-sitter-java and attaches, by exact byte span, +//! the symbols declared at a name (`decls`) and the resolved occurrences (`refs`). +//! +//! The scan's graph is the UDG. A dump symbol's id is that graph's FQN, and the CST only +//! points at it by id. Transport of those documents lives in [`wire`] and [`shm`] so it can +//! move into SemSitter later without the Java frontend knowing where the bytes were built. + +pub mod server; +pub mod shm; +pub mod types; +pub mod wire; + +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context, Result, anyhow}; +use serde_json::{Map, Value, json}; +use tree_sitter::{Node, Parser, Tree}; +use udg_gen::overlay_passes::overlay_registry; +use udg_gen::scanner::{ScanOptions, scan_timed_with_registry}; +use udg_gen::semantic_dump::languages; +use udg_gen::udg_model::graph::{DependencyTarget, NodeTier, UdgDependencyKind, UdgGraph}; +use udg_gen::udg_model::source_provider::SourceProvider; +use udg_family_jvm::external::classpath::LazyClasspath; +use udg_lang_sdk::settings::ResolvedSettings; +use udg_semantic::{DumpLanguage, DumpProject, GraphIds, language_of}; + +const BOM: &[u8] = b"\xEF\xBB\xBF"; +const STACK_SIZE: usize = 512 * 1024 * 1024; +const GRAPH_EDGES: &[(&str, &str)] = &[ + ("extends", "extends"), + ("implements", "implements"), + ("permits", "permits"), + ("returns", "returns"), + ("is type", "type"), + ("has param", "params"), + ("overrides", "overrides"), + ("annotated by", "annotations"), +]; + +/// Result of one SemSitter scan over a batch of files. +pub struct Analysis { + root: PathBuf, + files: Vec, + dumps: HashMap, + facts: HashMap, + calls: HashMap>, + project_types: types::ProjectTypes, + classpath: Arc, +} + +#[derive(Debug)] +struct ListedSources { + files: Vec, +} + +impl SourceProvider for ListedSources { + fn read(&self, path: &Path) -> io::Result { + Ok(String::from_utf8_lossy(&std::fs::read(path)?).into_owned()) + } + + fn read_bytes(&self, path: &Path) -> io::Result> { + std::fs::read(path) + } + + fn list_files(&self, root: &Path, _project_root: &Path, extensions: &[&str]) -> Vec { + let mut found: Vec = self + .files + .iter() + .filter(|f| f.starts_with(root)) + .filter(|f| { + f.extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| extensions.iter().any(|x| x.trim_start_matches('.').eq_ignore_ascii_case(e))) + }) + .cloned() + .collect(); + found.sort(); + found + } + + fn file_exists(&self, path: &Path) -> bool { + path.is_file() + } +} + +/// Scans `files` (Java sources) as one project rooted at `root`, resolving library types +/// against `classpath` (jars, zips and class directories, declarations only). +pub fn analyze(root: &Path, files: &[PathBuf], classpath: &[String]) -> Result { + analyze_dumping(root, files, classpath, None) +} + +/// [`analyze`], and when `dump` is set writes `udg.json`, each `semantic-dump/1` document and +/// the annotated CST as JSON under that directory. The scan itself stays in memory. +pub fn analyze_dumping(root: &Path, files: &[PathBuf], classpath: &[String], dump: Option<&Path>) -> Result { + let files: Vec = files.iter().filter_map(|f| std::fs::canonicalize(f).ok()).collect(); + let root = project_root(root, &files); + let mut opts = ScanOptions { + src_dirs: vec![root.clone()], + project_root: Some(root.clone()), + source_provider: Some(Arc::new(ListedSources { files: files.clone() })), + ..ScanOptions::default() + }; + opts.extras.modifiers = true; + opts.extras.structural = true; + opts.extras.type_reference_edges = true; + opts.extras.override_edges = true; + opts.extras.annotation_edges = true; + opts.extras.field_reference_edges = true; + opts.extras.decl_location = true; + opts.extras.field_items = true; + + let rel: Vec = files.iter().filter_map(|f| f.strip_prefix(&root).ok()).map(to_slash).collect(); + let mut facts = HashMap::new(); + let mut calls = HashMap::new(); + let mut raw_dumps: HashMap = HashMap::new(); + if !files.is_empty() { + let registry = overlay_registry(); + let (graph, _timings, _failures, _artefacts) = scan_timed_with_registry(&opts, ®istry)?; + facts = graph_facts(&graph); + calls = call_sites(&graph); + let settings = registry + .settings_report() + .unwrap_or_else(|| Arc::clone(ResolvedSettings::empty())); + raw_dumps = memory_dumps(&root, &rel, &graph, settings.as_ref())?; + if let Some(dir) = dump { + write_debug_bundle(dir, &graph, &raw_dumps)?; + } + } + + let dumps = raw_dumps + .iter() + .filter_map(|(path, text)| Some((path.clone(), serde_json::from_str(text).ok()?))) + .collect(); + let project_types = types::ProjectTypes::new(&facts); + let analysis = Analysis { + root, + files: rel, + dumps, + facts, + calls, + project_types, + classpath: shared_classpath(classpath), + }; + if let Some(dir) = dump { + write_cst_json(&analysis, dir)?; + } + Ok(analysis) +} + +/// The classpath of the previous analysis when it lists the same entries, so a client that analyzes +/// one file at a time lists and parses each jar once. +fn shared_classpath(entries: &[String]) -> Arc { + static LAST: std::sync::Mutex, Arc)>> = std::sync::Mutex::new(None); + let mut last = LAST.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some((listed, classpath)) = last.as_ref() + && listed.as_slice() == entries + { + return Arc::clone(classpath); + } + let classpath = Arc::new(LazyClasspath::new(entries.to_vec())); + *last = Some((entries.to_vec(), Arc::clone(&classpath))); + classpath +} + +fn memory_dumps(root: &Path, files: &[String], graph: &UdgGraph, settings: &ResolvedSettings) -> Result> { + let ids = GraphIds::of(graph); + let mut listed: Vec<&str> = files.iter().map(String::as_str).filter(|r| is_project_relative(r)).collect(); + listed.sort_unstable(); + listed.dedup(); + let plain = languages(); + let project = DumpProject { + graph, + root, + files: &listed, + settings, + expressions: false, + }; + let bound: Vec>> = plain.iter().map(|language| language.for_project(&project)).collect(); + let bound_langs: Vec<&dyn DumpLanguage> = plain + .iter() + .zip(&bound) + .map(|(language, project_language)| project_language.as_deref().unwrap_or(*language)) + .collect(); + let mut dumps = HashMap::new(); + for rel in listed { + let Some(language) = language_of(&bound_langs, rel) else { + continue; + }; + let Ok(stored) = std::fs::read(root.join(rel)) else { + continue; + }; + if let Some(document) = language.dump_stored(rel, stored, ids.file(rel)) { + dumps.insert(rel.to_string(), document); + } + } + Ok(dumps) +} + +fn is_project_relative(rel: &str) -> bool { + let path = Path::new(rel); + !path.is_absolute() + && path.components().all(|c| matches!(c, std::path::Component::Normal(_) | std::path::Component::CurDir)) +} + +fn write_debug_bundle(dir: &Path, graph: &UdgGraph, dumps: &HashMap) -> Result<()> { + std::fs::create_dir_all(dir)?; + let file = std::fs::File::create(dir.join("udg.json")).context("creating udg.json")?; + udg_gen::udg_graph_to_proto_json_writer(graph, udg_gen::EdgeFidelity::Full, file).context("writing udg.json")?; + for (rel, document) in dumps { + let target = dir.join("semdump").join(format!("{rel}.semdump.json")); + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(target, document).with_context(|| format!("writing semdump {rel}"))?; + } + Ok(()) +} + +fn write_cst_json(analysis: &Analysis, dir: &Path) -> Result<()> { + use std::io::Write; + for file in analysis.files() { + let target = dir.join("cst").join(format!("{file}.json")); + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent)?; + } + let mut out = std::fs::File::create(&target).with_context(|| format!("creating {}", target.display()))?; + serde_json::to_writer_pretty(&mut out, &analysis.cst(file)?)?; + writeln!(out)?; + } + Ok(()) +} + +impl Analysis { + pub fn root(&self) -> &Path { + &self.root + } + + /// Project-relative paths (forward slashes) of the analyzed files. + pub fn files(&self) -> &[String] { + &self.files + } + + /// Declarations of the types named `names` (erased binary names) and of their supertypes. + pub fn types(&self, names: &[String]) -> Value { + Value::Object(types::lookup(names, &self.project_types, &self.facts, self.classpath.index())) + } + + /// Resolves an absolute or project-relative path to the key used by [`Self::cst`]. + pub fn relative(&self, path: &str) -> Option { + if self.dumps.contains_key(path) || self.files.iter().any(|f| f == path) { + return Some(path.to_string()); + } + let canonical = std::fs::canonicalize(path).ok()?; + canonical.strip_prefix(&self.root).ok().map(to_slash) + } + + /// CST of one analyzed file with its semantics: tree, declared symbols and the graph facts + /// of every symbol id the file mentions. + pub fn cst(&self, path: &str) -> Result { + let rel = self.relative(path).ok_or_else(|| anyhow!("not analyzed: {path}"))?; + let source = std::fs::read(self.root.join(&rel)).with_context(|| format!("reading {rel}"))?; + let dump = self.dumps.get(&rel); + let calls = self.calls.get(&rel).map(Vec::as_slice).unwrap_or_default(); + let index = SemanticIndex::new(dump, calls, &source); + let mut document = document(&source, &index)?; + let symbols = dump.and_then(|d| d.get("symbols")).cloned().unwrap_or_else(|| json!([])); + let facts: Map = index + .ids + .iter() + .filter_map(|id| self.facts.get(id).map(|f| (id.clone(), f.clone()))) + .collect(); + let object = document.as_object_mut().expect("document is an object"); + object.insert("path".into(), Value::String(rel.clone())); + object.insert("semantic".into(), Value::Bool(dump.is_some())); + object.insert("symbols".into(), symbols); + object.insert("facts".into(), Value::Object(facts)); + Ok(document) + } +} + +/// CST of `source` without semantics. +pub fn parse_only(source: &[u8]) -> Result { + document(source, &SemanticIndex::default()) +} + +fn document(source: &[u8], index: &SemanticIndex) -> Result { + let source = source.to_vec(); + let index = index.clone(); + std::thread::Builder::new() + .stack_size(STACK_SIZE) + .spawn(move || { + let tree = parse(&source)?; + let lines = Lines::new(&source); + let root = tree.root_node(); + Ok(json!({ + "hasError": root.has_error(), + "tree": node_json(root, None, None, &source, &lines, &index), + })) + })? + .join() + .map_err(|_| anyhow!("CST serialization panicked"))? +} + +fn parse(source: &[u8]) -> Result { + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_java::LANGUAGE.into())?; + let tree = if source.starts_with(BOM) { + let mut blanked = source.to_vec(); + blanked[..BOM.len()].fill(b' '); + parser.parse(&blanked, None) + } else { + parser.parse(source, None) + }; + tree.ok_or_else(|| anyhow!("tree-sitter returned no tree")) +} + +/// One CST node. `called` is the graph call target of the enclosing `method_invocation`, used +/// for its `name` when the semantic dump has no occurrence there. +fn node_json( + node: Node, + field: Option<&str>, + called: Option<&str>, + source: &[u8], + lines: &Lines, + index: &SemanticIndex, +) -> Value { + let mut object = Map::new(); + let (start, end) = (node.start_byte(), node.end_byte()); + object.insert("k".into(), Value::String(node.kind().to_string())); + if node.is_named() { + object.insert("n".into(), Value::Bool(true)); + } + if let Some(field) = field { + object.insert("f".into(), Value::String(field.to_string())); + } + if node.is_missing() { + object.insert("m".into(), Value::Bool(true)); + } + let (start_line, start_col) = lines.position(source, start); + let (end_line, end_col) = lines.position(source, end); + object.insert("p".into(), json!([start_line, start_col, end_line, end_col])); + object.insert("b".into(), json!([start, end])); + let call = if node.kind() == "method_invocation" { + index.calls.get(&(start, end)).map(String::as_str) + } else { + None + }; + let mut cursor = node.walk(); + let children: Vec = if cursor.goto_first_child() { + let mut children = Vec::new(); + loop { + let child_field = cursor.field_name(); + let child_call = call.filter(|_| child_field == Some("name")); + children.push(node_json(cursor.node(), child_field, child_call, source, lines, index)); + if !cursor.goto_next_sibling() { + break; + } + } + children + } else { + Vec::new() + }; + if children.is_empty() || is_literal(node.kind()) { + object.insert( + "t".into(), + Value::String(String::from_utf8_lossy(&source[start..end]).into_owned()), + ); + } + if !children.is_empty() { + object.insert("c".into(), Value::Array(children)); + } + if children_free_or_name(node) { + if let Some(decls) = index.decls.get(&(start, end)) { + object.insert("decls".into(), json!(decls)); + } + if let Some(refs) = index.refs.get(&(start, end)) { + object.insert("refs".into(), Value::Array(refs.clone())); + } else if let Some(target) = called { + object.insert( + "refs".into(), + json!([{"access": "r", "outcome": "bound", "target": target, "reason": "graph-call"}]), + ); + } + } + Value::Object(object) +} + +fn children_free_or_name(node: Node) -> bool { + node.child_count() == 0 || matches!(node.kind(), "this" | "super") +} + +fn is_literal(kind: &str) -> bool { + matches!( + kind, + "string_literal" | "character_literal" | "text_block" | "decimal_integer_literal" + | "hex_integer_literal" | "octal_integer_literal" | "binary_integer_literal" + | "decimal_floating_point_literal" | "hex_floating_point_literal" + ) +} + +/// Byte offset to (1-based line, 0-based UTF-16 column), the convention of SonarJava tokens. +struct Lines { + starts: Vec, +} + +impl Lines { + fn new(source: &[u8]) -> Self { + let mut starts = vec![0]; + let mut i = 0; + while i < source.len() { + match source[i] { + b'\n' => starts.push(i + 1), + b'\r' if source.get(i + 1) != Some(&b'\n') => starts.push(i + 1), + _ => {} + } + i += 1; + } + Lines { starts } + } + + fn position(&self, source: &[u8], offset: usize) -> (usize, usize) { + let line = self.starts.partition_point(|&s| s <= offset) - 1; + let prefix = String::from_utf8_lossy(&source[self.starts[line]..offset]); + (line + 1, prefix.trim_start_matches('\u{feff}').encode_utf16().count()) + } + + fn offset(&self, (line, column): (usize, usize)) -> Option { + self.starts.get(line.checked_sub(1)?).map(|start| start + column) + } +} + +/// Semantic dump indexed by byte span. +#[derive(Default, Clone)] +struct SemanticIndex { + decls: HashMap<(usize, usize), Vec>, + refs: HashMap<(usize, usize), Vec>, + calls: HashMap<(usize, usize), String>, + ids: BTreeSet, +} + +impl SemanticIndex { + fn new(dump: Option<&Value>, calls: &[CallSite], source: &[u8]) -> Self { + let mut index = SemanticIndex::default(); + let lines = Lines::new(source); + for call in calls { + if let (Some(start), Some(end)) = (lines.offset(call.start), lines.offset(call.end)) { + index.ids.insert(call.target.clone()); + index.calls.insert((start, end), call.target.clone()); + } + } + let Some(dump) = dump else { + return index; + }; + for symbol in array(dump, "symbols") { + let Some(id) = symbol.get("id").and_then(Value::as_str) else { + continue; + }; + index.ids.insert(id.to_string()); + collect_type_ids(symbol.pointer("/facts/type"), &mut index.ids); + if let Some(span) = span(symbol) { + index.decls.entry(span).or_default().push(id.to_string()); + } + } + for occurrence in array(dump, "occurrences") { + let Some(span) = span(occurrence) else { + continue; + }; + let mut reference = occurrence.clone(); + if let Some(object) = reference.as_object_mut() { + for key in ["start", "end", "name", "line", "col"] { + object.remove(key); + } + } + if let Some(target) = occurrence.get("target").and_then(Value::as_str) { + index.ids.insert(target.to_string()); + } + for candidate in occurrence + .pointer("/facts/candidates") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + if let Some(id) = candidate.as_str().or_else(|| candidate.get("id").and_then(Value::as_str)) { + index.ids.insert(id.to_string()); + } + } + index.refs.entry(span).or_default().push(reference); + } + index + } +} + +fn collect_type_ids(ty: Option<&Value>, ids: &mut BTreeSet) { + let Some(ty) = ty else { + return; + }; + if let Some(id) = ty.get("id").and_then(Value::as_str) { + ids.insert(id.to_string()); + } + collect_type_ids(ty.get("elem"), ids); + for arg in ty.get("args").and_then(Value::as_array).into_iter().flatten() { + collect_type_ids(Some(arg), ids); + } +} + +fn array<'a>(value: &'a Value, key: &str) -> impl Iterator { + value.get(key).and_then(Value::as_array).into_iter().flatten() +} + +fn span(value: &Value) -> Option<(usize, usize)> { + Some(( + value.get("start")?.as_u64()? as usize, + value.get("end")?.as_u64()? as usize, + )) +} + +/// A resolved call edge of the graph, located at its call site (1-based lines, 0-based columns). +#[derive(Clone)] +struct CallSite { + start: (usize, usize), + end: (usize, usize), + target: String, +} + +fn call_sites(graph: &UdgGraph) -> HashMap> { + let mut sites: HashMap> = HashMap::new(); + for fqn in graph.items.keys() { + for dependency in graph.get_downstream_dependencies(fqn.as_str()) { + if dependency.kind != UdgDependencyKind::Calls { + continue; + } + let DependencyTarget::Item(target) = &dependency.to_node else { + continue; + }; + if target.tier == NodeTier::ExternalUnknown { + continue; + } + let Some(location) = &dependency.location else { + continue; + }; + let (Some(end_line), Some(end_offset)) = (location.end_line, location.end_line_offset) else { + continue; + }; + sites.entry(location.file.path.to_string()).or_default().push(CallSite { + start: (location.start_line as usize, location.start_line_offset as usize), + end: (end_line as usize, end_offset as usize), + target: target.fqn.as_str().to_string(), + }); + } + } + sites +} + +fn graph_facts(graph: &UdgGraph) -> HashMap { + graph + .items + .iter() + .map(|(fqn, item)| { + let mut edges: BTreeMap> = BTreeMap::new(); + for dependency in graph.get_downstream_dependencies(fqn.as_str()) { + let kind = serde_json::to_value(dependency.kind) + .ok() + .and_then(|k| k.as_str().map(str::to_string)) + .unwrap_or_default(); + if let Some((_, key)) = GRAPH_EDGES.iter().find(|(name, _)| *name == kind) { + let targets = edges.entry((*key).to_string()).or_default(); + let target = dependency.to_node.key().to_string(); + if !targets.contains(&target) { + targets.push(target); + } + } + } + let properties: Map = item + .properties + .iter() + .map(|p| { + let values: Vec = p.values.iter().map(|v| Value::String(v.value.to_string())).collect(); + (p.name.to_string(), Value::Array(values)) + }) + .collect(); + let mut object = Map::new(); + object.insert("kind".into(), serde_json::to_value(item.kind).unwrap_or(Value::Null)); + object.insert("tier".into(), serde_json::to_value(item.tier).unwrap_or(Value::Null)); + if let Some(parent) = &item.parent_item_fqn { + object.insert("parent".into(), Value::String(parent.as_str().to_string())); + } + if !properties.is_empty() { + object.insert("props".into(), Value::Object(properties)); + } + for (kind, targets) in edges { + object.insert(kind, json!(targets)); + } + (fqn.as_str().to_string(), Value::Object(object)) + }) + .collect() +} + +fn project_root(root: &Path, files: &[PathBuf]) -> PathBuf { + let root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); + if files.iter().all(|f| f.starts_with(&root)) { + return root; + } + let mut common = files.first().and_then(|f| f.parent()).map(Path::to_path_buf).unwrap_or(root); + for file in files { + while !file.starts_with(&common) { + if !common.pop() { + break; + } + } + } + common +} + +fn to_slash(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} diff --git a/semsitter-bridge/src/main.rs b/semsitter-bridge/src/main.rs new file mode 100644 index 00000000000..4c08abc83ce --- /dev/null +++ b/semsitter-bridge/src/main.rs @@ -0,0 +1,115 @@ +/* + * 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/ + */ + +use std::io::Write; +use std::path::PathBuf; + +use anyhow::{Result, anyhow, bail}; +use semsitter_bridge::server::{print_csts, serve_jsonl, serve_shm}; + +const USAGE: &str = "\ +usage: + semsitter-bridge serve [--shm | --jsonl] [--dump-json DIR] + semsitter-bridge cst --root DIR [--classpath CP] [--dump-json DIR] FILE... + semsitter-bridge parse [--dump-json DIR] FILE +CP uses the platform path separator. +--shm serves protobuf over shared memory, with one ping byte on stdin/stdout. +--jsonl is the line-delimited JSON server. +--dump-json writes udg.json, semantic-dump JSON and annotated CST JSON under DIR."; + +fn main() -> Result<()> { + let args: Vec = std::env::args().skip(1).collect(); + match args.first().map(String::as_str) { + None | Some("serve") => serve_command(&args[1..]), + Some("cst") => cst(&args[1..]), + Some("parse") => parse(&args[1..]), + Some("-h" | "--help") => { + println!("{USAGE}"); + Ok(()) + } + Some(other) => bail!("unknown command: {other}\n{USAGE}"), + } +} + +fn serve_command(args: &[String]) -> Result<()> { + let mut shm = false; + let mut dump = None; + let mut args = args.iter(); + while let Some(arg) = args.next() { + match arg.as_str() { + "--shm" => shm = true, + "--jsonl" => shm = false, + "--dump-json" => dump = Some(PathBuf::from(args.next().ok_or_else(|| anyhow!(USAGE))?)), + "-h" | "--help" => { + println!("{USAGE}"); + return Ok(()); + } + other => bail!("unknown argument: {other}\n{USAGE}"), + } + } + let stdin = std::io::stdin().lock(); + let stdout = std::io::stdout().lock(); + if shm { + serve_shm(stdin, stdout, dump) + } else { + serve_jsonl(stdin, stdout, dump) + } +} + +fn parse(args: &[String]) -> Result<()> { + let mut dump = None; + let mut file = None; + let mut args = args.iter(); + while let Some(arg) = args.next() { + match arg.as_str() { + "--dump-json" => dump = Some(PathBuf::from(args.next().ok_or_else(|| anyhow!(USAGE))?)), + other if file.is_none() => file = Some(other), + other => bail!("unknown argument: {other}\n{USAGE}"), + } + } + let file = file.ok_or_else(|| anyhow!(USAGE))?; + let cst = semsitter_bridge::parse_only(&std::fs::read(file)?)?; + if let Some(dir) = &dump { + std::fs::create_dir_all(dir)?; + std::fs::write(dir.join("parse.json"), serde_json::to_vec_pretty(&cst)?)?; + } + let mut stdout = std::io::stdout().lock(); + serde_json::to_writer(&mut stdout, &cst)?; + writeln!(stdout)?; + Ok(()) +} + +fn cst(args: &[String]) -> Result<()> { + let mut root = None; + let mut classpath = Vec::new(); + let mut dump = None; + let mut files = Vec::new(); + let mut args = args.iter(); + while let Some(arg) = args.next() { + match arg.as_str() { + "--root" => root = args.next().map(PathBuf::from), + "--classpath" => { + let value = args.next().ok_or_else(|| anyhow!(USAGE))?; + classpath.extend(std::env::split_paths(value).map(|p| p.to_string_lossy().into_owned())); + } + "--dump-json" => dump = Some(PathBuf::from(args.next().ok_or_else(|| anyhow!(USAGE))?)), + file => files.push(PathBuf::from(file)), + } + } + let root = root.ok_or_else(|| anyhow!(USAGE))?; + print_csts(&root, &files, &classpath, dump.as_deref(), std::io::stdout().lock()) +} diff --git a/semsitter-bridge/src/server.rs b/semsitter-bridge/src/server.rs new file mode 100644 index 00000000000..60c4348d102 --- /dev/null +++ b/semsitter-bridge/src/server.rs @@ -0,0 +1,231 @@ +/* + * 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/ + */ + +//! JSONL protocol: one request object per stdin line, one response object per stdout line. +//! +//! - `{"id":1,"op":"analyze","root":"/p","files":["/p/A.java"],"classpath":["/lib.jar"]}` +//! scans the batch and answers `{"id":1,"ok":true,"root":"/p","files":["A.java"]}`. +//! - `{"id":2,"op":"cst","path":"/p/A.java"}` answers `{"id":2,"ok":true,"cst":{...}}` with the +//! CST of a file of the last `analyze`. +//! - `{"id":3,"op":"types","names":["java.util.List"]}` answers `{"id":3,"ok":true,"types":{...}}` +//! with the declarations (kind, supertypes, members) of the named types and their supertypes. +//! - `{"id":4,"op":"parse","source":"class A {}"}` answers the CST without semantics. +//! - `{"id":5,"op":"quit"}` answers `{"id":5,"ok":true}` and stops the server. +//! +//! Failures answer `{"id":n,"ok":false,"error":"..."}` and keep the server running. + +use std::io::{BufRead, Read, Write}; +use std::path::{Path, PathBuf}; + +use anyhow::{Result, anyhow}; +use serde_json::{Map, Value, json}; + +use crate::shm::{ATTACHED, Link, READY}; +use crate::wire::{self, Request}; +use crate::{Analysis, analyze_dumping, parse_only}; + +pub struct Server { + analysis: Option, + dump: Option, + types_written: u32, +} + +impl Default for Server { + fn default() -> Self { + Self { analysis: None, dump: None, types_written: 0 } + } +} + +impl Server { + /// Answers one request line, and whether the server must stop. + pub fn handle(&mut self, line: &str) -> (Value, bool) { + let request: Value = match serde_json::from_str(line) { + Ok(request) => request, + Err(e) => return (json!({"ok": false, "error": format!("invalid request: {e}")}), false), + }; + let id = request.get("id").cloned().unwrap_or(Value::Null); + let op = request.get("op").and_then(Value::as_str).unwrap_or_default(); + let quit = op == "quit"; + let mut response = match self.dispatch(op, &request) { + Ok(body) => body, + Err(e) => json!({"ok": false, "error": format!("{e:#}")}), + }; + response.as_object_mut().expect("response is an object").insert("id".into(), id); + (response, quit) + } + + fn dispatch(&mut self, op: &str, request: &Value) -> Result { + match op { + "analyze" => { + let root = PathBuf::from(string(request, "root")?); + let files: Vec = strings(request, "files").into_iter().map(PathBuf::from).collect(); + let classpath = strings(request, "classpath"); + let analysis = analyze_dumping(&root, &files, &classpath, self.dump.as_deref())?; + let body = json!({ + "ok": true, + "root": analysis.root().to_string_lossy(), + "files": analysis.files(), + }); + self.analysis = Some(analysis); + Ok(body) + } + "cst" => { + let analysis = self.analysis.as_ref().ok_or_else(|| anyhow!("no analyze request yet"))?; + Ok(ok_with("cst", analysis.cst(string(request, "path")?)?)) + } + "types" => { + let analysis = self.analysis.as_ref().ok_or_else(|| anyhow!("no analyze request yet"))?; + let names = strings(request, "names"); + let types = analysis.types(&names); + self.write_types(&types)?; + Ok(ok_with("types", types)) + } + "parse" => { + let cst = parse_only(string(request, "source")?.as_bytes())?; + if let Some(dir) = &self.dump { + std::fs::create_dir_all(dir)?; + std::fs::write(dir.join("parse.json"), serde_json::to_vec_pretty(&cst)?)?; + } + Ok(ok_with("cst", cst)) + } + "quit" => Ok(json!({"ok": true})), + other => Err(anyhow!("unknown op: {other}")), + } + } +} + +impl Server { + fn write_types(&mut self, types: &Value) -> Result<()> { + let Some(dir) = &self.dump else { + return Ok(()); + }; + self.types_written += 1; + let target = dir.join("types").join(format!("{}.json", self.types_written)); + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(target, serde_json::to_vec_pretty(types)?)?; + Ok(()) + } + + fn handle_bytes(&mut self, bytes: &[u8]) -> (Vec, bool) { + let incoming = match wire::decode_request(bytes) { + Ok(incoming) => incoming, + Err(e) => return (wire::encode_response(0, &json!({"ok": false, "error": format!("{e:#}")})), false), + }; + let request = match &incoming.request { + Request::Analyze { root, files, classpath } => json!({ + "op": "analyze", + "root": root, + "files": files, + "classpath": classpath, + }), + Request::Cst { path } => json!({"op": "cst", "path": path}), + Request::Types { names } => json!({"op": "types", "names": names}), + Request::Parse { source } => json!({"op": "parse", "source": source}), + Request::Quit => json!({"op": "quit"}), + }; + let op = request.get("op").and_then(Value::as_str).unwrap_or_default(); + let quit = matches!(incoming.request, Request::Quit); + let mut response = match self.dispatch(op, &request) { + Ok(body) => body, + Err(e) => json!({"ok": false, "error": format!("{e:#}")}), + }; + response.as_object_mut().expect("response is an object").insert("id".into(), json!(incoming.id)); + (wire::encode_response(incoming.id, &response), quit) + } +} + +/// JSONL server. One request object per stdin line, one response object per stdout line. +pub fn serve(input: impl BufRead, output: impl Write) -> Result<()> { + serve_jsonl(input, output, None) +} + +pub fn serve_jsonl(input: impl BufRead, mut output: impl Write, dump: Option) -> Result<()> { + let mut server = Server { dump, ..Server::default() }; + for line in input.lines() { + let line = line?; + if line.trim().is_empty() { + continue; + } + let (response, quit) = server.handle(&line); + serde_json::to_writer(&mut output, &response)?; + output.write_all(b"\n")?; + output.flush()?; + if quit { + break; + } + } + Ok(()) +} + +/// Shared-memory server. Writes [`READY`] once the mappings exist, reads [`ATTACHED`], +/// then exchanges protobuf messages. Each message is announced by a single byte on the pipe. +pub fn serve_shm(mut input: impl Read, mut output: impl Write, dump: Option) -> Result<()> { + let id = std::process::id().to_string(); + let mut link = Link::create(&id)?; + output.write_all(&[READY])?; + output.flush()?; + let mut attached = [0u8; 1]; + input.read_exact(&mut attached)?; + if attached[0] != ATTACHED { + return Err(anyhow!("expected the peer to attach the shared mappings")); + } + link.unlink(); + let mut server = Server { dump, ..Server::default() }; + loop { + let request = link.read_request(&mut input, &mut output)?; + let (response, quit) = server.handle_bytes(&request); + link.write_response(&response, &mut output, &mut input)?; + if quit { + break; + } + } + Ok(()) +} + +/// CSTs of `files` (with semantics), one JSON object per line. +pub fn print_csts(root: &Path, files: &[PathBuf], classpath: &[String], dump: Option<&Path>, mut output: impl Write) -> Result<()> { + let analysis = analyze_dumping(root, files, classpath, dump)?; + for file in analysis.files() { + serde_json::to_writer(&mut output, &analysis.cst(file)?)?; + output.write_all(b"\n")?; + } + output.flush()?; + Ok(()) +} + +fn ok_with(key: &str, value: Value) -> Value { + let mut body = Map::new(); + body.insert("ok".into(), Value::Bool(true)); + body.insert(key.into(), value); + Value::Object(body) +} + +fn string<'a>(request: &'a Value, key: &str) -> Result<&'a str> { + request.get(key).and_then(Value::as_str).ok_or_else(|| anyhow!("missing string field: {key}")) +} + +fn strings(request: &Value, key: &str) -> Vec { + request + .get(key) + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect() +} diff --git a/semsitter-bridge/src/shm.rs b/semsitter-bridge/src/shm.rs new file mode 100644 index 00000000000..d059a855c0c --- /dev/null +++ b/semsitter-bridge/src/shm.rs @@ -0,0 +1,234 @@ +/* + * 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/ + */ + +//! Two shared mappings and a one-byte ping on the pipe. +//! +//! The payload never travels on the pipe. `1` means the mapping holds a protobuf message, +//! `2` means the peer grew it and this side must remap. The directory entry is removed once +//! both sides have mapped, so nothing is left to read as a file. On Linux the files live in +//! `/dev/shm`. + +use std::fs::OpenOptions; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{Ordering, fence}; + +use anyhow::{Result, bail}; +use memmap2::MmapMut; + +pub const READY: u8 = b'R'; +pub const ATTACHED: u8 = b'A'; +const DATA: u8 = 1; +const GROW: u8 = 2; +const INITIAL: usize = 32 * 1024 * 1024; +const MAX: usize = 512 * 1024 * 1024; +const HEADER: usize = 24; +const MAGIC: u32 = 0x3142_4A53; + +pub struct Link { + id: String, + request: Slot, + response: Slot, + created: bool, +} + +struct Slot { + path: PathBuf, + file: std::fs::File, + map: MmapMut, +} + +impl Link { + pub fn create(id: &str) -> Result { + Ok(Link { + request: Slot::create(&slot_path(id, "q"), INITIAL)?, + response: Slot::create(&slot_path(id, "a"), INITIAL)?, + id: id.to_string(), + created: true, + }) + } + + pub fn open(id: &str) -> Result { + Ok(Link { + request: Slot::open(&slot_path(id, "q"))?, + response: Slot::open(&slot_path(id, "a"))?, + id: id.to_string(), + created: false, + }) + } + + pub fn id(&self) -> &str { + &self.id + } + + /// Drops the directory entries. Mappings stay valid. + pub fn unlink(&self) { + let _ = std::fs::remove_file(slot_path(&self.id, "q")); + let _ = std::fs::remove_file(slot_path(&self.id, "a")); + } + + pub fn write_request(&mut self, bytes: &[u8], to_peer: &mut impl Write, from_peer: &mut impl Read) -> Result<()> { + self.request.write(bytes, to_peer, from_peer) + } + + pub fn read_request(&mut self, from_peer: &mut impl Read, to_peer: &mut impl Write) -> Result> { + self.request.read(from_peer, to_peer) + } + + pub fn write_response(&mut self, bytes: &[u8], to_peer: &mut impl Write, from_peer: &mut impl Read) -> Result<()> { + self.response.write(bytes, to_peer, from_peer) + } + + pub fn read_response(&mut self, from_peer: &mut impl Read, to_peer: &mut impl Write) -> Result> { + self.response.read(from_peer, to_peer) + } +} + +impl Drop for Link { + fn drop(&mut self) { + if self.created { + self.unlink(); + } + } +} + +impl Slot { + fn create(path: &Path, len: usize) -> Result { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = OpenOptions::new().read(true).write(true).create(true).truncate(true).open(path)?; + file.set_len(len as u64)?; + let mut slot = Slot { + path: path.to_path_buf(), + map: unsafe { MmapMut::map_mut(&file)? }, + file, + }; + slot.put_header(0, 0, len as u64); + Ok(slot) + } + + fn open(path: &Path) -> Result { + let file = OpenOptions::new().read(true).write(true).open(path)?; + let len = file.metadata()?.len() as usize; + Ok(Slot { + path: path.to_path_buf(), + map: unsafe { MmapMut::map_mut(&file)? }, + file, + }) + .and_then(|slot| { + if slot.map.len() != len { + bail!("mapping length mismatch for {}", path.display()); + } + Ok(slot) + }) + } + + fn write(&mut self, bytes: &[u8], to_peer: &mut impl Write, from_peer: &mut impl Read) -> Result<()> { + let need = HEADER + bytes.len(); + if need > self.map.len() { + let mut cap = self.map.len().max(INITIAL); + while cap < need { + if cap >= MAX { + bail!("protobuf message of {} bytes exceeds the shared mapping", bytes.len()); + } + cap = cap.saturating_mul(2).min(MAX); + } + self.file.set_len(cap as u64)?; + self.put_header(GROW as u32, cap as u64, cap as u64); + fence(Ordering::Release); + ping(to_peer, GROW)?; + let ack = read_ping(from_peer)?; + if ack != GROW { + bail!("shared mapping grow was not acknowledged"); + } + self.remap(cap)?; + } + self.map[HEADER..HEADER + bytes.len()].copy_from_slice(bytes); + self.put_header(0, bytes.len() as u64, self.map.len() as u64); + fence(Ordering::Release); + ping(to_peer, DATA)?; + Ok(()) + } + + fn read(&mut self, from_peer: &mut impl Read, to_peer: &mut impl Write) -> Result> { + loop { + match read_ping(from_peer)? { + GROW => { + let cap = self.header_len()? as usize; + self.file.set_len(cap as u64)?; + self.remap(cap)?; + fence(Ordering::Release); + ping(to_peer, GROW)?; + } + DATA => { + fence(Ordering::Acquire); + let len = self.header_len()? as usize; + if HEADER + len > self.map.len() { + bail!("payload of {len} bytes does not fit the shared mapping"); + } + return Ok(self.map[HEADER..HEADER + len].to_vec()); + } + other => bail!("unexpected shared-memory ping {other}"), + } + } + } + + fn remap(&mut self, len: usize) -> Result<()> { + self.map = unsafe { MmapMut::map_mut(&self.file)? }; + if self.map.len() < len { + bail!("remapped {} is shorter than {len}", self.path.display()); + } + Ok(()) + } + + fn put_header(&mut self, kind: u32, len: u64, cap: u64) { + self.map[0..4].copy_from_slice(&MAGIC.to_le_bytes()); + self.map[4..8].copy_from_slice(&kind.to_le_bytes()); + self.map[8..16].copy_from_slice(&len.to_le_bytes()); + self.map[16..24].copy_from_slice(&cap.to_le_bytes()); + } + + fn header_len(&self) -> Result { + let magic = u32::from_le_bytes(self.map[0..4].try_into().unwrap()); + if magic != MAGIC { + bail!("shared mapping has no bridge header"); + } + Ok(u64::from_le_bytes(self.map[8..16].try_into().unwrap())) + } +} + +fn ping(to_peer: &mut impl Write, byte: u8) -> Result<()> { + to_peer.write_all(&[byte])?; + to_peer.flush()?; + Ok(()) +} + +fn read_ping(from_peer: &mut impl Read) -> Result { + let mut byte = [0u8; 1]; + from_peer.read_exact(&mut byte)?; + Ok(byte[0]) +} + +pub fn slot_dir() -> PathBuf { + let shm = PathBuf::from("/dev/shm"); + if shm.is_dir() { shm } else { std::env::temp_dir() } +} + +fn slot_path(id: &str, which: &str) -> PathBuf { + slot_dir().join(format!("sjb-{id}-{which}")) +} diff --git a/semsitter-bridge/src/types.rs b/semsitter-bridge/src/types.rs new file mode 100644 index 00000000000..abff01b97c0 --- /dev/null +++ b/semsitter-bridge/src/types.rs @@ -0,0 +1,621 @@ +/* + * 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/ + */ + +//! Type declarations by erased binary name (`java.util.Map$Entry`), from the first source that +//! knows the type: the scanned project graph, the classpath index (jars and the JDK runtime +//! image), then the JDK table SemSitter embeds. + +use std::collections::{BTreeSet, HashMap, VecDeque}; + +use serde_json::{Map, Value, json}; +use udg_family_jvm::external::classpath::{ + Annotation, ClasspathDeclaration, ClasspathIndex, ClasspathType, ConstantValue, ElementValue, +}; +use udg_family_jvm::external::jdk_table::{JdkMemberKind, JdkType, jdk_type, parameter_type_variables}; + +const ACC_STATIC: u16 = 0x0008; +const ACC_FINAL: u16 = 0x0010; +const ACC_PUBLIC: u16 = 0x0001; +const ACC_PRIVATE: u16 = 0x0002; +const ACC_PROTECTED: u16 = 0x0004; +const ACC_ENUM: u16 = 0x4000; +const ACC_ABSTRACT: u16 = 0x0400; + +/// Strips the generic arity suffix of a graph id segment: ``java.util.List`1`` is `java.util.List`. +pub fn erase(id: &str) -> String { + let mut out = String::with_capacity(id.len()); + let mut chars = id.chars().peekable(); + while let Some(c) = chars.next() { + if c == '`' { + while chars.peek().is_some_and(char::is_ascii_digit) { + chars.next(); + } + } else { + out.push(c); + } + } + out +} + +/// Project types of the graph: erased name to graph id, and graph id to member ids. +#[derive(Default)] +pub struct ProjectTypes { + pub keys: HashMap, + pub members: HashMap>, +} + +impl ProjectTypes { + pub fn new(facts: &HashMap) -> Self { + let mut types = ProjectTypes::default(); + for (id, fact) in facts { + if fact.get("tier").and_then(Value::as_str) != Some("IN_PROJECT") { + continue; + } + if let Some(parent) = fact.get("parent").and_then(Value::as_str) { + types.members.entry(parent.to_string()).or_default().push(id.clone()); + } + if !id.contains('#') && is_type_kind(fact.get("kind").and_then(Value::as_str).unwrap_or_default()) { + types.keys.insert(erase(id), id.clone()); + } + } + for members in types.members.values_mut() { + members.sort(); + } + types + } +} + +fn is_type_kind(kind: &str) -> bool { + matches!(kind, "class" | "interface" | "enum" | "record" | "annotation" | "exception") +} + +/// Declarations of `names` and of all their supertypes. +pub fn lookup( + names: &[String], + project: &ProjectTypes, + facts: &HashMap, + classpath: &ClasspathIndex, +) -> Map { + let mut found = Map::new(); + let mut queue: VecDeque = names.iter().map(|n| erase(n)).collect(); + let mut seen: BTreeSet = BTreeSet::new(); + while let Some(name) = queue.pop_front() { + if !seen.insert(name.clone()) { + continue; + } + let info = project_type(&name, project, facts) + .or_else(|| classpath.get(&name).map(|ty| classpath_type(&name, ty, classpath.declaration(&name)))) + .or_else(|| jdk_type(&name).map(jdk_type_info)); + let Some(info) = info else { + continue; + }; + for key in ["super", "interfaces"] { + for supertype in strings(&info, key) { + queue.push_back(erase(&supertype)); + } + } + found.insert(name, info); + } + found +} + +fn strings(value: &Value, key: &str) -> Vec { + match value.get(key) { + Some(Value::String(s)) => vec![s.clone()], + Some(Value::Array(items)) => items.iter().filter_map(|i| i.as_str().map(str::to_string)).collect(), + _ => Vec::new(), + } +} + +fn project_type(name: &str, project: &ProjectTypes, facts: &HashMap) -> Option { + let id = project.keys.get(name)?; + let fact = facts.get(id)?; + let kind = fact.get("kind").and_then(Value::as_str).unwrap_or("class"); + let extends = strings(fact, "extends"); + let implements = strings(fact, "implements"); + let (superclass, interfaces) = if kind == "interface" || kind == "annotation" { + (None, extends) + } else { + (extends.into_iter().next(), implements) + }; + let members: Vec = project + .members + .get(id) + .into_iter() + .flatten() + .filter_map(|member| facts.get(member).map(|f| json!({"id": member, "facts": f}))) + .collect(); + Some(json!({ + "origin": "project", + "id": id, + "kind": kind, + "super": superclass, + "interfaces": interfaces, + "modifiers": fact.pointer("/props/modifiers").cloned().unwrap_or_else(|| json!([])), + "members": members, + })) +} + +fn classpath_type(name: &str, ty: &ClasspathType, declaration: Option<&ClasspathDeclaration>) -> Value { + let parameters = ty.signature.as_deref().map(type_parameters).unwrap_or_default(); + let no_annotations: &[Annotation] = &[]; + let no_throws: &[String] = &[]; + let mut members: Vec = ty + .methods + .iter() + .map(|m| { + let returned = m.signature.as_deref().and_then(|s| class_return_parameter(s, ¶meters)); + let declared = declaration.and_then(|facts| facts.methods.get(&(m.name.clone(), m.descriptor.clone()))); + json!({ + "k": "m", + "name": m.name, + "desc": m.descriptor, + "static": m.is_static, + "abstract": m.is_abstract, + "varargs": m.is_varargs, + "vis": "public", + "retParam": returned, + "retSelf": returned.is_some_and(|index| parameters[index].1 == name), + "retType": m.signature.as_deref().and_then(return_type), + "paramVars": m.signature.as_deref().map(|s| parameter_variables(s, ¶meters)), + "paramTypes": m.signature.as_deref().map(parameter_types), + "typeVars": m.signature.as_deref().map(|s| type_parameters(s).into_iter().map(|(name, _)| name).collect::>()), + "annotations": annotations_json(declared.map(|facts| facts.annotations.as_slice()).unwrap_or(no_annotations)), + "throws": declared.map(|facts| facts.exceptions.as_slice()).unwrap_or(no_throws), + }) + }) + .collect(); + members.extend(ty.fields.iter().filter_map(|f| { + let desc = field_descriptor(f.declared_type.as_ref()?); + let declared = declaration.and_then(|facts| facts.fields.get(&f.name)); + Some(json!({ + "k": "f", + "name": f.name, + "desc": desc, + "static": f.access_flags & ACC_STATIC != 0, + "final": f.access_flags & ACC_FINAL != 0, + "enum": f.access_flags & ACC_ENUM != 0, + "vis": access_visibility(f.access_flags), + "constant": f.constant.as_ref().and_then(ConstantValue::as_string), + "annotations": annotations_json(declared.map(|facts| facts.annotations.as_slice()).unwrap_or(no_annotations)), + })) + })); + let flags = declaration.map(|facts| facts.access_flags).unwrap_or(0); + let mut info = json!({ + "origin": "classpath", + "kind": classpath_kind(ty), + "super": ty.super_class, + "interfaces": ty.interfaces, + "modifiers": classpath_modifiers(flags), + "annotations": annotations_json(declaration.map(|facts| facts.annotations.as_slice()).unwrap_or(no_annotations)), + "typeParameters": parameters.len(), + "typeVars": parameters.iter().map(|(name, _)| name).collect::>(), + "members": members, + }); + if let (Some(signature), Some(object)) = (ty.signature.as_deref(), info.as_object_mut()) { + let (superclass, interfaces) = signature_supertypes(signature); + object.insert("superType".into(), json!(superclass)); + object.insert("interfaceTypes".into(), json!(interfaces)); + } + info +} + +/// The JVM descriptor of a classpath field type, which the classpath reader spells as a source +/// type: a primitive or a binary name, followed by one `[]` per array dimension. +fn field_descriptor(source: &str) -> String { + let element = source.trim_end_matches("[]"); + let dimensions = (source.len() - element.len()) / 2; + let base = match element { + "boolean" => "Z".to_string(), + "byte" => "B".to_string(), + "char" => "C".to_string(), + "short" => "S".to_string(), + "int" => "I".to_string(), + "long" => "J".to_string(), + "float" => "F".to_string(), + "double" => "D".to_string(), + name => format!("L{};", name.replace('.', "/")), + }; + format!("{}{base}", "[".repeat(dimensions)) +} + +fn annotations_json(annotations: &[Annotation]) -> Value { + Value::Array(annotations.iter().map(annotation_json).collect()) +} + +fn annotation_json(annotation: &Annotation) -> Value { + json!({ + "name": annotation.name, + "values": annotation.elements.iter().map(|(name, value)| element_json(name, value)).collect::>(), + }) +} + +fn element_json(name: &str, value: &ElementValue) -> Value { + match value { + ElementValue::Constant(ConstantValue::Boolean(value)) => json!({"name": name, "kind": "boolean", "value": value}), + ElementValue::Constant(ConstantValue::Byte(value)) => json!({"name": name, "kind": "byte", "value": value}), + ElementValue::Constant(ConstantValue::Char(value)) => json!({"name": name, "kind": "char", "value": value}), + ElementValue::Constant(ConstantValue::Short(value)) => json!({"name": name, "kind": "short", "value": value}), + ElementValue::Constant(ConstantValue::Int(value)) => json!({"name": name, "kind": "int", "value": value}), + ElementValue::Constant(ConstantValue::Long(value)) => json!({"name": name, "kind": "long", "value": value}), + ElementValue::Constant(ConstantValue::Float(value)) => json!({"name": name, "kind": "float", "value": value}), + ElementValue::Constant(ConstantValue::Double(value)) => json!({"name": name, "kind": "double", "value": value}), + ElementValue::Constant(ConstantValue::String(value)) => json!({"name": name, "kind": "string", "value": value}), + ElementValue::Class(value) => json!({"name": name, "kind": "class", "value": value}), + ElementValue::Enum { type_name, constant } => json!({"name": name, "kind": "enum", "type": type_name, "const": constant}), + ElementValue::Annotation(annotation) => json!({"name": name, "kind": "annotation", "value": annotation_json(annotation)}), + ElementValue::Array(items) => json!({ + "name": name, + "kind": "array", + "value": items.iter().map(|item| element_json("value", item)).collect::>(), + }), + } +} + +/// Class-file access flags as the modifier names `JSema` already applies to JDK types. +fn classpath_modifiers(flags: u16) -> Vec<&'static str> { + let mut modifiers = Vec::new(); + if flags & ACC_PUBLIC != 0 { + modifiers.push("public"); + } else if flags & ACC_PROTECTED != 0 { + modifiers.push("protected"); + } else if flags & ACC_PRIVATE != 0 { + modifiers.push("private"); + } + if flags & ACC_STATIC != 0 { + modifiers.push("static"); + } + if flags & ACC_FINAL != 0 { + modifiers.push("final"); + } + if flags & ACC_ABSTRACT != 0 { + modifiers.push("abstract"); + } + modifiers +} + +/// The classfile reader only distinguishes classes from interfaces. A record is a class whose superclass is `java.lang.Record`. +fn classpath_kind(ty: &ClasspathType) -> Value { + if ty.super_class.as_deref() == Some("java.lang.Record") { + return json!("record"); + } + serde_json::to_value(ty.kind).unwrap_or(Value::Null) +} + +/// Type parameters of a generic signature, `...`, with the erased dotted +/// class of their first bound. +fn type_parameters(signature: &str) -> Vec<(String, String)> { + let bytes = signature.as_bytes(); + let mut out = Vec::new(); + if bytes.first() != Some(&b'<') { + return out; + } + let mut i = 1; + while i < bytes.len() && bytes[i] != b'>' { + let start = i; + while i < bytes.len() && bytes[i] != b':' { + i += 1; + } + let name = signature[start..i].to_string(); + let mut bound = String::new(); + while i < bytes.len() && bytes[i] == b':' { + i += 1; + if i < bytes.len() && bytes[i] != b':' && bytes[i] != b'>' { + let begin = i; + i = skip_type(bytes, i); + if bound.is_empty() { + bound = erased_class(&signature[begin..i]); + } + } + } + out.push((name, bound)); + } + out +} + +fn skip_type(bytes: &[u8], mut i: usize) -> usize { + match bytes.get(i) { + Some(b'[') => skip_type(bytes, i + 1), + Some(b'L') => { + let mut depth = 0; + while i < bytes.len() { + match bytes[i] { + b'<' => depth += 1, + b'>' => depth -= 1, + b';' if depth == 0 => return i + 1, + _ => {} + } + i += 1; + } + i + } + Some(b'T') => bytes[i..].iter().position(|&b| b == b';').map_or(bytes.len(), |end| i + end + 1), + _ => i + 1, + } +} + +/// The generic superclass and interfaces of a class `Signature`, as type facts (`id`, `args`, +/// `kind` `typevar`/`array`/`wildcard`). +fn signature_supertypes(signature: &str) -> (Option, Vec) { + let bytes = signature.as_bytes(); + let mut i = 0; + if bytes.first() == Some(&b'<') { + let mut depth = 0; + while i < bytes.len() { + match bytes[i] { + b'<' => depth += 1, + b'>' => depth -= 1, + _ => {} + } + i += 1; + if depth == 0 { + break; + } + } + } + let superclass = (i < bytes.len()).then(|| signature_type(signature, &mut i)); + let mut interfaces = Vec::new(); + while i < bytes.len() { + interfaces.push(signature_type(signature, &mut i)); + } + (superclass, interfaces) +} + +/// One JVM type signature starting at `i`, which is moved past it. +fn signature_type(signature: &str, i: &mut usize) -> Value { + let bytes = signature.as_bytes(); + let Some(&head) = bytes.get(*i) else { + return json!({"kind": "unknown"}); + }; + *i += 1; + match head { + b'[' => json!({"kind": "array", "elem": signature_type(signature, i)}), + b'T' => { + let end = signature[*i..].find(';').map_or(bytes.len(), |e| *i + e); + let name = &signature[*i..end]; + *i = end + 1; + json!({"kind": "typevar", "id": name}) + } + b'*' => json!({"kind": "wildcard"}), + b'+' => json!({"kind": "wildcard", "extends": signature_type(signature, i)}), + b'-' => json!({"kind": "wildcard", "super": signature_type(signature, i)}), + b'L' => { + let mut name = Vec::new(); + let mut args = Vec::new(); + while *i < bytes.len() { + match bytes[*i] { + b';' => { + *i += 1; + break; + } + b'<' => { + *i += 1; + args.clear(); + while *i < bytes.len() && bytes[*i] != b'>' { + args.push(signature_type(signature, i)); + } + *i += 1; + } + b'.' => { + name.push(b'$'); + *i += 1; + } + b'/' => { + name.push(b'.'); + *i += 1; + } + c => { + name.push(c); + *i += 1; + } + } + } + json!({"id": String::from_utf8_lossy(&name), "args": args}) + } + primitive => json!({"id": primitive_name(primitive)}), + } +} + +fn primitive_name(code: u8) -> &'static str { + match code { + b'B' => "byte", + b'C' => "char", + b'D' => "double", + b'F' => "float", + b'I' => "int", + b'J' => "long", + b'S' => "short", + b'Z' => "boolean", + _ => "void", + } +} + +fn erased_class(signature: &str) -> String { + signature + .strip_prefix('L') + .map(|rest| rest.split(['<', ';']).next().unwrap_or_default().replace('/', ".")) + .unwrap_or_default() +} + +/// The JDK table's class and method parameter type variables, in the shape of [`parameter_variables`]. +fn jdk_parameter_variables(class: Option<&str>, method: Option<&str>) -> Option> { + if class.is_none() && method.is_none() { + return None; + } + let class = class.map(parameter_type_variables).unwrap_or_default(); + let method = method.map(parameter_type_variables).unwrap_or_default(); + let slot = |prefix: char, slots: &[Option<(usize, &str)>], i: usize| { + slots.get(i).copied().flatten().map(|(index, array)| format!("{prefix}{index}{array}")) + }; + Some( + (0..class.len().max(method.len())) + .map(|i| slot('c', &class, i).or_else(|| slot('m', &method, i)).unwrap_or_default()) + .collect(), + ) +} + +/// Per parameter of a method signature, the type variable it is: `c1` for the owner's second type +/// parameter, `m0[]` for an array of the method's first one, or empty. +fn parameter_variables(signature: &str, parameters: &[(String, String)]) -> Vec { + let own = type_parameters(signature); + let bytes = signature.as_bytes(); + let Some(open) = signature.find('(') else { + return Vec::new(); + }; + let mut out = Vec::new(); + let mut i = open + 1; + while i < bytes.len() && bytes[i] != b')' { + let end = skip_type(bytes, i); + let parameter = &signature[i..end]; + let element = parameter.trim_start_matches('['); + let array = "[]".repeat(parameter.len() - element.len()); + let variable = element.strip_prefix('T').and_then(|v| v.strip_suffix(';')); + out.push(match variable { + Some(name) => match own.iter().position(|(p, _)| p == name) { + Some(index) => format!("m{index}{array}"), + None => parameters.iter().position(|(p, _)| p == name).map_or_else(String::new, |index| format!("c{index}{array}")), + }, + None => String::new(), + }); + i = end; + } + out +} + +/// The return type of a method `Signature`, keeping the type variables it is written in terms of. +fn return_type(signature: &str) -> Option { + let close = signature.rfind(')')?; + let mut i = close + 1; + if i >= signature.len() { + return None; + } + Some(signature_type(signature, &mut i)) +} + +/// The declared parameter types of a method `Signature`, as type facts keeping the type variables +/// they are written in terms of, so that the caller can substitute them for the receiver's arguments. +fn parameter_types(signature: &str) -> Vec { + let bytes = signature.as_bytes(); + let Some(open) = signature.find('(') else { + return Vec::new(); + }; + let mut i = open + 1; + let mut out = Vec::new(); + while i < bytes.len() && bytes[i] != b')' { + out.push(signature_type(signature, &mut i)); + } + out +} + +/// Index of the class type parameter a method signature returns, when it returns one directly. +fn class_return_parameter(signature: &str, parameters: &[(String, String)]) -> Option { + let returned = signature[signature.rfind(')')? + 1..].strip_prefix('T')?; + let name = &returned[..returned.find(';')?]; + if type_parameters(signature).iter().any(|(own, _)| own == name) { + return None; + } + parameters.iter().position(|(parameter, _)| parameter == name) +} + +fn access_visibility(flags: u16) -> &'static str { + if flags & ACC_PUBLIC != 0 { + "public" + } else if flags & ACC_PROTECTED != 0 { + "protected" + } else if flags & ACC_PRIVATE != 0 { + "private" + } else { + "package" + } +} + +fn jdk_type_info(ty: &JdkType) -> Value { + let is_enum = serde_json::to_value(ty.kind).ok().and_then(|k| k.as_str().map(|k| k == "enum")).unwrap_or(false); + let own_descriptor = format!("L{};", ty.fqn.replace('.', "/")); + let members: Vec = ty + .members + .iter() + .map(|m| { + let constant = is_enum && m.kind == JdkMemberKind::Field && m.is_static && m.is_final && m.descriptor == own_descriptor; + json!({ + "enum": constant, + "k": if m.kind == JdkMemberKind::Method { "m" } else { "f" }, + "name": m.name, + "desc": m.descriptor, + "static": m.is_static, + "final": m.is_final, + "abstract": m.is_abstract, + "vis": format!("{:?}", m.visibility).to_lowercase(), + "retParam": m.return_type_class_param_index, + "paramVars": jdk_parameter_variables(m.parameter_type_variables, m.parameter_method_type_variables), + }) + }) + .collect(); + let mut modifiers = Vec::new(); + if ty.is_public { + modifiers.push("public"); + } + if ty.is_abstract { + modifiers.push("abstract"); + } + if ty.is_sealed { + modifiers.push("sealed"); + } + if is_enum { + modifiers.push("final"); + } + json!({ + "origin": "jdk", + "kind": serde_json::to_value(ty.kind).unwrap_or(Value::Null), + "super": ty.super_type, + "interfaces": ty.interfaces, + "modifiers": modifiers, + "typeParameters": ty.type_parameters, + "members": members, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn field_descriptors_round_trip_source_spellings() { + assert_eq!(field_descriptor("java.io.PrintStream"), "Ljava/io/PrintStream;"); + assert_eq!(field_descriptor("int"), "I"); + assert_eq!(field_descriptor("java.util.Map$Entry[][]"), "[[Ljava/util/Map$Entry;"); + assert_eq!(field_descriptor("long[]"), "[J"); + } + + #[test] + fn supertypes_keep_type_arguments_and_type_variables() { + let (superclass, interfaces) = signature_supertypes( + "Ljava/util/AbstractList;Ljava/util/List<[TE;>;Ljava/util/Map$Entry<*+Ljava/lang/Number;>;", + ); + assert_eq!(superclass, Some(json!({"id": "java.util.AbstractList", "args": [{"kind": "typevar", "id": "E"}]}))); + assert_eq!( + interfaces, + vec![ + json!({"id": "java.util.List", "args": [{"kind": "array", "elem": {"kind": "typevar", "id": "E"}}]}), + json!({"id": "java.util.Map$Entry", "args": [{"kind": "wildcard"}, {"kind": "wildcard", "extends": {"id": "java.lang.Number", "args": []}}]}), + ] + ); + // A member of a generic outer class keeps only its own arguments. + let (superclass, _) = signature_supertypes("Lp/Outer.Inner;"); + assert_eq!(superclass, Some(json!({"id": "p.Outer$Inner", "args": [{"id": "java.lang.String", "args": []}]}))); + } +} diff --git a/semsitter-bridge/src/wire.rs b/semsitter-bridge/src/wire.rs new file mode 100644 index 00000000000..a067a915ab3 --- /dev/null +++ b/semsitter-bridge/src/wire.rs @@ -0,0 +1,593 @@ +/* + * 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/ + */ + +//! Protobuf codec for [`super::proto`](../../proto/bridge.proto). The messages are the only +//! bytes that cross the process boundary; JSON stays a debug dump. + +use anyhow::{Result, anyhow}; +use serde_json::{Map, Value, json}; + +pub enum Request { + Analyze { root: String, files: Vec, classpath: Vec }, + Cst { path: String }, + Types { names: Vec }, + Parse { source: String }, + Quit, +} + +pub struct Incoming { + pub id: u32, + pub request: Request, +} + +pub fn decode_request(bytes: &[u8]) -> Result { + let mut id = 0u32; + let mut analyze = None; + let mut cst = None; + let mut types = None; + let mut parse = None; + let mut quit = false; + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Varint(v)) => id = v as u32, + (2, Wire::Bytes(b)) => analyze = Some(decode_analyze(&b)?), + (3, Wire::Bytes(b)) => cst = Some(decode_path(&b)?), + (4, Wire::Bytes(b)) => types = Some(decode_names(&b)?), + (5, Wire::Bytes(b)) => parse = Some(decode_source(&b)?), + (6, Wire::Varint(v)) => quit = v != 0, + _ => {} + } + } + let request = if let Some((root, files, classpath)) = analyze { + Request::Analyze { root, files, classpath } + } else if let Some(path) = cst { + Request::Cst { path } + } else if let Some(names) = types { + Request::Types { names } + } else if let Some(source) = parse { + Request::Parse { source } + } else if quit { + Request::Quit + } else { + return Err(anyhow!("request has no op")); + }; + Ok(Incoming { id, request }) +} + +pub fn encode_response(id: u32, body: &Value) -> Vec { + let mut out = Vec::new(); + varint_field(&mut out, 1, id as u64); + let ok = body.get("ok").and_then(Value::as_bool).unwrap_or(false); + if ok { + varint_field(&mut out, 2, 1); + } + if let Some(error) = body.get("error").and_then(Value::as_str) { + string_field(&mut out, 3, error); + } + if let Some(root) = body.get("root").and_then(Value::as_str) { + let mut msg = Vec::new(); + string_field(&mut msg, 1, root); + for file in strings(body, "files") { + string_field(&mut msg, 2, &file); + } + message_field(&mut out, 4, &msg); + } + if let Some(document) = body.get("cst") { + message_field(&mut out, 5, &encode_document(document)); + } + if let Some(types) = body.get("types") { + if let Ok(json) = serde_json::to_string(types) { + string_field(&mut out, 6, &json); + } + } + out +} + +pub fn decode_response(bytes: &[u8]) -> Result { + let mut id = Value::Null; + let mut ok = false; + let mut error = None; + let mut analyze = None; + let mut document = None; + let mut types = None; + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Varint(v)) => id = json!(v), + (2, Wire::Varint(v)) => ok = v != 0, + (3, Wire::Bytes(b)) => error = Some(String::from_utf8_lossy(&b).into_owned()), + (4, Wire::Bytes(b)) => analyze = Some(decode_analyze_result(&b)?), + (5, Wire::Bytes(b)) => document = Some(decode_document(&b)?), + (6, Wire::Bytes(b)) => types = Some(serde_json::from_slice(&b).unwrap_or(Value::Null)), + _ => {} + } + } + let mut object = Map::new(); + object.insert("id".into(), id); + object.insert("ok".into(), Value::Bool(ok)); + if let Some(error) = error { + object.insert("error".into(), Value::String(error)); + } + if let Some((root, files)) = analyze { + object.insert("root".into(), Value::String(root)); + object.insert("files".into(), json!(files)); + } + if let Some(document) = document { + object.insert("cst".into(), document); + } + if let Some(types) = types { + object.insert("types".into(), types); + } + Ok(Value::Object(object)) +} + +fn encode_document(document: &Value) -> Vec { + let mut out = Vec::new(); + if document.get("hasError").and_then(Value::as_bool) == Some(true) { + varint_field(&mut out, 1, 1); + } + if let Some(tree) = document.get("tree") { + message_field(&mut out, 2, &encode_node(tree)); + } + if let Some(path) = document.get("path").and_then(Value::as_str) { + string_field(&mut out, 3, path); + } + if document.get("semantic").and_then(Value::as_bool) == Some(true) { + varint_field(&mut out, 4, 1); + } + if let Some(symbols) = document.get("symbols") { + if let Ok(json) = serde_json::to_string(symbols) { + string_field(&mut out, 5, &json); + } + } + if let Some(facts) = document.get("facts").and_then(Value::as_object) { + for (key, fact) in facts { + let mut entry = Vec::new(); + string_field(&mut entry, 1, key); + message_field(&mut entry, 2, &encode_fact(fact)); + message_field(&mut out, 6, &entry); + } + } + out +} + +fn encode_fact(fact: &Value) -> Vec { + let mut out = Vec::new(); + if let Some(kind) = fact.get("kind").and_then(Value::as_str) { + string_field(&mut out, 1, kind); + } + if let Some(tier) = fact.get("tier").and_then(Value::as_str) { + string_field(&mut out, 2, tier); + } + if let Some(parent) = fact.get("parent").and_then(Value::as_str) { + string_field(&mut out, 3, parent); + } + if let Some(props) = fact.get("props").and_then(Value::as_object) { + for (name, values) in props { + let mut prop = Vec::new(); + string_field(&mut prop, 1, name); + for value in values.as_array().into_iter().flatten().filter_map(Value::as_str) { + string_field(&mut prop, 2, value); + } + message_field(&mut out, 4, &prop); + } + } + for (field, key) in [(5, "extends"), (6, "implements"), (7, "permits"), (8, "returns"), (9, "type"), (10, "params"), (11, "overrides"), (12, "annotations")] { + for value in strings(fact, key) { + string_field(&mut out, field, &value); + } + } + out +} + +fn encode_node(node: &Value) -> Vec { + let mut out = Vec::new(); + if let Some(kind) = node.get("k").and_then(Value::as_str) { + string_field(&mut out, 1, kind); + } + if node.get("n").and_then(Value::as_bool) == Some(true) { + varint_field(&mut out, 2, 1); + } + if let Some(field) = node.get("f").and_then(Value::as_str) { + string_field(&mut out, 3, field); + } + if node.get("m").and_then(Value::as_bool) == Some(true) { + varint_field(&mut out, 4, 1); + } + if let Some(position) = node.get("p").and_then(Value::as_array) { + for (field, value) in position.iter().enumerate().take(4) { + varint_field(&mut out, 5 + field as u32, value.as_u64().unwrap_or(0)); + } + } + if let Some(span) = node.get("b").and_then(Value::as_array) { + for (field, value) in span.iter().enumerate().take(2) { + varint_field(&mut out, 9 + field as u32, value.as_u64().unwrap_or(0)); + } + } + if let Some(text) = node.get("t").and_then(Value::as_str) { + string_field(&mut out, 11, text); + } + for child in node.get("c").and_then(Value::as_array).into_iter().flatten() { + message_field(&mut out, 12, &encode_node(child)); + } + for decl in strings(node, "decls") { + string_field(&mut out, 13, &decl); + } + for reference in node.get("refs").and_then(Value::as_array).into_iter().flatten() { + message_field(&mut out, 14, &encode_ref(reference)); + } + out +} + +fn encode_ref(reference: &Value) -> Vec { + let mut out = Vec::new(); + for (field, key) in [(1, "access"), (2, "outcome"), (3, "target"), (4, "reason")] { + if let Some(value) = reference.get(key).and_then(Value::as_str) { + string_field(&mut out, field, value); + } + } + if reference.get("bounded").and_then(Value::as_bool) == Some(true) { + varint_field(&mut out, 5, 1); + } + if let Some(facts) = reference.get("facts") { + if !facts.is_null() { + if let Ok(json) = serde_json::to_string(facts) { + string_field(&mut out, 6, &json); + } + } + } + out +} + +fn decode_document(bytes: &[u8]) -> Result { + let mut object = Map::new(); + object.insert("hasError".into(), Value::Bool(false)); + object.insert("semantic".into(), Value::Bool(false)); + object.insert("symbols".into(), json!([])); + object.insert("facts".into(), json!({})); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Varint(v)) => object.insert("hasError".into(), Value::Bool(v != 0)), + (2, Wire::Bytes(b)) => object.insert("tree".into(), decode_node(&b)?), + (3, Wire::Bytes(b)) => object.insert("path".into(), Value::String(text(&b))), + (4, Wire::Varint(v)) => object.insert("semantic".into(), Value::Bool(v != 0)), + (5, Wire::Bytes(b)) => object.insert("symbols".into(), serde_json::from_slice(&b).unwrap_or_else(|_| json!([]))), + (6, Wire::Bytes(b)) => { + let (key, fact) = decode_fact_entry(&b)?; + object + .entry("facts") + .or_insert_with(|| json!({})) + .as_object_mut() + .expect("facts is an object") + .insert(key, fact); + None + } + _ => None, + }; + } + Ok(Value::Object(object)) +} + +fn decode_fact_entry(bytes: &[u8]) -> Result<(String, Value)> { + let mut key = String::new(); + let mut fact = json!({}); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => key = text(&b), + (2, Wire::Bytes(b)) => fact = decode_fact(&b)?, + _ => {} + } + } + Ok((key, fact)) +} + +fn decode_fact(bytes: &[u8]) -> Result { + let mut object = Map::new(); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => object.insert("kind".into(), Value::String(text(&b))), + (2, Wire::Bytes(b)) => object.insert("tier".into(), Value::String(text(&b))), + (3, Wire::Bytes(b)) => object.insert("parent".into(), Value::String(text(&b))), + (4, Wire::Bytes(b)) => { + let (name, values) = decode_prop(&b)?; + let props = object.entry("props").or_insert_with(|| json!({})); + props.as_object_mut().expect("props").insert(name, json!(values)); + None + } + (5, Wire::Bytes(b)) => push_string(&mut object, "extends", text(&b)), + (6, Wire::Bytes(b)) => push_string(&mut object, "implements", text(&b)), + (7, Wire::Bytes(b)) => push_string(&mut object, "permits", text(&b)), + (8, Wire::Bytes(b)) => push_string(&mut object, "returns", text(&b)), + (9, Wire::Bytes(b)) => push_string(&mut object, "type", text(&b)), + (10, Wire::Bytes(b)) => push_string(&mut object, "params", text(&b)), + (11, Wire::Bytes(b)) => push_string(&mut object, "overrides", text(&b)), + (12, Wire::Bytes(b)) => push_string(&mut object, "annotations", text(&b)), + _ => None, + }; + } + Ok(Value::Object(object)) +} + +fn decode_prop(bytes: &[u8]) -> Result<(String, Vec)> { + let mut name = String::new(); + let mut values = Vec::new(); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => name = text(&b), + (2, Wire::Bytes(b)) => values.push(text(&b)), + _ => {} + } + } + Ok((name, values)) +} + +fn decode_node(bytes: &[u8]) -> Result { + let mut object = Map::new(); + let mut position = [0u64; 4]; + let mut span = [0u64; 2]; + let mut saw_position = false; + let mut saw_span = false; + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => object.insert("k".into(), Value::String(text(&b))), + (2, Wire::Varint(v)) if v != 0 => object.insert("n".into(), Value::Bool(true)), + (3, Wire::Bytes(b)) => object.insert("f".into(), Value::String(text(&b))), + (4, Wire::Varint(v)) if v != 0 => object.insert("m".into(), Value::Bool(true)), + (5..=8, Wire::Varint(v)) => { + position[(field - 5) as usize] = v; + saw_position = true; + None + } + (9 | 10, Wire::Varint(v)) => { + span[(field - 9) as usize] = v; + saw_span = true; + None + } + (11, Wire::Bytes(b)) => object.insert("t".into(), Value::String(text(&b))), + (12, Wire::Bytes(b)) => { + let children = object.entry("c").or_insert_with(|| json!([])); + children.as_array_mut().expect("children").push(decode_node(&b)?); + None + } + (13, Wire::Bytes(b)) => push_string(&mut object, "decls", text(&b)), + (14, Wire::Bytes(b)) => { + let refs = object.entry("refs").or_insert_with(|| json!([])); + refs.as_array_mut().expect("refs").push(decode_ref(&b)?); + None + } + _ => None, + }; + } + if saw_position { + object.insert("p".into(), json!(position)); + } + if saw_span { + object.insert("b".into(), json!(span)); + } + Ok(Value::Object(object)) +} + +fn decode_ref(bytes: &[u8]) -> Result { + let mut object = Map::new(); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => object.insert("access".into(), Value::String(text(&b))), + (2, Wire::Bytes(b)) => object.insert("outcome".into(), Value::String(text(&b))), + (3, Wire::Bytes(b)) => object.insert("target".into(), Value::String(text(&b))), + (4, Wire::Bytes(b)) => object.insert("reason".into(), Value::String(text(&b))), + (5, Wire::Varint(v)) if v != 0 => object.insert("bounded".into(), Value::Bool(true)), + (6, Wire::Bytes(b)) => object.insert("facts".into(), serde_json::from_slice(&b).unwrap_or(Value::Null)), + _ => None, + }; + } + Ok(Value::Object(object)) +} + +fn decode_analyze(bytes: &[u8]) -> Result<(String, Vec, Vec)> { + let mut root = String::new(); + let mut files = Vec::new(); + let mut classpath = Vec::new(); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => root = text(&b), + (2, Wire::Bytes(b)) => files.push(text(&b)), + (3, Wire::Bytes(b)) => classpath.push(text(&b)), + _ => {} + } + } + Ok((root, files, classpath)) +} + +fn decode_analyze_result(bytes: &[u8]) -> Result<(String, Vec)> { + let mut root = String::new(); + let mut files = Vec::new(); + for (field, value) in fields(bytes)? { + match (field, value) { + (1, Wire::Bytes(b)) => root = text(&b), + (2, Wire::Bytes(b)) => files.push(text(&b)), + _ => {} + } + } + Ok((root, files)) +} + +fn decode_path(bytes: &[u8]) -> Result { + for (field, value) in fields(bytes)? { + if field == 1 { + if let Wire::Bytes(b) = value { + return Ok(text(&b)); + } + } + } + Ok(String::new()) +} + +fn decode_names(bytes: &[u8]) -> Result> { + let mut names = Vec::new(); + for (field, value) in fields(bytes)? { + if field == 1 { + if let Wire::Bytes(b) = value { + names.push(text(&b)); + } + } + } + Ok(names) +} + +fn decode_source(bytes: &[u8]) -> Result { + decode_path(bytes) +} + +fn push_string(object: &mut Map, key: &str, value: String) -> Option { + let array = object.entry(key).or_insert_with(|| json!([])); + array.as_array_mut().expect(key).push(Value::String(value)); + None +} + +fn strings(value: &Value, key: &str) -> Vec { + value + .get(key) + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect() +} + +fn text(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes).into_owned() +} + +enum Wire { + Varint(u64), + Bytes(Vec), +} + +fn fields(bytes: &[u8]) -> Result> { + let mut out = Vec::new(); + let mut i = 0; + while i < bytes.len() { + let (key, next) = varint_at(bytes, i)?; + i = next; + let field = (key >> 3) as u32; + match key & 7 { + 0 => { + let (value, next) = varint_at(bytes, i)?; + i = next; + out.push((field, Wire::Varint(value))); + } + 2 => { + let (len, next) = varint_at(bytes, i)?; + i = next; + let len = len as usize; + if i + len > bytes.len() { + return Err(anyhow!("truncated protobuf field {field}")); + } + out.push((field, Wire::Bytes(bytes[i..i + len].to_vec()))); + i += len; + } + 1 => i += 8, + 5 => i += 4, + other => return Err(anyhow!("unsupported protobuf wire type {other}")), + } + } + Ok(out) +} + +fn varint_at(bytes: &[u8], mut i: usize) -> Result<(u64, usize)> { + let mut value = 0u64; + let mut shift = 0; + while i < bytes.len() && shift < 64 { + let byte = bytes[i]; + i += 1; + value |= ((byte & 0x7f) as u64) << shift; + if byte & 0x80 == 0 { + return Ok((value, i)); + } + shift += 7; + } + Err(anyhow!("truncated varint")) +} + +fn varint_field(out: &mut Vec, field: u32, value: u64) { + varint(out, (field as u64) << 3); + varint(out, value); +} + +fn string_field(out: &mut Vec, field: u32, value: &str) { + bytes_field(out, field, value.as_bytes()); +} + +fn message_field(out: &mut Vec, field: u32, value: &[u8]) { + bytes_field(out, field, value); +} + +fn bytes_field(out: &mut Vec, field: u32, value: &[u8]) { + varint(out, ((field as u64) << 3) | 2); + varint(out, value.len() as u64); + out.extend_from_slice(value); +} + +fn varint(out: &mut Vec, mut value: u64) { + loop { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + out.push(byte); + if value == 0 { + break; + } + } +} + +pub fn encode_request(id: u32, request: &Request) -> Vec { + let mut out = Vec::new(); + varint_field(&mut out, 1, id as u64); + match request { + Request::Analyze { root, files, classpath } => { + let mut msg = Vec::new(); + string_field(&mut msg, 1, root); + for file in files { + string_field(&mut msg, 2, file); + } + for entry in classpath { + string_field(&mut msg, 3, entry); + } + message_field(&mut out, 2, &msg); + } + Request::Cst { path } => { + let mut msg = Vec::new(); + string_field(&mut msg, 1, path); + message_field(&mut out, 3, &msg); + } + Request::Types { names } => { + let mut msg = Vec::new(); + for name in names { + string_field(&mut msg, 1, name); + } + message_field(&mut out, 4, &msg); + } + Request::Parse { source } => { + let mut msg = Vec::new(); + string_field(&mut msg, 1, source); + message_field(&mut out, 5, &msg); + } + Request::Quit => varint_field(&mut out, 6, 1), + } + out +} diff --git a/semsitter-bridge/tests/cst.rs b/semsitter-bridge/tests/cst.rs new file mode 100644 index 00000000000..42b81862bbe --- /dev/null +++ b/semsitter-bridge/tests/cst.rs @@ -0,0 +1,128 @@ +/* + * 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/ + */ + +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +fn write(dir: &Path, rel: &str, text: &str) -> PathBuf { + let path = dir.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, text).unwrap(); + path +} + +fn find<'a>(node: &'a Value, text: &str, out: &mut Vec<&'a Value>) { + if node.get("t").and_then(Value::as_str) == Some(text) { + out.push(node); + } + for child in node.get("c").and_then(Value::as_array).into_iter().flatten() { + find(child, text, out); + } +} + +fn nodes<'a>(cst: &'a Value, text: &str) -> Vec<&'a Value> { + let mut out = Vec::new(); + find(&cst["tree"], text, &mut out); + out +} + +#[test] +fn cst_carries_declarations_references_and_facts() { + let dir = tempfile::tempdir().unwrap(); + let a = write( + dir.path(), + "src/p/A.java", + "package p;\nimport java.util.List;\npublic class A {\n int f;\n String name(List l) { return l.get(f); }\n}\n", + ); + let b = write( + dir.path(), + "src/p/B.java", + "package p;\nclass B extends A {\n void run() { A a = new A(); String s = a.name(null); }\n}\n", + ); + + let analysis = semsitter_bridge::analyze(dir.path(), &[a.clone(), b], &[]).unwrap(); + assert_eq!(analysis.files(), ["src/p/A.java", "src/p/B.java"]); + + let cst_a = analysis.cst(a.to_str().unwrap()).unwrap(); + assert_eq!(cst_a["path"], "src/p/A.java"); + assert_eq!(cst_a["semantic"], true); + assert_eq!(cst_a["tree"]["k"], "program"); + assert_eq!(cst_a["hasError"], false); + + let class_name = nodes(&cst_a, "A")[0]; + assert_eq!(class_name["decls"][0], "p.A"); + assert_eq!(class_name["p"], serde_json::json!([3, 13, 3, 14])); + + let get = nodes(&cst_a, "get")[0]; + let target = get["refs"][0]["target"].as_str().unwrap(); + assert!(target.starts_with("java.util.List"), "{target}"); + + let field_use = nodes(&cst_a, "f")[1]; + assert_eq!(field_use["refs"][0]["target"], "p.A#f"); + + let cst_b = analysis.cst("src/p/B.java").unwrap(); + assert_eq!(cst_b["facts"]["p.B"]["extends"][0], "p.A"); + let name_call = nodes(&cst_b, "name")[0]; + assert_eq!(name_call["refs"][0]["target"], "p.A#name(java.util.List`1)"); + assert_eq!(cst_b["facts"]["p.A#name(java.util.List`1)"]["returns"][0], "java.lang.String"); +} + +#[test] +fn types_come_from_the_project_graph_and_the_jdk_table_with_their_supertypes() { + let dir = tempfile::tempdir().unwrap(); + let a = write(dir.path(), "p/A.java", "package p;\npublic class A extends java.util.ArrayList { int f; void m() {} }\n"); + let analysis = semsitter_bridge::analyze(dir.path(), &[a], &[]).unwrap(); + let types = analysis.types(&["p.A".to_string()]); + assert_eq!(types["p.A"]["origin"], "project"); + assert_eq!(types["p.A"]["super"], "java.util.ArrayList`1"); + let member_ids: Vec<&str> = types["p.A"]["members"].as_array().unwrap().iter().map(|m| m["id"].as_str().unwrap()).collect(); + assert!(member_ids.contains(&"p.A#m()"), "{member_ids:?}"); + assert_eq!(types["java.util.ArrayList"]["origin"], "jdk"); + assert_eq!(types["java.util.AbstractList"]["super"], "java.util.AbstractCollection"); + let list_get = types["java.util.List"]["members"].as_array().unwrap().iter().find(|m| m["name"] == "get").unwrap(); + assert_eq!(list_get["desc"], "(I)Ljava/lang/Object;"); +} + +#[test] +fn dump_json_writes_the_udg_the_semantic_dump_and_the_cst() { + let dir = tempfile::tempdir().unwrap(); + let dump = tempfile::tempdir().unwrap(); + let file = write(dir.path(), "A.java", "class A { int f; }"); + semsitter_bridge::analyze_dumping(dir.path(), &[file], &[], Some(dump.path())).unwrap(); + assert!(dump.path().join("udg.json").is_file()); + let semdump = std::fs::read_to_string(dump.path().join("semdump/A.java.semdump.json")).unwrap(); + assert!(semdump.contains("semantic-dump/1"), "{semdump}"); + let cst = std::fs::read_to_string(dump.path().join("cst/A.java.json")).unwrap(); + assert!(cst.contains("\"k\": \"program\""), "{cst}"); +} + +#[test] +fn a_bom_is_neither_a_syntax_error_nor_a_column() { + let cst = semsitter_bridge::parse_only(b"\xEF\xBB\xBFclass A {}").unwrap(); + assert_eq!(cst["hasError"], false); + assert_eq!(nodes(&cst, "class")[0]["p"], serde_json::json!([1, 0, 1, 5])); +} + +#[test] +fn parse_only_keeps_every_token_and_utf16_columns() { + let cst = semsitter_bridge::parse_only("class É { String s = \"é\"; }".as_bytes()).unwrap(); + let literal = &nodes(&cst, "\"é\"")[0]; + assert_eq!(literal["k"], "string_literal"); + assert_eq!(literal["p"], serde_json::json!([1, 21, 1, 24])); + assert_eq!(nodes(&cst, "{").len(), 1); +} diff --git a/semsitter-bridge/tests/jsonl.rs b/semsitter-bridge/tests/jsonl.rs new file mode 100644 index 00000000000..7c9f99d04f2 --- /dev/null +++ b/semsitter-bridge/tests/jsonl.rs @@ -0,0 +1,58 @@ +/* + * 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/ + */ + +use serde_json::{Value, json}; +use semsitter_bridge::server::serve; + +fn exchange(requests: &[Value]) -> Vec { + let input: String = requests.iter().map(|r| format!("{r}\n")).collect(); + let mut output = Vec::new(); + serve(input.as_bytes(), &mut output).unwrap(); + String::from_utf8(output).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect() +} + +#[test] +fn analyze_then_cst_then_quit() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("A.java"); + std::fs::write(&file, "class A { int f; int g() { return f; } }").unwrap(); + let responses = exchange(&[ + json!({"id": 1, "op": "analyze", "root": dir.path(), "files": [file], "classpath": []}), + json!({"id": 2, "op": "cst", "path": file}), + json!({"id": 3, "op": "cst", "path": "Missing.java"}), + json!({"id": 4, "op": "quit"}), + json!({"id": 5, "op": "parse", "source": "class B {}"}), + ]); + assert_eq!(responses.len(), 4); + assert_eq!(responses[0]["files"], json!(["A.java"])); + assert_eq!(responses[1]["id"], 2); + assert_eq!(responses[1]["cst"]["symbols"].as_array().unwrap().iter().filter(|s| s["name"] == "f").count(), 1); + assert_eq!(responses[2]["ok"], false); + assert_eq!(responses[3], json!({"id": 4, "ok": true})); +} + +#[test] +fn parse_needs_no_analysis_and_errors_keep_the_server_alive() { + let responses = exchange(&[ + json!({"id": 1, "op": "cst", "path": "A.java"}), + json!({"id": 2, "op": "nope"}), + json!({"id": 3, "op": "parse", "source": "class B {}"}), + ]); + assert_eq!(responses[0]["ok"], false); + assert_eq!(responses[1]["error"], "unknown op: nope"); + assert_eq!(responses[2]["cst"]["tree"]["k"], "program"); +} diff --git a/semsitter-bridge/tests/shm.rs b/semsitter-bridge/tests/shm.rs new file mode 100644 index 00000000000..645baab9662 --- /dev/null +++ b/semsitter-bridge/tests/shm.rs @@ -0,0 +1,79 @@ +/* + * 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/ + */ + +use std::io::{Read, Write}; +use std::thread; + +use semsitter_bridge::server::serve_shm; +use semsitter_bridge::shm::{Link, ATTACHED, READY}; +use semsitter_bridge::wire::{self, Request}; + +#[test] +fn protobuf_round_trips_a_tree() { + let document = serde_json::json!({ + "ok": true, + "cst": { + "hasError": false, + "semantic": true, + "path": "A.java", + "symbols": [{"id": "A", "kind": "class"}], + "facts": {"A": {"kind": "class", "tier": "IN_PROJECT", "extends": ["java.lang.Object"]}}, + "tree": {"k": "program", "n": true, "p": [1, 0, 1, 10], "b": [0, 10], "t": "class A {}", "decls": ["A"], + "refs": [{"access": "r", "outcome": "bound", "target": "A"}]} + } + }); + let decoded = wire::decode_response(&wire::encode_response(7, &document)).unwrap(); + assert_eq!(decoded["id"], 7); + assert_eq!(decoded["cst"]["tree"]["k"], "program"); + assert_eq!(decoded["cst"]["tree"]["decls"], serde_json::json!(["A"])); + assert_eq!(decoded["cst"]["tree"]["refs"][0]["target"], "A"); + assert_eq!(decoded["cst"]["symbols"][0]["id"], "A"); + assert_eq!(decoded["cst"]["facts"]["A"]["extends"], serde_json::json!(["java.lang.Object"])); +} + +#[cfg(unix)] +#[test] +fn shared_memory_serves_a_parse() { + use std::os::unix::net::UnixStream; + + let (server_side, mut client_side) = UnixStream::pair().unwrap(); + let server = thread::spawn(move || { + let input = server_side.try_clone().unwrap(); + serve_shm(input, server_side, None).unwrap(); + }); + + let mut ready = [0u8; 1]; + client_side.read_exact(&mut ready).unwrap(); + assert_eq!(ready[0], READY); + let mut link = Link::open(&std::process::id().to_string()).unwrap(); + client_side.write_all(&[ATTACHED]).unwrap(); + client_side.flush().unwrap(); + + let mut to_server = client_side.try_clone().unwrap(); + let mut from_server = client_side; + let request = wire::encode_request(3, &Request::Parse { source: "class B {}".into() }); + link.write_request(&request, &mut to_server, &mut from_server).unwrap(); + let response = link.read_response(&mut from_server, &mut to_server).unwrap(); + let decoded = wire::decode_response(&response).unwrap(); + assert_eq!(decoded["ok"], true); + assert_eq!(decoded["cst"]["tree"]["k"], "program"); + + let quit = wire::encode_request(4, &Request::Quit); + link.write_request(&quit, &mut to_server, &mut from_server).unwrap(); + let _ = link.read_response(&mut from_server, &mut to_server).unwrap(); + server.join().unwrap(); +} diff --git a/sonar-java-plugin/license-resources/0bsd.txt b/sonar-java-plugin/license-resources/0bsd.txt new file mode 100644 index 00000000000..5045695ca88 --- /dev/null +++ b/sonar-java-plugin/license-resources/0bsd.txt @@ -0,0 +1,12 @@ +Copyright (C) 2006-2024 Lasse Collin and others + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/sonar-java-plugin/pom.xml b/sonar-java-plugin/pom.xml index a390364e2d4..bcadd0df715 100644 --- a/sonar-java-plugin/pom.xml +++ b/sonar-java-plugin/pom.xml @@ -152,6 +152,13 @@ ${project.build.directory}/generated-resources/profiles + + ${maven.multiModuleProjectDirectory}/semsitter-bridge/prebuilt + + bridge-*.xz + + bridge + @@ -184,8 +191,8 @@ - 21500000 - 19000000 + 45000000 + 10000000 ${project.build.directory}/${project.build.finalName}.jar @@ -233,6 +240,18 @@ ** + + org.apache.commons:commons-compress + + ** + + + + org.tukaani:xz + + ** + + xerces:xercesImpl @@ -282,6 +301,7 @@ 2.7.1 xmlOutput + ${project.basedir}/src/license/licenses.xml false 20000 ${project.build.directory}/temp-licenses/THIRD_PARTY_LICENSES @@ -301,6 +321,10 @@ http://www.opensource.org/licenses/bsd-license.php ${project.baseUri}/license-resources/bsd.txt + + https://opensource.org/license/0bsd + ${project.baseUri}/license-resources/0bsd.txt + diff --git a/sonar-java-plugin/src/license/licenses.xml b/sonar-java-plugin/src/license/licenses.xml new file mode 100644 index 00000000000..35a6081b821 --- /dev/null +++ b/sonar-java-plugin/src/license/licenses.xml @@ -0,0 +1,22 @@ + + + + + \Qorg.tukaani\E + \Qxz\E + + + \Q0BSD\E + \Qrepo\E + + + + + 0BSD + https://opensource.org/license/0bsd + repo + + + + + diff --git a/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/commons-codec.commons-codec-LICENSE.txt b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/commons-codec.commons-codec-LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/commons-codec.commons-codec-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/commons-io.commons-io-LICENSE.txt b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/commons-io.commons-io-LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/commons-io.commons-io-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/org.apache.commons.commons-compress-LICENSE.txt b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/org.apache.commons.commons-compress-LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/org.apache.commons.commons-compress-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/org.tukaani.xz-LICENSE.txt b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/org.tukaani.xz-LICENSE.txt new file mode 100644 index 00000000000..5045695ca88 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/licenses/THIRD_PARTY_LICENSES/org.tukaani.xz-LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (C) 2006-2024 Lasse Collin and others + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.