diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java index 45c3fc73c..6c105a61b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java @@ -78,6 +78,10 @@ public class BeanRegistrarDeclarationReconciler implements JdtAstReconciler { if (ASTUtils.findInTypeHierarchy(type, Set.of(Annotations.BEAN_REGISTRAR_INTERFACE)) == null) { return true; } + + if (!isIndexComplete) { + throw new RequiredCompleteIndexException(); + } List configBeans = new ArrayList<>(); Path p = Path.of(docURI); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java index c6127ad26..2546a15a8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java @@ -10,8 +10,10 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.reconcilers; +/** + * Indicates that a reconciler or indexer requires the full AST (including method bodies) + * to do its work accordingly. + */ public class RequiredCompleteAstException extends RuntimeException { - - private static final long serialVersionUID = 1L; - + private static final long serialVersionUID = -3422411902406544588L; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteIndexException.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteIndexException.java new file mode 100644 index 000000000..131ecd55b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteIndexException.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +/** + * Indicates that a reconciler relies on the index to be complete before it can + * reconcile a file. As a result, the corresponding file will be re-parsed and re-reconciled + * once the index is complete. + * + * A reconciler should throw this exception only in case it really requires + * access to the index and check other AST-related information first to avoid + * re-reconciling too many files when it is not really necessary. + * + * For example the reconciler that checks BeanRegistrar implementations against + * Import annotations in the index should only throw this exception if the class + * is indeed an implementation of BeanRegistrar. + */ +public class RequiredCompleteIndexException extends RuntimeException { + private static final long serialVersionUID = -6155363860106363727L; +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java index fc6f2cafc..ee4723605 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java @@ -64,6 +64,7 @@ import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.reconcilers.CachedDiagnostics; import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteIndexException; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -104,7 +105,7 @@ public class SpringIndexerJava implements SpringIndexer { private final JavaProjectFinder projectFinder; private final ProgressService progressService; private final CompilationUnitCache cuCache; - + private boolean scanTestJavaSources = false; private JsonObject validationSeveritySettings; private int scanChunkSize = 1000; @@ -219,7 +220,101 @@ public class SpringIndexerJava implements SpringIndexer { this.cache.removeFiles(beansCacheKey, files, CachedBean.class); this.cache.removeFiles(diagnosticsCacheKey, files, CachedDiagnostics.class); } + + /** + * Computes document symbols ad-hoc, based on the given doc URI and the given content, + * re-using the AST from the compilation unit cache. This is meant to compute symbols + * for files opened in editors, so that symbols can be based on editor buffer content, + * not the file content on disc. + * + * @deprecated (will use computeDocumentSymbols in the future exclusively) + */ + public List computeSymbols(IJavaProject project, String docURI, String content) throws Exception { + if (content != null) { + URI uri = URI.create(docURI); + + return cuCache.withCompilationUnit(project, uri, cu -> { + String file = UriUtil.toFileString(docURI); + SpringIndexerJavaScanResult result = new SpringIndexerJavaScanResult(project, new String[] {file}); + IProblemCollector voidProblemCollector = new IProblemCollector() { + @Override + public void endCollecting() { + } + + @Override + public void beginCollecting() { + } + + @Override + public void accept(ReconcileProblem problem) { + } + }; + + AtomicReference docRef = new AtomicReference<>(); + SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, + 0, docRef, content, voidProblemCollector, new ArrayList<>(), true, true, result); + + scanAST(context, false); + + return result.getGeneratedSymbols().stream().map(s -> s.getEnhancedSymbol()).collect(Collectors.toList()); + }); + } + + return Collections.emptyList(); + } + + /** + * Computes document symbols ad-hoc, based on the given doc URI and the given content, + * re-using the AST from the compilation unit cache. This is meant to compute symbols + * for files opened in editors, so that symbols can be based on editor buffer content, + * not the file content on disc. + */ + @Override + public List computeDocumentSymbols(IJavaProject project, String docURI, String content) throws Exception { + if (content != null) { + URI uri = URI.create(docURI); + + return cuCache.withCompilationUnit(project, uri, cu -> { + String file = UriUtil.toFileString(docURI); + + SpringIndexerJavaScanResult result = new SpringIndexerJavaScanResult(project, new String[] {file}); + + IProblemCollector voidProblemCollector = new IProblemCollector() { + @Override + public void endCollecting() { + } + + @Override + public void beginCollecting() { + } + + @Override + public void accept(ReconcileProblem problem) { + } + }; + + AtomicReference docRef = new AtomicReference<>(); + SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, + 0, docRef, content, voidProblemCollector, new ArrayList<>(), true, true, result); + + scanAST(context, false); + + List indexElements = result.getGeneratedBeans().stream() + .map(cachedBean -> cachedBean.getBean()) + .toList(); + + return SpringIndexToSymbolsConverter.createDocumentSymbols(indexElements); + }); + } + + return Collections.emptyList(); + } + + /** + * filter the list of given documents down to those that should be processed and that don't have + * valid up-to-date cache data anymore + */ private DocumentDescriptor[] filterDocuments(IJavaProject project, DocumentDescriptor[] updatedDocs) { IndexCacheKey symbolsCacheKey = getCacheKey(project, SYMBOL_KEY); IndexCacheKey beansCacheKey = getCacheKey(project, BEANS_KEY); @@ -232,6 +327,10 @@ public class SpringIndexerJava implements SpringIndexer { .toArray(DocumentDescriptor[]::new); } + /** + * checks whether the given doc URI is from inside the given projects "interesting" folders + * This is used to filter out file updates for files that are outside of the source folders (for example) + */ private boolean shouldProcessDocument(IJavaProject project, String docURI) { try { Path path = new File(new URI(docURI)).toPath(); @@ -281,126 +380,47 @@ public class SpringIndexerJava implements SpringIndexer { CompilationUnit cu = (CompilationUnit) parser.createAST(null); if (cu != null) { - List generatedSymbols = new ArrayList(); - List generatedBeans = new ArrayList(); - List generatedDiagnostics = new ArrayList(); + String file = UriUtil.toFileString(docURI); + + SpringIndexerJavaScanResult result = new SpringIndexerJavaScanResult(project, new String[] {file}); AtomicReference docRef = new AtomicReference<>(); - String file = UriUtil.toFileString(docURI); - BiConsumer diagnosticsAggregator = new BiConsumer<>() { @Override public void accept(String docURI, Diagnostic diagnostic) { - generatedDiagnostics.add(new CachedDiagnostics(docURI, diagnostic)); + result.getGeneratedDiagnostics().add(new CachedDiagnostics(docURI, diagnostic)); } }; IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator); SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, - lastModified, docRef, content, generatedSymbols, generatedBeans, problemCollector, new ArrayList<>(), !ignoreMethodBodies); + lastModified, docRef, content, problemCollector, new ArrayList<>(), !ignoreMethodBodies, true, result); scanAST(context, true); + result.publishResults(symbolHandler); + + // update cache + IndexCacheKey symbolCacheKey = getCacheKey(project, SYMBOL_KEY); IndexCacheKey beansCacheKey = getCacheKey(project, BEANS_KEY); IndexCacheKey diagnosticsCacheKey = getCacheKey(project, DIAGNOSTICS_KEY); - this.cache.update(symbolCacheKey, file, lastModified, generatedSymbols, context.getDependencies(), CachedSymbol.class); - this.cache.update(beansCacheKey, file, lastModified, generatedBeans, context.getDependencies(), CachedBean.class); - this.cache.update(diagnosticsCacheKey, file, lastModified, generatedDiagnostics, context.getDependencies(), CachedDiagnostics.class); -// dependencyTracker.dump(); - - WorkspaceSymbol[] symbols = generatedSymbols.stream().map(cachedSymbol -> cachedSymbol.getEnhancedSymbol()).toArray(WorkspaceSymbol[]::new); - List beans = generatedBeans.stream().filter(cachedBean -> cachedBean.getBean() != null).map(cachedBean -> cachedBean.getBean()).toList(); - List diagnostics = generatedDiagnostics.stream().filter(cachedDiagnostics -> cachedDiagnostics.getDiagnostic() != null).map(cachedDiagnostic -> cachedDiagnostic.getDiagnostic()).collect(Collectors.toList()); - - symbolHandler.addSymbols(project, docURI, symbols, beans, diagnostics); + this.cache.update(symbolCacheKey, file, lastModified, result.getGeneratedSymbols(), context.getDependencies(), CachedSymbol.class); + this.cache.update(beansCacheKey, file, lastModified, result.getGeneratedBeans(), context.getDependencies(), CachedBean.class); + this.cache.update(diagnosticsCacheKey, file, lastModified, result.getGeneratedDiagnostics(), context.getDependencies(), CachedDiagnostics.class); Set scannedFiles = new HashSet<>(); scannedFiles.add(file); fileScannedEvent(file); + + reconcileWithCompleteIndex(project, result.getMarkedForReconcilingWithCompleteIndex()); + scanAffectedFiles(project, context.getScannedTypes(), scannedFiles); } } - public List computeSymbols(IJavaProject project, String docURI, String content) throws Exception { - if (content != null) { - URI uri = URI.create(docURI); - - return cuCache.withCompilationUnit(project, uri, cu -> { - List generatedSymbols = new ArrayList(); - List generatedBeans = new ArrayList(); - - IProblemCollector voidProblemCollector = new IProblemCollector() { - @Override - public void endCollecting() { - } - - @Override - public void beginCollecting() { - } - - @Override - public void accept(ReconcileProblem problem) { - } - }; - - AtomicReference docRef = new AtomicReference<>(); - String file = UriUtil.toFileString(docURI); - SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, - 0, docRef, content, generatedSymbols, generatedBeans, voidProblemCollector, new ArrayList<>(), true); - - scanAST(context, false); - - return generatedSymbols.stream().map(s -> s.getEnhancedSymbol()).collect(Collectors.toList()); - }); - } - - return Collections.emptyList(); - } - - @Override - public List computeDocumentSymbols(IJavaProject project, String docURI, String content) throws Exception { - if (content != null) { - URI uri = URI.create(docURI); - - return cuCache.withCompilationUnit(project, uri, cu -> { - List generatedSymbols = new ArrayList(); - List generatedBeans = new ArrayList(); - - IProblemCollector voidProblemCollector = new IProblemCollector() { - @Override - public void endCollecting() { - } - - @Override - public void beginCollecting() { - } - - @Override - public void accept(ReconcileProblem problem) { - } - }; - - AtomicReference docRef = new AtomicReference<>(); - String file = UriUtil.toFileString(docURI); - SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, - 0, docRef, content, generatedSymbols, generatedBeans, voidProblemCollector, new ArrayList<>(), true); - - scanAST(context, false); - - List indexElements = generatedBeans.stream() - .map(cachedBean -> cachedBean.getBean()) - .toList(); - - return SpringIndexToSymbolsConverter.createDocumentSymbols(indexElements); - }); - } - - return Collections.emptyList(); - } - private Set scanFilesInternally(IJavaProject project, DocumentDescriptor[] docs) throws Exception { final boolean ignoreMethodBodies = false; @@ -420,16 +440,13 @@ public class SpringIndexerJava implements SpringIndexer { lastModified[i] = docs[i].getLastModified(); } - List generatedSymbols = new ArrayList(); - List generatedBeans = new ArrayList(); - List generatedDiagnostics = new ArrayList(); - + SpringIndexerJavaScanResult result = new SpringIndexerJavaScanResult(project, javaFiles); Multimap dependencies = MultimapBuilder.hashKeys().hashSetValues().build(); BiConsumer diagnosticsAggregator = new BiConsumer<>() { @Override public void accept(String docURI, Diagnostic diagnostic) { - generatedDiagnostics.add(new CachedDiagnostics(docURI, diagnostic)); + result.getGeneratedDiagnostics().add(new CachedDiagnostics(docURI, diagnostic)); } }; @@ -447,7 +464,7 @@ public class SpringIndexerJava implements SpringIndexer { IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator); SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath, - lastModified, docRef, null, generatedSymbols, generatedBeans, problemCollector, new ArrayList<>(), !ignoreMethodBodies); + lastModified, docRef, null, problemCollector, new ArrayList<>(), !ignoreMethodBodies, false, result); scanAST(context, true); @@ -468,20 +485,19 @@ public class SpringIndexerJava implements SpringIndexer { parser.cleanup(); } - WorkspaceSymbol[] symbols = generatedSymbols.stream().map(cachedSymbol -> cachedSymbol.getEnhancedSymbol()).toArray(WorkspaceSymbol[]::new); - Map> beans = generatedBeans.stream().filter(cachedBean -> cachedBean.getBean() != null).collect(Collectors.groupingBy(CachedBean::getDocURI, Collectors.mapping(CachedBean::getBean, Collectors.toList()))); - Map> diagnosticsByDoc = generatedDiagnostics.stream().filter(cachedDiagnostic -> cachedDiagnostic.getDiagnostic() != null).collect(Collectors.groupingBy(CachedDiagnostics::getDocURI, Collectors.mapping(CachedDiagnostics::getDiagnostic, Collectors.toList()))); - addEmptyDiagnostics(diagnosticsByDoc, javaFiles); - symbolHandler.addSymbols(project, symbols, beans, diagnosticsByDoc); - + // update the cache + IndexCacheKey symbolsCacheKey = getCacheKey(project, SYMBOL_KEY); IndexCacheKey beansCacheKey = getCacheKey(project, BEANS_KEY); IndexCacheKey diagnosticsCacheKey = getCacheKey(project, DIAGNOSTICS_KEY); - this.cache.update(symbolsCacheKey, javaFiles, lastModified, generatedSymbols, dependencies, CachedSymbol.class); - this.cache.update(beansCacheKey, javaFiles, lastModified, generatedBeans, dependencies, CachedBean.class); - this.cache.update(diagnosticsCacheKey, javaFiles, lastModified, generatedDiagnostics, dependencies, CachedDiagnostics.class); + this.cache.update(symbolsCacheKey, javaFiles, lastModified, result.getGeneratedSymbols(), dependencies, CachedSymbol.class); + this.cache.update(beansCacheKey, javaFiles, lastModified, result.getGeneratedBeans(), dependencies, CachedBean.class); + this.cache.update(diagnosticsCacheKey, javaFiles, lastModified, result.getGeneratedDiagnostics(), dependencies, CachedDiagnostics.class); + result.publishResults(symbolHandler); + reconcileWithCompleteIndex(project, result.getMarkedForReconcilingWithCompleteIndex()); + return scannedTypes; } @@ -519,6 +535,10 @@ public class SpringIndexerJava implements SpringIndexer { } private void scanFiles(IJavaProject project, String[] javaFiles, boolean clean) throws Exception { + + SpringIndexerJavaScanResult result = null; + + // check cached elements first IndexCacheKey symbolsCacheKey = getCacheKey(project, SYMBOL_KEY); IndexCacheKey beansCacheKey = getCacheKey(project, BEANS_KEY); IndexCacheKey diagnosticsCacheKey = getCacheKey(project, DIAGNOSTICS_KEY); @@ -527,21 +547,26 @@ public class SpringIndexerJava implements SpringIndexer { Pair> cachedBeans = this.cache.retrieve(beansCacheKey, javaFiles, CachedBean.class); Pair> cachedDiagnostics = this.cache.retrieve(diagnosticsCacheKey, javaFiles, CachedDiagnostics.class); - CachedSymbol[] symbols; - CachedBean[] beans; - CachedDiagnostics[] diagnostics; + if (!clean && cachedSymbols != null && cachedBeans != null && cachedDiagnostics != null) { + // use cached data - if (clean || cachedSymbols == null || cachedBeans == null || cachedDiagnostics == null) { - List generatedSymbols = new ArrayList(); - List generatedBeans = new ArrayList(); - List generatedDiagnostics = new ArrayList(); + result = new SpringIndexerJavaScanResult(project, javaFiles, symbolHandler, cachedSymbols.getLeft(), cachedBeans.getLeft(), cachedDiagnostics.getLeft()); + this.dependencyTracker.restore(cachedSymbols.getRight()); - log.info("scan java files, AST parse, pass 1 for files: {}", javaFiles.length); + log.info("scan java files used cached data: {} - no. of cached symbols retrieved: {}", project.getElementName(), result.getGeneratedSymbols().size()); + log.info("scan java files restored cached dependency data: {} - no. of cached dependencies: {}", cachedSymbols.getRight().size()); + } + else { + // continue scanning everything + + result = new SpringIndexerJavaScanResult(project, javaFiles); + + final SpringIndexerJavaScanResult finalResult = result; BiConsumer diagnosticsAggregator = new BiConsumer<>() { @Override public void accept(String docURI, Diagnostic diagnostic) { - generatedDiagnostics.add(new CachedDiagnostics(docURI, diagnostic)); + finalResult.getGeneratedDiagnostics().add(new CachedDiagnostics(docURI, diagnostic)); } }; @@ -550,49 +575,30 @@ public class SpringIndexerJava implements SpringIndexer { for (int i = 0; i < chunks.size(); i++) { log.info("scan java files, AST parse, chunk {} for files: {}", i, javaFiles.length); - String[] pass2Files = scanFiles(project, annotations, chunks.get(i), generatedSymbols, generatedBeans, diagnosticsAggregator, true); + String[] pass2Files = scanFiles(project, annotations, chunks.get(i), diagnosticsAggregator, true, result); if (pass2Files.length > 0) { log.info("scan java files, AST parse, pass 2, chunk {} for files: {}", i, javaFiles.length); - scanFiles(project, annotations, pass2Files, generatedSymbols, generatedBeans, diagnosticsAggregator, false); + scanFiles(project, annotations, pass2Files, diagnosticsAggregator, false, result); } } - log.info("scan java files done, number of symbols created: " + generatedSymbols.size()); + log.info("scan java files done, number of symbols created: " + result.getGeneratedSymbols().size()); - this.cache.store(symbolsCacheKey, javaFiles, generatedSymbols, dependencyTracker.getAllDependencies(), CachedSymbol.class); - this.cache.store(beansCacheKey, javaFiles, generatedBeans, dependencyTracker.getAllDependencies(), CachedBean.class); - this.cache.store(diagnosticsCacheKey, javaFiles, generatedDiagnostics, dependencyTracker.getAllDependencies(), CachedDiagnostics.class); -// dependencyTracker.dump(); - - symbols = (CachedSymbol[]) generatedSymbols.toArray(new CachedSymbol[generatedSymbols.size()]); - beans = (CachedBean[]) generatedBeans.toArray(new CachedBean[generatedBeans.size()]); - diagnostics = (CachedDiagnostics[]) generatedDiagnostics.toArray(new CachedDiagnostics[generatedDiagnostics.size()]); - } - else { - symbols = cachedSymbols.getLeft(); - beans = cachedBeans.getLeft(); - diagnostics = cachedDiagnostics.getLeft(); - - log.info("scan java files used cached data: {} - no. of cached symbols retrieved: {}", project.getElementName(), symbols.length); - this.dependencyTracker.restore(cachedSymbols.getRight()); - log.info("scan java files restored cached dependency data: {} - no. of cached dependencies: {}", cachedSymbols.getRight().size()); - } - - if (symbols != null && beans != null) { - WorkspaceSymbol[] enhancedSymbols = Arrays.stream(symbols).map(cachedSymbol -> cachedSymbol.getEnhancedSymbol()).toArray(WorkspaceSymbol[]::new); - Map> allBeans = Arrays.stream(beans).filter(cachedBean -> cachedBean.getBean() != null).collect(Collectors.groupingBy(CachedBean::getDocURI, Collectors.mapping(CachedBean::getBean, Collectors.toList()))); - Map> diagnosticsByDoc = Arrays.stream(diagnostics).filter(cachedDiagnostic -> cachedDiagnostic.getDiagnostic() != null).collect(Collectors.groupingBy(CachedDiagnostics::getDocURI, Collectors.mapping(CachedDiagnostics::getDiagnostic, Collectors.toList()))); - addEmptyDiagnostics(diagnosticsByDoc, javaFiles); - symbolHandler.addSymbols(project, enhancedSymbols, allBeans, diagnosticsByDoc); + this.cache.store(symbolsCacheKey, javaFiles, result.getGeneratedSymbols(), dependencyTracker.getAllDependencies(), CachedSymbol.class); + this.cache.store(beansCacheKey, javaFiles, result.getGeneratedBeans(), dependencyTracker.getAllDependencies(), CachedBean.class); + this.cache.store(diagnosticsCacheKey, javaFiles, result.getGeneratedDiagnostics(), dependencyTracker.getAllDependencies(), CachedDiagnostics.class); } + result.publishResults(symbolHandler); + reconcileWithCompleteIndex(project, result.getMarkedForReconcilingWithCompleteIndex()); + log.info("reconciling stats - counter: " + reconciler.getStatsCounter()); log.info("reconciling stats - timer: " + reconciler.getStatsTimer()); } - private String[] scanFiles(IJavaProject project, AnnotationHierarchies annotations, String[] javaFiles, List generatedSymbols, List generatedBeans, - BiConsumer diagnosticsAggregator, boolean ignoreMethodBodies) throws Exception { + private String[] scanFiles(IJavaProject project, AnnotationHierarchies annotations, String[] javaFiles, + BiConsumer diagnosticsAggregator, boolean ignoreMethodBodies, SpringIndexerJavaScanResult result) throws Exception { PercentageProgressTask progressTask = this.progressService.createPercentageProgressTask(INDEX_FILES_TASK_ID + project.getElementName(), javaFiles.length, "Spring Tools: Indexing Java Sources for '" + project.getElementName() + "'"); @@ -611,7 +617,7 @@ public class SpringIndexerJava implements SpringIndexer { IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator); SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath, - lastModified, docRef, null, generatedSymbols, generatedBeans, problemCollector, nextPassFiles, !ignoreMethodBodies); + lastModified, docRef, null, problemCollector, nextPassFiles, !ignoreMethodBodies, false, result); scanAST(context, true); progressTask.increment(); @@ -632,15 +638,68 @@ public class SpringIndexerJava implements SpringIndexer { } } - private void addEmptyDiagnostics(Map> diagnosticsByDoc, String[] javaFiles) { - for (int i = 0; i < javaFiles.length; i++) { - File file = new File(javaFiles[i]); - String docURI = UriUtil.toUri(file).toASCIIString(); - - if (!diagnosticsByDoc.containsKey(docURI)) { - diagnosticsByDoc.put(docURI, Collections.emptyList()); - } + private void reconcileWithCompleteIndex(IJavaProject project, Map markedForReconcilingWithCompleteIndex) throws Exception { + if (markedForReconcilingWithCompleteIndex.isEmpty()) { + return; } + + boolean ignoreMethodBodies = false; + + String[] javaFiles = markedForReconcilingWithCompleteIndex.keySet().toArray(String[]::new); + long[] modificationTimestamps = new long[javaFiles.length]; + for (int i = 0; i < javaFiles.length; i++) { + modificationTimestamps[i] = markedForReconcilingWithCompleteIndex.get(javaFiles[i]); + } + + // reconcile + SpringIndexerJavaScanResult reconcilingResult = new SpringIndexerJavaScanResult(project, javaFiles); + + BiConsumer diagnosticsAggregator = new BiConsumer<>() { + @Override + public void accept(String docURI, Diagnostic diagnostic) { + reconcilingResult.getGeneratedDiagnostics().add(new CachedDiagnostics(docURI, diagnostic)); + } + }; + + FileASTRequestor requestor = new FileASTRequestor() { + + @Override + public void acceptAST(String sourceFilePath, CompilationUnit cu) { + File file = new File(sourceFilePath); + String docURI = UriUtil.toUri(file).toASCIIString(); + long lastModified = file.lastModified(); + AtomicReference docRef = new AtomicReference<>(); + + IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator); + + SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath, + lastModified, docRef, null, problemCollector, new ArrayList<>(), !ignoreMethodBodies, true, reconcilingResult); + + try { + DocumentUtils.getTempTextDocument(docURI, docRef, null); + reconcile(context); + } catch (Exception e) { + log.error("problem creating temp document during re-reconciling for: " + docURI, e); + } + + } + }; + + AnnotationHierarchies annotations = new AnnotationHierarchies(); + ASTParserCleanupEnabled parser = createParser(project, annotations, ignoreMethodBodies); + try { + parser.createASTs(javaFiles, null, new String[0], requestor, null); + } + finally { + parser.cleanup(); + } + + // cache update + IndexCacheKey diagnosticsCacheKey = getCacheKey(project, DIAGNOSTICS_KEY); + this.cache.update(diagnosticsCacheKey, javaFiles, modificationTimestamps, reconcilingResult.getGeneratedDiagnostics(), dependencyTracker.getAllDependencies(), CachedDiagnostics.class); + + // publish + reconcilingResult.publishResults(symbolHandler); } private void scanAST(final SpringIndexerJavaContext context, boolean includeReconcile) { @@ -739,20 +798,13 @@ public class SpringIndexerJava implements SpringIndexer { dependencyTracker.update(context.getFile(), context.getDependencies());; } - + private void reconcile(SpringIndexerJavaContext context) { // reconciling IProblemCollector problemCollector = new IProblemCollector() { List problems = new ArrayList<>(); - @Override - public void endCollecting() { - for (ReconcileProblem p : problems) { - context.getProblemCollector().accept(p); - } - } - @Override public void beginCollecting() { problems.clear(); @@ -762,12 +814,21 @@ public class SpringIndexerJava implements SpringIndexer { public void accept(ReconcileProblem problem) { problems.add(problem); } + + @Override + public void endCollecting() { + for (ReconcileProblem p : problems) { + context.getProblemCollector().accept(p); + } + } }; try { + problemCollector.beginCollecting(); - reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), problemCollector, context.isFullAst()); + reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), problemCollector, context.isFullAst(), context.isIndexComplete()); problemCollector.endCollecting(); + } catch (RequiredCompleteAstException e) { if (!context.isFullAst()) { // Let problems be found in the next pass, don't add the problems to the aggregate problems collector to not duplicate them with the next pass @@ -777,6 +838,9 @@ public class SpringIndexerJava implements SpringIndexer { problemCollector.endCollecting(); log.error("Complete AST required but it is complete already. Parsing ", context.getDocURI()); } + } catch (RequiredCompleteIndexException e) { + context.getResult().markForReconcilingWithCompleteIndex(context.getFile(), context.getLastModified()); + log.error("Complete AST required but it is complete already. Parsing ", context.getDocURI()); } } @@ -812,9 +876,9 @@ public class SpringIndexerJava implements SpringIndexer { AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(node); for (Iterator itr = annotationHierarchies.iterator(typeBinding); itr.hasNext();) { IAnnotationBinding ab = itr.next(); - /* - * If meta annotations of the current annotation is a "sub-type" of one of the annotations from symbol providers then add it to meta annotations - */ + // + // If meta annotations of the current annotation is a "sub-type" of one of the annotations from symbol providers then add it to meta annotations + // if (annotationHierarchies.isAnnotatedWithAnnotationByBindingKey(ab, symbolProviders::containsKey)) { metaAnnotations.add(ab.getAnnotationType()); } @@ -934,7 +998,6 @@ public class SpringIndexerJava implements SpringIndexer { .map(file -> file.getAbsolutePath() + "#" + file.lastModified()) .collect(Collectors.joining(",")); -// return new IndexCacheKey(project.getElementName(), "java", elementType, DigestUtils.md5Hex(GENERATION + "-" + classpathIdentifier).toUpperCase()); return new IndexCacheKey(project.getElementName(), "java", elementType, DigestUtils.md5Hex(GENERATION + "-" + validationSeveritySettings.toString() + "-" + classpathIdentifier).toUpperCase()); } @@ -1017,6 +1080,10 @@ public class SpringIndexerJava implements SpringIndexer { } } + public void setScanChunkSize(int chunkSize) { + this.scanChunkSize = chunkSize; + } + public void setValidationSeveritySettings(JsonObject javaValidationSettingsJson) { this.validationSeveritySettings = javaValidationSettingsJson; } @@ -1025,10 +1092,6 @@ public class SpringIndexerJava implements SpringIndexer { this.fileScanListener = fileScanListener; } - public void setScanChunkSize(int chunkSize) { - this.scanChunkSize = chunkSize; - } - private void fileScannedEvent(String file) { if (fileScanListener != null) { fileScanListener.fileScanned(file); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java index 5b326111b..9aa7f4383 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java @@ -35,15 +35,15 @@ public class SpringIndexerJavaContext { private final long lastModified; private final AtomicReference docRef; private final String content; - private final List generatedSymbols; - private final List beans; private final IProblemCollector getProblemCollector; private final List nextPassFiles; private final boolean fullAst; + private final boolean isIndexComplete; + private final SpringIndexerJavaScanResult scanResult; private final Set dependencies = new HashSet<>(); private final Set scannedTypes = new HashSet<>(); - + public SpringIndexerJavaContext( IJavaProject project, CompilationUnit cu, @@ -52,11 +52,11 @@ public class SpringIndexerJavaContext { long lastModified, AtomicReference docRef, String content, - List generatedSymbols, - List beans, IProblemCollector problemCollector, List nextPassFiles, - boolean fullAst + boolean fullAst, + boolean isIndexComplete, + SpringIndexerJavaScanResult scanResult ) { super(); this.project = project; @@ -66,11 +66,11 @@ public class SpringIndexerJavaContext { this.lastModified = lastModified; this.docRef = docRef; this.content = content; - this.generatedSymbols = generatedSymbols; this.getProblemCollector = problemCollector; - this.beans = beans; this.nextPassFiles = nextPassFiles; this.fullAst = fullAst; + this.isIndexComplete = isIndexComplete; + this.scanResult = scanResult; } public IJavaProject getProject() { @@ -100,13 +100,17 @@ public class SpringIndexerJavaContext { public String getContent() { return content; } + + public SpringIndexerJavaScanResult getResult() { + return scanResult; + } public List getGeneratedSymbols() { - return generatedSymbols; + return getResult().getGeneratedSymbols(); } public List getBeans() { - return beans; + return getResult().getGeneratedBeans(); } public List getNextPassFiles() { @@ -147,15 +151,19 @@ public class SpringIndexerJavaContext { return fullAst; } + public boolean isIndexComplete() { + return isIndexComplete; + } + public void resetDocumentRelatedElements(String docURI) { - Iterator beansIterator = beans.iterator(); + Iterator beansIterator = getBeans().iterator(); while (beansIterator.hasNext()) { if (beansIterator.next().getDocURI().equals(docURI)) { beansIterator.remove(); } } - Iterator symbolsIterator = generatedSymbols.iterator(); + Iterator symbolsIterator = getGeneratedSymbols().iterator(); while (symbolsIterator.hasNext()) { if (symbolsIterator.next().getDocURI().equals(docURI)) { symbolsIterator.remove(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaDependencyTracker.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaDependencyTracker.java index b1f3d38b8..94de83448 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaDependencyTracker.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaDependencyTracker.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2020 Pivotal, Inc. + * Copyright (c) 2019, 2025 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,7 +13,6 @@ package org.springframework.ide.vscode.boot.java.utils; import java.util.Collection; import java.util.Set; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,10 +25,6 @@ public class SpringIndexerJavaDependencyTracker { private Multimap dependencies = MultimapBuilder.hashKeys().hashSetValues().build(); - public void addDependency(String sourceFile, ITypeBinding dependsOn) { - dependencies.put(sourceFile, dependsOn.getKey()); - } - public void dump() { log.info("=== Dependencies ==="); for (String sourceFile : dependencies.keySet()) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaScanResult.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaScanResult.java new file mode 100644 index 000000000..4a01ea64e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaScanResult.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.springframework.ide.vscode.boot.java.beans.CachedBean; +import org.springframework.ide.vscode.boot.java.reconcilers.CachedDiagnostics; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; +import org.springframework.ide.vscode.commons.util.UriUtil; + +/** + * Class to capture the major results of scanning one or more files + * for Spring symbol creation, indexing, and reconciling + * + * @author Martin Lippert + */ +public class SpringIndexerJavaScanResult { + + private final Map markedForReconciling; + + private final List generatedSymbols; + private final List generatedBeans; + private final List generatedDiagnostics; + + private final IJavaProject project; + private final String[] javaFiles; + + public SpringIndexerJavaScanResult(IJavaProject project, String[] javaFiles) { + this.project = project; + this.javaFiles = javaFiles; + + this.markedForReconciling = new HashMap<>(); + this.generatedSymbols = new ArrayList(); + this.generatedBeans = new ArrayList(); + this.generatedDiagnostics = new ArrayList(); + } + + public SpringIndexerJavaScanResult(IJavaProject project, String[] javaFiles, SymbolHandler symbolHandler, + CachedSymbol[] symbols, CachedBean[] beans, CachedDiagnostics[] diagnostics) { + + this.project = project; + this.javaFiles = javaFiles; + + this.markedForReconciling = new HashMap<>(); + this.generatedSymbols = Arrays.asList(symbols); + this.generatedBeans = Arrays.asList(beans); + this.generatedDiagnostics = Arrays.asList(diagnostics); + } + + public Map getMarkedForReconcilingWithCompleteIndex() { + return markedForReconciling; + } + + public void markForReconcilingWithCompleteIndex(String file, long lastModified) { + this.markedForReconciling.put(file, lastModified); + } + + public List getGeneratedBeans() { + return generatedBeans; + } + + public List getGeneratedSymbols() { + return generatedSymbols; + } + + public List getGeneratedDiagnostics() { + return generatedDiagnostics; + } + + public void publishResults(SymbolHandler symbolHandler) { + WorkspaceSymbol[] enhancedSymbols = generatedSymbols.stream().map(cachedSymbol -> cachedSymbol.getEnhancedSymbol()).toArray(WorkspaceSymbol[]::new); + Map> allBeans = generatedBeans.stream().filter(cachedBean -> cachedBean.getBean() != null).collect(Collectors.groupingBy(CachedBean::getDocURI, Collectors.mapping(CachedBean::getBean, Collectors.toList()))); + Map> diagnosticsByDoc = generatedDiagnostics.stream().filter(cachedDiagnostic -> cachedDiagnostic.getDiagnostic() != null).collect(Collectors.groupingBy(CachedDiagnostics::getDocURI, Collectors.mapping(CachedDiagnostics::getDiagnostic, Collectors.toList()))); + + addEmptyDiagnostics(diagnosticsByDoc, javaFiles); // to make sure that files without diagnostics publish an empty array of diagnostics + + symbolHandler.addSymbols(this.project, enhancedSymbols, allBeans, diagnosticsByDoc); + } + + private void addEmptyDiagnostics(Map> diagnosticsByDoc, String[] javaFiles) { + for (int i = 0; i < javaFiles.length; i++) { + File file = new File(javaFiles[i]); + String docURI = UriUtil.toUri(file).toASCIIString(); + + if (!diagnosticsByDoc.containsKey(docURI)) { + diagnosticsByDoc.put(docURI, Collections.emptyList()); + } + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/RandomSampleClass.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/RandomSampleClass.java new file mode 100644 index 000000000..e69de29bb