Skip to content

Commit

Permalink
GH-1431: replaced scan pass logic with exception handling and removed…
Browse files Browse the repository at this point in the history
… AbstractSymbolProvider

GH-1431: changed indexing of components to index embedded components like bean methods, request mappings, etc.

GH-1431: added new tests for index structure
Signed-off-by: Martin Lippert <[email protected]>
  • Loading branch information
martinlippert committed Feb 26, 2025
1 parent 0f081b9 commit 025382b
Show file tree
Hide file tree
Showing 49 changed files with 1,834 additions and 871 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*******************************************************************************
* Copyright (c) 2017, 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.lsp4j.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException;
import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouterSymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.FunctionUtils;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;

import reactor.util.function.Tuple2;

/**
* @author Martin Lippert
* @author Kris De Volder
*/
public class BeansIndexer {

private static final Logger log = LoggerFactory.getLogger(BeansIndexer.class);

public static void indexBeanMethod(Bean configBean, Annotation node, SpringIndexerJavaContext context, TextDocument doc) {
if (node == null) return;

ASTNode parent = node.getParent();
if (parent == null || !(parent instanceof MethodDeclaration)) return;

MethodDeclaration method = (MethodDeclaration) parent;
if (isMethodAbstract(method)) return;

boolean isWebfluxRouter = WebfluxRouterSymbolProvider.isWebfluxRouterBean(method);
if (isWebfluxRouter) {
if (!context.isFullAst()) {
throw new RequiredCompleteAstException();
}
}

boolean isFunction = isFunctionBean(method);
ITypeBinding beanType = getBeanType(method);
String markerString = getAnnotations(method);

for (Tuple2<String, DocumentRegion> nameAndRegion : BeanUtils.getBeanNamesFromBeanAnnotationWithRegions(node, doc)) {
try {
Location location = new Location(doc.getUri(), doc.toRange(nameAndRegion.getT2()));

String beanLabel = beanLabel(isFunction, nameAndRegion.getT1(), beanType.getName(), "@Bean" + markerString);

InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(method, doc);

Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(beanType, supertypes);

Collection<Annotation> annotationsOnMethod = ASTUtils.getAnnotations(method);
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);

Bean beanDefinition = new Bean(nameAndRegion.getT1(), beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, false, beanLabel);

if (isWebfluxRouter) {
WebfluxRouterSymbolProvider.createWebfluxElements(beanDefinition, method, context, doc);
}

configBean.addChild(beanDefinition);

} catch (BadLocationException e) {
log.error("", e);
}
}
}

public static String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) {
StringBuilder symbolLabel = new StringBuilder();
symbolLabel.append('@');
symbolLabel.append(isFunctionBean ? '>' : '+');
symbolLabel.append(' ');
symbolLabel.append('\'');
symbolLabel.append(beanName);
symbolLabel.append('\'');

markerString = markerString != null && markerString.length() > 0 ? " (" + markerString + ") " : " ";
symbolLabel.append(markerString);

symbolLabel.append(beanType);
return symbolLabel.toString();
}

public static ITypeBinding getBeanType(MethodDeclaration method) {
return method.getReturnType2().resolveBinding();
}

public static boolean isFunctionBean(MethodDeclaration method) {
String returnType = null;

if (method.getReturnType2().isParameterizedType()) {
ParameterizedType paramType = (ParameterizedType) method.getReturnType2();
Type type = paramType.getType();
ITypeBinding typeBinding = type.resolveBinding();
returnType = typeBinding.getBinaryName();
}
else {
returnType = method.getReturnType2().resolveBinding().getQualifiedName();
}

return FunctionUtils.FUNCTION_FUNCTION_TYPE.equals(returnType) || FunctionUtils.FUNCTION_CONSUMER_TYPE.equals(returnType)
|| FunctionUtils.FUNCTION_SUPPLIER_TYPE.equals(returnType);
}

public static String getAnnotations(MethodDeclaration method) {
StringBuilder result = new StringBuilder();

List<?> modifiers = method.modifiers();
for (Object modifier : modifiers) {
if (modifier instanceof Annotation) {
Annotation annotation = (Annotation) modifier;
IAnnotationBinding annotationBinding = annotation.resolveAnnotationBinding();
String type = annotationBinding.getAnnotationType().getBinaryName();

if (type != null && !Annotations.BEAN.equals(type)) {
result.append(' ');
result.append(annotation.toString());
}
}
}
return result.toString();
}

public static boolean isMethodAbstract(MethodDeclaration method) {
List<?> modifiers = method.modifiers();
for (Object modifier : modifiers) {
if (modifier instanceof Modifier && ((Modifier) modifier).isAbstract()) {
return true;
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,29 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider;
import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouterSymbolProvider;
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
import org.springframework.ide.vscode.boot.java.utils.FunctionUtils;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava.SCAN_PASS;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
Expand All @@ -54,7 +43,7 @@
* @author Martin Lippert
* @author Kris De Volder
*/
public class BeansSymbolProvider extends AbstractSymbolProvider {
public class BeansSymbolProvider implements SymbolProvider {

private static final Logger log = LoggerFactory.getLogger(BeansSymbolProvider.class);

Expand All @@ -66,91 +55,37 @@ public void addSymbols(Annotation node, ITypeBinding typeBinding, Collection<ITy
if (parent == null || !(parent instanceof MethodDeclaration)) return;

MethodDeclaration method = (MethodDeclaration) parent;
if (isMethodAbstract(method)) return;
if (BeansIndexer.isMethodAbstract(method)) return;

List<SpringIndexElement> childElements = new ArrayList<>();

boolean isWebfluxRouter = WebfluxRouterSymbolProvider.isWebfluxRouterBean(method);

// for webflux details, we need full method body ASTs
if (isWebfluxRouter) {
Block methodBody = method.getBody();
if ((methodBody == null || methodBody.statements() == null || methodBody.statements().size() == 0)
&& SCAN_PASS.ONE.equals(context.getPass())) {
context.getNextPassFiles().add(context.getFile());
return;
}
else {
WebfluxRouterSymbolProvider.createWebfluxElements(method, context, doc, childElements);
}
} else if (!isWebfluxRouter && SCAN_PASS.TWO.equals(context.getPass())) {
return;
}

boolean isFunction = isFunctionBean(method);
boolean isFunction = BeansIndexer.isFunctionBean(method);

ITypeBinding beanType = getBeanType(method);
String markerString = getAnnotations(method);
ITypeBinding beanType = BeansIndexer.getBeanType(method);
String markerString = BeansIndexer.getAnnotations(method);

// lookup parent config
SpringIndexElement configParent = findNearestConfigBean(context.getBeans(), doc.getUri());

for (Tuple2<String, DocumentRegion> nameAndRegion : BeanUtils.getBeanNamesFromBeanAnnotationWithRegions(node, doc)) {
try {
Location location = new Location(doc.getUri(), doc.toRange(nameAndRegion.getT2()));

WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel(isFunction, nameAndRegion.getT1(), beanType.getName(), "@Bean" + markerString),
BeansIndexer.beanLabel(isFunction, nameAndRegion.getT1(), beanType.getName(), "@Bean" + markerString),
SymbolKind.Interface,
Either.forLeft(location)
);

InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(method, doc);

Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(beanType, supertypes);

Collection<Annotation> annotationsOnMethod = ASTUtils.getAnnotations(method);
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);

Bean beanDefinition = new Bean(nameAndRegion.getT1(), beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, false, symbol.getName());
if (childElements.size() > 0) {
for (SpringIndexElement springIndexElement : childElements) {
beanDefinition.addChild(springIndexElement);
}
}

context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));

if (configParent != null) {
configParent.addChild(beanDefinition);
}
else {
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
}

} catch (BadLocationException e) {
log.error("", e);
}
}
}

private SpringIndexElement findNearestConfigBean(List<CachedBean> beans, String docURI) {
int i = beans.size() - 1;

while (i >= 0 && beans.get(i).getDocURI().equals(docURI)) {
if (beans.get(i).getBean() instanceof Bean bean && bean.isConfiguration() && docURI.equals(docURI)) {
return beans.get(i).getBean();
}
i--;
}

return null;
@Override
public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
indexFunctionBeans(typeDeclaration, context, doc);
}

@Override
protected void addSymbolsPass1(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
// this checks function beans that are defined as implementations of Function interfaces
private void indexFunctionBeans(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
ITypeBinding functionBean = FunctionUtils.getFunctionBean(typeDeclaration, doc);
if (functionBean != null) {
try {
Expand All @@ -159,7 +94,7 @@ protected void addSymbolsPass1(TypeDeclaration typeDeclaration, SpringIndexerJav
Location beanLocation = new Location(doc.getUri(), doc.toRange(ASTUtils.nodeRegion(doc, typeDeclaration.getName())));

WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel(true, beanName, beanType.getName(), null),
BeansIndexer.beanLabel(true, beanName, beanType.getName(), null),
SymbolKind.Interface,
Either.forLeft(beanLocation));

Expand All @@ -183,70 +118,4 @@ protected void addSymbolsPass1(TypeDeclaration typeDeclaration, SpringIndexerJav
}
}

public static String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) {
StringBuilder symbolLabel = new StringBuilder();
symbolLabel.append('@');
symbolLabel.append(isFunctionBean ? '>' : '+');
symbolLabel.append(' ');
symbolLabel.append('\'');
symbolLabel.append(beanName);
symbolLabel.append('\'');

markerString = markerString != null && markerString.length() > 0 ? " (" + markerString + ") " : " ";
symbolLabel.append(markerString);

symbolLabel.append(beanType);
return symbolLabel.toString();
}

protected ITypeBinding getBeanType(MethodDeclaration method) {
return method.getReturnType2().resolveBinding();
}

private boolean isFunctionBean(MethodDeclaration method) {
String returnType = null;

if (method.getReturnType2().isParameterizedType()) {
ParameterizedType paramType = (ParameterizedType) method.getReturnType2();
Type type = paramType.getType();
ITypeBinding typeBinding = type.resolveBinding();
returnType = typeBinding.getBinaryName();
}
else {
returnType = method.getReturnType2().resolveBinding().getQualifiedName();
}

return FunctionUtils.FUNCTION_FUNCTION_TYPE.equals(returnType) || FunctionUtils.FUNCTION_CONSUMER_TYPE.equals(returnType)
|| FunctionUtils.FUNCTION_SUPPLIER_TYPE.equals(returnType);
}

private String getAnnotations(MethodDeclaration method) {
StringBuilder result = new StringBuilder();

List<?> modifiers = method.modifiers();
for (Object modifier : modifiers) {
if (modifier instanceof Annotation) {
Annotation annotation = (Annotation) modifier;
IAnnotationBinding annotationBinding = annotation.resolveAnnotationBinding();
String type = annotationBinding.getAnnotationType().getBinaryName();

if (type != null && !Annotations.BEAN.equals(type)) {
result.append(' ');
result.append(annotation.toString());
}
}
}
return result.toString();
}

private boolean isMethodAbstract(MethodDeclaration method) {
List<?> modifiers = method.modifiers();
for (Object modifier : modifiers) {
if (modifier instanceof Modifier && ((Modifier) modifier).isAbstract()) {
return true;
}
}
return false;
}

}
Loading

0 comments on commit 025382b

Please sign in to comment.