Skip to content

Commit

Permalink
HQL and JPQL syntax validation reconciling
Browse files Browse the repository at this point in the history
  • Loading branch information
BoykoAlex committed May 2, 2024
1 parent 4367287 commit 385c35e
Show file tree
Hide file tree
Showing 16 changed files with 839 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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;
Expand Down Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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;
Expand Down Expand Up @@ -49,6 +50,8 @@ public List<SemanticTokenData> computeTokens(String text, int initialOffset) {

Map<Token, String> semantics = new HashMap<>();

parser.removeErrorListener(ConsoleErrorListener.INSTANCE);

parser.addParseListener(new HqlBaseListener() {

private void processTerminalNode(TerminalNode node) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,14 +20,13 @@
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;
Expand Down Expand Up @@ -69,44 +69,61 @@ public List<SemanticTokenData> computeTokens(IJavaProject jp, CompilationUnit cu
@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));
}
}
}
Expand All @@ -118,33 +135,31 @@ public boolean visit(MethodInvocation node) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@

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;

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
Expand All @@ -38,4 +41,9 @@ public Optional<SemanticTokensHandler> getSemanticTokensHandler() {
return Optional.of(semanticTokensHandler);
}

@Override
public Optional<IReconcileEngine> getReconcileEngine() {
return Optional.of(reconcileEngine);
}

}
Original file line number Diff line number Diff line change
@@ -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();

}

}
Loading

0 comments on commit 385c35e

Please sign in to comment.