Working on mismatched component error analysis and component-type ClassNode resolution.

This commit is contained in:
JesseBrault0709 2024-05-07 11:32:17 +02:00
parent 1b851d2def
commit 8b3dc7a476
22 changed files with 364 additions and 285 deletions

View File

@ -14,6 +14,7 @@ slf4j = '2.0.12'
antlr = { module = 'org.antlr:antlr4', version.ref = 'antlr' }
antlr-runtime = { module = 'org.antlr:antlr4-runtime', version.ref = 'antlr' }
asm = 'org.ow2.asm:asm:9.7'
classgraph = 'io.github.classgraph:classgraph:4.8.172'
gradle-tooling = 'org.gradle:gradle-tooling-api:8.6'
groovy = { module = 'org.apache.groovy:groovy', version.ref = 'groovy' }
groovy-all = { module = 'org.apache.groovy:groovy-all', version.ref = 'groovy' }

View File

@ -35,6 +35,7 @@ dependencies {
libs.groovy,
libs.groovy.templates,
libs.antlr.runtime,
libs.classgraph,
project(':view-components'),
project(':views')
)

View File

@ -1,10 +1,13 @@
package groowt.view.web;
import groovy.lang.GroovyClassLoader;
import groowt.view.component.*;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import groowt.view.component.compiler.CachingComponentTemplateCompiler;
import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.component.factory.ComponentTemplateSource;
import groowt.view.web.analysis.MismatchedComponentTypeError;
import groowt.view.web.analysis.MismatchedComponentTypeErrorAnalysis;
import groowt.view.web.antlr.CompilationUnitParseResult;
import groowt.view.web.antlr.ParserUtil;
import groowt.view.web.antlr.TokenList;
@ -25,6 +28,7 @@ import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Objects;
public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler
@ -92,7 +96,8 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem
) {
final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader);
// TODO: analysis
final List<MismatchedComponentTypeError> mismatchedComponentTypeErrors =
MismatchedComponentTypeErrorAnalysis.check(parseResult.getCompilationUnitContext());
final var tokenList = new TokenList(parseResult.getTokenStream());
final var astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList));

View File

@ -0,0 +1,53 @@
package groowt.view.web;
import groowt.view.component.ViewComponent;
import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.web.ast.node.Node;
import groowt.view.web.util.SourcePosition;
public class WebViewComponentTemplateCompileException extends ComponentTemplateCompileException {
private final Node node;
public WebViewComponentTemplateCompileException(
String message,
Class<? extends ViewComponent> forClass,
Object templateSource,
Node node
) {
super(message, forClass, templateSource);
this.node = node;
}
public WebViewComponentTemplateCompileException(
String message,
Throwable cause,
Class<? extends ViewComponent> forClass,
Object templateSource,
Node node
) {
super(message, cause, forClass, templateSource);
this.node = node;
}
public WebViewComponentTemplateCompileException(
Throwable cause,
Class<? extends ViewComponent> forClass,
Object templateSource,
Node node
) {
super(cause, forClass, templateSource);
this.node = node;
}
public Node getNode() {
return this.node;
}
@Override
public String getMessage() {
final SourcePosition start = this.node.getTokenRange().getStartPosition();
return "Line " + start.line() + ", column " + start.column() + ": " + super.getMessage();
}
}

View File

@ -1,6 +0,0 @@
package groowt.view.web.analysis;
public interface AnalysisError<T> {
T subject();
String message();
}

View File

@ -1,7 +0,0 @@
package groowt.view.web.analysis;
import java.util.List;
public sealed interface Analyzer<T, E> permits ParseTreeAnalyzer, AstAnalyzer {
List<E> analyze(T t);
}

View File

@ -1,6 +0,0 @@
package groowt.view.web.analysis;
import groowt.view.web.ast.node.Node;
@FunctionalInterface
public non-sealed interface AstAnalyzer<T extends Node, E extends AstError<T>> extends Analyzer<Node, E> {}

View File

@ -1,5 +0,0 @@
package groowt.view.web.analysis;
import groowt.view.web.ast.node.Node;
public interface AstError<T extends Node> extends AnalysisError<T> {}

View File

@ -1,16 +0,0 @@
package groowt.view.web.analysis;
import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext;
import org.antlr.v4.runtime.tree.ParseTree;
import java.util.List;
public final class MismatchedComponentTypeAnalyzer
implements ParseTreeAnalyzer<ComponentWithChildrenContext, MismatchedComponentTypeError> {
@Override
public List<MismatchedComponentTypeError> analyze(ParseTree parseTree) {
return MismatchedComponentTypeErrorAnalyzerKt.check(parseTree);
}
}

View File

@ -1,6 +0,0 @@
package groowt.view.web.analysis;
import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext;
public record MismatchedComponentTypeError(ComponentWithChildrenContext subject, String message)
implements ParseTreeAnalysisError<ComponentWithChildrenContext> {}

View File

@ -1,3 +1,4 @@
@file:JvmName("MismatchedComponentTypeErrorAnalysis")
package groowt.view.web.analysis
import groowt.view.web.antlr.WebViewComponentsParser.ComponentTypeContext
@ -51,6 +52,8 @@ private fun doCheck(tree: ParseTree, destination: MutableList<MismatchedComponen
}
}
data class MismatchedComponentTypeError(val tree: ParseTree, val message: String)
fun check(tree: ParseTree): List<MismatchedComponentTypeError> {
val result: MutableList<MismatchedComponentTypeError> = ArrayList()
doCheck(tree, result)

View File

@ -1,5 +0,0 @@
package groowt.view.web.analysis;
import org.antlr.v4.runtime.tree.ParseTree;
public interface ParseTreeAnalysisError<T extends ParseTree> extends AnalysisError<T> {}

View File

@ -1,7 +0,0 @@
package groowt.view.web.analysis;
import org.antlr.v4.runtime.tree.ParseTree;
@FunctionalInterface
public non-sealed interface ParseTreeAnalyzer<T extends ParseTree, E extends ParseTreeAnalysisError<T>>
extends Analyzer<ParseTree, E> {}

View File

@ -1,129 +0,0 @@
package groowt.view.web.analysis.classes;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.control.CompilationFailedException;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
public non-sealed class ClassLoaderClassLocator implements ClassLocator {
private static final Logger logger = LoggerFactory.getLogger(ClassLoaderClassLocator.class);
protected sealed interface CachedLocatedClass
permits ClazzCachedLocatedClass, FailedGroovyCachedLocatedClass, CustomCachedLocatedClass {}
protected record ClazzCachedLocatedClass(Class<?> cached) implements CachedLocatedClass {}
protected record FailedGroovyCachedLocatedClass(
CompilationFailedException exception) implements CachedLocatedClass {}
protected non-sealed interface CustomCachedLocatedClass extends CachedLocatedClass {
Class<?> get();
}
protected final ClassLoader classLoader;
private final Map<String, CachedLocatedClass> cache = new HashMap<>();
public ClassLoaderClassLocator(ClassLoader classLoader) {
this.classLoader = classLoader;
}
public ClassLoaderClassLocator() {
this.classLoader = Thread.currentThread().getContextClassLoader();
}
protected final void addToCache(String name, CachedLocatedClass cachedLocatedClass) {
this.cache.put(name, cachedLocatedClass);
}
protected final boolean cacheHas(String name) {
return this.cache.containsKey(name);
}
protected final void removeFromCacheIf(BiPredicate<? super String, ? super CachedLocatedClass> predicate) {
final List<String> targets = new ArrayList<>();
this.cache.forEach((name, cached) -> {
if (predicate.test(name, cached)) {
targets.add(name);
}
});
targets.forEach(this.cache::remove);
}
protected final <T extends CachedLocatedClass> void removeFromCacheIf(Class<T> ofType, BiPredicate<? super String, T> predicate) {
this.removeFromCacheIf((name, cached) -> {
if (ofType.isAssignableFrom(cached.getClass())) {
return predicate.test(name, ofType.cast(cached));
}
return false;
});
}
protected final <T extends CachedLocatedClass> void removeFromCacheByType(Class<T> type) {
this.removeFromCacheIf((name, cached) -> type.isAssignableFrom(cached.getClass()));
}
protected final <T extends CachedLocatedClass> Map<String, T> getFromCacheByType(Class<T> type) {
final Map<String, T> result = new HashMap<>();
this.cache.forEach((name, cached) -> {
if (type.isAssignableFrom(cached.getClass())) {
result.put(name, type.cast(cached));
}
});
return result;
}
protected final @Nullable Class<?> loadFromCache(String name) {
final var cachedLocated = this.cache.getOrDefault(name, null);
if (cachedLocated == null) {
return null;
} else {
return switch (cachedLocated) {
case ClazzCachedLocatedClass(var cached) -> cached;
case CustomCachedLocatedClass custom -> custom.get();
case FailedGroovyCachedLocatedClass(var exception) ->
throw new RuntimeException("Cannot load Groovy class because compilation failed.", exception);
};
}
}
protected @Nullable Class<?> searchClassLoader(String name) {
if (classLoader instanceof GroovyClassLoader gcl) {
try {
Class<?> clazz = gcl.loadClass(name, true, true, false);
this.addToCache(name, new ClazzCachedLocatedClass(clazz));
return clazz;
} catch (ClassNotFoundException ignored) {
// Ignored
} catch (CompilationFailedException cfe) {
logger.warn("Could not compile class: {}", name);
this.addToCache(name, new FailedGroovyCachedLocatedClass(cfe));
// return null because we don't actually have a class
return null;
}
} else {
try {
Class<?> clazz = classLoader.loadClass(name);
this.addToCache(name, new ClazzCachedLocatedClass(clazz));
return clazz;
} catch (ClassNotFoundException ignored) {}
} return null;
}
@Override
public boolean hasClassForFQN(String name) {
return this.cacheHas(name) || this.searchClassLoader(name) != null;
}
public void clearCache() {
this.cache.clear();
}
}

View File

@ -1,5 +0,0 @@
package groowt.view.web.analysis.classes;
public sealed interface ClassLocator permits ClassLoaderClassLocator, PreambleAwareClassLocator {
boolean hasClassForFQN(String name);
}

View File

@ -1,90 +0,0 @@
package groowt.view.web.analysis.classes;
import groovy.lang.GroovyClassLoader;
import groowt.view.web.antlr.MergedGroovyCodeToken;
import groowt.view.web.antlr.WebViewComponentsParser.PreambleContext;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.builder.AstStringCompiler;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public non-sealed class PreambleAwareClassLocator extends ClassLoaderClassLocator implements ClassLocator {
private final List<ClassNode> currentClassNodes = new ArrayList<>();
private GroovyClassLoader currentGroovyClassLoader;
public PreambleAwareClassLocator(ClassLoader classLoader) {
super(classLoader);
}
protected class ClassNodeCachedLocatedClass implements CustomCachedLocatedClass {
private final ClassNode classNode;
private Class<?> lazyLoadedClass;
public ClassNodeCachedLocatedClass(ClassNode classNode) {
this.classNode = classNode;
}
@Override
public Class<?> get() {
if (this.lazyLoadedClass == null) {
try {
final File tmp = File.createTempFile("preambleContextAwareClassLocator", "_" + System.currentTimeMillis());
this.lazyLoadedClass = currentGroovyClassLoader.defineClass(this.classNode, null, tmp.getAbsolutePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return this.lazyLoadedClass;
}
}
public void setCurrentPreamble(PreambleContext preambleContext) {
this.currentGroovyClassLoader = new GroovyClassLoader(this.classLoader);
this.currentClassNodes.clear();
final MergedGroovyCodeToken groovyCodeToken = (MergedGroovyCodeToken) preambleContext.GroovyCode().getSymbol();
final String groovyCode = groovyCodeToken.getText();
final List<ASTNode> astNodes = new AstStringCompiler().compile(groovyCode);
astNodes.forEach(groovyASTNode -> {
if (groovyASTNode instanceof ClassNode classNode) {
this.currentClassNodes.add(classNode);
}
});
}
private @Nullable ClassNode searchPreambleSimpleName(String simpleName) {
for (final ClassNode classNode : this.currentClassNodes) {
if (classNode.getNameWithoutPackage().equals(simpleName)) {
this.addToCache(classNode.getName(), new ClassNodeCachedLocatedClass(classNode));
return classNode;
}
}
return null;
}
@Override
public boolean hasClassForFQN(String name) {
return super.hasClassForFQN(name) || this.searchPreambleSimpleName(name) != null;
}
private boolean hasSimpleNameInCache(String simpleName) {
final Collection<ClassNodeCachedLocatedClass> allCached = this.getFromCacheByType(ClassNodeCachedLocatedClass.class).values();
for (final ClassNodeCachedLocatedClass cached : allCached) {
if (cached.classNode.getNameWithoutPackage().equals(simpleName)) {
return true;
}
}
return false;
}
public boolean hasClassForSimpleName(String simpleName) {
return this.hasSimpleNameInCache(simpleName) || this.searchPreambleSimpleName(simpleName) != null;
}
}

View File

@ -56,7 +56,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
}
public void setAppendOrAddStatementFactory(AppendOrAddStatementFactory appendOrAddStatementFactory) {
this.appendOrAddStatementFactory = Objects.requireNonNull(appendOrAddStatementFactory);
this.appendOrAddStatementFactory = appendOrAddStatementFactory;
}
// ViewComponent c0

View File

@ -0,0 +1,39 @@
package groowt.view.web.transpile.resolve;
import org.jetbrains.annotations.NotNull;
public sealed class ClassIdentifier permits ClassIdentifierWithFqn {
private final String alias;
public ClassIdentifier(@NotNull String alias) {
this.alias = alias;
}
/**
* @return the alias (the name without the package name)
*/
public String getAlias() {
return this.alias;
}
@Override
public final boolean equals(Object obj) {
if (this == obj) return true;
if (obj instanceof ClassIdentifier other) {
return this.alias.equals(other.alias);
}
return false;
}
@Override
public final int hashCode() {
return this.alias.hashCode();
}
@Override
public String toString() {
return "ClassIdentifier(" + this.alias + ")";
}
}

View File

@ -0,0 +1,23 @@
package groowt.view.web.transpile.resolve;
import org.jetbrains.annotations.NotNull;
public final class ClassIdentifierWithFqn extends ClassIdentifier {
private final String fqn;
public ClassIdentifierWithFqn(@NotNull String alias, @NotNull String fqn) {
super(alias);
this.fqn = fqn;
}
public String getFqn() {
return this.fqn;
}
@Override
public String toString() {
return "ClassIdentifierWithFqn(" + this.getAlias() + ", " + this.fqn + ")";
}
}

View File

@ -0,0 +1,14 @@
package groowt.view.web.transpile.resolve;
import groowt.view.web.util.Either;
import org.codehaus.groovy.ast.ClassNode;
import org.jetbrains.annotations.Nullable;
public interface ComponentClassNodeResolver {
record ClassNodeResolveError(ClassIdentifier identifier, String getMessage, @Nullable Throwable getCause) {}
Either<ClassNodeResolveError, ClassNode> getClassForFqn(String fqn);
Either<ClassNodeResolveError, ClassNode> getClassForNameWithoutPackage(String nameWithoutPackage);
}

View File

@ -0,0 +1,174 @@
package groowt.view.web.transpile.resolve;
import groowt.view.web.util.Either;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class DefaultComponentClassNodeResolver implements ComponentClassNodeResolver {
private static final Pattern aliasPattern =
Pattern.compile("^(\\P{Lu}+\\.)*(?<alias>\\p{Lu}.+(\\.\\p{Lu}.+)*)$");
protected static ClassNode getClassNode(Class<?> clazz) {
return ClassHelper.makeCached(clazz);
}
protected static String getAlias(String name) {
final var matcher = aliasPattern.matcher(name);
if (matcher.matches()) {
return matcher.group("alias");
} else {
throw new IllegalArgumentException("Cannot determine alias from " + name);
}
}
private final ModuleNode moduleNode;
protected final ClassLoader classLoader;
private final ClassInfoList classInfoList;
private final Map<ClassIdentifier, @Nullable ClassNode> cache = new HashMap<>();
public DefaultComponentClassNodeResolver(
ModuleNode moduleNode,
ClassLoader classLoader,
ClassInfoList webViewComponentClassInfoList
) {
this.moduleNode = moduleNode;
this.classLoader = classLoader;
this.classInfoList = webViewComponentClassInfoList;
}
protected final void addToCache(ClassIdentifier identifier, ClassNode clazz) {
this.cache.put(identifier, clazz);
}
protected final Either<ClassNodeResolveError, ClassNode> resolveWithClassLoader(ClassIdentifierWithFqn identifier) {
try {
Class<?> clazz = this.classLoader.loadClass(identifier.getFqn());
final var classNode = getClassNode(clazz);
return Either.right(classNode);
} catch (ClassNotFoundException classNotFoundException) {
return Either.left(
new ClassNodeResolveError(
identifier,
"Could not find class " + identifier.getFqn() + " with classLoader " +
this.classLoader,
classNotFoundException
)
);
}
}
protected final Either<ClassNodeResolveError, ClassNode> resolveWithClassGraph(ClassIdentifier identifier) {
final List<ClassInfo> potential = this.classInfoList.stream()
.filter(classInfo -> classInfo.getSimpleName().equals(identifier.getAlias()))
.toList();
if (potential.size() > 1) {
final var error = new ClassNodeResolveError(
identifier,
"There is more than one class on the classpath implementing WebViewComponent that has " +
"the simple name " + identifier.getAlias() + ". Please explicitly import the desired " +
"component class in the preamble, or use the fully qualified name of the component " +
"you wish to use.",
null
);
return Either.left(error);
} else if (potential.size() == 1) {
final var classInfo = potential.getFirst();
final ClassNode result = getClassNode(classInfo.loadClass());
return Either.right(result);
} else {
final var error = new ClassNodeResolveError(
identifier,
"Could not resolve a class implementing WebViewComponent for " + identifier.getAlias(),
null
);
return Either.left(error);
}
}
protected final @Nullable ClassNode findInCacheFqn(String fqn) {
for (final var entry : this.cache.entrySet()) {
final var identifier = entry.getKey();
final var classNode = entry.getValue();
if (classNode != null
&& identifier instanceof ClassIdentifierWithFqn withFqn
&& withFqn.getFqn().equals(fqn)
) {
return classNode;
}
}
return null;
}
protected final @Nullable ClassNode findInCacheSimpleName(String simpleName) {
for (final var entry : this.cache.entrySet()) {
final var identifier = entry.getKey();
final var classNode = entry.getValue();
if (classNode != null && identifier.getAlias().equals(simpleName)) {
return classNode;
}
}
return null;
}
@Override
public Either<ClassNodeResolveError, ClassNode> getClassForFqn(String fqn) {
// try cache
final var identifier = new ClassIdentifierWithFqn(getAlias(fqn), fqn);
final var fromCache = this.findInCacheFqn(fqn);
if (fromCache != null) {
return Either.right(fromCache);
}
// do not try preamble, because it is a fully qualified name; i.e., it needs no import
// try classLoader
final var classLoaderResolved = this.resolveWithClassLoader(identifier);
if (classLoaderResolved.isRight()) {
this.addToCache(identifier, classLoaderResolved.asRight().get());
}
return classLoaderResolved;
}
@Override
public Either<ClassNodeResolveError, ClassNode> getClassForNameWithoutPackage(String nameWithoutPackage) {
// try cache
final var identifier = new ClassIdentifier(nameWithoutPackage);
final var fromCache = this.findInCacheSimpleName(nameWithoutPackage);
if (fromCache != null) {
return Either.right(fromCache);
}
// try imports
final var importedClassNode = this.moduleNode.getImportType(nameWithoutPackage);
if (importedClassNode != null) {
this.addToCache(
new ClassIdentifierWithFqn(
importedClassNode.getName(),
importedClassNode.getNameWithoutPackage()
),
importedClassNode
);
return Either.right(importedClassNode);
}
// try classgraph
final var classGraphResolved = this.resolveWithClassGraph(identifier);
if (classGraphResolved.isRight()) {
final ClassNode resolvedClassNode = classGraphResolved.asRight().get();
final var withFqn = new ClassIdentifierWithFqn(nameWithoutPackage, resolvedClassNode.getName());
this.addToCache(withFqn, resolvedClassNode);
}
return classGraphResolved;
}
}

View File

@ -0,0 +1,48 @@
package groowt.view.web.util;
public sealed interface Either<E, T> {
@SuppressWarnings("unchecked")
static <E, T> Either<E, T> left(E error) {
return (Either<E, T>) new Left<>((Class<E>) error.getClass(), error);
}
@SuppressWarnings("unchecked")
static <E, T> Either<E, T> right(T item) {
return (Either<E, T>) new Right<>((Class<T>) item.getClass(), item);
}
record Left<E>(Class<E> errorClass, E error) implements Either<E, Object> {
public E get() {
return this.errorClass.cast(this.error);
}
}
record Right<T>(Class<T> itemClass, T item) implements Either<Object, T> {
public T get() {
return this.itemClass.cast(this.item);
}
}
default boolean isLeft() {
return this instanceof Either.Left;
}
default boolean isRight() {
return this instanceof Either.Right;
}
@SuppressWarnings("unchecked")
default Left<E> asLeft() {
return (Left<E>) this;
}
@SuppressWarnings("unchecked")
default Right<T> asRight() {
return (Right<T>) this;
}
}