HQL and JPQL syntax validation reconciling

This commit is contained in:
aboyko
2024-04-30 10:05:20 -04:00
parent 43672879e5
commit 385c35e832
16 changed files with 839 additions and 47 deletions

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.app;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.HqlSemanticTokens;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.QueryJdtAstReconciler;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSemanticTokens;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSupportState;
@@ -114,5 +115,9 @@ public class JdtConfig {
@Bean JdtDataQuerySemanticTokensProvider jpqlJdtSemanticTokensProvider(JpqlSemanticTokens jpqlProvider, HqlSemanticTokens hqlProvider, JpqlSupportState supportState) {
return new JdtDataQuerySemanticTokensProvider(jpqlProvider, hqlProvider, supportState);
}
@Bean QueryJdtAstReconciler dataQueryReconciler() {
return new QueryJdtAstReconciler();
}
}

View File

@@ -37,5 +37,8 @@ public class SpringProblemCategories {
public static final ProblemCategory VERSION_VALIDATION = new ProblemCategory("version-validation", "Versions and Support Ranges",
new Toggle("Enablement", EnumSet.of(OFF, ON), ON, "boot-java.validation.java.version-validation"));
public static final ProblemCategory JPQL = new ProblemCategory("jpql-validation", "JPQL",
new Toggle("Enablement", EnumSet.of(OFF, ON), ON, "boot-java.validation.jpql"));;
}

View File

@@ -35,6 +35,7 @@ public class Annotations {
public static final String JPA_JAVAX_EMBEDDED_ID = "javax.persistence.EmbeddedId";
public static final String JPA_JAKARTA_ID_CLASS = "jakarta.persistence.IdClass";
public static final String JPA_JAVAX_ID_CLASS = "javax.persistence.IdClass";
public static final String DATA_QUERY = "org.springframework.data.jpa.repository.Query";
public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
public static final String INJECT = "javax.inject.Inject";

View File

@@ -0,0 +1,75 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, 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:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.util.BitSet;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.springframework.ide.vscode.boot.java.handlers.Reconciler;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.parser.hql.HqlLexer;
import org.springframework.ide.vscode.parser.hql.HqlParser;
public class HqlReconciler implements Reconciler {
@Override
public void reconcile(String text, int startPosition, IProblemCollector problemCollector) {
HqlLexer lexer = new HqlLexer(CharStreams.fromString(text));
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
HqlParser parser = new HqlParser(antlrTokens);
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
parser.addErrorListener(new ANTLRErrorListener() {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
String msg, RecognitionException e) {
Token token = (Token) offendingSymbol;
int offset = token.getStartIndex();
int length = token.getStopIndex() - token.getStartIndex() + 1;
if (token.getStartIndex() >= token.getStopIndex()) {
offset = token.getStartIndex() - token.getCharPositionInLine();
length = token.getCharPositionInLine() + 1;
}
problemCollector.accept(new ReconcileProblemImpl(QueryProblemType.EXPRESSION_SYNTAX, msg, startPosition + offset, length));
}
@Override
public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction,
ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex,
BitSet conflictingAlts, ATNConfigSet configs) {
}
@Override
public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact,
BitSet ambigAlts, ATNConfigSet configs) {
}
});
parser.ql_statement();
}
}

View File

@@ -18,6 +18,7 @@ import java.util.stream.Collectors;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
@@ -49,6 +50,8 @@ public class HqlSemanticTokens implements SemanticTokensDataProvider {
Map<Token, String> semantics = new HashMap<>();
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
parser.addParseListener(new HqlBaseListener() {
private void processTerminalNode(TerminalNode node) {

View File

@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -19,14 +20,13 @@ import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TextBlock;
import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
@@ -69,44 +69,61 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv
@Override
public boolean visit(NormalAnnotation a) {
if (isQueryAnnotation(a)) {
Expression queryValueNode = ((List<?>) a.values()).stream()
.filter(MemberValuePair.class::isInstance)
.map(MemberValuePair.class::cast)
.filter(p -> "value".equals(p.getName().getIdentifier()))
.findFirst().map(p -> p.getValue())
.get();
if (queryValueNode instanceof StringLiteral) {
IAnnotationBinding annotationBinding = a.resolveAnnotationBinding();
String query = getJpaQuery(annotationBinding);
if (query != null && !query.isBlank()) {
int valueOffset = queryValueNode.getStartPosition() + 1;
tokensData.addAll(provider.computeTokens(query, valueOffset));
List<?> values = a.values();
Expression queryExpression = null;
boolean isNative = false;
for (Object value : values) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
if (name != null) {
switch (name) {
case "value":
queryExpression = pair.getValue();
break;
case "nativeQuery":
Expression expression = pair.getValue();
if (expression != null) {
Object o = expression.resolveConstantExpressionValue();
if (o instanceof Boolean b) {
isNative = b.booleanValue();
}
}
break;
}
}
}
}
if (queryExpression != null) {
if (isNative) {
//TODO: SQL semantic tokens
} else {
tokensData.addAll(computeTokensForQueryExpression(provider, queryExpression));
}
}
return false;
}
return false;
}
@Override
public boolean visit(SingleMemberAnnotation a) {
if (isQueryAnnotation(a) && a.getValue() instanceof StringLiteral) {
IAnnotationBinding annotationBinding = a.resolveAnnotationBinding();
String query = getJpaQuery(annotationBinding);
if (query != null && !query.isBlank()) {
int valueOffset = a.getValue().getStartPosition() + 1;
tokensData.addAll(provider.computeTokens(query, valueOffset));
}
if (isQueryAnnotation(a)) {
tokensData.addAll(computeTokensForQueryExpression(provider, a.getValue()));
}
return false;
}
@Override
public boolean visit(MethodInvocation node) {
if ("createQuery".equals(node.getName().getIdentifier()) && node.arguments().size() <= 2 && node.arguments().get(0) instanceof StringLiteral queryExpr) {
if ("createQuery".equals(node.getName().getIdentifier()) && node.arguments().size() <= 2 && node.arguments().get(0) instanceof Expression queryExpr) {
IMethodBinding methodBinding = node.resolveMethodBinding();
if ("jakarta.persistence.EntityManager".equals(methodBinding.getDeclaringClass().getQualifiedName())) {
if (methodBinding.getParameterTypes().length <= 2 && "java.lang.String".equals(methodBinding.getParameterTypes()[0].getQualifiedName())) {
tokensData.addAll(provider.computeTokens(queryExpr.getLiteralValue(), queryExpr.getStartPosition() + 1));
tokensData.addAll(computeTokensForQueryExpression(provider, queryExpr));
}
}
}
@@ -118,33 +135,31 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv
return tokensData;
}
private static List<SemanticTokenData> computeTokensForQueryExpression(SemanticTokensDataProvider provider, Expression valueExp) {
String query = null;
int offset = 0;
if (valueExp instanceof StringLiteral sl) {
query = sl.getEscapedValue();
query = query.substring(1, query.length() - 1);
offset = sl.getStartPosition() + 1; // +1 to skip over opening "
} else if (valueExp instanceof TextBlock tb) {
query = tb.getEscapedValue();
query = query.substring(3, query.length() - 3);
offset = tb.getStartPosition() + 3; // +3 to skip over opening """
}
if (query != null) {
return provider.computeTokens(query, offset);
}
return Collections.emptyList();
}
private static boolean isQueryAnnotation(Annotation a) {
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
}
private static String getJpaQuery(IAnnotationBinding annotationBinding) {
if (annotationBinding != null && annotationBinding.getAnnotationType() != null) {
if (FQN_QUERY.equals(annotationBinding.getAnnotationType().getQualifiedName())) {
String query = null;
boolean isNative = false;
for (IMemberValuePairBinding pair : annotationBinding.getAllMemberValuePairs()) {
switch (pair.getName()) {
case "value":
query = (String) pair.getValue();
break;
case "nativeQuery":
isNative = (Boolean) pair.getValue();
break;
default:
}
}
return isNative ? null : query;
}
}
return null;
}
@Override
public boolean isApplicable(IJavaProject project) {
return supportState.isEnabled() && SpringProjectUtil.hasDependencyStartingWith(project, "spring-data-jpa", null);

View File

@@ -15,6 +15,7 @@ import java.util.Set;
import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensHandler;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
@@ -22,10 +23,12 @@ import org.springframework.ide.vscode.commons.util.text.LanguageId;
public class JpaQueryPropertiesLanguageServerComponents implements LanguageServerComponents {
private final QueryPropertiesSemanticTokensHandler semanticTokensHandler;
private final NamedQueryPropertiesReconcileEngine reconcileEngine;
public JpaQueryPropertiesLanguageServerComponents(SimpleTextDocumentService documents, JavaProjectFinder projectsFinder,
JpqlSemanticTokens jpqlSemanticTokensProvider, HqlSemanticTokens hqlSematicTokensProvider, JpqlSupportState supportState) {
this.semanticTokensHandler = new QueryPropertiesSemanticTokensHandler(documents, projectsFinder, jpqlSemanticTokensProvider, hqlSematicTokensProvider, supportState);
this.reconcileEngine = new NamedQueryPropertiesReconcileEngine(projectsFinder);
}
@Override
@@ -38,4 +41,9 @@ public class JpaQueryPropertiesLanguageServerComponents implements LanguageServe
return Optional.of(semanticTokensHandler);
}
@Override
public Optional<IReconcileEngine> getReconcileEngine() {
return Optional.of(reconcileEngine);
}
}

View File

@@ -0,0 +1,76 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, 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:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.util.BitSet;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.springframework.ide.vscode.boot.java.handlers.Reconciler;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.parser.jpql.JpqlLexer;
import org.springframework.ide.vscode.parser.jpql.JpqlParser;
public class JpqlReconciler implements Reconciler {
@Override
public void reconcile(String text, int startPosition, IProblemCollector problemCollector) {
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(text));
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
JpqlParser parser = new JpqlParser(antlrTokens);
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
parser.addErrorListener(new ANTLRErrorListener() {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
String msg, RecognitionException e) {
Token token = (Token) offendingSymbol;
int offset = token.getStartIndex();
int length = token.getStopIndex() - token.getStartIndex() + 1;
if (token.getStartIndex() >= token.getStopIndex()) {
offset = token.getStartIndex() - token.getCharPositionInLine();
length = token.getCharPositionInLine() + 1;
}
problemCollector.accept(new ReconcileProblemImpl(QueryProblemType.EXPRESSION_SYNTAX, msg, startPosition + offset, length));
}
@Override
public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction,
ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex,
BitSet conflictingAlts, ATNConfigSet configs) {
}
@Override
public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact,
BitSet ambigAlts, ATNConfigSet configs) {
}
});
parser.ql_statement();
}
}

View File

@@ -18,6 +18,7 @@ import java.util.stream.Collectors;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
@@ -51,6 +52,8 @@ public class JpqlSemanticTokens implements SemanticTokensDataProvider {
JpqlParser parser = new JpqlParser(antlrTokens);
Map<Token, String> semantics = new HashMap<>();
parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
parser.addParseListener(new JpqlBaseListener() {
private void processTerminalNode(TerminalNode node) {

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, 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:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.springframework.ide.vscode.boot.java.handlers.Reconciler;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.KeyValuePair;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value;
public class NamedQueryPropertiesReconcileEngine implements IReconcileEngine {
private final JavaProjectFinder projectFinder;
public NamedQueryPropertiesReconcileEngine(JavaProjectFinder projectFinder) {
this.projectFinder = projectFinder;
}
@Override
public void reconcile(IDocument doc, IProblemCollector problemCollector) {
try {
Reconciler reconciler = projectFinder.find(new TextDocumentIdentifier(doc.getUri()))
.map(p -> SpringProjectUtil.hasDependencyStartingWith(p, "spring-data-jpa", null) ? new HqlReconciler() : new JpqlReconciler())
.orElse(new JpqlReconciler());
AntlrParser parser = new AntlrParser();
ParseResults parseResults = parser.parse(doc.get());
for (KeyValuePair pair : parseResults.ast.getNodes(KeyValuePair.class)) {
Value value = pair.getValue();
reconciler.reconcile(value.decode(), value.getOffset(), problemCollector);
}
} finally {
problemCollector.endCollecting();
}
}
}

View File

@@ -0,0 +1,150 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, 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:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.net.URI;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TextBlock;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.handlers.Reconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
public class QueryJdtAstReconciler implements JdtAstReconciler {
@Override
public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) throws RequiredCompleteAstException {
return new ASTVisitor() {
@Override
public boolean visit(NormalAnnotation node) {
Set<String> allAnnotations = AnnotationHierarchies.getTransitiveSuperAnnotations(node.resolveTypeBinding());
if (!allAnnotations.contains(Annotations.DATA_QUERY)) {
return false;
}
List<?> values = node.values();
Expression queryExpression = null;
boolean isNative = false;
for (Object value : values) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
if (name != null) {
switch (name) {
case "value":
queryExpression = pair.getValue();
break;
case "nativeQuery":
Expression expression = pair.getValue();
if (expression != null) {
Object o = expression.resolveConstantExpressionValue();
if (o instanceof Boolean b) {
isNative = b.booleanValue();
}
}
break;
}
}
}
}
if (queryExpression != null) {
if (isNative) {
//TODO: SQL syntax validation
} else {
reconcileExpression(getQueryReconciler(project), queryExpression, problemCollector);
}
}
return false;
}
@Override
public boolean visit(SingleMemberAnnotation node) {
Set<String> allAnnotations = AnnotationHierarchies.getTransitiveSuperAnnotations(node.resolveTypeBinding());
if (!allAnnotations.contains(Annotations.DATA_QUERY)) {
return false;
}
reconcileExpression(getQueryReconciler(project), node.getValue(), problemCollector);
return false;
}
@Override
public boolean visit(MethodInvocation node) {
if ("createQuery".equals(node.getName().getIdentifier()) && node.arguments().size() <= 2 && node.arguments().get(0) instanceof Expression queryExpr) {
IMethodBinding methodBinding = node.resolveMethodBinding();
if ("jakarta.persistence.EntityManager".equals(methodBinding.getDeclaringClass().getQualifiedName())) {
if (methodBinding.getParameterTypes().length <= 2 && "java.lang.String".equals(methodBinding.getParameterTypes()[0].getQualifiedName())) {
reconcileExpression(getQueryReconciler(project), queryExpr, problemCollector);
}
}
}
return super.visit(node);
}
};
}
/*
* Gets either HQL or JPQL reconciler
*/
private static Reconciler getQueryReconciler(IJavaProject project) {
return SpringProjectUtil.hasDependencyStartingWith(project, "hibernate-core", null) ? new HqlReconciler() : new JpqlReconciler();
}
private void reconcileExpression(Reconciler reconciler, Expression valueExp, IProblemCollector problemCollector) {
String query = null;
int offset = 0;
if (valueExp instanceof StringLiteral sl) {
query = sl.getEscapedValue();
query = query.substring(1, query.length() - 1);
offset = sl.getStartPosition() + 1; // +1 to skip over opening "
} else if (valueExp instanceof TextBlock tb) {
query = tb.getEscapedValue();
query = query.substring(3, query.length() - 3);
offset = tb.getStartPosition() + 3; // +3 to skip over opening """
}
if (query != null) {
reconciler.reconcile(query, offset, problemCollector);
}
}
@Override
public boolean isApplicable(IJavaProject project) {
return SpringProjectUtil.hasDependencyStartingWith(project, "spring-data-jpa", null);
}
@Override
public ProblemType getProblemType() {
return QueryProblemType.EXPRESSION_SYNTAX;
}
}

View File

@@ -0,0 +1,70 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, 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:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import static org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity.ERROR;
import org.springframework.ide.vscode.boot.common.SpringProblemCategories;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemCategory;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
public enum QueryProblemType implements ProblemType {
EXPRESSION_SYNTAX(ERROR, "Syntax", "Query Expression Syntax");
private final ProblemSeverity defaultSeverity;
private String description;
private String label;
private QueryProblemType(ProblemSeverity defaultSeverity, String description) {
this(defaultSeverity, description, null);
}
private QueryProblemType(ProblemSeverity defaultSeverity, String description, String label) {
this.description = description;
this.defaultSeverity = defaultSeverity;
this.label = label;
}
@Override
public ProblemSeverity getDefaultSeverity() {
return defaultSeverity;
}
public String getLabel() {
if (label==null) {
label = createDefaultLabel();
}
return label;
}
@Override
public String getDescription() {
return description;
}
private String createDefaultLabel() {
String label = this.toString().substring(5).toLowerCase().replace('_', ' ');
return Character.toUpperCase(label.charAt(0)) + label.substring(1);
}
@Override
public String getCode() {
return name();
}
@Override
public ProblemCategory getCategory() {
return SpringProblemCategories.JPQL;
}
}

View File

@@ -70,7 +70,7 @@ public class AnnotationNodeReconciler implements JdtAstReconciler {
new AnnotationParamReconciler(SPRING_POST_FILTER, "value", "", "", spelExpressionReconciler),
new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, null, "", "", spelExpressionReconciler),
new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", "", spelExpressionReconciler),
new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", "", spelExpressionReconciler)
};
config.addListener(evt -> this.spelExpressionReconciler.setEnabled(config.isSpelExpressionValidationEnabled()));

View File

@@ -91,6 +91,54 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void singleMemberAnnotationWithTextBlock() throws Exception {
String source = """
package my.package
import org.springframework.data.jpa.repository.Query;
public interface OwnerRepository {
@Query(\"""
SELECT DISTINCT owner FROM Owner owner
\""")
void findByLastName();
}
""";
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(125, 131, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("SELECT");
token = tokens.get(1);
assertThat(token).isEqualTo(new SemanticTokenData(132, 140, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("DISTINCT");
token = tokens.get(2);
assertThat(token).isEqualTo(new SemanticTokenData(141, 146, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
token = tokens.get(3);
assertThat(token).isEqualTo(new SemanticTokenData(147, 151, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("FROM");
token = tokens.get(4);
assertThat(token).isEqualTo(new SemanticTokenData(152, 157, "class", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("Owner");
token = tokens.get(5);
assertThat(token).isEqualTo(new SemanticTokenData(158, 163, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void normalAnnotation() throws Exception {
String source = """
@@ -184,6 +232,55 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void createQueryMethodWithTextBlock() throws Exception {
String source = """
package my.package
import jakarta.persistence.EntityManager;
public interface OwnerRepository {
default void findByLastName(EntityManager manager) {
manager.createQuery(\"""
SELECT DISTINCT owner FROM Owner owner
\""")
}
}
""";
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(181, 187, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("SELECT");
token = tokens.get(1);
assertThat(token).isEqualTo(new SemanticTokenData(188, 196, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("DISTINCT");
token = tokens.get(2);
assertThat(token).isEqualTo(new SemanticTokenData(197, 202, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
token = tokens.get(3);
assertThat(token).isEqualTo(new SemanticTokenData(203, 207, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("FROM");
token = tokens.get(4);
assertThat(token).isEqualTo(new SemanticTokenData(208, 213, "class", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("Owner");
token = tokens.get(5);
assertThat(token).isEqualTo(new SemanticTokenData(214, 219, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void nativeQuery() throws Exception {
String source = """

View File

@@ -0,0 +1,235 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, 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:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.io.File;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class QueryReconcilerTest {
@Autowired
private BootLanguageServerHarness harness;
@Autowired
private JavaProjectFinder projectFinder;
private File directory;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/spring-modulith-example-full/").toURI());
String projectDir = directory.toURI().toString();
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
}
@Test
void noErrorsInPropsFile() throws Exception {
String source = """
query1=SELECT ptype FROM PetType ptype ORDER BY ptype.name
""";
String docUri = directory.toPath().resolve("src/main/resources/jpa-named-queries.properties").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JPA_QUERY_PROPERTIES, source, docUri);
editor.assertProblems();
}
@Test
void errorsInPropsFile() throws Exception {
String source = """
query1=SELECTX ptype FROM PetType ptype ORDER BY ptype.name
""";
String docUri = directory.toPath().resolve("src/main/resources/jpa-named-queries.properties").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JPA_QUERY_PROPERTIES, source, docUri);
editor.assertProblems("SELECTX|mismatched input 'SELECTX'");
}
@Test
void noErrorsForHqlInPropsFile() throws Exception {
String source = """
query1=SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName%
""";
String docUri = directory.toPath().resolve("src/main/resources/jpa-named-queries.properties").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JPA_QUERY_PROPERTIES, source, docUri);
editor.assertProblems();
}
@Test
void errorsForJpqlInPropsFile() throws Exception {
directory = new File(ProjectsHarness.class.getResource("/test-projects/super-property-nav-sample/").toURI());
String projectDir = directory.toURI().toString();
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
String source = """
query1=SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName%
""";
String docUri = directory.toPath().resolve("src/main/resources/jpa-named-queries.properties").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JPA_QUERY_PROPERTIES, source, docUri);
editor.assertProblems("WHERE|no viable alternative");
}
@Test
void noErrors() throws Exception {
String source = """
package example.demo;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
public interface OwnerRepository extends Repository<Object, Integer> {
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
List<Object> findPetTypes();
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems();
}
@Test
void errorReported() throws Exception {
String source = """
package example.demo;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
public interface OwnerRepository extends Repository<Object, Integer> {
@Query("SELECTX ptype FROM PetType ptype ORDER BY ptype.name")
List<Object> findPetTypes();
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems("SELECTX|mismatched input 'SELECTX'");
}
@Test
void textBlock() throws Exception {
String source = """
package example.demo;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
public interface OwnerRepository extends Repository<Object, Integer> {
@Query(\"""
SELECTX ptype FROM PetType ptype ORDER BY ptype.name
\""")
List<Object> findPetTypes();
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems("SELECTX|mismatched input 'SELECTX'");
}
@Test
void normalAnnotation() throws Exception {
String source = """
package example.demo;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
public interface OwnerRepository extends Repository<Object, Integer> {
@Query(value = "SELECTX ptype FROM PetType ptype ORDER BY ptype.name", nativeQuery = false)
List<Object> findPetTypes();
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems("SELECTX|mismatched input 'SELECTX'");
}
@Test
void nativeSqlAnnotation() throws Exception {
String source = """
package example.demo;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
public interface OwnerRepository extends Repository<Object, Integer> {
@Query(value = "SELECTX ptype FROM PetType ptype ORDER BY ptype.name", nativeQuery = true)
List<Object> findPetTypes();
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems();
}
@Test
void noErrorForHql() throws Exception {
String source = """
package example.demo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
public interface OwnerRepository extends Repository<Object, Integer> {
@Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName% ")
Page<Object> findByLastName(@Param("lastName") String lastName, Pageable pageable);
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems();
}
}

View File

@@ -1188,4 +1188,4 @@
"extensionDependencies": [
"redhat.java"
]
}
}