Some gString/closure transpilation fixing, moving groovy util, and star imports.

This commit is contained in:
JesseBrault0709 2024-05-13 09:13:16 +02:00
parent a2c6b787f7
commit 494da75fe9
14 changed files with 224 additions and 52 deletions

View File

@ -1,16 +1,19 @@
package groowt.view.web.ast; package groowt.view.web.ast;
import groowt.view.web.antlr.TokenList; 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.LeafNode;
import groowt.view.web.ast.node.Node; import groowt.view.web.ast.node.Node;
import groowt.view.web.ast.node.TreeNode; import groowt.view.web.ast.node.TreeNode;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects;
public final class NodeUtil { public final class NodeUtil {
public static boolean isAnyOfType(Node subject, Class<?>... nodeTypes) { public static boolean isAnyOfType(Node subject, Class<?>... nodeTypes) {
Objects.requireNonNull(subject);
for (final var type : nodeTypes) { for (final var type : nodeTypes) {
if (type.isAssignableFrom(subject.getClass())) { if (type.isAssignableFrom(subject.getClass())) {
return true; return true;
@ -19,7 +22,19 @@ public final class NodeUtil {
return false; return false;
} }
@SuppressWarnings("unchecked")
public static boolean hasExtensionOfType(Node subject, Class<?>... extensionTypes) {
Objects.requireNonNull(subject);
for (final var extensionType : extensionTypes) {
if (subject.hasExtension((Class<? extends NodeExtension>) extensionType)) {
return true;
}
}
return false;
}
public static boolean isAnyOfType(Node subject, List<Class<? extends Node>> nodeTypes) { public static boolean isAnyOfType(Node subject, List<Class<? extends Node>> nodeTypes) {
Objects.requireNonNull(subject);
for (final var type : nodeTypes) { for (final var type : nodeTypes) {
if (type.isAssignableFrom(subject.getClass())) { if (type.isAssignableFrom(subject.getClass())) {
return true; return true;

View File

@ -38,7 +38,7 @@ public class ClosureValueNode extends AbstractLeafNode implements ValueNode {
} }
protected String toValidGroovyCode(List<Token> groovyTokens) { protected String toValidGroovyCode(List<Token> 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() { public GroovyCodeNodeExtension getGroovyCode() {

View File

@ -4,9 +4,9 @@ import groowt.view.component.context.ComponentResolveException;
import groowt.view.component.runtime.ComponentCreateException; import groowt.view.component.runtime.ComponentCreateException;
import groowt.view.web.WebViewComponentBugError; import groowt.view.web.WebViewComponentBugError;
import groowt.view.web.ast.node.*; 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.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.Provider;
import groowt.view.web.util.SourcePosition; import groowt.view.web.util.SourcePosition;
import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.*;
@ -352,15 +352,9 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
TranspilerState state TranspilerState state
) { ) {
final var createArgs = new ArgumentListExpression(); final var createArgs = new ArgumentListExpression();
final VariableExpression resolvedVariableExpression; final VariableExpression currentResolved = state.getCurrentResolved();
final Variable currentResolved = state.getCurrentResolved(); createArgs.addExpression(currentResolved);
if (currentResolved instanceof VariableExpression) {
resolvedVariableExpression = (VariableExpression) currentResolved;
} else {
resolvedVariableExpression = new VariableExpression(currentResolved);
}
createArgs.addExpression(resolvedVariableExpression);
final List<AttrNode> attrNodes = componentNode.getArgs().getAttributes(); final List<AttrNode> attrNodes = componentNode.getArgs().getAttributes();
if (attrNodes.isEmpty()) { if (attrNodes.isEmpty()) {

View File

@ -8,7 +8,7 @@ import groowt.view.web.ast.extension.GStringScriptletExtension;
import groowt.view.web.ast.node.GStringBodyTextNode; import groowt.view.web.ast.node.GStringBodyTextNode;
import groowt.view.web.ast.node.JStringBodyTextNode; import groowt.view.web.ast.node.JStringBodyTextNode;
import groowt.view.web.ast.node.Node; 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.FilteringIterable;
import groowt.view.web.util.Option; import groowt.view.web.util.Option;
import groowt.view.web.util.TokenRange; import groowt.view.web.util.TokenRange;
@ -46,15 +46,11 @@ public class DefaultGStringTranspiler implements GStringTranspiler {
} }
} }
protected Option<ConstantExpression> checkNextAfterDollar(Node current, @Nullable Node next) { protected Option<ConstantExpression> checkNextAfterDollar(@Nullable Node next) {
if (!(next instanceof JStringBodyTextNode)) { if (next != null && next.hasExtension(GStringNodeExtension.class)) {
return Option.liftLazy(() -> { return Option.liftLazy(() -> {
final ConstantExpression expression = this.jStringTranspiler.createEmptyStringLiteral(); final ConstantExpression expression = this.jStringTranspiler.createEmptyStringLiteral();
if (next != null) { this.positionSetter.setToStartOf(expression, next);
this.positionSetter.setToStartOf(expression, next);
} else {
this.positionSetter.setToStartOf(expression, current);
}
return expression; return expression;
}); });
} else { } else {
@ -114,13 +110,13 @@ public class DefaultGStringTranspiler implements GStringTranspiler {
return new PathResult( return new PathResult(
propertyExpression, propertyExpression,
this.checkPrevBeforeDollar(prev, current), this.checkPrevBeforeDollar(prev, current),
this.checkNextAfterDollar(current, next) this.checkNextAfterDollar(next)
); );
} else { } else {
return new PathResult( return new PathResult(
begin, begin,
this.checkPrevBeforeDollar(prev, current), this.checkPrevBeforeDollar(prev, current),
this.checkNextAfterDollar(current, next) this.checkNextAfterDollar(next)
); );
} }
} }
@ -187,13 +183,13 @@ public class DefaultGStringTranspiler implements GStringTranspiler {
case GStringScriptletExtension scriptlet -> { case GStringScriptletExtension scriptlet -> {
checkPrevBeforeDollar(prev, current).ifPresent(texts::add); checkPrevBeforeDollar(prev, current).ifPresent(texts::add);
values.add(this.handleScriptlet(scriptlet)); 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( throw new IllegalStateException(
"incorrect amount of texts vs. values: " + texts.size() + " " + values.size() "incorrect amount of texts vs. values: " + texts.size() + " " + values.size()
); );

View File

@ -11,8 +11,8 @@ import groowt.view.web.compiler.MultipleWebViewComponentCompileErrorsException;
import groowt.view.web.compiler.WebViewComponentTemplateCompileException; import groowt.view.web.compiler.WebViewComponentTemplateCompileException;
import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit;
import groowt.view.web.runtime.DefaultWebViewRenderContext; 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.resolve.ClassLoaderComponentClassNodeResolver;
import groowt.view.web.transpile.util.GroovyUtil;
import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.BlockStatement;

View File

@ -1,18 +1,20 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import groowt.view.web.WebViewComponentBugError;
import groowt.view.web.ast.node.*; import groowt.view.web.ast.node.*;
import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import groowt.view.web.transpile.TranspilerUtil.TranspilerState;
import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.groovy.GroovyUtil;
import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; import groowt.view.web.transpile.groovy.GroovyUtil.ConvertResult;
import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement; import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List;
import static groowt.view.web.transpile.TranspilerUtil.getStringLiteral; import static groowt.view.web.transpile.TranspilerUtil.getStringLiteral;
// TODO: set positions // TODO: set positions
@ -24,16 +26,36 @@ public class DefaultValueNodeTranspiler implements ValueNodeTranspiler {
this.componentTranspiler = componentTranspiler; this.componentTranspiler = componentTranspiler;
} }
protected ClosureExpression closureValue(ClosureValueNode closureValueNode) { // TODO: positions
protected Expression handleClosureNode(ClosureValueNode closureValueNode) {
final var rawCode = closureValueNode.getGroovyCode().getAsValidGroovyCode(); final var rawCode = closureValueNode.getGroovyCode().getAsValidGroovyCode();
final ConvertResult convertResult = GroovyUtil.convert(rawCode); final ClosureExpression convertedClosure = GroovyUtil.getClosure(rawCode);
final @Nullable BlockStatement blockStatement = convertResult.blockStatement(); final Statement closureCode = convertedClosure.getCode();
if (blockStatement == null || blockStatement.isEmpty()) { if (closureCode instanceof BlockStatement blockStatement) {
throw new IllegalStateException("block statement is null or empty"); final List<Statement> 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) { private Expression gStringValue(GStringValueNode gStringValueNode) {
@ -69,7 +91,7 @@ public class DefaultValueNodeTranspiler implements ValueNodeTranspiler {
@Override @Override
public Expression createExpression(ValueNode valueNode, TranspilerState state) { public Expression createExpression(ValueNode valueNode, TranspilerState state) {
return switch (valueNode) { return switch (valueNode) {
case ClosureValueNode closureValueNode -> this.closureValue(closureValueNode); case ClosureValueNode closureValueNode -> this.handleClosureNode(closureValueNode);
case GStringValueNode gStringValueNode -> this.gStringValue(gStringValueNode); case GStringValueNode gStringValueNode -> this.gStringValue(gStringValueNode);
case JStringValueNode jStringValueNode -> this.jStringValue(jStringValueNode); case JStringValueNode jStringValueNode -> this.jStringValue(jStringValueNode);
case EmptyClosureValueNode emptyClosureValueNode -> this.emptyClosureValue(emptyClosureValueNode); case EmptyClosureValueNode emptyClosureValueNode -> this.emptyClosureValue(emptyClosureValueNode);

View File

@ -1,5 +1,6 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.ModuleNode;
@ -55,9 +56,16 @@ public class WebViewComponentModuleNode extends ModuleNode {
.orElse(null); .orElse(null);
} }
/**
* @param alias the name of interest
* @return a standard (non-static, non-star) import, or {@code null} if there is none
*/
@Override @Override
public @Nullable ImportNode getImport(String alias) { 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) { protected void putToAll(String alias, ImportNode importNode) {
@ -88,4 +96,52 @@ public class WebViewComponentModuleNode extends ModuleNode {
this.putToAll(alias, importNode); 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<AnnotationNode> 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<AnnotationNode> 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<AnnotationNode> 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<AnnotationNode> annotations) {
final var importNode = new ImportNode(type, name);
importNode.addAnnotations(annotations);
this.addStaticStarImport(name, importNode);
}
} }

View File

@ -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.*;
import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.expr.*;

View File

@ -1,11 +1,15 @@
package groowt.view.web.transpile.util; package groowt.view.web.transpile.groovy;
import groovy.lang.GroovyCodeSource; import groovy.lang.GroovyCodeSource;
import groowt.view.web.WebViewComponentBugError;
import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.builder.AstStringCompiler; 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.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.CompilerConfiguration;
@ -97,6 +101,28 @@ public final class GroovyUtil {
return new ConvertResult(moduleNode, blockStatement, scriptClassNode, classNodes); 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() {} private GroovyUtil() {}
} }

View File

@ -1,4 +1,4 @@
package groowt.view.web.transpile.util package groowt.view.web.transpile.groovy
import org.codehaus.groovy.ast.ASTNode import org.codehaus.groovy.ast.ASTNode

View File

@ -20,18 +20,34 @@ public class ModuleNodeComponentClassNodeResolver extends CachingComponentClassN
@Override @Override
public Either<ClassNodeResolveException, ClassNode> getClassForNameWithoutPackage(String nameWithoutPackage) { public Either<ClassNodeResolveException, ClassNode> getClassForNameWithoutPackage(String nameWithoutPackage) {
return super.getClassForNameWithoutPackage(nameWithoutPackage).flatMapLeft(ignored -> { return super.getClassForNameWithoutPackage(nameWithoutPackage).flatMapLeft(ignored -> {
// try imports first // try regular imports first
final var importedClassNode = this.moduleNode.getImportType(nameWithoutPackage); final var importedClassNode = this.moduleNode.getImportType(nameWithoutPackage);
if (importedClassNode != null) { if (importedClassNode != null) {
this.addClassNode(importedClassNode); this.addClassNode(importedClassNode);
return Either.right(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 // try pre-pending package and asking for fqn
final var packageName = this.moduleNode.getPackageName(); final var packageName = this.moduleNode.getPackageName();
final String fqn; final String fqn;
if (packageName.endsWith(".")) { if (packageName.endsWith(".")) {
fqn = this.moduleNode + nameWithoutPackage; fqn = this.moduleNode.getPackageName() + nameWithoutPackage;
} else { } else {
fqn = this.moduleNode.getPackageName() + "." + nameWithoutPackage; fqn = this.moduleNode.getPackageName() + "." + nameWithoutPackage;
} }

View File

@ -7,17 +7,13 @@ class BaseWebViewComponentTests extends AbstractWebViewComponentTests {
static final class Greeter extends BaseWebViewComponent { static final class Greeter extends BaseWebViewComponent {
private final String target final String target
Greeter(Map<String, Object> attr) { Greeter(Map<String, Object> attr) {
super('Hello, $target!') super('Hello, $target!')
this.target = Objects.requireNonNull(attr.get("target")) this.target = Objects.requireNonNull(attr.get("target"))
} }
String getTarget() {
return this.target
}
} }
static final class UsingGreeter extends BaseWebViewComponent { static final class UsingGreeter extends BaseWebViewComponent {

View File

@ -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('<Echo greeting={"Hello, World!"}>$greeting</Echo>', 'Hello, World!')
}
@Test
void closureValueWithVariableExpressionEvaluatesToValue() {
this.doTest(
'<Echo greeting="Hello, World!"><Echo subGreeting={greeting}>$subGreeting</Echo></Echo>',
'Hello, World!'
)
}
@Test
void closureWithPropertyExpressionEvaluatesToValue() {
this.doTest(
'''
---
import groovy.transform.Field
@Field
Map greetings = [hello: 'Hello!']
---
<Echo greeting={greetings.hello}>$greeting</Echo>
'''.stripIndent().trim(),
'Hello!'
)
}
@Test
void closureWithMethodCallIsClosure() {
this.doTest(
'''
---
def helper(String input) {
input.capitalize()
}
---
<Echo subHelper={ helper('lowercase') }>${ -> subHelper.call() }</Echo>
'''.stripIndent().trim(), 'Lowercase'
)
}
}

View File

@ -1,9 +1,9 @@
package groowt.view.web.tools 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 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 = ''' def src = '''
import some.Thing import some.Thing