diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index e1afae4ba..f56a4cf6d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -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(); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java index 2927f435d..940bec697 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java @@ -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"));; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index 9483e4547..fafa97e3e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -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"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlReconciler.java new file mode 100644 index 000000000..a47e437ec --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlReconciler.java @@ -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(); + + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java index 5e8ebb0bc..e8c35818c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java @@ -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 semantics = new HashMap<>(); + parser.removeErrorListener(ConsoleErrorListener.INSTANCE); + parser.addParseListener(new HqlBaseListener() { private void processTerminalNode(TerminalNode node) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java index 81a71fc1f..5421edc8e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java @@ -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 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); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java index d5daff569..09108f770 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java @@ -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 getReconcileEngine() { + return Optional.of(reconcileEngine); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlReconciler.java new file mode 100644 index 000000000..cd85f8601 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlReconciler.java @@ -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(); + + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java index b93fd3f19..dbaab2d43 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java @@ -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 semantics = new HashMap<>(); + parser.removeErrorListener(ConsoleErrorListener.INSTANCE); + parser.addParseListener(new JpqlBaseListener() { private void processTerminalNode(TerminalNode node) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/NamedQueryPropertiesReconcileEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/NamedQueryPropertiesReconcileEngine.java new file mode 100644 index 000000000..5f907d95d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/NamedQueryPropertiesReconcileEngine.java @@ -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(); + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java new file mode 100644 index 000000000..f10497a29 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java @@ -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 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 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; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryProblemType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryProblemType.java new file mode 100644 index 000000000..61eb6cc16 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryProblemType.java @@ -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; + } +} + diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java index c6df45df1..a74c6dfb5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java @@ -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())); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java index ad7a83205..a4036c9db 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java @@ -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 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 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 = """ diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryReconcilerTest.java new file mode 100644 index 000000000..28463492c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryReconcilerTest.java @@ -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 { + + @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") + List 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 { + + @Query("SELECTX ptype FROM PetType ptype ORDER BY ptype.name") + List 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 { + + @Query(\""" + SELECTX ptype FROM PetType ptype ORDER BY ptype.name + \""") + List 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 { + + @Query(value = "SELECTX ptype FROM PetType ptype ORDER BY ptype.name", nativeQuery = false) + List 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 { + + @Query(value = "SELECTX ptype FROM PetType ptype ORDER BY ptype.name", nativeQuery = true) + List 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 { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName% ") + Page 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(); + } +} diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 743b63a19..b033642e1 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -1188,4 +1188,4 @@ "extensionDependencies": [ "redhat.java" ] -} +} \ No newline at end of file