Skip to content

Commit 23d4294

Browse files
author
Alexander Chen
committed
Added diagnostic and CodeAction to use let to replace with
Signed-off-by: Alexander Chen <alchen@redhat.com>
1 parent d9b8f83 commit 23d4294

File tree

6 files changed

+231
-13
lines changed

6 files changed

+231
-13
lines changed

qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/commons/CodeActionFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ public static CodeAction replace(String title, Range range, String replaceText,
106106
return replace(title, Collections.singletonList(replace), document, diagnostic);
107107
}
108108

109+
@SuppressWarnings("null")
110+
public static CodeAction replace(String title, List<Range> ranges, String replaceText, TextDocumentItem document,
111+
Diagnostic diagnostic) {
112+
List<TextEdit> edits = null;
113+
for (Range range : ranges) {
114+
edits.add(new TextEdit(range, replaceText));
115+
}
116+
return replace(title, edits, document, diagnostic);
117+
}
118+
109119
public static CodeAction replace(String title, List<TextEdit> replace, TextDocumentItem document,
110120
Diagnostic diagnostic) {
111121

qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/QuteCodeActions.java

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import java.text.MessageFormat;
2020
import java.util.ArrayList;
2121
import java.util.Collections;
22+
import java.util.HashSet;
2223
import java.util.List;
24+
import java.util.Set;
2325
import java.util.concurrent.CompletableFuture;
2426
import java.util.logging.Level;
2527
import java.util.logging.Logger;
@@ -29,6 +31,7 @@
2931
import org.eclipse.lsp4j.Diagnostic;
3032
import org.eclipse.lsp4j.Position;
3133
import org.eclipse.lsp4j.Range;
34+
import org.eclipse.lsp4j.TextEdit;
3235

3336
import com.google.gson.JsonObject;
3437
import com.redhat.qute.ls.commons.BadLocationException;
@@ -40,8 +43,11 @@
4043
import com.redhat.qute.parser.template.Expression;
4144
import com.redhat.qute.parser.template.Node;
4245
import com.redhat.qute.parser.template.NodeKind;
46+
import com.redhat.qute.parser.template.Parameter;
4347
import com.redhat.qute.parser.template.Section;
48+
import com.redhat.qute.parser.template.SectionKind;
4449
import com.redhat.qute.parser.template.Template;
50+
import com.redhat.qute.parser.template.sections.WithSection;
4551
import com.redhat.qute.project.QuteProject;
4652
import com.redhat.qute.services.commands.QuteClientCommandConstants;
4753
import com.redhat.qute.services.diagnostics.QuteErrorCode;
@@ -72,6 +78,8 @@ class QuteCodeActions {
7278

7379
private static final String EXCLUDED_VALIDATION_TITLE = "Exclude this file from validation.";
7480

81+
private static final String QUTE_DEPRICATED_WITH_SECTION = "Replace `#with` with `#let`.";
82+
7583
public CompletableFuture<List<CodeAction>> doCodeActions(Template template, CodeActionContext context, Range range,
7684
SharedSettings sharedSettings) {
7785
List<CodeAction> codeActions = new ArrayList<>();
@@ -109,6 +117,15 @@ public CompletableFuture<List<CodeAction>> doCodeActions(Template template, Code
109117
// Create `undefinedTag`"
110118
doCodeActionsForUndefinedSectionTag(template, diagnostic, codeActions);
111119
break;
120+
case NotRecommendedWithSection:
121+
// The following Qute template:
122+
// {#with }
123+
//
124+
// will provide a quickfix like:
125+
//
126+
// Replace `with` with `let`.
127+
doCodeActionsForNotRecommendedWithSection(template, diagnostic, codeActions);
128+
break;
112129
default:
113130
break;
114131
}
@@ -186,6 +203,127 @@ private static void doCodeActionToDisableValidation(Template template, List<Diag
186203
codeActions.add(disableValidationForTemplateQuickFix);
187204
}
188205

206+
/**
207+
* Create CodeAction for unrecommended `with` Qute syntax.
208+
*
209+
* e.g. <code>
210+
* {#with item}
211+
* {name}
212+
* {/with}
213+
* </code> becomes <code>
214+
* {#let name=item.name}
215+
* {name}
216+
* {/let}
217+
* </code>
218+
*
219+
* @param template the Qute template.
220+
* @param diagnostic the diagnostic list that this CodeAction will fix.
221+
* @param codeActions the list of CodeActions to perform.
222+
*
223+
*/
224+
private static void doCodeActionsForNotRecommendedWithSection(Template template, Diagnostic diagnostic,
225+
List<CodeAction> codeActions) {
226+
Range withSectionRange = diagnostic.getRange();
227+
try {
228+
// 1. Retrieve the #with section node using diagnostic range
229+
int withSectionStart = template.offsetAt(withSectionRange.getStart());
230+
WithSection withSection = (WithSection) template.findNodeAt(withSectionStart + 1);
231+
232+
// 2. Initialize TextEdit array
233+
List<TextEdit> edits = new ArrayList<TextEdit>();
234+
235+
// 2.1 Create text edit to update section start from #with to #let
236+
// and collect all object parts from expression for text edit
237+
// (e.g. {name} -> {#let name=item.name})
238+
edits.add(createWithSectionOpenEdit(template, withSection));
239+
240+
// 2.2 Create text edit to update section end from /with to /let
241+
edits.add(createWithSectionCloseEdit(template, withSection));
242+
243+
// 3. Create CodeAction
244+
CodeAction replaceWithSection = CodeActionFactory.replace(QUTE_DEPRICATED_WITH_SECTION, edits,
245+
template.getTextDocument(), diagnostic);
246+
codeActions.add(replaceWithSection);
247+
} catch (BadLocationException e) {
248+
LOGGER.log(Level.SEVERE, "Creation of not recommended with section code action failed", e);
249+
}
250+
}
251+
252+
/**
253+
* Create text edit to replace unrecommended with section opening tag
254+
*
255+
* @param template the Qute template.
256+
* @param withSection the Qute with section
257+
* @return
258+
* @throws BadLocationException
259+
*/
260+
private static TextEdit createWithSectionOpenEdit(Template template, WithSection withSection)
261+
throws BadLocationException {
262+
String objectPartName = withSection.getObjectParameter() != null ? withSection.getObjectParameter().getName()
263+
: "";
264+
// Use set to avoid duplicate parameters
265+
Set<String> withObjectParts = new HashSet<String>();
266+
267+
// Retrieve all expressions in #with section
268+
findObjectParts(withSection, withObjectParts);
269+
List<String> letObjectPartParameterList = new ArrayList<String>();
270+
for (String objectPart : withObjectParts) {
271+
letObjectPartParameterList.add(MessageFormat.format("{1}={0}.{1}", objectPartName, objectPart));
272+
}
273+
274+
// Build text edit string
275+
String letObjectPartParameters = String.join(" ", letObjectPartParameterList);
276+
String withSectionOpenReplacementText = MessageFormat.format("#let {0}", letObjectPartParameters);
277+
Range withSectionOpen = new Range(template.positionAt(withSection.getStartTagNameOpenOffset()),
278+
template.positionAt(withSection.getStartTagCloseOffset()));
279+
280+
return new TextEdit(withSectionOpen, withSectionOpenReplacementText);
281+
}
282+
283+
/**
284+
* Find all object parts by traversing AST Nodes, while retrieveing Expressions
285+
* nested in Sections with recursion
286+
*
287+
* @param node current node in AST traversal
288+
* @param objectParts set of found object parts
289+
*/
290+
private static void findObjectParts(Node node, Set<String> objectParts) {
291+
List<Node> children = node.getChildren();
292+
// Base case: Node is an expression
293+
if (children.isEmpty()) {
294+
if (node.getKind() == NodeKind.Expression) {
295+
objectParts.add(((Expression) node).getContent());
296+
}
297+
}
298+
for (Node child : children) {
299+
if (child.getKind() == NodeKind.Expression) {
300+
objectParts.add(((Expression) child).getContent());
301+
} else if (child.getKind() == NodeKind.Section && ((Section) child).getSectionKind() != SectionKind.WITH) {
302+
for (Parameter param : ((Section) child).getParameters()) {
303+
objectParts.add(param.getValue());
304+
}
305+
// Recursive call to traverse nested non-WithSection Sections
306+
findObjectParts(child, objectParts);
307+
}
308+
}
309+
}
310+
311+
/**
312+
* Create text edit to replace unrecommended with section closing tag
313+
*
314+
* @param template the Qute template.
315+
* @param withSection the Qute with section
316+
* @return
317+
* @throws BadLocationException
318+
*/
319+
private static TextEdit createWithSectionCloseEdit(Template template, WithSection withSection)
320+
throws BadLocationException {
321+
String withSectionCloseReplacementText = "/let";
322+
Range withSectionClose = new Range(template.positionAt(withSection.getEndTagNameOpenOffset()),
323+
template.positionAt(withSection.getEndTagCloseOffset()));
324+
return new TextEdit(withSectionClose, withSectionCloseReplacementText);
325+
}
326+
189327
/**
190328
* Create the configuration update (done on client side) quick fix.
191329
*

qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/QuteDiagnostics.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import com.redhat.qute.parser.template.Template;
5555
import com.redhat.qute.parser.template.sections.IncludeSection;
5656
import com.redhat.qute.parser.template.sections.LoopSection;
57+
import com.redhat.qute.parser.template.sections.WithSection;
5758
import com.redhat.qute.project.JavaMemberResult;
5859
import com.redhat.qute.project.QuteProject;
5960
import com.redhat.qute.project.datamodel.JavaDataModelCache;
@@ -248,6 +249,9 @@ private void validateDataModel(Node parent, Template template, QuteValidationSet
248249
case INCLUDE:
249250
validateIncludeSection((IncludeSection) section, diagnostics);
250251
break;
252+
case WITH:
253+
validateWithSection((WithSection) section, diagnostics);
254+
break;
251255
default:
252256
validateSectionTag(section, template, resolvingJavaTypeContext, diagnostics);
253257
}
@@ -348,6 +352,21 @@ private static void validateIncludeSection(IncludeSection includeSection, List<D
348352
}
349353
}
350354

355+
/**
356+
* Report that `#with` section is deprecated.
357+
*
358+
* @param withSection the with section
359+
* @param diagnostics the diagnostics to fill
360+
*/
361+
private static void validateWithSection(WithSection withSection, List<Diagnostic> diagnostics) {
362+
if (withSection.getObjectParameter() != null) {
363+
Range range = QutePositionUtility.createRange(withSection);
364+
Diagnostic diagnostic = createDiagnostic(range, DiagnosticSeverity.Warning,
365+
QuteErrorCode.NotRecommendedWithSection);
366+
diagnostics.add(diagnostic);
367+
}
368+
}
369+
351370
private ResolvedJavaTypeInfo validateExpression(Expression expression, Section ownerSection, Template template,
352371
QuteValidationSettings validationSettings, ResolutionContext resolutionContext,
353372
ResolvingJavaTypeContext resolvingJavaTypeContext, List<Diagnostic> diagnostics) {

qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/diagnostics/DiagnosticDataFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
/**
2626
* Diagnostic factory.
27-
*
27+
*
2828
* @author Angelo ZERR
2929
*
3030
*/
@@ -55,4 +55,5 @@ public static Diagnostic createDiagnostic(Range range, String message, Diagnosti
5555
errorCode != null ? errorCode.getCode() : null);
5656
return diagnostic;
5757
}
58+
5859
}

qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/services/diagnostics/QuteErrorCode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ public enum QuteErrorCode implements IQuteErrorCode {
4646

4747
UndefinedSectionTag("No section helper found for `{0}`."), //
4848

49-
SyntaxError("Syntax error: `{0}`.");
49+
SyntaxError("Syntax error: `{0}`."),
50+
51+
// Error code for deprecated #with section
52+
NotRecommendedWithSection("`with` is not recommended. Use `let` instead.");
5053

5154
private final String rawMessage;
5255

qute.ls/com.redhat.qute.ls/src/test/java/com/redhat/qute/services/diagnostics/QuteDiagnosticsInExpressionWithWithSectionTest.java

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,63 @@
2929
*/
3030
public class QuteDiagnosticsInExpressionWithWithSectionTest {
3131

32+
@Test
33+
public void missingObject() throws Exception {
34+
String template = "{#with}\r\n" + //
35+
"{/with}";
36+
Diagnostic d = d(0, 6, 0, 6, QuteErrorCode.SyntaxError,
37+
"Parser error on line 1: mandatory section parameters not declared for {#with}: [object]",
38+
DiagnosticSeverity.Error);
39+
40+
testDiagnosticsFor(template, d);
41+
testCodeActionsFor(template, d);
42+
}
43+
3244
@Test
3345
public void undefinedObject() throws Exception {
3446
String template = "{#with item}\r\n" + //
3547
"{/with}";
3648

37-
Diagnostic d = d(0, 7, 0, 11, QuteErrorCode.UndefinedObject, "`item` cannot be resolved to an object.",
49+
Diagnostic d1 = d(0, 7, 0, 11, QuteErrorCode.UndefinedObject, "`item` cannot be resolved to an object.",
3850
DiagnosticSeverity.Warning);
39-
d.setData(DiagnosticDataFactory.createUndefinedVariableData("item", false));
51+
d1.setData(DiagnosticDataFactory.createUndefinedVariableData("item", false));
4052

41-
testDiagnosticsFor(template, d);
42-
testCodeActionsFor(template, d, //
43-
ca(d, te(0, 0, 0, 0, "{@java.lang.String item}\r\n")));
53+
Diagnostic d2 = d(0, 0, 1, 7, QuteErrorCode.NotRecommendedWithSection,
54+
"`with` is not recommended. Use `let` instead.", DiagnosticSeverity.Warning);
55+
56+
testDiagnosticsFor(template, d1, d2);
57+
testCodeActionsFor(template, d1, //
58+
ca(d1, te(0, 0, 0, 0, "{@java.lang.String item}\r\n")));
59+
testCodeActionsFor(template, d2, //
60+
ca(d2, te(0, 1, 0, 11, "#let "), te(1, 1, 1, 6, "/let")));
4461
}
4562

4663
@Test
47-
public void noError() throws Exception {
64+
public void singleSection() throws Exception {
4865
String template = "{@org.acme.Item item}\r\n" + //
4966
"{#with item}\r\n" + //
5067
" <h1>{name}</h1> \r\n" + //
5168
" <p>{price}</p> \r\n" + //
5269
"{/with}";
53-
testDiagnosticsFor(template);
70+
Diagnostic d = d(1, 0, 4, 7, QuteErrorCode.NotRecommendedWithSection,
71+
"`with` is not recommended. Use `let` instead.", DiagnosticSeverity.Warning);
72+
testDiagnosticsFor(template, d);
73+
testCodeActionsFor(template, d, //
74+
ca(d, te(1, 1, 1, 11, "#let price=item.price name=item.name"), te(4, 1, 4, 6, "/let")));
75+
}
76+
77+
@Test
78+
public void nestedParameter() throws Exception {
79+
String template = "{@org.acme.Item item}\r\n" + //
80+
"{#with item}\r\n" + //
81+
" {#let foo=name} \r\n" + //
82+
" {/let} \r\n" + //
83+
"{/with}";
84+
Diagnostic d = d(1, 0, 4, 7, QuteErrorCode.NotRecommendedWithSection,
85+
"`with` is not recommended. Use `let` instead.", DiagnosticSeverity.Warning);
86+
testDiagnosticsFor(template, d);
87+
testCodeActionsFor(template, d, //
88+
ca(d, te(1, 1, 1, 11, "#let name=item.name"), te(4, 1, 4, 6, "/let")));
5489
}
5590

5691
@Test
@@ -68,13 +103,25 @@ public void nested() throws Exception {
68103
" {/with}\r\n" + //
69104
"{/with}";
70105

106+
Diagnostic d1 = d(1, 0, 11, 7, QuteErrorCode.NotRecommendedWithSection,
107+
"`with` is not recommended. Use `let` instead.", DiagnosticSeverity.Warning);
108+
109+
Diagnostic d2 = d(4, 2, 10, 9, QuteErrorCode.NotRecommendedWithSection,
110+
"`with` is not recommended. Use `let` instead.", DiagnosticSeverity.Warning);
111+
71112
Diagnostic d = d(6, 5, 6, 12, QuteErrorCode.UndefinedObject, "`average` cannot be resolved to an object.",
72113
DiagnosticSeverity.Warning);
73-
d.setData(DiagnosticDataFactory.createUndefinedVariableData("average", false));
114+
d3.setData(DiagnosticDataFactory.createUndefinedVariableData("average", false));
74115

75-
testDiagnosticsFor(template, d);
76-
testCodeActionsFor(template, d, //
77-
ca(d, te(0, 0, 0, 0, "{@java.lang.String average}\r\n")));
116+
testDiagnosticsFor(template, d1, d2, d3);
117+
testCodeActionsFor(template, d1, //
118+
ca(d1, te(1, 1, 1, 11, "#let reviews=item.reviews name=item.name"), te(11, 1, 11, 6, "/let")));
119+
testCodeActionsFor(template, d2, //
120+
ca(d2, te(4, 3, 4, 16,
121+
"#let average=reviews.average size=reviews.size name=reviews.name data:item.name=reviews.data:item.name item.name=reviews.item.name"),
122+
te(10, 3, 10, 8, "/let")));
123+
testCodeActionsFor(template, d3, //
124+
ca(d3, te(0, 0, 0, 0, "{@java.lang.String average}\r\n")));
78125

79126
}
80127
}

0 commit comments

Comments
 (0)