diff --git a/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java b/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java index 80d273a..c880631 100644 --- a/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java +++ b/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java @@ -1,16 +1,19 @@ package groowt.view.web.ast; import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtension; import groowt.view.web.ast.node.LeafNode; import groowt.view.web.ast.node.Node; import groowt.view.web.ast.node.TreeNode; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Objects; public final class NodeUtil { public static boolean isAnyOfType(Node subject, Class... nodeTypes) { + Objects.requireNonNull(subject); for (final var type : nodeTypes) { if (type.isAssignableFrom(subject.getClass())) { return true; @@ -19,7 +22,19 @@ public final class NodeUtil { return false; } + @SuppressWarnings("unchecked") + public static boolean hasExtensionOfType(Node subject, Class... extensionTypes) { + Objects.requireNonNull(subject); + for (final var extensionType : extensionTypes) { + if (subject.hasExtension((Class) extensionType)) { + return true; + } + } + return false; + } + public static boolean isAnyOfType(Node subject, List> nodeTypes) { + Objects.requireNonNull(subject); for (final var type : nodeTypes) { if (type.isAssignableFrom(subject.getClass())) { return true; diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java index 550ad79..23356b7 100644 --- a/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java +++ b/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java @@ -38,7 +38,7 @@ public class ClosureValueNode extends AbstractLeafNode implements ValueNode { } protected String toValidGroovyCode(List groovyTokens) { - return "{ " + groovyTokens.stream().map(Token::getText).collect(Collectors.joining()) + "\n}"; + return "def c = { " + groovyTokens.stream().map(Token::getText).collect(Collectors.joining()) + "\n}"; } public GroovyCodeNodeExtension getGroovyCode() { diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java index 10a295a..b05002c 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java @@ -4,9 +4,9 @@ import groowt.view.component.context.ComponentResolveException; import groowt.view.component.runtime.ComponentCreateException; import groowt.view.web.WebViewComponentBugError; import groowt.view.web.ast.node.*; +import groowt.view.web.transpile.groovy.GroovyUtil; +import groowt.view.web.transpile.groovy.GroovyUtil.ConvertResult; import groowt.view.web.transpile.resolve.ComponentClassNodeResolver; -import groowt.view.web.transpile.util.GroovyUtil; -import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; import groowt.view.web.util.Provider; import groowt.view.web.util.SourcePosition; import org.codehaus.groovy.ast.*; @@ -352,15 +352,9 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { TranspilerState state ) { final var createArgs = new ArgumentListExpression(); - - final VariableExpression resolvedVariableExpression; - final Variable currentResolved = state.getCurrentResolved(); - if (currentResolved instanceof VariableExpression) { - resolvedVariableExpression = (VariableExpression) currentResolved; - } else { - resolvedVariableExpression = new VariableExpression(currentResolved); - } - createArgs.addExpression(resolvedVariableExpression); + + final VariableExpression currentResolved = state.getCurrentResolved(); + createArgs.addExpression(currentResolved); final List attrNodes = componentNode.getArgs().getAttributes(); if (attrNodes.isEmpty()) { diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java index b7b2166..2e3449e 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java @@ -8,7 +8,7 @@ import groowt.view.web.ast.extension.GStringScriptletExtension; import groowt.view.web.ast.node.GStringBodyTextNode; import groowt.view.web.ast.node.JStringBodyTextNode; import groowt.view.web.ast.node.Node; -import groowt.view.web.transpile.util.GroovyUtil; +import groowt.view.web.transpile.groovy.GroovyUtil; import groowt.view.web.util.FilteringIterable; import groowt.view.web.util.Option; import groowt.view.web.util.TokenRange; @@ -46,15 +46,11 @@ public class DefaultGStringTranspiler implements GStringTranspiler { } } - protected Option checkNextAfterDollar(Node current, @Nullable Node next) { - if (!(next instanceof JStringBodyTextNode)) { + protected Option checkNextAfterDollar(@Nullable Node next) { + if (next != null && next.hasExtension(GStringNodeExtension.class)) { return Option.liftLazy(() -> { final ConstantExpression expression = this.jStringTranspiler.createEmptyStringLiteral(); - if (next != null) { - this.positionSetter.setToStartOf(expression, next); - } else { - this.positionSetter.setToStartOf(expression, current); - } + this.positionSetter.setToStartOf(expression, next); return expression; }); } else { @@ -114,13 +110,13 @@ public class DefaultGStringTranspiler implements GStringTranspiler { return new PathResult( propertyExpression, this.checkPrevBeforeDollar(prev, current), - this.checkNextAfterDollar(current, next) + this.checkNextAfterDollar(next) ); } else { return new PathResult( begin, this.checkPrevBeforeDollar(prev, current), - this.checkNextAfterDollar(current, next) + this.checkNextAfterDollar(next) ); } } @@ -187,13 +183,13 @@ public class DefaultGStringTranspiler implements GStringTranspiler { case GStringScriptletExtension scriptlet -> { checkPrevBeforeDollar(prev, current).ifPresent(texts::add); values.add(this.handleScriptlet(scriptlet)); - checkNextAfterDollar(current, next).ifPresent(texts::add); + checkNextAfterDollar(next).ifPresent(texts::add); } } } } - if (texts.size() != values.size() + 1) { + if (!(texts.size() == values.size() || texts.size() == values.size() + 1)) { throw new IllegalStateException( "incorrect amount of texts vs. values: " + texts.size() + " " + values.size() ); diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java index b37c6eb..59d1ae2 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java @@ -11,8 +11,8 @@ import groowt.view.web.compiler.MultipleWebViewComponentCompileErrorsException; import groowt.view.web.compiler.WebViewComponentTemplateCompileException; import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; import groowt.view.web.runtime.DefaultWebViewRenderContext; +import groowt.view.web.transpile.groovy.GroovyUtil; import groowt.view.web.transpile.resolve.ClassLoaderComponentClassNodeResolver; -import groowt.view.web.transpile.util.GroovyUtil; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.BlockStatement; diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java index ea103cb..06facce 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java @@ -1,18 +1,20 @@ package groowt.view.web.transpile; +import groowt.view.web.WebViewComponentBugError; import groowt.view.web.ast.node.*; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; -import groowt.view.web.transpile.util.GroovyUtil; -import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; +import groowt.view.web.transpile.groovy.GroovyUtil; +import groowt.view.web.transpile.groovy.GroovyUtil.ConvertResult; import org.codehaus.groovy.ast.Parameter; -import org.codehaus.groovy.ast.expr.ClosureExpression; -import org.codehaus.groovy.ast.expr.ConstantExpression; -import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.EmptyStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; import org.jetbrains.annotations.Nullable; +import java.util.List; + import static groowt.view.web.transpile.TranspilerUtil.getStringLiteral; // TODO: set positions @@ -24,16 +26,36 @@ public class DefaultValueNodeTranspiler implements ValueNodeTranspiler { this.componentTranspiler = componentTranspiler; } - protected ClosureExpression closureValue(ClosureValueNode closureValueNode) { + // TODO: positions + protected Expression handleClosureNode(ClosureValueNode closureValueNode) { final var rawCode = closureValueNode.getGroovyCode().getAsValidGroovyCode(); - final ConvertResult convertResult = GroovyUtil.convert(rawCode); - final @Nullable BlockStatement blockStatement = convertResult.blockStatement(); - if (blockStatement == null || blockStatement.isEmpty()) { - throw new IllegalStateException("block statement is null or empty"); + final ClosureExpression convertedClosure = GroovyUtil.getClosure(rawCode); + final Statement closureCode = convertedClosure.getCode(); + if (closureCode instanceof BlockStatement blockStatement) { + final List statements = blockStatement.getStatements(); + if (statements.isEmpty()) { + throw new WebViewComponentBugError(new IllegalArgumentException( + "Did not expect ClosureValueNode to produce no statements." + )); + } else if (statements.size() == 1) { + final Statement statement = statements.getFirst(); + if (statement instanceof ExpressionStatement expressionStatement) { + final Expression expression = expressionStatement.getExpression(); + return switch (expression) { + case ConstantExpression ignored -> expression; + case VariableExpression ignored -> expression; + case PropertyExpression ignored -> expression; + default -> convertedClosure; + }; + } else { + throw new IllegalArgumentException("A component closure value must produce a value."); + } + } else { + return convertedClosure; + } + } else { + return convertedClosure; } - final ExpressionStatement exprStmt = (ExpressionStatement) blockStatement.getStatements().getFirst(); - // TODO: set pos - return (ClosureExpression) exprStmt.getExpression(); } private Expression gStringValue(GStringValueNode gStringValueNode) { @@ -69,7 +91,7 @@ public class DefaultValueNodeTranspiler implements ValueNodeTranspiler { @Override public Expression createExpression(ValueNode valueNode, TranspilerState state) { return switch (valueNode) { - case ClosureValueNode closureValueNode -> this.closureValue(closureValueNode); + case ClosureValueNode closureValueNode -> this.handleClosureNode(closureValueNode); case GStringValueNode gStringValueNode -> this.gStringValue(gStringValueNode); case JStringValueNode jStringValueNode -> this.jStringValue(jStringValueNode); case EmptyClosureValueNode emptyClosureValueNode -> this.emptyClosureValue(emptyClosureValueNode); diff --git a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java index ebf64b7..ea40625 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java +++ b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java @@ -1,5 +1,6 @@ package groowt.view.web.transpile; +import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.ModuleNode; @@ -55,9 +56,16 @@ public class WebViewComponentModuleNode extends ModuleNode { .orElse(null); } + /** + * @param alias the name of interest + * @return a standard (non-static, non-star) import, or {@code null} if there is none + */ @Override public @Nullable ImportNode getImport(String alias) { - return this.allImports.get(alias); + return this.imports.stream() + .filter(importNode -> importNode.getAlias().equals(alias)) + .findFirst() + .orElse(null); } protected void putToAll(String alias, ImportNode importNode) { @@ -88,4 +96,52 @@ public class WebViewComponentModuleNode extends ModuleNode { this.putToAll(alias, importNode); } + @Override + public void addImport(String alias, ClassNode type) { + this.addImport(new ImportNode(type, alias)); + } + + @Override + public void addImport(String alias, ClassNode type, List annotations) { + final var importNode = new ImportNode(type, alias); + importNode.addAnnotations(annotations); + this.addImport(importNode); + } + + @Override + public void addStarImport(String packageName) { + this.addStarImport(new ImportNode(packageName)); + } + + @Override + public void addStarImport(String packageName, List annotations) { + final var importNode = new ImportNode(packageName); + importNode.addAnnotations(annotations); + this.addStarImport(importNode); + } + + @Override + public void addStaticImport(ClassNode type, String fieldName, String alias) { + this.addStaticImport(alias, new ImportNode(type, fieldName, alias)); + } + + @Override + public void addStaticImport(ClassNode type, String fieldName, String alias, List annotations) { + final var importNode = new ImportNode(type, fieldName, alias); + importNode.addAnnotations(annotations); + this.addStaticImport(alias, importNode); + } + + @Override + public void addStaticStarImport(String name, ClassNode type) { + this.addStaticStarImport(name, new ImportNode(type, name)); + } + + @Override + public void addStaticStarImport(String name, ClassNode type, List annotations) { + final var importNode = new ImportNode(type, name); + importNode.addAnnotations(annotations); + this.addStaticStarImport(name, importNode); + } + } diff --git a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java b/web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyPrettyPrinter.java similarity index 99% rename from web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java rename to web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyPrettyPrinter.java index 1be4663..faad7bd 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java +++ b/web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyPrettyPrinter.java @@ -1,4 +1,4 @@ -package groowt.view.web.transpile.util; +package groowt.view.web.transpile.groovy; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; diff --git a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java b/web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyUtil.java similarity index 73% rename from web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java rename to web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyUtil.java index 28002a8..573dd61 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java +++ b/web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyUtil.java @@ -1,11 +1,15 @@ -package groowt.view.web.transpile.util; +package groowt.view.web.transpile.groovy; import groovy.lang.GroovyCodeSource; +import groowt.view.web.WebViewComponentBugError; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.builder.AstStringCompiler; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.CompilerConfiguration; @@ -97,6 +101,28 @@ public final class GroovyUtil { return new ConvertResult(moduleNode, blockStatement, scriptClassNode, classNodes); } + /** + * @param source must be of form {@code def c = { ... }} and the closure must not be empty. + * @return the block inside the closure + */ + public static ClosureExpression getClosure(String source) { + final ConvertResult convertResult = convert(source); + final BlockStatement blockStatement = convertResult.blockStatement(); + if (blockStatement == null) { + throw new WebViewComponentBugError(new IllegalArgumentException( + "Did not expect the source to produce no BlockStatement." + )); + } + if (blockStatement.isEmpty()) { + throw new WebViewComponentBugError(new IllegalArgumentException( + "Did not expect the BlockStatement to be empty." + )); + } + final ExpressionStatement exprStmt = (ExpressionStatement) blockStatement.getStatements().getFirst(); + final BinaryExpression binaryExpression = (BinaryExpression) exprStmt.getExpression(); + return (ClosureExpression) binaryExpression.getRightExpression(); + } + private GroovyUtil() {} } diff --git a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt b/web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyUtil.kt similarity index 83% rename from web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt rename to web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyUtil.kt index 2f59d16..6399403 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt +++ b/web-views/src/main/java/groowt/view/web/transpile/groovy/GroovyUtil.kt @@ -1,4 +1,4 @@ -package groowt.view.web.transpile.util +package groowt.view.web.transpile.groovy import org.codehaus.groovy.ast.ASTNode diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java index d67eba4..9105428 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java @@ -20,18 +20,34 @@ public class ModuleNodeComponentClassNodeResolver extends CachingComponentClassN @Override public Either getClassForNameWithoutPackage(String nameWithoutPackage) { return super.getClassForNameWithoutPackage(nameWithoutPackage).flatMapLeft(ignored -> { - // try imports first + // try regular imports first final var importedClassNode = this.moduleNode.getImportType(nameWithoutPackage); if (importedClassNode != null) { this.addClassNode(importedClassNode); return Either.right(importedClassNode); } + // try star imports + final var starImports = this.moduleNode.getStarImports(); + for (final var starImport : starImports) { + final var packageName = starImport.getPackageName(); + final String fqn; + if (packageName.endsWith(".")) { + fqn = packageName + nameWithoutPackage; + } else { + fqn = packageName + "." + nameWithoutPackage; + } + final var withPackage = this.getClassForFqn(fqn); + if (withPackage.isRight()) { + return withPackage; + } + } + // try pre-pending package and asking for fqn final var packageName = this.moduleNode.getPackageName(); final String fqn; if (packageName.endsWith(".")) { - fqn = this.moduleNode + nameWithoutPackage; + fqn = this.moduleNode.getPackageName() + nameWithoutPackage; } else { fqn = this.moduleNode.getPackageName() + "." + nameWithoutPackage; } diff --git a/web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy b/web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy index 8fd24fc..00f974f 100644 --- a/web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy +++ b/web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy @@ -7,17 +7,13 @@ class BaseWebViewComponentTests extends AbstractWebViewComponentTests { static final class Greeter extends BaseWebViewComponent { - private final String target + final String target Greeter(Map attr) { super('Hello, $target!') this.target = Objects.requireNonNull(attr.get("target")) } - String getTarget() { - return this.target - } - } static final class UsingGreeter extends BaseWebViewComponent { diff --git a/web-views/src/test/groovy/groowt/view/web/SimpleWebViewComponentTests.groovy b/web-views/src/test/groovy/groowt/view/web/SimpleWebViewComponentTests.groovy new file mode 100644 index 0000000..e241db5 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/SimpleWebViewComponentTests.groovy @@ -0,0 +1,51 @@ +package groowt.view.web + +import groowt.view.web.lib.AbstractWebViewComponentTests +import org.junit.jupiter.api.Test + +class SimpleWebViewComponentTests extends AbstractWebViewComponentTests { + + @Test + void closureValueWithConstantExpressionEvaluatesToValue() { + this.doTest('$greeting', 'Hello, World!') + } + + @Test + void closureValueWithVariableExpressionEvaluatesToValue() { + this.doTest( + '$subGreeting', + 'Hello, World!' + ) + } + + @Test + void closureWithPropertyExpressionEvaluatesToValue() { + this.doTest( + ''' + --- + import groovy.transform.Field + + @Field + Map greetings = [hello: 'Hello!'] + --- + $greeting + '''.stripIndent().trim(), + 'Hello!' + ) + } + + @Test + void closureWithMethodCallIsClosure() { + this.doTest( + ''' + --- + def helper(String input) { + input.capitalize() + } + --- + ${ -> subHelper.call() } + '''.stripIndent().trim(), 'Lowercase' + ) + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy index 2e67e0b..60b0afb 100644 --- a/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy +++ b/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy @@ -1,9 +1,9 @@ package groowt.view.web.tools -import groowt.view.web.transpile.util.GroovyUtil +import groowt.view.web.transpile.groovy.GroovyUtil import org.codehaus.groovy.ast.ImportNode -import static groowt.view.web.transpile.util.GroovyUtil.formatGroovy +import static groowt.view.web.transpile.groovy.GroovyUtil.formatGroovy def src = ''' import some.Thing