From 7649a67e1d2978f66727670b405dadd220425aea Mon Sep 17 00:00:00 2001 From: Mickael Istria Date: Fri, 12 Jan 2024 20:20:13 +0100 Subject: [PATCH] Switch to make some operations rely on DOM instead of ECJ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced 2 system property switches to control whether some IDE operations are using ECJ parser (legacy/default) or whether to make those operations powered by a full DOM. * CompilationUnit.DOM_BASED_OPERATIONS will make codeSelect (hover, link to definition...), buildStructure (JDT Project element model), reconciler (code diagnostics feedback); this one seems currently working ✔ * CompilationUnit.codeComplete.DOM_BASED_OPERATIONS controls completion (unlike other operations, completion based on DOM is far from being complete or straightforward) 🏗️ * SourceIndexer.DOM_BASED_INDEXER controls whether the indexation of a source document should first build/use a full DOM. This one is currently incomplete 🏗️ The DOM-based operation can then allow to plug alternative compiler then ECJ and still get JDT functionalities working. Also-By: David Thompson Also-By: Snjezana Peco --- .github/workflows/ci-dom-javac.yml | 53 + Jenkinsfile | 42 +- .../jdt/core/tests/model/ResolveTests.java | 43 +- .../core/tests/model/TypeResolveTests.java | 9 +- .../jdt/internal/core/ASTHolderCUInfo.java | 2 +- .../jdt/internal/core/CompilationUnit.java | 598 +++++++-- .../internal/core/DOMCompletionEngine.java | 188 +++ .../internal/core/DOMToModelPopulator.java | 1179 +++++++++++++++++ .../core/ReconcileWorkingCopyOperation.java | 106 +- .../core/search/JavaSearchDocument.java | 2 +- .../search/indexing/DOMToIndexVisitor.java | 134 ++ .../core/search/indexing/SourceIndexer.java | 46 + 12 files changed, 2266 insertions(+), 136 deletions(-) create mode 100644 .github/workflows/ci-dom-javac.yml create mode 100644 org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMCompletionEngine.java create mode 100644 org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMToModelPopulator.java create mode 100644 org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/DOMToIndexVisitor.java diff --git a/.github/workflows/ci-dom-javac.yml b/.github/workflows/ci-dom-javac.yml new file mode 100644 index 00000000000..cff970c4659 --- /dev/null +++ b/.github/workflows/ci-dom-javac.yml @@ -0,0 +1,53 @@ +name: Continuous Integration with DOM/Javac +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-dom + cancel-in-progress: true + +on: + push: + branches: [ 'dom-based-operations', 'dom-with-javac' ] + pull_request: + branches: [ 'dom-based-operations', 'dom-with-javac' ] + +jobs: + build-dom-javac: + runs-on: ubuntu-latest + steps: + - name: Install xmllint + shell: bash + run: | + sudo apt update + sudo apt install -y libxml2-utils + - uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + - name: Enable DOM-first and Javac + run: sed -i 's$$ -DCompilationUnit.DOM_BASED_OPERATIONS=true -DSourceIndexer.DOM_BASED_INDEXER=false -DICompilationUnitResolver=org.eclipse.jdt.core.dom.JavacCompilationUnitResolver -DAbstractImageBuilder.compiler=org.eclipse.jdt.internal.javac.JavacCompiler_$g' */pom.xml + - name: Set up JDKs ☕ + uses: actions/setup-java@v3 + with: + java-version: | + 8 + 17 + 20 + mvn-toolchain-id: | + JavaSE-1.8 + JavaSE-17 + JavaSE-20 + distribution: 'temurin' + - name: Set up Maven + uses: stCarolas/setup-maven@07fbbe97d97ef44336b7382563d66743297e442f # v4.5 + with: + maven-version: 3.9.3 + - name: Build with Maven 🏗️ + run: | + mvn clean install --batch-mode -f org.eclipse.jdt.core.compiler.batch -DlocalEcjVersion=99.99 + mvn -U clean verify --batch-mode --fail-at-end -Ptest-on-javase-20 -Pbree-libs -Papi-check -Djava.io.tmpdir=$WORKSPACE/tmp -Dproject.build.sourceEncoding=UTF-8 -Dtycho.surefire.argLine="--add-modules ALL-SYSTEM -Dcompliance=1.8,11,17,20 -Djdt.performance.asserts=disabled" -Dcbi-ecj-version=99.99 + - name: Test Report + if: success() || failure() # run this step even if previous step failed + run: | + echo ▶️ TESTS RUN: $(xmllint --xpath 'string(/testsuite/@tests)' */target/surefire-reports/TEST-*.xml | awk '{n += $1}; END{print n}' -) + echo ❌ FAILURES: $(xmllint --xpath 'string(/testsuite/@failures)' */target/surefire-reports/TEST-*.xml | awk '{n += $1}; END{print n}' -) + echo 💥 ERRORS: $(xmllint --xpath 'string(/testsuite/@errors)' */target/surefire-reports/TEST-*.xml | awk '{n += $1}; END{print n}' -) + echo 🛑 SKIPPED: $(xmllint --xpath 'string(/testsuite/@skipped)' */target/surefire-reports/TEST-*.xml | awk '{n += $1}; END{print n}' -) \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 67af97167a7..38f58268fc2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,7 +13,7 @@ pipeline { jdk 'openjdk-jdk21-latest' } stages { - stage('Build') { + stage('Build and Test using ECJ') { steps { sh """#!/bin/bash -x @@ -30,10 +30,10 @@ pipeline { # export MAVEN_OPTS="-Xmx2G" mvn clean install -f org.eclipse.jdt.core.compiler.batch -DlocalEcjVersion=99.99 -Dmaven.repo.local=$WORKSPACE/.m2/repository -DcompilerBaselineMode=disable -DcompilerBaselineReplace=none - + + # Build and test without DOM-first to ensure no regression takes place mvn -U clean verify --batch-mode --fail-at-end -Dmaven.repo.local=$WORKSPACE/.m2/repository \ -Ptest-on-javase-21 -Pbree-libs -Papi-check -Pjavadoc -Pp2-repo \ - -Dmaven.test.failure.ignore=true \ -Dcompare-version-with-baselines.skip=false \ -Djava.io.tmpdir=$WORKSPACE/tmp -Dproject.build.sourceEncoding=UTF-8 \ -Dtycho.surefire.argLine="--add-modules ALL-SYSTEM -Dcompliance=1.8,11,17,20,21 -Djdt.performance.asserts=disabled" \ @@ -54,8 +54,40 @@ pipeline { // The eclipse compiler name is changed because the logfile not only contains ECJ but also API warnings. // "pattern:" is used to collect warnings in dedicated files avoiding output of junit tests treated as warnings junit '**/target/surefire-reports/*.xml' - discoverGitReferenceBuild referenceJob: 'eclipse.jdt.core-github/master' - recordIssues publishAllIssues:false, ignoreQualityGate:true, tool: eclipse(name: 'Compiler and API Tools', pattern: '**/target/compilelogs/*.xml'), qualityGates: [[threshold: 1, type: 'DELTA', unstable: true]] + //discoverGitReferenceBuild referenceJob: 'eclipse.jdt.core-github/master' + //recordIssues publishAllIssues:false, ignoreQualityGate:true, tool: eclipse(name: 'Compiler and API Tools', pattern: '**/target/compilelogs/*.xml'), qualityGates: [[threshold: 1, type: 'DELTA', unstable: true]] + } + } + } + stage("Build and Test with DOM-first") { + steps { + sh """ + # Then enable DOM-first + sed -i 's|| -DCompilationUnit.DOM_BASED_OPERATIONS=true -DSourceIndexer.DOM_BASED_INDEXER=false -DICompilationUnitResolver=org.eclipse.jdt.core.dom.JavacCompilationUnitResolver -DAbstractImageBuilder.compiler=org.eclipse.jdt.internal.javac.JavacCompiler_|g' */pom.xml + # and build/run it + mvn -U clean verify --batch-mode --fail-at-end -Dmaven.repo.local=$WORKSPACE/.m2/repository \ + -Ptest-on-javase-21 -Pbree-libs -Papi-check -Pjavadoc -Pp2-repo \ + -Dcompare-version-with-baselines.skip=false \ + -Djava.io.tmpdir=$WORKSPACE/tmp -Dproject.build.sourceEncoding=UTF-8 \ + -Dtycho.surefire.argLine="--add-modules ALL-SYSTEM -Dcompliance=1.8,11,17,20,21 -Djdt.performance.asserts=disabled -DCompilationUnit.DOM_BASED_OPERATIONS=true -DSourceIndexer.DOM_BASED_INDEXER=false -DICompilationUnitResolver=org.eclipse.jdt.core.dom.JavacCompilationUnitResolver -DAbstractImageBuilder.compiler=org.eclipse.jdt.internal.javac.JavacCompiler_ " \ + -Dtycho.surefire.error=ignore -Dtycho.surefire.failure=ignore \ + -DDetectVMInstallationsJob.disabled=true \ + -Dtycho.apitools.debug \ + -Dcbi-ecj-version=99.99 + """ + } + post { + always { + archiveArtifacts artifacts: '*.log,*/target/work/data/.metadata/*.log,*/tests/target/work/data/.metadata/*.log,apiAnalyzer-workspace/.metadata/*.log,repository/target/repository/**', allowEmptyArchive: true + // The following lines use the newest build on master that did not fail a reference + // To not fail master build on failed test maven needs to be started with "-Dmaven.test.failure.ignore=true" it will then only marked unstable. + // To not fail the build also "unstable: true" is used to only mark the build unstable instead of failing when qualityGates are missed + // Also do not record mavenConsole() as failing tests are logged with ERROR duplicating the failure into the "Maven" plugin + // To accept unstable builds (test errors or new warnings introduced by third party changes) as reference using "ignoreQualityGate:true" + // To only show warnings related to the PR on a PR using "publishAllIssues:false" + // The eclipse compiler name is changed because the logfile not only contains ECJ but also API warnings. + // "pattern:" is used to collect warnings in dedicated files avoiding output of junit tests treated as warnings + junit '**/target/surefire-reports/*.xml' } } } diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests.java index a905843f886..5bd535359ed 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests.java @@ -14,6 +14,7 @@ package org.eclipse.jdt.core.tests.model; import java.io.IOException; +import java.util.Set; import junit.framework.Test; @@ -975,10 +976,9 @@ public void testLocalVarIsStructureKnown() throws JavaModelException { */ public void testLocalVarTypeSignature1() throws JavaModelException { ILocalVariable localVar = getLocalVariable("/Resolve/src/ResolveLocalName.java", "var1 = new Object();", "var1"); - assertEquals( - "Unexpected type signature", - "QObject;", - localVar.getTypeSignature()); + assertTrue("Unexpected type signature", + Set.of("QObject;", "Ljava.lang.Object;").contains( + localVar.getTypeSignature())); } /* * Resolve a local reference and ensure its type signature is correct. @@ -1466,10 +1466,9 @@ public void testDuplicateLocals1() throws JavaModelException { elements ); - assertEquals( - "Unexpected type", - "QTestString;", - ((ILocalVariable)elements[0]).getTypeSignature()); + assertTrue("Unexpected type", + Set.of("QTestString;", "Ltest.TestString;").contains( + ((ILocalVariable)elements[0]).getTypeSignature())); assertFalse(((ILocalVariable)elements[0]).isParameter()); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=144858 @@ -1509,10 +1508,9 @@ public void testDuplicateLocals2() throws JavaModelException { elements ); - assertEquals( - "Unexpected type", - "QTestException;", - ((ILocalVariable)elements[0]).getTypeSignature()); + assertTrue("Unexpected type", + Set.of("QTestException;", "Ltest.TestException;").contains( + ((ILocalVariable)elements[0]).getTypeSignature())); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=144858 public void testDuplicateLocals3() throws JavaModelException { @@ -1548,10 +1546,9 @@ public void testDuplicateLocals3() throws JavaModelException { elements ); - assertEquals( - "Unexpected type", - "QTestString;", - ((ILocalVariable)elements[0]).getTypeSignature()); + assertTrue("Unexpected type", + Set.of("QTestString;", "Ltest.TestString;").contains( + ((ILocalVariable)elements[0]).getTypeSignature())); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=144858 public void testDuplicateLocals4() throws JavaModelException { @@ -1589,10 +1586,9 @@ public void testDuplicateLocals4() throws JavaModelException { elements ); - assertEquals( - "Unexpected type", - "QTestString;", - ((ILocalVariable)elements[0]).getTypeSignature()); + assertTrue("Unexpected type", + Set.of("QTestString;", "Ltest.TestString;").contains( + ((ILocalVariable)elements[0]).getTypeSignature())); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=144858 public void testDuplicateLocals5() throws JavaModelException { @@ -1630,10 +1626,9 @@ public void testDuplicateLocals5() throws JavaModelException { elements ); - assertEquals( - "Unexpected type", - "QTestString;", - ((ILocalVariable)elements[0]).getTypeSignature()); + assertTrue("Unexpected type", + Set.of("QTestString;", "Ltest.TestString;").contains( + ((ILocalVariable)elements[0]).getTypeSignature())); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=165662 public void testDuplicateLocalsType1() throws JavaModelException { diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TypeResolveTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TypeResolveTests.java index 0808618fb1d..75838fa4a27 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TypeResolveTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TypeResolveTests.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; @@ -1399,7 +1400,9 @@ public void test531046g() throws CoreException, IOException { IJavaElement[] elements = unit.codeSelect(source.lastIndexOf(select), select.length()); assertEquals("should not be empty", 1, elements.length); ILocalVariable variable = (ILocalVariable) elements[0]; - assertEquals("incorrect type", "&QCharSequence;:QComparable;", variable.getTypeSignature()); + assertTrue("incorrect type", + Set.of("&QCharSequence;:QComparable;", "&Ljava.lang.CharSequence;:Ljava.lang.Comparable;").contains( + variable.getTypeSignature())); } finally { deleteProject("P"); } @@ -1424,7 +1427,9 @@ public void test531046h() throws CoreException, IOException { IJavaElement[] elements = unit.codeSelect(source.lastIndexOf(select), select.length()); assertEquals("should not be empty", 1, elements.length); ILocalVariable variable = (ILocalVariable) elements[0]; - assertEquals("incorrect type", "&QCharSequence;:QComparable;", variable.getTypeSignature()); + assertTrue("incorrect type", + Set.of("&QCharSequence;:QComparable;", "&Ljava.lang.CharSequence;:Ljava.lang.Comparable;").contains( + variable.getTypeSignature())); } finally { deleteProject("P"); } diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ASTHolderCUInfo.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ASTHolderCUInfo.java index cae44607b34..37e3571a7ed 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ASTHolderCUInfo.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ASTHolderCUInfo.java @@ -19,7 +19,7 @@ import org.eclipse.jdt.core.dom.CompilationUnit; public class ASTHolderCUInfo extends CompilationUnitElementInfo { - int astLevel; + public int astLevel; boolean resolveBindings; int reconcileFlags; Map problems = null; diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/CompilationUnit.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/CompilationUnit.java index 3598751a242..125a29b08bb 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/CompilationUnit.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/CompilationUnit.java @@ -19,11 +19,40 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.eclipse.core.resources.*; import org.eclipse.core.runtime.*; import org.eclipse.jdt.core.*; import org.eclipse.jdt.core.compiler.*; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; 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.ClassInstanceCreation; +import org.eclipse.jdt.core.dom.Comment; +import org.eclipse.jdt.core.dom.ConstructorInvocation; +import org.eclipse.jdt.core.dom.FieldAccess; +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.MethodDeclaration; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.MethodReference; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.jdt.core.dom.ParameterizedType; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.SuperConstructorInvocation; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.VariableDeclaration; +import org.eclipse.jdt.core.search.IJavaSearchConstants; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.TypeNameMatchRequestor; import org.eclipse.jdt.internal.compiler.IProblemFactory; import org.eclipse.jdt.internal.compiler.SourceElementParser; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; @@ -32,8 +61,11 @@ import org.eclipse.jdt.internal.compiler.lookup.TypeConstants; import org.eclipse.jdt.internal.compiler.problem.AbortCompilationUnit; import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; +import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; import org.eclipse.jdt.internal.compiler.util.SuffixConstants; import org.eclipse.jdt.internal.core.util.DeduplicationUtil; +import org.eclipse.jdt.internal.core.search.BasicSearchEngine; +import org.eclipse.jdt.internal.core.search.TypeNameMatchRequestorWrapper; import org.eclipse.jdt.internal.core.util.MementoTokenizer; import org.eclipse.jdt.internal.core.util.Messages; import org.eclipse.jdt.internal.core.util.Util; @@ -47,11 +79,23 @@ * @see ICompilationUnit */ public class CompilationUnit extends Openable implements ICompilationUnit, org.eclipse.jdt.internal.compiler.env.ICompilationUnit, SuffixConstants { + /** + * Internal synonym for deprecated constant AST.JSL2 + * to alleviate deprecation warnings. + * @deprecated + */ + /*package*/ static final int JLS2_INTERNAL = AST.JLS2; + + public static boolean DOM_BASED_OPERATIONS = Boolean.getBoolean(CompilationUnit.class.getSimpleName() + ".DOM_BASED_OPERATIONS"); //$NON-NLS-1$ + public static boolean DOM_BASED_COMPLETION = Boolean.getBoolean(CompilationUnit.class.getSimpleName() + ".codeComplete.DOM_BASED_OPERATIONS"); //$NON-NLS-1$ + private static final IImportDeclaration[] NO_IMPORTS = new IImportDeclaration[0]; protected final String name; public final WorkingCopyOwner owner; + private org.eclipse.jdt.core.dom.CompilationUnit ast; + /** * Constructs a handle to a compilation unit with the given name in the * specified package for the specified owner @@ -102,103 +146,157 @@ public void becomeWorkingCopy(IProgressMonitor monitor) throws JavaModelExceptio protected boolean buildStructure(OpenableElementInfo info, final IProgressMonitor pm, Map newElements, IResource underlyingResource) throws JavaModelException { CompilationUnitElementInfo unitInfo = (CompilationUnitElementInfo) info; - // ensure buffer is opened - IBuffer buffer = getBufferManager().getBuffer(CompilationUnit.this); - if (buffer == null) { - openBuffer(pm, unitInfo); // open buffer independently from the info, since we are building the info - } - // generate structure and compute syntax problems if needed - CompilationUnitStructureRequestor requestor = new CompilationUnitStructureRequestor(this, unitInfo, newElements); JavaModelManager.PerWorkingCopyInfo perWorkingCopyInfo = getPerWorkingCopyInfo(); IJavaProject project = getJavaProject(); - - boolean createAST; - boolean resolveBindings; - int reconcileFlags; - Map problems; - if (info instanceof ASTHolderCUInfo) { - ASTHolderCUInfo astHolder = (ASTHolderCUInfo) info; - createAST = astHolder.astLevel != NO_AST; - resolveBindings = astHolder.resolveBindings; - reconcileFlags = astHolder.reconcileFlags; - problems = astHolder.problems; - } else { - createAST = false; - resolveBindings = false; - reconcileFlags = 0; - problems = null; - } - + boolean createAST = info instanceof ASTHolderCUInfo astHolder ? astHolder.astLevel != NO_AST : false; + boolean resolveBindings = info instanceof ASTHolderCUInfo astHolder ? astHolder.resolveBindings : false; + int reconcileFlags = info instanceof ASTHolderCUInfo astHolder ? astHolder.reconcileFlags : 0; boolean computeProblems = perWorkingCopyInfo != null && perWorkingCopyInfo.isActive() && project != null && JavaProject.hasJavaNature(project.getProject()); - IProblemFactory problemFactory = new DefaultProblemFactory(); Map options = this.getOptions(true); if (!computeProblems) { // disable task tags checking to speed up parsing options.put(JavaCore.COMPILER_TASK_TAGS, ""); //$NON-NLS-1$ } - CompilerOptions compilerOptions = new CompilerOptions(options); - compilerOptions.ignoreMethodBodies = (reconcileFlags & ICompilationUnit.IGNORE_METHOD_BODIES) != 0; - SourceElementParser parser = new SourceElementParser( - requestor, - problemFactory, - compilerOptions, - true/*report local declarations*/, - !createAST /*optimize string literals only if not creating a DOM AST*/); - parser.reportOnlyOneSyntaxError = !computeProblems; - parser.setMethodsFullRecovery(true); - parser.setStatementsRecovery((reconcileFlags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) != 0); - - if (!computeProblems && !resolveBindings && !createAST) // disable javadoc parsing if not computing problems, not resolving and not creating ast - parser.javadocParser.checkDocComment = false; - requestor.parser = parser; // update timestamp (might be IResource.NULL_STAMP if original does not exist) if (underlyingResource == null) { underlyingResource = getResource(); } // underlying resource is null in the case of a working copy on a class file in a jar - if (underlyingResource != null) + if (underlyingResource != null) { unitInfo.timestamp = ((IFile)underlyingResource).getModificationStamp(); + } + + // ensure buffer is opened + IBuffer buffer = getBufferManager().getBuffer(CompilationUnit.this); + if (buffer == null) { + openBuffer(pm, unitInfo); // open buffer independently from the info, since we are building the info + } - // compute other problems if needed - CompilationUnitDeclaration compilationUnitDeclaration = null; CompilationUnit source = cloneCachingContents(); - try { - if (computeProblems) { - if (problems == null) { - // report problems to the problem requestor - problems = new HashMap<>(); - compilationUnitDeclaration = CompilationUnitProblemFinder.process(source, parser, this.owner, problems, createAST, reconcileFlags, pm); - try { - perWorkingCopyInfo.beginReporting(); - for (CategorizedProblem[] categorizedProblems : problems.values()) { - if (categorizedProblems == null) continue; - for (CategorizedProblem categorizedProblem : categorizedProblems) { - perWorkingCopyInfo.acceptProblem(categorizedProblem); + Map problems = info instanceof ASTHolderCUInfo astHolder ? astHolder.problems : null; + if (DOM_BASED_OPERATIONS) { + ASTParser astParser = ASTParser.newParser(info instanceof ASTHolderCUInfo astHolder && astHolder.astLevel > 0 ? astHolder.astLevel : AST.getJLSLatest()); + astParser.setWorkingCopyOwner(getOwner()); + astParser.setSource(this instanceof ClassFileWorkingCopy ? source : this); + astParser.setProject(getJavaProject()); + astParser.setStatementsRecovery((reconcileFlags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) != 0); + astParser.setResolveBindings(computeProblems || resolveBindings); + astParser.setBindingsRecovery((reconcileFlags & ICompilationUnit.ENABLE_BINDINGS_RECOVERY) != 0); + astParser.setIgnoreMethodBodies((reconcileFlags & ICompilationUnit.IGNORE_METHOD_BODIES) != 0); + astParser.setCompilerOptions(options); + ASTNode dom = null; + try { + dom = astParser.createAST(pm); + } catch (AbortCompilationUnit e) { + var problem = e.problem; + if (problem == null && e.exception instanceof IOException ioEx) { + String path = source.getPath().toString(); + String exceptionTrace = ioEx.getClass().getName() + ':' + ioEx.getMessage(); + problem = new DefaultProblemFactory().createProblem( + path.toCharArray(), + IProblem.CannotReadSource, + new String[] { path, exceptionTrace }, + new String[] { path, exceptionTrace }, + ProblemSeverities.AbortCompilation | ProblemSeverities.Error | ProblemSeverities.Fatal, + 0, 0, 1, 0); + } + if (problems != null) { + problems.put(Integer.toString(CategorizedProblem.CAT_BUILDPATH), + new CategorizedProblem[] { problem }); + } else if (perWorkingCopyInfo != null) { + perWorkingCopyInfo.beginReporting(); + perWorkingCopyInfo.acceptProblem(problem); + perWorkingCopyInfo.endReporting(); + } + } + if (dom instanceof org.eclipse.jdt.core.dom.CompilationUnit newAST) { + if (computeProblems) { + if (perWorkingCopyInfo != null && problems == null) { + try { + perWorkingCopyInfo.beginReporting(); + for (IProblem problem : newAST.getProblems()) { + perWorkingCopyInfo.acceptProblem(problem); } + } finally { + perWorkingCopyInfo.endReporting(); } - } finally { - perWorkingCopyInfo.endReporting(); + } else if (newAST.getProblems().length > 0) { + problems.put(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, Stream.of(newAST.getProblems()).filter(CategorizedProblem.class::isInstance) + .map(CategorizedProblem.class::cast) + .toArray(CategorizedProblem[]::new)); } - } else { - // collect problems - compilationUnitDeclaration = CompilationUnitProblemFinder.process(source, parser, this.owner, problems, createAST, reconcileFlags, pm); } - } else { - compilationUnitDeclaration = parser.parseCompilationUnit(source, true /*full parse to find local elements*/, pm); + if (info instanceof ASTHolderCUInfo astHolder) { + astHolder.ast = newAST; + } + newAST.accept(new DOMToModelPopulator(newElements, this, unitInfo)); + // unitInfo.setModule(); + // unitInfo.setSourceLength(newSourceLength); + boolean structureKnown = true; + for (IProblem problem : newAST.getProblems()) { + structureKnown &= (IProblem.Syntax & problem.getID()) == 0; + } + unitInfo.setIsStructureKnown(structureKnown); } + } else { + CompilerOptions compilerOptions = new CompilerOptions(options); + compilerOptions.ignoreMethodBodies = (reconcileFlags & ICompilationUnit.IGNORE_METHOD_BODIES) != 0; + CompilationUnitStructureRequestor requestor = new CompilationUnitStructureRequestor(this, unitInfo, newElements); + IProblemFactory problemFactory = new DefaultProblemFactory(); + SourceElementParser parser = new SourceElementParser( + requestor, + problemFactory, + compilerOptions, + true/*report local declarations*/, + !createAST /*optimize string literals only if not creating a DOM AST*/); + parser.reportOnlyOneSyntaxError = !computeProblems; + parser.setMethodsFullRecovery(true); + parser.setStatementsRecovery((reconcileFlags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) != 0); + + if (!computeProblems && !resolveBindings && !createAST) // disable javadoc parsing if not computing problems, not resolving and not creating ast + parser.javadocParser.checkDocComment = false; + requestor.parser = parser; + + // compute other problems if needed + CompilationUnitDeclaration compilationUnitDeclaration = null; + try { + if (computeProblems) { + if (problems == null) { + // report problems to the problem requestor + problems = new HashMap<>(); + compilationUnitDeclaration = CompilationUnitProblemFinder.process(source, parser, this.owner, problems, createAST, reconcileFlags, pm); + try { + perWorkingCopyInfo.beginReporting(); + for (CategorizedProblem[] categorizedProblems : problems.values()) { + if (categorizedProblems == null) continue; + for (CategorizedProblem categorizedProblem : categorizedProblems) { + perWorkingCopyInfo.acceptProblem(categorizedProblem); + } + } + } finally { + perWorkingCopyInfo.endReporting(); + } + } else { + // collect problems + compilationUnitDeclaration = CompilationUnitProblemFinder.process(source, parser, this.owner, problems, createAST, reconcileFlags, pm); + } + } else { + compilationUnitDeclaration = parser.parseCompilationUnit(source, true /*full parse to find local elements*/, pm); + } - if (createAST) { - int astLevel = ((ASTHolderCUInfo) info).astLevel; - org.eclipse.jdt.core.dom.CompilationUnit cu = AST.convertCompilationUnit(astLevel, compilationUnitDeclaration, options, computeProblems, source, reconcileFlags, pm); - ((ASTHolderCUInfo) info).ast = cu; + if (createAST) { + int astLevel = ((ASTHolderCUInfo) info).astLevel; + org.eclipse.jdt.core.dom.CompilationUnit cu = AST.convertCompilationUnit(astLevel, compilationUnitDeclaration, options, computeProblems, source, reconcileFlags, pm); + ((ASTHolderCUInfo) info).ast = cu; + } + } finally { + if (compilationUnitDeclaration != null) { + unitInfo.hasFunctionalTypes = compilationUnitDeclaration.hasFunctionalTypes(); + compilationUnitDeclaration.cleanUp(); + } } - } finally { - if (compilationUnitDeclaration != null) { - unitInfo.hasFunctionalTypes = compilationUnitDeclaration.hasFunctionalTypes(); - compilationUnitDeclaration.cleanUp(); - } } return unitInfo.isStructureKnown(); @@ -357,6 +455,10 @@ public void codeComplete(int offset, CompletionRequestor requestor, WorkingCopyO @Override public void codeComplete(int offset, CompletionRequestor requestor, WorkingCopyOwner workingCopyOwner, IProgressMonitor monitor) throws JavaModelException { + if (DOM_BASED_COMPLETION) { + new DOMCompletionEngine(offset, getOrBuildAST(workingCopyOwner), requestor, monitor).run(); + return; + } codeComplete( this, isWorkingCopy() ? (org.eclipse.jdt.internal.compiler.env.ICompilationUnit) getOriginalElement() : this, @@ -379,8 +481,345 @@ public IJavaElement[] codeSelect(int offset, int length) throws JavaModelExcepti */ @Override public IJavaElement[] codeSelect(int offset, int length, WorkingCopyOwner workingCopyOwner) throws JavaModelException { - return super.codeSelect(this, offset, length, workingCopyOwner); + if (DOM_BASED_OPERATIONS) { + if (offset < 0) { + throw new JavaModelException(new IndexOutOfBoundsException(offset), IJavaModelStatusConstants.INDEX_OUT_OF_BOUNDS); + } + if (offset + length > getSource().length()) { + throw new JavaModelException(new IndexOutOfBoundsException(offset + length), IJavaModelStatusConstants.INDEX_OUT_OF_BOUNDS); + } + org.eclipse.jdt.core.dom.CompilationUnit currentAST = getOrBuildAST(workingCopyOwner); + if (currentAST == null) { + return new IJavaElement[0]; + } + int initialOffset = offset, initialLength = length; + boolean insideComment = ((List)currentAST.getCommentList()).stream() + .anyMatch(comment -> comment.getStartPosition() <= initialOffset && comment.getStartPosition() + comment.getLength() >= initialOffset + initialLength); + if (!insideComment) { // trim whitespaces and surrounding comments + boolean changed = false; + do { + changed = false; + if (length > 0 && Character.isWhitespace(getSource().charAt(offset))) { + offset++; + length--; + changed = true; + } + if (length > 0 && Character.isWhitespace(getSource().charAt(offset + length - 1))) { + length--; + changed = true; + } + List comments = currentAST.getCommentList(); + // leading comment + int offset1 = offset, length1 = length; + OptionalInt leadingCommentEnd = comments.stream().filter(comment -> { + int commentEndOffset = comment.getStartPosition() + comment.getLength() -1; + return comment.getStartPosition() <= offset1 && commentEndOffset > offset1 && commentEndOffset < offset1 + length1 - 1; + }).mapToInt(comment -> comment.getStartPosition() + comment.getLength() - 1) + .findAny(); + if (length > 0 && leadingCommentEnd.isPresent()) { + changed = true; + int newStart = leadingCommentEnd.getAsInt(); + int removedLeading = newStart + 1 - offset; + offset = newStart + 1; + length -= removedLeading; + } + // Trailing comment + int offset2 = offset, length2 = length; + OptionalInt trailingCommentStart = comments.stream().filter(comment -> { + return comment.getStartPosition() >= offset2 + && comment.getStartPosition() < offset2 + length2 + && comment.getStartPosition() + comment.getLength() > offset2 + length2; + }).mapToInt(Comment::getStartPosition) + .findAny(); + if (length > 0 && trailingCommentStart.isPresent()) { + changed = true; + int newEnd = trailingCommentStart.getAsInt(); + int removedTrailing = offset + length - 1 - newEnd; + length -= removedTrailing; + } + } while (changed); + } + NodeFinder finder = new NodeFinder(currentAST, offset, length); + ASTNode node = finder.getCoveredNode() != null && finder.getCoveringNode().getStartPosition() + finder.getCoveringNode().getLength() > offset + length ? + finder.getCoveredNode() : + finder.getCoveringNode(); + org.eclipse.jdt.core.dom.ImportDeclaration importDecl = findImportDeclaration(node); + if (importDecl != null && importDecl.isStatic()) { + IBinding importBinding = importDecl.resolveBinding(); + if (importBinding instanceof IMethodBinding methodBinding) { + ArrayDeque overloadedMethods = Stream.of(methodBinding.getDeclaringClass().getDeclaredMethods()) // + .filter(otherMethodBinding -> { + return methodBinding.getName().equals(otherMethodBinding.getName()); + }) // + .map(binding -> binding.getJavaElement()) // + .collect(Collectors.toCollection(ArrayDeque::new)); + IJavaElement[] reorderedOverloadedMethods = new IJavaElement[overloadedMethods.size()]; + Iterator reverseIterator = overloadedMethods.descendingIterator(); + for (int i = 0; i < reorderedOverloadedMethods.length; i++) { + reorderedOverloadedMethods[i] = reverseIterator.next(); + } + return reorderedOverloadedMethods; + } + return new IJavaElement[] { importBinding.getJavaElement() }; + } else if (findTypeDeclaration(node) == null) { + IBinding binding = resolveBinding(node); + if (binding != null) { + IJavaElement element = binding.getJavaElement(); + if (element != null) { + return new IJavaElement[] { element }; + } else if (binding instanceof ITypeBinding typeBinding) { + // fallback to calling index, inspired/copied from SelectionEngine + List indexMatch = new ArrayList<>(); + TypeNameMatchRequestor requestor = new TypeNameMatchRequestor() { + @Override + public void acceptTypeNameMatch(org.eclipse.jdt.core.search.TypeNameMatch match) { + indexMatch.add(match.getType()); + } + }; + IJavaSearchScope scope = BasicSearchEngine.createWorkspaceScope(); + new BasicSearchEngine(getOwner()).searchAllTypeNames( + typeBinding.getPackage() != null ? typeBinding.getPackage().getName().toCharArray() : null, + SearchPattern.R_EXACT_MATCH, + typeBinding.getName().toCharArray(), + SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE, + IJavaSearchConstants.TYPE, + scope, + new TypeNameMatchRequestorWrapper(requestor, scope), + IJavaSearchConstants.CANCEL_IF_NOT_READY_TO_SEARCH, + new NullProgressMonitor()); + if (!indexMatch.isEmpty()) { + return indexMatch.toArray(IJavaElement[]::new); + } + } else if (binding instanceof IVariableBinding variableBinding && variableBinding.getDeclaringMethod() != null && variableBinding.getDeclaringMethod().isCompactConstructor()) { + // workaround for JavaSearchBugs15Tests.testBug558812_012 + if (variableBinding.getDeclaringMethod().getJavaElement() instanceof IMethod method) { + Optional parameter = Arrays.stream(method.getParameters()).filter(param -> Objects.equals(param.getElementName(), variableBinding.getName())).findAny(); + if (parameter.isPresent()) { + return new IJavaElement[] { parameter.get() }; + } + } + } + ASTNode bindingNode = currentAST.findDeclaringNode(binding); + if (bindingNode != null) { + IJavaElement parent = getElementAt(bindingNode.getStartPosition()); + if (parent != null && bindingNode instanceof SingleVariableDeclaration variableDecl) { + return new IJavaElement[] { DOMToModelPopulator.toLocalVariable(variableDecl, (JavaElement)parent) }; + } + } + } + } + // fallback: crawl the children of this unit + IJavaElement currentElement = this; + boolean newChildFound; + int finalOffset = offset; + int finalLength = length; + do { + newChildFound = false; + if (currentElement instanceof IParent parentElement) { + Optional candidate = Stream.of(parentElement.getChildren()) + .filter(ISourceReference.class::isInstance) + .map(ISourceReference.class::cast) + .filter(sourceRef -> { + try { + ISourceRange elementRange = sourceRef.getSourceRange(); + return elementRange != null + && elementRange.getOffset() >= 0 + && elementRange.getOffset() <= finalOffset + && elementRange.getOffset() + elementRange.getLength() >= finalOffset + finalLength; + } catch (JavaModelException e) { + return false; + } + }).map(IJavaElement.class::cast) + .findAny(); + if (candidate.isPresent()) { + newChildFound = true; + currentElement = candidate.get(); + } + } + } while (newChildFound); + return currentElement instanceof JavaElement impl && + impl.getElementInfo() instanceof AnnotatableInfo annotable && + annotable.getNameSourceStart() >= 0 && + annotable.getNameSourceStart() <= offset && + annotable.getNameSourceEnd() >= offset ? + new IJavaElement[] { currentElement } : + new IJavaElement[0]; + } else { + return super.codeSelect(this, offset, length, workingCopyOwner); + } +} +static IBinding resolveBinding(ASTNode node) { + if (node instanceof MethodDeclaration decl) { + return decl.resolveBinding(); + } + if (node instanceof MethodInvocation invocation) { + return invocation.resolveMethodBinding(); + } + if (node instanceof VariableDeclaration decl) { + return decl.resolveBinding(); + } + if (node instanceof FieldAccess access) { + return access.resolveFieldBinding(); + } + if (node instanceof Type type) { + return type.resolveBinding(); + } + if (node instanceof Name aName) { + ClassInstanceCreation newInstance = findConstructor(aName); + if (newInstance != null) { + var constructorBinding = newInstance.resolveConstructorBinding(); + if (constructorBinding != null) { + var constructorElement = constructorBinding.getJavaElement(); + if (constructorElement != null) { + boolean hasSource = true; + try { + hasSource = ((ISourceReference)constructorElement.getParent()).getSource() != null; + } catch (Exception e) { + hasSource = false; + } + if ((constructorBinding.getParameterTypes().length > 0 /*non-default*/ || + constructorElement instanceof SourceMethod || !hasSource)) { + return constructorBinding; + } + } else if (newInstance.resolveTypeBinding().isAnonymous()) { + // it's not in the anonymous class body, check for constructor decl in parent types + + ITypeBinding superclassBinding = newInstance.getType().resolveBinding(); + + while (superclassBinding != null) { + Optional potentialConstructor = Stream.of(superclassBinding.getDeclaredMethods()) // + .filter(methodBinding -> { + return methodBinding.isConstructor() && + matchSignatures(constructorBinding, methodBinding); + }) // + .findFirst(); + if (potentialConstructor.isPresent()) { + IMethodBinding theConstructor = potentialConstructor.get(); + if (theConstructor.isDefaultConstructor()) { + return theConstructor.getDeclaringClass(); + } + return theConstructor; + } + superclassBinding = superclassBinding.getSuperclass(); + } + return null; + } + } + } + IBinding res = aName.resolveBinding(); + if (res != null) { + return res; + } + return resolveBinding(aName.getParent()); + } + if (node instanceof org.eclipse.jdt.core.dom.LambdaExpression lambda) { + return lambda.resolveMethodBinding(); + } + if (node instanceof MethodReference methodRef) { + return methodRef.resolveMethodBinding(); + } + if (node instanceof org.eclipse.jdt.core.dom.TypeParameter typeParameter) { + return typeParameter.resolveBinding(); + } + if (node instanceof SuperConstructorInvocation superConstructor) { + return superConstructor.resolveConstructorBinding(); + } + if (node instanceof ConstructorInvocation constructor) { + return constructor.resolveConstructorBinding(); + } + if (node instanceof org.eclipse.jdt.core.dom.Annotation annotation) { + return annotation.resolveTypeBinding(); + } + return null; +} + +private static ClassInstanceCreation findConstructor(ASTNode node) { + while (node != null && !(node instanceof ClassInstanceCreation)) { + ASTNode parent = node.getParent(); + if ((parent instanceof SimpleType type && type.getName() == node) || + (parent instanceof ClassInstanceCreation constructor && constructor.getType() == node) || + (parent instanceof ParameterizedType parameterized && parameterized.getType() == node)) { + node = parent; + } else { + node = null; + } + } + return (ClassInstanceCreation)node; } + +private static AbstractTypeDeclaration findTypeDeclaration(ASTNode node) { + ASTNode cursor = node; + while (cursor != null && (cursor instanceof Type || cursor instanceof Name)) { + cursor = cursor.getParent(); + } + if (cursor instanceof AbstractTypeDeclaration typeDecl && typeDecl.getName() == node) { + return typeDecl; + } + return null; +} + +private static org.eclipse.jdt.core.dom.ImportDeclaration findImportDeclaration(ASTNode node) { + while (node != null && !(node instanceof org.eclipse.jdt.core.dom.ImportDeclaration)) { + node = node.getParent(); + } + return (org.eclipse.jdt.core.dom.ImportDeclaration)node; +} + +private static boolean matchSignatures(IMethodBinding invocation, IMethodBinding declaration) { + if (declaration.getTypeParameters().length == 0) { + return invocation.isSubsignature(declaration); + } + if (invocation.getParameterTypes().length != declaration.getParameterTypes().length) { + return false; + } + for (int i = 0; i < invocation.getParameterTypes().length; i++) { + if (declaration.getParameterTypes()[i].isTypeVariable()) { + if (declaration.getParameterTypes()[i].getTypeBounds().length > 0) { + ITypeBinding[] bounds = declaration.getParameterTypes()[i].getTypeBounds(); + for (int j = 0; j < bounds.length; j++) { + if (!invocation.getParameterTypes()[i].isSubTypeCompatible(bounds[j])) { + return false; + } + } + } + } else if (!invocation.getParameterTypes()[i].isSubTypeCompatible(declaration.getParameterTypes()[i])) { + return false; + } + + } + return true; +} + + +private org.eclipse.jdt.core.dom.CompilationUnit getOrBuildAST(WorkingCopyOwner workingCopyOwner) { + // https://github.com/eclipse-jdtls/eclipse-jdt-core-incubator/pull/133 + // we should find some better condition than this as we would like to avoid calling twice this method + // on the same unsaved working copy does re-create an AST every time + boolean createAst = this.ast == null; + if (!createAst) { + try { + createAst = this.isWorkingCopy() && this.hasUnsavedChanges(); + } catch (JavaModelException e) { + createAst = true; + } + } + if (createAst) { + ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); // TODO use Java project info + parser.setWorkingCopyOwner(workingCopyOwner); + parser.setSource(this); + // greedily enable everything assuming the AST will be used extensively for edition + parser.setResolveBindings(true); + parser.setStatementsRecovery(true); + parser.setBindingsRecovery(true); + parser.setCompilerOptions(getOptions(true)); + if (parser.createAST(null) instanceof org.eclipse.jdt.core.dom.CompilationUnit newAST) { + this.ast = newAST; + } + } + return this.ast; +} + + /** * @see IWorkingCopy#commit(boolean, IProgressMonitor) * @deprecated @@ -1137,7 +1576,7 @@ public org.eclipse.jdt.core.dom.CompilationUnit makeConsistent(int astLevel, boo openWhenClosed(info, true, monitor); org.eclipse.jdt.core.dom.CompilationUnit result = info.ast; info.ast = null; - return result; + return astLevel != NO_AST ? result : null; } else { openWhenClosed(createElementInfo(), true, monitor); return null; @@ -1455,6 +1894,17 @@ public void setOptions(Map newOptions) { public Map getCustomOptions() { try { Map customOptions = this.getCompilationUnitElementInfo().getCustomOptions(); + IJavaProject parentProject = getJavaProject(); + Map parentOptions = parentProject == null ? JavaCore.getOptions() : parentProject.getOptions(true); + if (JavaCore.ENABLED.equals(parentOptions.get(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES)) && + AST.newAST(parentOptions).apiLevel() != AST.getJLSLatest()) { + // Disable preview features for older Java releases as it causes the compiler to fail later + if (customOptions != null) { + customOptions.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.DISABLED); + } else { + customOptions = Map.of(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.DISABLED); + } + } return customOptions == null ? Collections.emptyMap() : customOptions; } catch (JavaModelException e) { // do nothing diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMCompletionEngine.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMCompletionEngine.java new file mode 100644 index 00000000000..a1923cda9a5 --- /dev/null +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMCompletionEngine.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.jdt.internal.core; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.CompletionContext; +import org.eclipse.jdt.core.CompletionProposal; +import org.eclipse.jdt.core.CompletionRequestor; +import org.eclipse.jdt.core.Signature; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FieldAccess; +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.MethodDeclaration; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.jdt.core.dom.Statement; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.VariableDeclarationStatement; + +class DOMCompletionEngine implements Runnable { + + private final int offset; + private final CompilationUnit unit; + private CompletionRequestor requestor; + + DOMCompletionEngine(int offset, CompilationUnit unit, CompletionRequestor requestor, IProgressMonitor monitor) { + this.offset = offset; + this.unit = unit; + this.requestor = requestor; + } + + private static Collection visibleBindings(ASTNode node, int offset) { + if (node instanceof Block block) { + return ((List)block.statements()).stream() + .filter(statement -> statement.getStartPosition() < offset) + .filter(VariableDeclarationStatement.class::isInstance) + .map(VariableDeclarationStatement.class::cast) + .flatMap(decl -> ((List)decl.fragments()).stream()) + .map(VariableDeclarationFragment::resolveBinding) + .toList(); + } else if (node instanceof MethodDeclaration method) { + return Stream.of((List)method.parameters(), (List)method.typeParameters()) + .flatMap(List::stream) + .map(org.eclipse.jdt.internal.core.CompilationUnit::resolveBinding) + .filter(Objects::nonNull) + .toList(); + } else if (node instanceof TypeDeclaration type) { + VariableDeclarationFragment[] fields = Arrays.stream(type.getFields()) + .map(decl -> (List)decl.fragments()) + .flatMap(List::stream) + .toArray(VariableDeclarationFragment[]::new); + return Stream.of(fields, type.getMethods(), type.getTypes()) + .flatMap(Arrays::stream) + .map(org.eclipse.jdt.internal.core.CompilationUnit::resolveBinding) + .filter(Objects::nonNull) + .toList(); + } + return List.of(); + } + + @Override + public void run() { + this.requestor.beginReporting(); + this.requestor.acceptContext(new CompletionContext()); + ASTNode toComplete = NodeFinder.perform(this.unit, this.offset, 0); + if (toComplete instanceof FieldAccess fieldAccess) { + processMembers(fieldAccess.resolveTypeBinding()); + } else if (toComplete.getParent() instanceof FieldAccess fieldAccess) { + processMembers(fieldAccess.getExpression().resolveTypeBinding()); + } + Collection scope = new HashSet<>(); + ASTNode current = toComplete; + while (current != null) { + scope.addAll(visibleBindings(current, this.offset)); + current = current.getParent(); + } + // TODO also include other visible content: classpath, static methods... + scope.stream().map(this::toProposal).forEach(this.requestor::accept); + this.requestor.endReporting(); + } + + private void processMembers(ITypeBinding typeBinding) { + if (typeBinding == null) { + return; + } + Arrays.stream(typeBinding.getDeclaredFields()).map(this::toProposal).forEach(this.requestor::accept); + Arrays.stream(typeBinding.getDeclaredMethods()).map(this::toProposal).forEach(this.requestor::accept); + if (typeBinding.getInterfaces() != null) { + Arrays.stream(typeBinding.getInterfaces()).forEach(this::processMembers); + } + processMembers(typeBinding.getSuperclass()); + } + + private CompletionProposal toProposal(IBinding binding) { + int kind = + binding instanceof ITypeBinding ? CompletionProposal.TYPE_REF : + binding instanceof IMethodBinding ? CompletionProposal.METHOD_REF : + binding instanceof IVariableBinding variableBinding ? CompletionProposal.LOCAL_VARIABLE_REF : + -1; + CompletionProposal res = new CompletionProposal() { + @Override + public int getKind() { + return kind; + } + @Override + public char[] getName() { + return binding.getName().toCharArray(); + } + @Override + public char[] getCompletion() { + return binding.getName().toCharArray(); + } + @Override + public char[] getSignature() { + if (binding instanceof IMethodBinding methodBinding) { + return Signature.createMethodSignature( + Arrays.stream(methodBinding.getParameterTypes()) + .map(ITypeBinding::getName) + .map(String::toCharArray) + .map(type -> Signature.createTypeSignature(type, true).toCharArray()) + .toArray(char[][]::new), + Signature.createTypeSignature(methodBinding.getReturnType().getQualifiedName().toCharArray(), true).toCharArray()); + } + if (binding instanceof IVariableBinding variableBinding) { + return Signature.createTypeSignature(variableBinding.getType().getQualifiedName().toCharArray(), true).toCharArray(); + } + if (binding instanceof ITypeBinding typeBinding) { + return Signature.createTypeSignature(typeBinding.getQualifiedName().toCharArray(), true).toCharArray(); + } + return new char[] {}; + } + @Override + public int getReplaceStart() { + return DOMCompletionEngine.this.offset; + } + @Override + public int getReplaceEnd() { + return getReplaceStart(); + } + @Override + public int getFlags() { + return 0; //TODO + } + @Override + public char[] getReceiverSignature() { + if (binding instanceof IMethodBinding method) { + return Signature.createTypeSignature(method.getDeclaredReceiverType().getQualifiedName().toCharArray(), true).toCharArray(); + } + if (binding instanceof IVariableBinding variable && variable.isField()) { + return Signature.createTypeSignature(variable.getDeclaringClass().getQualifiedName().toCharArray(), true).toCharArray(); + } + return new char[]{}; + } + @Override + public char[] getDeclarationSignature() { + if (binding instanceof IMethodBinding method) { + return Signature.createTypeSignature(method.getDeclaringClass().getQualifiedName().toCharArray(), true).toCharArray(); + } + if (binding instanceof IVariableBinding variable && variable.isField()) { + return Signature.createTypeSignature(variable.getDeclaringClass().getQualifiedName().toCharArray(), true).toCharArray(); + } + return new char[]{}; + } + }; + return res; + } + +} diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMToModelPopulator.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMToModelPopulator.java new file mode 100644 index 00000000000..c1b4bf9d63b --- /dev/null +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DOMToModelPopulator.java @@ -0,0 +1,1179 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.jdt.internal.core; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.Stack; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IAnnotation; +import org.eclipse.jdt.core.IImportDeclaration; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IMemberValuePair; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.Signature; +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.ASTVisitor; +import org.eclipse.jdt.core.dom.AbstractTagElement; +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.ArrayInitializer; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.BooleanLiteral; +import org.eclipse.jdt.core.dom.CharacterLiteral; +import org.eclipse.jdt.core.dom.ClassInstanceCreation; +import org.eclipse.jdt.core.dom.Comment; +import org.eclipse.jdt.core.dom.CreationReference; +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.FieldDeclaration; +import org.eclipse.jdt.core.dom.IExtendedModifier; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.Initializer; +import org.eclipse.jdt.core.dom.LambdaExpression; +import org.eclipse.jdt.core.dom.MarkerAnnotation; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.ModuleDeclaration; +import org.eclipse.jdt.core.dom.ModuleModifier; +import org.eclipse.jdt.core.dom.ModulePackageAccess; +import org.eclipse.jdt.core.dom.Name; +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.PackageDeclaration; +import org.eclipse.jdt.core.dom.ParameterizedType; +import org.eclipse.jdt.core.dom.PrefixExpression; +import org.eclipse.jdt.core.dom.ProvidesDirective; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.RecordDeclaration; +import org.eclipse.jdt.core.dom.RequiresDirective; +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.StringLiteral; +import org.eclipse.jdt.core.dom.SuperMethodReference; +import org.eclipse.jdt.core.dom.TagElement; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.TypeLiteral; +import org.eclipse.jdt.core.dom.TypeMethodReference; +import org.eclipse.jdt.core.dom.UsesDirective; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.env.IElementInfo; +import org.eclipse.jdt.internal.compiler.lookup.TypeConstants; +import org.eclipse.jdt.internal.compiler.parser.RecoveryScanner; +import org.eclipse.jdt.internal.compiler.parser.Scanner; +import org.eclipse.jdt.internal.compiler.parser.TerminalTokens; +import org.eclipse.jdt.internal.core.ModuleDescriptionInfo.ModuleReferenceInfo; +import org.eclipse.jdt.internal.core.ModuleDescriptionInfo.PackageExportInfo; +import org.eclipse.jdt.internal.core.ModuleDescriptionInfo.ServiceInfo; +import org.eclipse.jdt.internal.core.util.Util; + +/** + * Process an AST to populate a tree of IJavaElement->JavaElementInfo. + * DOM-first approach to what legacy implements through ECJ parser and CompilationUnitStructureRequestor + */ +class DOMToModelPopulator extends ASTVisitor { + + private final Map toPopulate; + private final Stack elements = new Stack<>(); + private final Stack infos = new Stack<>(); + private final Set currentTypeParameters = new HashSet<>(); + private final CompilationUnitElementInfo unitInfo; + private ImportContainer importContainer; + private ImportContainerInfo importContainerInfo; + private final CompilationUnit root; + private Boolean alternativeDeprecated = null; + + public DOMToModelPopulator(Map newElements, CompilationUnit root, CompilationUnitElementInfo unitInfo) { + this.toPopulate = newElements; + this.elements.push(root); + this.infos.push(unitInfo); + this.root = root; + this.unitInfo = unitInfo; + } + + private static void addAsChild(JavaElementInfo parentInfo, IJavaElement childElement) { + if (childElement instanceof SourceRefElement element) { + while (Stream.of(parentInfo.getChildren()) + .filter(other -> other.getElementType() == element.getElementType()) + .filter(other -> Objects.equals(other.getHandleIdentifier(), element.getHandleIdentifier())) + .findAny().isPresent()) { + element.incOccurrenceCount(); + } + } + if (parentInfo instanceof AnnotatableInfo annotable && childElement instanceof IAnnotation annotation) { + if (Stream.of(annotable.annotations).noneMatch(annotation::equals)) { + IAnnotation[] newAnnotations = Arrays.copyOf(annotable.annotations, annotable.annotations.length + 1); + newAnnotations[newAnnotations.length - 1] = annotation; + annotable.annotations = newAnnotations; + } + return; + } + if (childElement instanceof TypeParameter typeParam) { + if (parentInfo instanceof SourceTypeElementInfo type) { + type.typeParameters = Arrays.copyOf(type.typeParameters, type.typeParameters.length + 1); + type.typeParameters[type.typeParameters.length - 1] = typeParam; + return; + } + if (parentInfo instanceof SourceMethodElementInfo method) { + method.typeParameters = Arrays.copyOf(method.typeParameters, method.typeParameters.length + 1); + method.typeParameters[method.typeParameters.length - 1] = typeParam; + return; + } + } + if (parentInfo instanceof ImportContainerInfo importContainer && childElement instanceof org.eclipse.jdt.internal.core.ImportDeclaration importDecl) { + IJavaElement[] newImports = Arrays.copyOf(importContainer.getChildren(), importContainer.getChildren().length + 1); + newImports[newImports.length - 1] = importDecl; + importContainer.children = newImports; + return; + } + // if nothing more specialized, add as child + if (parentInfo instanceof SourceTypeElementInfo type) { + type.children = Arrays.copyOf(type.children, type.children.length + 1); + type.children[type.children.length - 1] = childElement; + return; + } + if (parentInfo instanceof OpenableElementInfo openable) { + openable.addChild(childElement); + return; + } + if (parentInfo instanceof SourceMethodWithChildrenInfo method) { + IJavaElement[] newElements = Arrays.copyOf(method.children, method.children.length + 1); + newElements[newElements.length - 1] = childElement; + method.children = newElements; + return; + } + if (parentInfo instanceof SourceFieldWithChildrenInfo field) { + IJavaElement[] newElements = Arrays.copyOf(field.children, field.children.length + 1); + newElements[newElements.length - 1] = childElement; + field.children = newElements; + return; + } + if (parentInfo instanceof SourceConstructorWithChildrenInfo constructor) { + IJavaElement[] newElements = Arrays.copyOf(constructor.children, constructor.children.length + 1); + newElements[newElements.length - 1] = childElement; + constructor.children = newElements; + return; + } + if (parentInfo instanceof InitializerWithChildrenInfo info) { + IJavaElement[] newElements = Arrays.copyOf(info.getChildren(), info.getChildren().length + 1); + newElements[newElements.length - 1] = childElement; + info.children = newElements; + return; + } + } + + @Override + public boolean visit(org.eclipse.jdt.core.dom.CompilationUnit node) { + this.unitInfo.setSourceLength(node.getLength()); + return true; + } + + @Override + public boolean visit(PackageDeclaration node) { + org.eclipse.jdt.internal.core.PackageDeclaration newElement = new org.eclipse.jdt.internal.core.PackageDeclaration(this.root, node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + AnnotatableInfo newInfo = new AnnotatableInfo(); + setSourceRange(newInfo, node); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(PackageDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(ImportDeclaration node) { + if (this.importContainer == null) { + this.importContainer = this.root.getImportContainer(); + this.importContainerInfo = new ImportContainerInfo(); + JavaElementInfo parentInfo = this.infos.peek(); + addAsChild(parentInfo, this.importContainer); + this.toPopulate.put(this.importContainer, this.importContainerInfo); + } + org.eclipse.jdt.internal.core.ImportDeclaration newElement = new org.eclipse.jdt.internal.core.ImportDeclaration(this.importContainer, node.getName().toString(), node.isOnDemand()); + this.elements.push(newElement); + addAsChild(this.importContainerInfo, newElement); + ImportDeclarationElementInfo newInfo = new ImportDeclarationElementInfo(); + setSourceRange(newInfo, node); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + int nameSourceEnd = node.getName().getStartPosition() + node.getName().getLength() - 1; + if (node.isOnDemand()) { + nameSourceEnd = node.getStartPosition() + node.getLength() - 1; + char[] contents = this.root.getContents(); + List comments = domUnit(node).getCommentList(); + boolean changed = false; + do { + while (contents[nameSourceEnd] == ';' || Character.isWhitespace(contents[nameSourceEnd])) { + nameSourceEnd--; + changed = true; + } + final int currentEnd = nameSourceEnd; + int newEnd = comments.stream() + .filter(comment -> comment.getStartPosition() <= currentEnd && comment.getStartPosition() + comment.getLength() >= currentEnd) + .findAny() + .map(comment -> comment.getStartPosition() - 1) + .orElse(currentEnd); + changed = (currentEnd != newEnd); + nameSourceEnd = newEnd; + } while (changed); + } + newInfo.setNameSourceEnd(nameSourceEnd); + newInfo.setFlags(node.isStatic() ? Flags.AccStatic : 0); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(ImportDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(TypeDeclaration node) { + if (node.getAST().apiLevel() > 2) { + ((List)node.typeParameters()) + .stream() + .map(org.eclipse.jdt.core.dom.TypeParameter::getName) + .map(Name::getFullyQualifiedName) + .forEach(this.currentTypeParameters::add); + } + SourceType newElement = new SourceType(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceTypeElementInfo newInfo = new SourceTypeElementInfo(); + boolean isDeprecated = isNodeDeprecated(node); + char[][] categories = getCategories(node); + newInfo.addCategories(newElement, categories); + JavaElementInfo toPopulateCategories = this.infos.peek(); + while (toPopulateCategories != null) { + if (toPopulateCategories instanceof SourceTypeElementInfo parentTypeInfo) { + parentTypeInfo.addCategories(newElement, categories); + toPopulateCategories = (JavaElementInfo)parentTypeInfo.getEnclosingType(); + } else { + break; + } + } + if (node.getAST().apiLevel() > 2) { + newInfo.setSuperInterfaceNames(((List)node.superInterfaceTypes()).stream().map(Type::toString).map(String::toCharArray).toArray(char[][]::new)); + } + if (node.getAST().apiLevel() > 2 && node.getSuperclassType() != null) { + newInfo.setSuperclassName(node.getSuperclassType().toString().toCharArray()); + } + if (node.getAST().apiLevel() >= AST.JLS17) { + newInfo.setPermittedSubtypeNames(((List)node.permittedTypes()).stream().map(Type::toString).map(String::toCharArray).toArray(char[][]::new)); + } + setSourceRange(newInfo, node); + newInfo.setFlags(toModelFlags(node.getModifiers(), isDeprecated) | (node.isInterface() ? Flags.AccInterface : 0)); + + newInfo.setHandle(newElement); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(TypeDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + if (decl.getAST().apiLevel() > 2) { + ((List)decl.typeParameters()) + .stream() + .map(org.eclipse.jdt.core.dom.TypeParameter::getName) + .map(Name::getFullyQualifiedName) + .forEach(this.currentTypeParameters::remove); + } + } + + @Override + public boolean visit(AnnotationTypeDeclaration node) { + SourceType newElement = new SourceType(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceTypeElementInfo newInfo = new SourceTypeElementInfo(); + setSourceRange(newInfo, node); + char[][] categories = getCategories(node); + newInfo.addCategories(newElement, categories); + JavaElementInfo toPopulateCategories = this.infos.peek(); + while (toPopulateCategories != null) { + if (toPopulateCategories instanceof SourceTypeElementInfo parentTypeInfo) { + parentTypeInfo.addCategories(newElement, categories); + toPopulateCategories = (JavaElementInfo)parentTypeInfo.getEnclosingType(); + } else { + break; + } + } + boolean isDeprecated = isNodeDeprecated(node); + newInfo.setFlags(toModelFlags(node.getModifiers(), isDeprecated) | Flags.AccInterface | Flags.AccAnnotation); + newInfo.setHandle(newElement); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + + @Override + public void endVisit(AnnotationTypeDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(EnumDeclaration node) { + SourceType newElement = new SourceType(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceTypeElementInfo newInfo = new SourceTypeElementInfo(); + setSourceRange(newInfo, node); + char[][] categories = getCategories(node); + newInfo.addCategories(newElement, categories); + JavaElementInfo toPopulateCategories = this.infos.peek(); + while (toPopulateCategories != null) { + if (toPopulateCategories instanceof SourceTypeElementInfo parentTypeInfo) { + parentTypeInfo.addCategories(newElement, categories); + toPopulateCategories = (JavaElementInfo)parentTypeInfo.getEnclosingType(); + } else { + break; + } + } + boolean isDeprecated = isNodeDeprecated(node); + newInfo.setFlags(toModelFlags(node.getModifiers(), isDeprecated) | Flags.AccEnum); + newInfo.setHandle(newElement); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + newInfo.setSuperInterfaceNames(((List)node.superInterfaceTypes()).stream().map(Type::toString).map(String::toCharArray).toArray(char[][]::new)); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(EnumDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(EnumConstantDeclaration node) { + IJavaElement parent = this.elements.peek(); + SourceField newElement = new SourceField(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceFieldWithChildrenInfo info = new SourceFieldWithChildrenInfo(new IJavaElement[0]); + info.setTypeName(parent.getElementName().toCharArray()); + setSourceRange(info, node); + boolean isDeprecated = isNodeDeprecated(node); + info.setFlags(toModelFlags(node.getModifiers(), isDeprecated) | ClassFileConstants.AccEnum); + info.setNameSourceStart(node.getName().getStartPosition()); + info.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + // TODO populate info + this.infos.push(info); + this.toPopulate.put(newElement, info); + return true; + } + @Override + public void endVisit(EnumConstantDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(RecordDeclaration node) { + SourceType newElement = new SourceType(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceTypeElementInfo newInfo = new SourceTypeElementInfo(); + setSourceRange(newInfo, node); + char[][] categories = getCategories(node); + newInfo.addCategories(newElement, categories); + newInfo.setSuperclassName(Record.class.getName().toCharArray()); + newInfo.setSuperInterfaceNames(((List)node.superInterfaceTypes()).stream().map(Type::toString).map(String::toCharArray).toArray(char[][]::new)); + JavaElementInfo toPopulateCategories = this.infos.peek(); + while (toPopulateCategories != null) { + if (toPopulateCategories instanceof SourceTypeElementInfo parentTypeInfo) { + parentTypeInfo.addCategories(newElement, categories); + toPopulateCategories = (JavaElementInfo)parentTypeInfo.getEnclosingType(); + } else { + break; + } + } + boolean isDeprecated = isNodeDeprecated(node); + newInfo.setFlags(toModelFlags(node.getModifiers(), isDeprecated) | Flags.AccRecord); + newInfo.setHandle(newElement); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(RecordDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(SingleVariableDeclaration node) { + if (node.getParent() instanceof RecordDeclaration) { + SourceField newElement = new SourceField(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceFieldElementInfo newInfo = new SourceFieldElementInfo(); + setSourceRange(newInfo, node); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + newInfo.setTypeName(node.getType().toString().toCharArray()); + newInfo.isRecordComponent = true; + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + } + return true; + } + @Override + public void endVisit(SingleVariableDeclaration decl) { + if (decl.getParent() instanceof RecordDeclaration recordDecl) { + this.elements.pop(); + this.infos.pop(); + } + } + + @Override + public boolean visit(MethodDeclaration method) { + if (method.getAST().apiLevel() > 2) { + ((List)method.typeParameters()) + .stream() + .map(org.eclipse.jdt.core.dom.TypeParameter::getName) + .map(Name::getFullyQualifiedName) + .forEach(this.currentTypeParameters::add); + } + List parameters = method.parameters(); + if (method.getAST().apiLevel() >= AST.JLS16 + && method.isCompactConstructor() + && (parameters == null || parameters.isEmpty()) + && method.getParent() instanceof RecordDeclaration parentRecord) { + parameters = parentRecord.recordComponents(); + } + SourceMethod newElement = new SourceMethod(this.elements.peek(), + method.getName().getIdentifier(), + parameters.stream() + .map(this::createSignature) + .toArray(String[]::new)); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceMethodElementInfo info = method.isConstructor() ? + new SourceConstructorWithChildrenInfo(new IJavaElement[0]) : + new SourceMethodWithChildrenInfo(new IJavaElement[0]); + info.setArgumentNames(parameters.stream().map(param -> param.getName().toString().toCharArray()).toArray(char[][]::new)); + info.arguments = parameters.stream().map(param -> toLocalVariable(param, newElement)).toArray(LocalVariable[]::new); + if (method.getAST().apiLevel() > 2) { + if (method.getReturnType2() != null) { + info.setReturnType(method.getReturnType2().toString().toCharArray()); + } else { + info.setReturnType("void".toCharArray()); //$NON-NLS-1$ + } + } + if (this.infos.peek() instanceof SourceTypeElementInfo parentInfo) { + parentInfo.addCategories(newElement, getCategories(method)); + } + if (method.getAST().apiLevel() >= AST.JLS8) { + info.setExceptionTypeNames(((List)method.thrownExceptionTypes()).stream().map(Type::toString).map(String::toCharArray).toArray(char[][]::new)); + } + setSourceRange(info, method); + boolean isDeprecated = isNodeDeprecated(method); + info.setFlags(toModelFlags(method.getModifiers(), isDeprecated) + | ((method.getAST().apiLevel() > AST.JLS2 && ((List)method.parameters()).stream().anyMatch(SingleVariableDeclaration::isVarargs)) ? Flags.AccVarargs : 0)); + info.setNameSourceStart(method.getName().getStartPosition()); + info.setNameSourceEnd(method.getName().getStartPosition() + method.getName().getLength() - 1); + this.infos.push(info); + this.toPopulate.put(newElement, info); + return true; + } + @Override + public void endVisit(MethodDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + if (decl.getAST().apiLevel() > 2) { + ((List)decl.typeParameters()) + .stream() + .map(org.eclipse.jdt.core.dom.TypeParameter::getName) + .map(Name::getFullyQualifiedName) + .forEach(this.currentTypeParameters::remove); + } + } + + @Override + public boolean visit(AnnotationTypeMemberDeclaration method) { + SourceMethod newElement = new SourceMethod(this.elements.peek(), + method.getName().getIdentifier(), + new String[0]); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceAnnotationMethodInfo info = new SourceAnnotationMethodInfo(); + info.setReturnType(method.getType().toString().toCharArray()); + setSourceRange(info, method); + ((SourceTypeElementInfo)this.infos.peek()).addCategories(newElement, getCategories(method)); + boolean isDeprecated = isNodeDeprecated(method); + info.setFlags(toModelFlags(method.getModifiers(), isDeprecated)); + info.setNameSourceStart(method.getName().getStartPosition()); + info.setNameSourceEnd(method.getName().getStartPosition() + method.getName().getLength() - 1); + Expression defaultExpr = method.getDefault(); + if (defaultExpr != null) { + Entry value = memberValue(defaultExpr); + org.eclipse.jdt.internal.core.MemberValuePair mvp = new org.eclipse.jdt.internal.core.MemberValuePair(newElement.getElementName(), value.getKey(), value.getValue()); + info.defaultValue = mvp; + info.defaultValueStart = defaultExpr.getStartPosition(); + info.defaultValueEnd = defaultExpr.getStartPosition() + defaultExpr.getLength(); + } + this.infos.push(info); + this.toPopulate.put(newElement, info); + return true; + } + @Override + public void endVisit(AnnotationTypeMemberDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(org.eclipse.jdt.core.dom.TypeParameter node) { + TypeParameter newElement = new TypeParameter(this.elements.peek(), node.getName().getFullyQualifiedName()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + TypeParameterElementInfo info = new TypeParameterElementInfo(); + setSourceRange(info, node); + info.nameStart = node.getName().getStartPosition(); + info.nameEnd = node.getName().getStartPosition() + node.getName().getLength() - 1; + info.bounds = ((List)node.typeBounds()).stream().map(Type::toString).map(String::toCharArray).toArray(char[][]::new); + info.boundsSignatures = ((List)node.typeBounds()).stream().map(Util::getSignature).map(String::toCharArray).toArray(char[][]::new); + this.infos.push(info); + this.toPopulate.put(newElement, info); + return true; + } + @Override + public void endVisit(org.eclipse.jdt.core.dom.TypeParameter typeParam) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(NormalAnnotation node) { + Annotation newElement = new Annotation(this.elements.peek(), node.getTypeName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + AnnotationInfo newInfo = new AnnotationInfo(); + setSourceRange(newInfo, node); + newInfo.nameStart = node.getTypeName().getStartPosition(); + newInfo.nameEnd = node.getTypeName().getStartPosition() + node.getTypeName().getLength() - 1; + newInfo.members = ((List)node.values()) + .stream() + .map(domMemberValuePair -> { + Entry value = memberValue(domMemberValuePair.getValue()); + return new org.eclipse.jdt.internal.core.MemberValuePair(domMemberValuePair.getName().toString(), value.getKey(), value.getValue()); + }) + .toArray(IMemberValuePair[]::new); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(NormalAnnotation decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(MarkerAnnotation node) { + Annotation newElement = new Annotation(this.elements.peek(), node.getTypeName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + AnnotationInfo newInfo = new AnnotationInfo(); + setSourceRange(newInfo, node); + newInfo.nameStart = node.getTypeName().getStartPosition(); + newInfo.nameEnd = node.getTypeName().getStartPosition() + node.getTypeName().getLength() - 1; + newInfo.members = new IMemberValuePair[0]; + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(MarkerAnnotation decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(SingleMemberAnnotation node) { + Annotation newElement = new Annotation(this.elements.peek(), node.getTypeName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + AnnotationInfo newInfo = new AnnotationInfo(); + setSourceRange(newInfo, node); + newInfo.nameStart = node.getTypeName().getStartPosition(); + newInfo.nameEnd = node.getTypeName().getStartPosition() + node.getTypeName().getLength() - 1; + Entry value = memberValue(node.getValue()); + newInfo.members = new IMemberValuePair[] { new org.eclipse.jdt.internal.core.MemberValuePair("value", value.getKey(), value.getValue()) }; //$NON-NLS-1$ + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(SingleMemberAnnotation decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(AnonymousClassDeclaration decl) { + SourceType newElement = new SourceType(this.elements.peek(), ""); //$NON-NLS-1$ + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + SourceTypeElementInfo newInfo = new SourceTypeElementInfo() { + @Override + public boolean isAnonymousMember() { + return true; + } + }; + JavaElementInfo toPopulateCategories = this.infos.peek(); + while (toPopulateCategories != null) { + if (toPopulateCategories instanceof SourceTypeElementInfo parentTypeInfo) { + toPopulateCategories = (JavaElementInfo)parentTypeInfo.getEnclosingType(); + } else { + break; + } + } + newInfo.setSourceRangeEnd(decl.getStartPosition() + decl.getLength() - 1); + newInfo.setHandle(newElement); + setSourceRange(newInfo, decl); + if (decl.getParent() instanceof ClassInstanceCreation constructorInvocation) { + if (constructorInvocation.getAST().apiLevel() > 2) { + ((List)constructorInvocation.typeArguments()) + .stream() + .map(SimpleType::getName) + .map(Name::getFullyQualifiedName) + .forEach(this.currentTypeParameters::add); + Type type = constructorInvocation.getType(); + newInfo.setSuperclassName(type.toString().toCharArray()); + newInfo.setNameSourceStart(type.getStartPosition()); + // TODO consider leading comments just like in setSourceRange(newInfo, node); + newInfo.setSourceRangeStart(constructorInvocation.getStartPosition()); + int length; + if (type instanceof ParameterizedType pType) { + length= pType.getType().getLength(); + } else { + length = type.getLength(); + } + newInfo.setNameSourceEnd(type.getStartPosition() + length - 1); + } else { + newInfo.setNameSourceStart(constructorInvocation.getName().getStartPosition()); + newInfo.setSourceRangeStart(constructorInvocation.getName().getStartPosition()); + newInfo.setNameSourceEnd(constructorInvocation.getName().getStartPosition() + constructorInvocation.getName().getLength() - 1); + } + } + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(AnonymousClassDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + if (decl.getParent() instanceof ClassInstanceCreation constructorInvocation) { + if (constructorInvocation.getAST().apiLevel() > 2) { + ((List)constructorInvocation.typeArguments()) + .stream() + .map(SimpleType::getName) + .map(Name::getFullyQualifiedName) + .forEach(this.currentTypeParameters::remove); + } + } + } + + public Entry memberValue(Expression dom) { + if (dom == null || + dom instanceof NullLiteral nullLiteral || + (dom instanceof SimpleName name && ( + "MISSING".equals(name.getIdentifier()) || //$NON-NLS-1$ // better compare with internal SimpleName.MISSING + Arrays.equals(RecoveryScanner.FAKE_IDENTIFIER, name.getIdentifier().toCharArray())))) { + return new SimpleEntry<>(null, IMemberValuePair.K_UNKNOWN); + } + if (dom instanceof StringLiteral stringValue) { + return new SimpleEntry<>(stringValue.getLiteralValue(), IMemberValuePair.K_STRING); + } + if (dom instanceof BooleanLiteral booleanValue) { + return new SimpleEntry<>(booleanValue.booleanValue(), IMemberValuePair.K_BOOLEAN); + } + if (dom instanceof CharacterLiteral charValue) { + return new SimpleEntry<>(charValue.charValue(), IMemberValuePair.K_CHAR); + } + if (dom instanceof TypeLiteral typeLiteral) { + return new SimpleEntry<>(typeLiteral.getType(), IMemberValuePair.K_CLASS); + } + if (dom instanceof SimpleName simpleName) { + return new SimpleEntry<>(simpleName.toString(), IMemberValuePair.K_SIMPLE_NAME); + } + if (dom instanceof QualifiedName qualifiedName) { + return new SimpleEntry<>(qualifiedName.toString(), IMemberValuePair.K_QUALIFIED_NAME); + } + if (dom instanceof org.eclipse.jdt.core.dom.Annotation annotation) { + return new SimpleEntry<>(toModelAnnotation(annotation, null), IMemberValuePair.K_ANNOTATION); + } + if (dom instanceof ArrayInitializer arrayInitializer) { + var values = ((List)arrayInitializer.expressions()).stream().map(this::memberValue).toList(); + var types = values.stream().map(Entry::getValue).distinct().toList(); + return new SimpleEntry<>(values.stream().map(Entry::getKey).toArray(), types.size() == 1 ? types.get(0) : IMemberValuePair.K_UNKNOWN); + } + if (dom instanceof NumberLiteral number) { + String token = number.getToken(); + int type = toAnnotationValuePairType(token); + Object value = token; + if ((type == IMemberValuePair.K_LONG && token.endsWith("L")) || //$NON-NLS-1$ + (type == IMemberValuePair.K_FLOAT && token.endsWith("f"))) { //$NON-NLS-1$ + value = token.substring(0, token.length() - 1); + } + if (value instanceof String valueString) { + // I tried using `yield`, but this caused ECJ to throw an AIOOB, preventing compilation + switch (type) { + case IMemberValuePair.K_INT: { + try { + value = Integer.parseInt(valueString); + } catch (NumberFormatException e) { + type = IMemberValuePair.K_LONG; + value = Long.parseLong(valueString); + } + break; + } + case IMemberValuePair.K_LONG: value = Long.parseLong(valueString); break; + case IMemberValuePair.K_SHORT: value = Short.parseShort(valueString); break; + case IMemberValuePair.K_BYTE: value = Byte.parseByte(valueString); break; + case IMemberValuePair.K_FLOAT: value = Float.parseFloat(valueString); break; + case IMemberValuePair.K_DOUBLE: value = Double.parseDouble(valueString); break; + default: throw new IllegalArgumentException("Type not (yet?) supported"); //$NON-NLS-1$ + } + } + return new SimpleEntry<>(value, type); + } + if (dom instanceof PrefixExpression prefixExpression) { + Expression operand = prefixExpression.getOperand(); + if (!(operand instanceof NumberLiteral) && !(operand instanceof BooleanLiteral)) { + return new SimpleEntry<>(null, IMemberValuePair.K_UNKNOWN); + } + Entry entry = memberValue(prefixExpression.getOperand()); + return new SimpleEntry<>(prefixExpression.getOperator().toString() + entry.getKey(), entry.getValue()); + } + return new SimpleEntry<>(null, IMemberValuePair.K_UNKNOWN); + } + + private int toAnnotationValuePairType(String token) { + // inspired by NumberLiteral.setToken + Scanner scanner = new Scanner(); + scanner.setSource(token.toCharArray()); + try { + int tokenType = scanner.getNextToken(); + return switch(tokenType) { + case TerminalTokens.TokenNameDoubleLiteral -> IMemberValuePair.K_DOUBLE; + case TerminalTokens.TokenNameIntegerLiteral -> IMemberValuePair.K_INT; + case TerminalTokens.TokenNameFloatingPointLiteral -> IMemberValuePair.K_FLOAT; + case TerminalTokens.TokenNameLongLiteral -> IMemberValuePair.K_LONG; + case TerminalTokens.TokenNameMINUS -> + switch (scanner.getNextToken()) { + case TerminalTokens.TokenNameDoubleLiteral -> IMemberValuePair.K_DOUBLE; + case TerminalTokens.TokenNameIntegerLiteral -> IMemberValuePair.K_INT; + case TerminalTokens.TokenNameFloatingPointLiteral -> IMemberValuePair.K_FLOAT; + case TerminalTokens.TokenNameLongLiteral -> IMemberValuePair.K_LONG; + default -> throw new IllegalArgumentException("Invalid number literal : >" + token + "<"); //$NON-NLS-1$//$NON-NLS-2$ + }; + default -> throw new IllegalArgumentException("Invalid number literal : >" + token + "<"); //$NON-NLS-1$//$NON-NLS-2$ + }; + } catch (InvalidInputException ex) { + ILog.get().error(ex.getMessage(), ex); + return IMemberValuePair.K_UNKNOWN; + } + } + + private Annotation toModelAnnotation(org.eclipse.jdt.core.dom.Annotation domAnnotation, JavaElement parent) { + IMemberValuePair[] members; + if (domAnnotation instanceof NormalAnnotation normalAnnotation) { + members = ((List)normalAnnotation.values()).stream().map(domMemberValuePair -> { + Entry value = memberValue(domMemberValuePair.getValue()); + return new org.eclipse.jdt.internal.core.MemberValuePair(domMemberValuePair.getName().toString(), value.getKey(), value.getValue()); + }).toArray(IMemberValuePair[]::new); + } else if (domAnnotation instanceof SingleMemberAnnotation single) { + Entry value = memberValue(single.getValue()); + members = new IMemberValuePair[] { new org.eclipse.jdt.internal.core.MemberValuePair("value", value.getKey(), value.getValue())}; //$NON-NLS-1$ + } else { + members = new IMemberValuePair[0]; + } + + return new Annotation(parent, domAnnotation.getTypeName().toString()) { + @Override + public IMemberValuePair[] getMemberValuePairs() { + return members; + } + }; + } + + static LocalVariable toLocalVariable(SingleVariableDeclaration parameter, JavaElement parent) { + return new LocalVariable(parent, + parameter.getName().getIdentifier(), + getStartConsideringLeadingComments(parameter), + parameter.getStartPosition() + parameter.getLength() - 1, + parameter.getName().getStartPosition(), + parameter.getName().getStartPosition() + parameter.getName().getLength() - 1, + Util.getSignature(parameter.getType()), + null, // should be populated while navigating children + parameter.getFlags(), + parameter.getParent() instanceof MethodDeclaration); + } + + @Override + public boolean visit(FieldDeclaration field) { + JavaElementInfo parentInfo = this.infos.peek(); + JavaElement parentElement = this.elements.peek(); + boolean isDeprecated = isNodeDeprecated(field); + char[][] categories = getCategories(field); + for (VariableDeclarationFragment fragment : (Collection) field.fragments()) { + SourceField newElement = new SourceField(parentElement, fragment.getName().toString()); + this.elements.push(newElement); + addAsChild(parentInfo, newElement); + SourceFieldWithChildrenInfo info = new SourceFieldWithChildrenInfo(new IJavaElement[0]); + info.setTypeName(field.getType().toString().toCharArray()); + setSourceRange(info, field); + if (parentInfo instanceof SourceTypeElementInfo parentTypeInfo) { + parentTypeInfo.addCategories(newElement, categories); + } + info.setFlags(toModelFlags(field.getModifiers(), isDeprecated)); + info.setNameSourceStart(fragment.getName().getStartPosition()); + info.setNameSourceEnd(fragment.getName().getStartPosition() + fragment.getName().getLength() - 1); + Expression initializer = fragment.getInitializer(); + if (((field.getParent() instanceof TypeDeclaration type && type.isInterface()) + || Flags.isFinal(field.getModifiers())) + && initializer != null && initializer.getStartPosition() >= 0) { + info.initializationSource = Arrays.copyOfRange(this.root.getContents(), initializer.getStartPosition(), initializer.getStartPosition() + initializer.getLength()); + } + this.infos.push(info); + this.toPopulate.put(newElement, info); + if (field.getAST().apiLevel() >= AST.JLS3) { + List modifiers = field.modifiers(); + if (modifiers != null) { + modifiers.stream() + .filter(org.eclipse.jdt.core.dom.Annotation.class::isInstance) + .map(org.eclipse.jdt.core.dom.Annotation.class::cast) + .forEach(annotation -> annotation.accept(this)); // force processing of annotation on each fragment + } + } + } + return true; + } + @Override + public void endVisit(FieldDeclaration decl) { + int numFragments = decl.fragments().size(); + for (int i = 0; i < numFragments; i++) { + this.elements.pop(); + this.infos.pop(); + } + } + + private String createSignature(SingleVariableDeclaration decl) { + String initialSignature = Util.getSignature(decl.getType()); + int extraDimensions = decl.getExtraDimensions(); + if (decl.getAST().apiLevel() > AST.JLS2 && decl.isVarargs()) { + extraDimensions++; + } + return Signature.createArraySignature(initialSignature, extraDimensions); + } + + @Override + public boolean visit(Initializer node) { + org.eclipse.jdt.internal.core.Initializer newElement = new org.eclipse.jdt.internal.core.Initializer(this.elements.peek(), 1); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + InitializerElementInfo newInfo = new InitializerWithChildrenInfo(new IJavaElement[0]); + setSourceRange(newInfo, node); + newInfo.setFlags(toModelFlags(node.getModifiers(), false)); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + return true; + } + @Override + public void endVisit(Initializer decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(ModuleDeclaration node) { + SourceModule newElement = new SourceModule(this.elements.peek(), node.getName().toString()); + this.elements.push(newElement); + addAsChild(this.infos.peek(), newElement); + ModuleDescriptionInfo newInfo = new ModuleDescriptionInfo(); + newInfo.setHandle(newElement); + newInfo.name = node.getName().toString().toCharArray(); + newInfo.setNameSourceStart(node.getName().getStartPosition()); + newInfo.setNameSourceEnd(node.getName().getStartPosition() + node.getName().getLength() - 1); + setSourceRange(newInfo, node); + List moduleStatements = node.moduleStatements(); + LinkedHashSet requires = new LinkedHashSet<>(moduleStatements.stream() + .filter(RequiresDirective.class::isInstance) + .map(RequiresDirective.class::cast) + .map(this::toModuleReferenceInfo) + .toList()); + if (!Arrays.equals(node.getName().toString().toCharArray(), TypeConstants.JAVA_BASE)) { + ModuleReferenceInfo ref = new ModuleReferenceInfo(); + ref.name = TypeConstants.JAVA_BASE; + requires.add(ref); + } + newInfo.requires = requires.toArray(ModuleReferenceInfo[]::new); + newInfo.exports = moduleStatements.stream() + .filter(ExportsDirective.class::isInstance) + .map(ExportsDirective.class::cast) + .map(this::toPackageExportInfo) + .toArray(PackageExportInfo[]::new); + newInfo.opens = moduleStatements.stream() + .filter(OpensDirective.class::isInstance) + .map(OpensDirective.class::cast) + .map(this::toPackageExportInfo) + .toArray(PackageExportInfo[]::new); + newInfo.usedServices = moduleStatements.stream() + .filter(UsesDirective.class::isInstance) + .map(UsesDirective.class::cast) + .map(UsesDirective::getName) + .map(Name::toString) + .map(String::toCharArray) + .toArray(char[][]::new); + newInfo.services = moduleStatements.stream() + .filter(ProvidesDirective.class::isInstance) + .map(ProvidesDirective.class::cast) + .map(this::toServiceInfo) + .toArray(ServiceInfo[]::new); + this.infos.push(newInfo); + this.toPopulate.put(newElement, newInfo); + + this.unitInfo.setModule(newElement); + try { + this.root.getJavaProject().setModuleDescription(newElement); + } catch (JavaModelException e) { + ILog.get().error(e.getMessage(), e); + } + return true; + } + @Override + public void endVisit(ModuleDeclaration decl) { + this.elements.pop(); + this.infos.pop(); + } + + @Override + public boolean visit(LambdaExpression node) { + this.unitInfo.hasFunctionalTypes = true; + return true; + } + @Override + public boolean visit(CreationReference node) { + this.unitInfo.hasFunctionalTypes = true; + return true; + } + @Override + public boolean visit(ExpressionMethodReference node) { + this.unitInfo.hasFunctionalTypes = true; + return true; + } + @Override + public boolean visit(TypeMethodReference node) { + this.unitInfo.hasFunctionalTypes = true; + return true; + } + @Override + public boolean visit(SuperMethodReference node) { + this.unitInfo.hasFunctionalTypes = true; + return true; + } + + + private ModuleReferenceInfo toModuleReferenceInfo(RequiresDirective node) { + ModuleReferenceInfo res = new ModuleReferenceInfo(); + res.modifiers = + (ModuleModifier.isTransitive(node.getModifiers()) ? ClassFileConstants.ACC_TRANSITIVE : 0) | + (ModuleModifier.isStatic(node.getModifiers()) ? Flags.AccStatic : 0); + res.name = node.getName().toString().toCharArray(); + setSourceRange(res, node); + return res; + } + private PackageExportInfo toPackageExportInfo(ModulePackageAccess node) { + PackageExportInfo res = new PackageExportInfo(); + res.pack = node.getName().toString().toCharArray(); + setSourceRange(res, node); + List modules = node.modules(); + res.target = modules == null || modules.isEmpty() ? null : + modules.stream().map(name -> name.toString().toCharArray()).toArray(char[][]::new); + return res; + } + private ServiceInfo toServiceInfo(ProvidesDirective node) { + ServiceInfo res = new ServiceInfo(); + res.flags = node.getFlags(); + res.serviceName = node.getName().toString().toCharArray(); + res.implNames = ((List)node.implementations()).stream().map(Name::toString).map(String::toCharArray).toArray(char[][]::new); + setSourceRange(res, node); + return res; + } + private boolean isNodeDeprecated(BodyDeclaration node) { + if (node.getJavadoc() != null) { + boolean javadocDeprecated = node.getJavadoc().tags().stream() // + .anyMatch(tag -> { + return TagElement.TAG_DEPRECATED.equals(((AbstractTagElement)tag).getTagName()); + }); + if (javadocDeprecated) { + return true; + } + } + if (node.getAST().apiLevel() <= 2) { + return false; + } + return ((List)node.modifiers()).stream() // + .anyMatch(modifier -> { + if (!isAnnotation(modifier)) { + return false; + } + String potentiallyUnqualifiedAnnotationType = ((org.eclipse.jdt.core.dom.Annotation)modifier).getTypeName().toString(); + if ("java.lang.Deprecated".equals(potentiallyUnqualifiedAnnotationType)) { //$NON-NLS-1$ + return true; + } + return "Deprecated".equals(potentiallyUnqualifiedAnnotationType) && !hasAlternativeDeprecated(); //$NON-NLS-1$ + }); + } + private static boolean isAnnotation(ASTNode node) { + int nodeType = node.getNodeType(); + return nodeType == ASTNode.MARKER_ANNOTATION || nodeType == ASTNode.SINGLE_MEMBER_ANNOTATION + || nodeType == ASTNode.NORMAL_ANNOTATION; + } + private boolean hasAlternativeDeprecated() { + if (this.alternativeDeprecated != null) { + return this.alternativeDeprecated; + } + if (this.importContainer != null) { + try { + IJavaElement[] importElements = this.importContainer.getChildren(); + for (IJavaElement child : importElements) { + IImportDeclaration importDeclaration = (IImportDeclaration) child; + // It's possible that the user has imported + // an annotation called "Deprecated" using a wildcard import + // that replaces "java.lang.Deprecated" + // However, it's very costly and complex to check if they've done this, + // so I haven't bothered. + if (!importDeclaration.isOnDemand() + && importDeclaration.getElementName().endsWith("Deprecated")) { //$NON-NLS-1$ + this.alternativeDeprecated = true; + return this.alternativeDeprecated; + } + } + } catch (JavaModelException e) { + // do nothing + } + } + this.alternativeDeprecated = false; + return this.alternativeDeprecated; + } + private char[][] getCategories(BodyDeclaration decl) { + if (decl.getJavadoc() != null) { + return ((List)decl.getJavadoc().tags()).stream() // + .filter(tag -> "@category".equals(tag.getTagName()) && ((List)tag.fragments()).size() > 0) //$NON-NLS-1$ + .map(tag -> ((List)tag.fragments()).get(0)) // + .map(fragment -> { + String fragmentString = fragment.toString(); + /** + * I think this is a bug in JDT, but I am replicating the behaviour. + * + * @see CompilationUnitTests.testGetCategories13() + */ + int firstAsterix = fragmentString.indexOf('*'); + return fragmentString.substring(0, firstAsterix != -1 ? firstAsterix : fragmentString.length()); + }) // + .flatMap(fragment -> (Stream)Stream.of(fragment.split("\\s+"))) // //$NON-NLS-1$ + .filter(category -> category.length() > 0) // + .map(category -> (category).toCharArray()) // + .toArray(char[][]::new); + } + return new char[0][0]; + } + + private static org.eclipse.jdt.core.dom.CompilationUnit domUnit(ASTNode node) { + while (node != null && !(node instanceof org.eclipse.jdt.core.dom.CompilationUnit)) { + node = node.getParent(); + } + return (org.eclipse.jdt.core.dom.CompilationUnit)node; + } + + private static void setSourceRange(SourceRefElementInfo info, ASTNode node) { + info.setSourceRangeStart(getStartConsideringLeadingComments(node)); + info.setSourceRangeEnd(node.getStartPosition() + node.getLength() - 1); + } + + private static int getStartConsideringLeadingComments(ASTNode node) { + int start = node.getStartPosition(); + var unit = domUnit(node); + int index = unit.firstLeadingCommentIndex(node); + if (index >= 0 && index <= unit.getCommentList().size()) { + Comment comment = (Comment)unit.getCommentList().get(index); + start = comment.getStartPosition(); + } + return start; + } + + private static int toModelFlags(int domModifiers, boolean isDeprecated) { + int res = 0; + if (Modifier.isAbstract(domModifiers)) res |= Flags.AccAbstract; + if (Modifier.isDefault(domModifiers)) res |= Flags.AccDefaultMethod; + if (Modifier.isFinal(domModifiers)) res |= Flags.AccFinal; + if (Modifier.isNative(domModifiers)) res |= Flags.AccNative; + if (Modifier.isNonSealed(domModifiers)) res |= Flags.AccNonSealed; + if (Modifier.isPrivate(domModifiers)) res |= Flags.AccPrivate; + if (Modifier.isProtected(domModifiers)) res |= Flags.AccProtected; + if (Modifier.isPublic(domModifiers)) res |= Flags.AccPublic; + if (Modifier.isSealed(domModifiers)) res |= Flags.AccSealed; + if (Modifier.isStatic(domModifiers)) res |= Flags.AccStatic; + if (Modifier.isStrictfp(domModifiers)) res |= Flags.AccStrictfp; + if (Modifier.isSynchronized(domModifiers)) res |= Flags.AccSynchronized; + if (Modifier.isTransient(domModifiers)) res |= Flags.AccTransient; + if (Modifier.isVolatile(domModifiers)) res |= Flags.AccVolatile; + if (isDeprecated) res |= Flags.AccDeprecated; + return res; + } +} diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ReconcileWorkingCopyOperation.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ReconcileWorkingCopyOperation.java index e9c01f14f02..489b27e62f9 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ReconcileWorkingCopyOperation.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ReconcileWorkingCopyOperation.java @@ -13,6 +13,8 @@ *******************************************************************************/ package org.eclipse.jdt.internal.core; +import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -22,9 +24,15 @@ import org.eclipse.jdt.core.*; import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.core.compiler.CompilationParticipant; +import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.core.compiler.ReconcileContext; import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.PrimitiveType; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; +import org.eclipse.jdt.internal.compiler.problem.AbortCompilationUnit; +import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; +import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; import org.eclipse.jdt.internal.core.util.Messages; import org.eclipse.jdt.internal.core.util.Util; @@ -172,7 +180,6 @@ public org.eclipse.jdt.core.dom.CompilationUnit makeConsistent(CompilationUnit w if (this.ast != null) return this.ast; // no need to recompute AST if known already - CompilationUnitDeclaration unit = null; try { JavaModelManager.getJavaModelManager().abortOnMissingSource.set(Boolean.TRUE); CompilationUnit source = workingCopy.cloneCachingContents(); @@ -180,33 +187,77 @@ public org.eclipse.jdt.core.dom.CompilationUnit makeConsistent(CompilationUnit w if (JavaProject.hasJavaNature(workingCopy.getJavaProject().getProject()) && (this.reconcileFlags & ICompilationUnit.FORCE_PROBLEM_DETECTION) != 0) { this.resolveBindings = this.requestorIsActive; - if (this.problems == null) + if (this.problems == null) { this.problems = new HashMap<>(); - unit = - CompilationUnitProblemFinder.process( - source, - this.workingCopyOwner, - this.problems, - this.astLevel != ICompilationUnit.NO_AST/*creating AST if level is not NO_AST */, - this.reconcileFlags, - this.progressMonitor); - if (this.progressMonitor != null) this.progressMonitor.worked(1); - } - - // create AST if needed - if (this.astLevel != ICompilationUnit.NO_AST - && unit !=null/*unit is null if working copy is consistent && (problem detection not forced || non-Java project) -> don't create AST as per API*/) { + } Map options = workingCopy.getJavaProject().getOptions(true); - // convert AST - this.ast = - AST.convertCompilationUnit( - this.astLevel, - unit, - options, - this.resolveBindings, - source, - this.reconcileFlags, - this.progressMonitor); + if (CompilationUnit.DOM_BASED_OPERATIONS) { + try { + ASTParser parser = ASTParser.newParser(this.astLevel > 0 ? this.astLevel : AST.getJLSLatest()); + parser.setResolveBindings(this.resolveBindings || (this.reconcileFlags & ICompilationUnit.FORCE_PROBLEM_DETECTION) != 0); + parser.setCompilerOptions(options); + parser.setSource(source); + org.eclipse.jdt.core.dom.CompilationUnit newAST = (org.eclipse.jdt.core.dom.CompilationUnit) parser.createAST(this.progressMonitor); + if ((this.reconcileFlags & ICompilationUnit.FORCE_PROBLEM_DETECTION) != 0 && newAST != null) { + newAST.getAST().resolveWellKnownType(PrimitiveType.BOOLEAN.toString()); //trigger resolution and analysis + } + CategorizedProblem[] newProblems = Arrays.stream(newAST.getProblems()) + .filter(CategorizedProblem.class::isInstance) + .map(CategorizedProblem.class::cast) + .toArray(CategorizedProblem[]::new); + this.problems.put(Integer.toString(CategorizedProblem.CAT_UNSPECIFIED), newProblems); // TODO better category + if (this.astLevel != ICompilationUnit.NO_AST) { + this.ast = newAST; + } + } catch (AbortCompilationUnit ex) { + var problem = ex.problem; + if (problem == null && ex.exception instanceof IOException ioEx) { + String path = source.getPath().toString(); + String exceptionTrace = ioEx.getClass().getName() + ':' + ioEx.getMessage(); + problem = new DefaultProblemFactory().createProblem( + path.toCharArray(), + IProblem.CannotReadSource, + new String[] { path, exceptionTrace }, + new String[] { path, exceptionTrace }, + ProblemSeverities.AbortCompilation | ProblemSeverities.Error | ProblemSeverities.Fatal, + 0, 0, 1, 0); + } + this.problems.put(Integer.toString(CategorizedProblem.CAT_BUILDPATH), + new CategorizedProblem[] { problem }); + } + } else { + CompilationUnitDeclaration unit = null; + try { + unit = CompilationUnitProblemFinder.process( + source, + this.workingCopyOwner, + this.problems, + this.astLevel != ICompilationUnit.NO_AST/*creating AST if level is not NO_AST */, + this.reconcileFlags, + this.progressMonitor); + if (this.progressMonitor != null) this.progressMonitor.worked(1); + + // create AST if needed + if (this.astLevel != ICompilationUnit.NO_AST + && unit !=null/*unit is null if working copy is consistent && (problem detection not forced || non-Java project) -> don't create AST as per API*/) { + // convert AST + this.ast = + AST.convertCompilationUnit( + this.astLevel, + unit, + options, + this.resolveBindings, + source, + this.reconcileFlags, + this.progressMonitor); + } + } finally { + if (unit != null) { + unit.cleanUp(); + } + } + } + if (this.ast != null) { if (this.deltaBuilder.delta == null) { this.deltaBuilder.delta = new JavaElementDelta(workingCopy); @@ -222,9 +273,6 @@ public org.eclipse.jdt.core.dom.CompilationUnit makeConsistent(CompilationUnit w // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=100919) } finally { JavaModelManager.getJavaModelManager().abortOnMissingSource.remove(); - if (unit != null) { - unit.cleanUp(); - } } return this.ast; } diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/JavaSearchDocument.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/JavaSearchDocument.java index 682db632773..78d98fa94cc 100644 --- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/JavaSearchDocument.java +++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/JavaSearchDocument.java @@ -87,7 +87,7 @@ public String getEncoding() { } return null; } - private IFile getFile() { + public IFile getFile() { if (this.file == null) this.file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(getPath())); return this.file; diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/DOMToIndexVisitor.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/DOMToIndexVisitor.java new file mode 100644 index 00000000000..44ad4760d2f --- /dev/null +++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/DOMToIndexVisitor.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat, Inc. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.search.indexing; + +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.PrimitiveType; +import org.eclipse.jdt.core.dom.RecordDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; + +class DOMToIndexVisitor extends ASTVisitor { + + private SourceIndexer sourceIndexer; + + public DOMToIndexVisitor(SourceIndexer sourceIndexer) { + this.sourceIndexer = sourceIndexer; + } + + @Override + public boolean visit(TypeDeclaration type) { + if (type.isInterface()) { + this.sourceIndexer.addInterfaceDeclaration(type.getModifiers(), getPackage(type), type.getName().toString().toCharArray(), null, ((List)type.superInterfaceTypes()).stream().map(superInterface -> superInterface.toString().toCharArray()).toArray(char[][]::new), null, false); + } else { + this.sourceIndexer.addClassDeclaration(type.getModifiers(), getPackage(type), type.getName().toString().toCharArray(), null, type.getSuperclassType() == null ? null : type.getSuperclassType().toString().toCharArray(), + ((List)type.superInterfaceTypes()).stream().map(superInterface -> superInterface.toString().toCharArray()).toArray(char[][]::new), null, isSecondary(type)); + } + // TODO other types + return true; + } + + private boolean isSecondary(TypeDeclaration type) { + return type.getParent() instanceof CompilationUnit unit && + unit.types().size() > 1 && + unit.types().indexOf(type) > 0; + // TODO: check name? + } + + private char[] getPackage(ASTNode node) { + while (node != null && !(node instanceof CompilationUnit)) { + node = node.getParent(); + } + return node == null ? null : + node instanceof CompilationUnit unit && unit.getPackage() != null ? unit.getPackage().getName().toString().toCharArray() : + null; + } + + @Override + public boolean visit(RecordDeclaration recordDecl) { + // copied processing of TypeDeclaration + this.sourceIndexer.addClassDeclaration(recordDecl.getModifiers(), getPackage(recordDecl), recordDecl.getName().toString().toCharArray(), null, null, + ((List)recordDecl.superInterfaceTypes()).stream().map(type -> type.toString().toCharArray()).toArray(char[][]::new), null, false); + return true; + } + + @Override + public boolean visit(MethodDeclaration method) { + char[] methodName = method.getName().toString().toCharArray(); + char[][] parameterTypes = ((List)method.parameters()).stream() + .filter(SingleVariableDeclaration.class::isInstance) + .map(SingleVariableDeclaration.class::cast) + .map(SingleVariableDeclaration::getType) + .map(Type::toString) + .map(String::toCharArray) + .toArray(char[][]::new); + char[] returnType = null; + if (method.getReturnType2() instanceof SimpleType simple) { + returnType = simple.getName().toString().toCharArray(); + } else if (method.getReturnType2() instanceof PrimitiveType primitive) { + returnType = primitive.getPrimitiveTypeCode().toString().toCharArray(); + } else if (method.getReturnType2() == null) { + // do nothing + } else { + returnType = method.getReturnType2().toString().toCharArray(); + } + char[][] exceptionTypes = ((List)method.thrownExceptionTypes()).stream() + .map(Type::toString) + .map(String::toCharArray) + .toArray(char[][]::new); + this.sourceIndexer.addMethodDeclaration(methodName, parameterTypes, returnType, exceptionTypes); + char[][] parameterNames = ((List)method.parameters()).stream() + .map(VariableDeclaration::getName) + .map(SimpleName::toString) + .map(String::toCharArray) + .toArray(char[][]::new); + this.sourceIndexer.addMethodDeclaration(null, + null /* TODO: fully qualified name of enclosing type? */, + methodName, + parameterTypes.length, + null, + parameterTypes, + parameterNames, + returnType, + method.getModifiers(), + getPackage(method), + 0 /* TODO What to put here? */, + exceptionTypes, + 0 /* TODO ExtraFlags.IsLocalType ? */); + return true; + } + + @Override + public boolean visit(FieldDeclaration field) { + char[] typeName = field.getType().toString().toCharArray(); + for (VariableDeclarationFragment fragment: (List)field.fragments()) { + this.sourceIndexer.addFieldDeclaration(typeName, fragment.getName().toString().toCharArray()); + } + return true; + } + + // TODO (cf SourceIndexer and SourceIndexerRequestor) + // * Module: addModuleDeclaration/addModuleReference/addModuleExportedPackages + // * Lambda: addIndexEntry/addClassDeclaration + // * addMethodReference + // * addConstructorReference +} diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SourceIndexer.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SourceIndexer.java index 7d79204e333..fb724b55c80 100644 --- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SourceIndexer.java +++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SourceIndexer.java @@ -15,13 +15,20 @@ import static org.eclipse.jdt.internal.core.JavaModelManager.trace; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.compiler.CharOperation; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.search.SearchDocument; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies; @@ -48,12 +55,14 @@ import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; import org.eclipse.jdt.internal.compiler.util.SuffixConstants; +import org.eclipse.jdt.internal.core.ASTHolderCUInfo; import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; import org.eclipse.jdt.internal.core.JavaModel; import org.eclipse.jdt.internal.core.JavaModelManager; import org.eclipse.jdt.internal.core.JavaProject; import org.eclipse.jdt.internal.core.SourceTypeElementInfo; import org.eclipse.jdt.internal.core.jdom.CompilationUnit; +import org.eclipse.jdt.internal.core.search.JavaSearchDocument; import org.eclipse.jdt.internal.core.search.matching.JavaSearchNameEnvironment; import org.eclipse.jdt.internal.core.search.matching.MethodPattern; import org.eclipse.jdt.internal.core.search.processing.JobManager; @@ -88,6 +97,10 @@ public SourceIndexer(SearchDocument document) { } @Override public void indexDocument() { + if (Boolean.getBoolean(getClass().getSimpleName() + ".DOM_BASED_INDEXER")) { //$NON-NLS-1$ + indexDocumentFromDOM(); + return; + } // Create a new Parser String documentPath = this.document.getPath(); SourceElementParser parser = this.document.getParser(); @@ -271,4 +284,37 @@ public void indexResolvedDocument() { } } } + + /** + * @return whether the operatin was successful + */ + private boolean indexDocumentFromDOM() { + if (this.document instanceof JavaSearchDocument javaSearchDoc) { + IFile file = javaSearchDoc.getFile(); + try { + if (JavaProject.hasJavaNature(file.getProject())) { + IJavaProject javaProject = JavaCore.create(file.getProject()); + IClasspathEntry cpEntry = javaProject.findContainingClasspathEntry(file); + IJavaElement element = javaProject.findElement(file.getFullPath().makeRelativeTo(cpEntry.getPath())); + if (element instanceof org.eclipse.jdt.internal.core.CompilationUnit modelUnit) { + // TODO check element info: if has AST and flags are set sufficiently, just reuse instead of rebuilding + ASTParser astParser = ASTParser.newParser(modelUnit.getElementInfo() instanceof ASTHolderCUInfo astHolder ? astHolder.astLevel : AST.getJLSLatest()); + astParser.setSource(modelUnit); + astParser.setResolveBindings(false); + astParser.setProject(javaProject); + astParser.setIgnoreMethodBodies(true); + org.eclipse.jdt.core.dom.ASTNode dom = astParser.createAST(null); + if (dom != null) { + dom.accept(new DOMToIndexVisitor(this)); + return true; + } + } + } + } catch (Exception ex) { + ILog.get().error("Failed to index document from DOM for " + this.document.getPath(), ex); //$NON-NLS-1$ + } + } + ILog.get().warn("Could not convert DOM to Index for " + this.document.getPath()); //$NON-NLS-1$ + return false; + } }