Skip to content

Commit

Permalink
feat: Qute: add arguments metadata for user-defined tags
Browse files Browse the repository at this point in the history
Fixes #928

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Aug 9, 2024
1 parent baa52b5 commit 9f4c520
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.redhat.qute.project.datamodel.ExtendedDataModelParameter;
import com.redhat.qute.project.datamodel.ExtendedDataModelTemplate;
import com.redhat.qute.utils.FileUtils;
import com.redhat.qute.utils.UserTagUtils;

public class Template extends Node {

Expand Down Expand Up @@ -191,7 +192,15 @@ public JavaTypeInfoProvider findInInitialDataModel(Part part) {
return parameter;
}
// Try to find the class name from @CheckedTemplate
return getParameterDataModel(partName).getNow(null);
parameter = getParameterDataModel(partName).getNow(null);
if (parameter != null) {
return parameter;
}
// Try special keys (ex: _args)
if (UserTagUtils.isUserTag(this)) {
return UserTagUtils.getSpecialKey(partName);
}
return null;
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public abstract class UserTag extends Snippet {
private final String templateId;
private Map<String, UserTagParameter> parameters;
private final QuteProject project;
private boolean hasArgs;

public UserTag(String fileName, QuteProject project) {
String name = UserTagUtils.getUserTagName(fileName);
Expand Down Expand Up @@ -182,10 +183,22 @@ public String getTemplateId() {
*/
public Collection<UserTagParameter> getParameters() {
if (parameters == null) {
parameters = collectParameters();
UserTagInfoCollector collector = getUserTagCollector();
if (collector != null) {
parameters = collector.getParameters();
hasArgs = collector.hasArgs();
} else {
parameters = Collections.emptyMap();
hasArgs = false;
}
}
return parameters.values();
}

public boolean hasArgs() {
getParameters();
return hasArgs;
}

/**
* Returns all required parameters names.
Expand Down Expand Up @@ -223,14 +236,14 @@ public UserTagParameter findParameter(String parameterName) {
*
* @return parameters of the user tag.
*/
private Map<String, UserTagParameter> collectParameters() {
private UserTagInfoCollector getUserTagCollector() {
Template template = getTemplate();
if (template == null) {
return Collections.emptyMap();
return null;
}
UserTagParameterCollector collector = new UserTagParameterCollector(project);
UserTagInfoCollector collector = new UserTagInfoCollector(project);
template.accept(collector);
return collector.getParameters();
return collector;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@
import com.redhat.qute.project.QuteProject;
import com.redhat.qute.project.datamodel.resolvers.ValueResolver;
import com.redhat.qute.utils.StringUtils;
import com.redhat.qute.utils.UserTagUtils;

/**
* User tag parameters collector.
* User tag info collector to get:
*
* <ul>
* <li>if user tag uses '_args'</li>
* <li>user tag parameters</li>
* </ul>
*
* @author Angelo ZERR
*
*/
public class UserTagParameterCollector extends ASTVisitor {
public class UserTagInfoCollector extends ASTVisitor {

private final QuteProject project;

Expand All @@ -51,6 +57,8 @@ public class UserTagParameterCollector extends ASTVisitor {

private List<String> globalVariables;

private boolean hasArgs;

private static class ParamInfo {
public final String name;
public final boolean assigned; // true if parameter comes from a #let, #set and false otherwise.
Expand All @@ -63,7 +71,7 @@ public ParamInfo(String name, boolean assigned, String defaultValue) {
}
}

public UserTagParameterCollector(QuteProject project) {
public UserTagInfoCollector(QuteProject project) {
this.project = project;
this.parameters = new LinkedHashMap<>();
this.parameterNamesStack = new ArrayList<>();
Expand Down Expand Up @@ -154,6 +162,12 @@ public boolean visit(MethodPart node) {
public boolean visit(ObjectPart node) {
if (isValid(node)) {
String partName = node.getPartName(); // {foo}
boolean isArgs = UserTagUtils.ARGS_OBJECT_PART_NAME.equals(partName);
if (isArgs) {
// {_args.skip("readonly")..}
hasArgs = true;
return super.visit(node);
}

// Get the parameter info from the parameter stack
ParamInfo paramInfo = getParamInfo(partName);
Expand Down Expand Up @@ -244,4 +258,15 @@ public boolean isValid(ObjectPart node) {
public Map<String, UserTagParameter> getParameters() {
return parameters;
}

/**
* Returns true if the user tag declares an object part with _args and false
* otherwise.
*
* @return true if the user tag declares an object part with _args and false
* otherwise.
*/
public boolean hasArgs() {
return hasArgs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
import com.redhat.qute.parser.expression.PropertyPart;
import com.redhat.qute.parser.template.CaseOperator;
import com.redhat.qute.parser.template.Expression;
import com.redhat.qute.parser.template.ExpressionParameter;
import com.redhat.qute.parser.template.JavaTypeInfoProvider;
import com.redhat.qute.parser.template.LiteralSupport;
import com.redhat.qute.parser.template.Node;
Expand Down Expand Up @@ -396,17 +395,18 @@ private static void validateSectionTag(Section section, Template template,
diagnostics.add(diagnostic);
} else {
existingParameters.add(paramName);
// Check if the declared parameter is defined in the user tag
UserTagParameter userTagParameter = userTag.findParameter(paramName);
if (userTagParameter == null) {
Range range = QutePositionUtility.selectParameterName(parameter);
Diagnostic diagnostic = createDiagnostic(range, DiagnosticSeverity.Warning,
QuteErrorCode.UndefinedParameter, paramName, tagName);
diagnostics.add(diagnostic);
if (!userTag.hasArgs()) {
// Check if the declared parameter is defined in the user tag
UserTagParameter userTagParameter = userTag.findParameter(paramName);
if (userTagParameter == null) {
Range range = QutePositionUtility.selectParameterName(parameter);
Diagnostic diagnostic = createDiagnostic(range, DiagnosticSeverity.Warning,
QuteErrorCode.UndefinedParameter, paramName, tagName);
diagnostics.add(diagnostic);
}
}
}
}

// Check if all required parameters are declared
List<String> missingRequiredParameters = new ArrayList<>();
for (UserTagParameter parameter : userTag.getParameters()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ private CompletableFuture<CompletionList> doCompleteExpressionForObjectPart(Comp
}

if (UserTagUtils.isUserTag(template)) {
// provide completion for 'it' and 'nested-content'
// provide completion for 'it', 'nested-content', '_args'
Collection<SectionMetadata> metadatas = UserTagUtils.getSpecialKeys();
for (SectionMetadata metadata : metadatas) {
String name = metadata.getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class UserTagUtils {
public static final String IT_OBJECT_PART_NAME = "it";
public static final String NESTED_CONTENT_OBJECT_PART_NAME = "nested-content";

public static final String ARGS_OBJECT_PART_NAME = "_args";
private static final Map<String, SectionMetadata> SPECIAL_KEYS;

static {
Expand All @@ -50,6 +51,9 @@ public class UserTagUtils {
"`it` is a special key that is replaced with the first unnamed parameter of the tag."));
register(new SectionMetadata(NESTED_CONTENT_OBJECT_PART_NAME, Object.class.getName(),
"`nested-content` is a special key that will be replaced by the content of the tag"));
register(new SectionMetadata(ARGS_OBJECT_PART_NAME, "io.quarkus.qute.UserTagSectionHelper.Arguments",
"`io.quarkus.qute.UserTagSectionHelper.Arguments` metadata are accessible in a tag using the `_args` alias."
+ "\nSee [here](https://quarkus.io/guides/qute-reference#arguments) for more information."));
}

private static void register(SectionMetadata metadata) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ public class QuteAssert {

public static final String FILE_URI = "test.qute";

public static final int USER_TAG_SIZE = 9 /*
public static final int USER_TAG_SIZE = 10 /*
* #input, #bundleStyle, #form, #title, #simpleTitle, #user, #formElement,
* #inputRequired, #myTag
* #inputRequired, #myTag, #tagWithArgs
*/;

public static final int SECTION_SNIPPET_SIZE = 15 /* #each, #for, ... #fragment ... */ + USER_TAG_SIZE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void specialKeys() throws Exception {
testCompletionFor(template, //
"src/main/resources/templates/tags/form.html", //
"tags/form", //
RESOLVERS_SIZE /* item, inject:bean, config:getConfigProperty */ + 2 /* it, nested-content */ + 1 /*
RESOLVERS_SIZE /* item, inject:bean, config:getConfigProperty */ + 3 /* it, nested-content, _args */ + 1 /*
* global
* variables
*/, //
Expand All @@ -64,6 +64,7 @@ public void specialKeys() throws Exception {
c("VARCHAR_SIZE", "VARCHAR_SIZE", r(1, 1, 1, 1)), //
c("it", "it", r(1, 1, 1, 1)), //
c("nested-content", "nested-content", r(1, 1, 1, 1)), //
c("_args", "_args", r(1, 1, 1, 1)), //
c("uri:Login", "uri:Login", r(1, 1, 1, 1)), //
c("msg:hello_name(name : String) : String", "msg:hello_name(name)", r(1, 1, 1, 1)), //
c("msg2:hello() : String", "msg2:hello", r(1, 1, 1, 1)), //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void includeTemplateIds() throws Exception {
// Without snippet
testCompletionFor(template, //
false, // no snippet support
15 /* all files from src/test/resources/templates */ - 1 /* README.md */ - USER_TAG_SIZE, //
16 /* all files from src/test/resources/templates */ - 1 /* README.md */ - USER_TAG_SIZE, //
c("base", "base", r(0, 10, 0, 10)),
c("test.json", "test.json", r(0, 10, 0, 10)),
c("test.html", "test.html", r(0, 10, 0, 10)),
Expand All @@ -55,7 +55,7 @@ public void includeTemplateIdsSelf() throws Exception {
testCompletionFor(template, //
"src/test/resources/templates/base.html",
false, // no snippet support
15 /* all files from src/test/resources/templates */ - 1 /* base.html */ - 1 /* README.md */
16 /* all files from src/test/resources/templates */ - 1 /* base.html */ - 1 /* README.md */
- USER_TAG_SIZE, //
// c("base", "base", r(0, 10, 0, 10)),
c("test.json", "test.json", r(0, 10, 0, 10)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,24 @@ public void existingStartTagAndExistingEndTag() throws Exception {
0);
}

@Test
public void tagWithArgs() throws Exception {
// Here the content of tagWithArgs.html:

// {_args.filter('readonly').asHtmlAttributes}
// {_args}
// {foo}

String template = "{#tagWithArgs |}";

testCompletionFor(template, //
false, // no snippet support
1, //
c("foo", "foo=\"foo\"", r(0, 14, 0, 14)));

testCompletionFor(template, //
true, // snippet support
1, //
c("foo", "foo=\"${1:foo}\"$0", r(0, 14, 0, 14)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ public void bundleStyle() {
// test with default value declared in bundleStyle.html user tag
// {#let name?="main.css"}
// In this case:

// - name is optional
String template = "{#bundleStyle /}";
testDiagnosticsFor(template);
// - name can be overridden
template = "{#bundleStyle name='foo.css'/}";
testDiagnosticsFor(template);
}

@Test
public void definedRequiredParameterName() {
String template = "{#input name='' /}";
Expand All @@ -66,6 +66,22 @@ public void undefinedParameterName() {
testDiagnosticsFor(template, d);
}

@Test
public void ignoreUndefinedParameterNameWhenArgs() throws Exception {
// tagWithArgs.html contains an expression which uses _args
// in this case, we ignore the QuteErrorCode.UndefinedParameter error.
String template = "{#tagWithArgs name='' XXXX='' /}";

// There is no error with name (just an error with foo parameter which is required)
Diagnostic d = d(0, 1, 0, 13, QuteErrorCode.MissingRequiredParameter,
"Missing required parameter(s) `foo` of `tagWithArgs` user tag.",
DiagnosticSeverity.Warning);
testDiagnosticsFor(template, d);
testCodeActionsFor(template, d, //
ca(d, te(0, 29, 0, 30, //
" foo=\"foo\"")));
}

@Test
public void duplicateNameParameter() {
String template = "{#input name='' name='' /}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{_args.filter('readonly').asHtmlAttributes}
{_args}
{foo}

0 comments on commit 9f4c520

Please sign in to comment.