Commit 06cfc165 authored by Carlos Galindo's avatar Carlos Galindo
Browse files

Basic support for enums and static variables

* Simple enums, with empty constructors.
* Initializations are not handled.

Assumptions: there are no circular dependencies in initialization of static variables and enums are immutable.
parent b27bc83e
Loading
Loading
Loading
Loading
+69 −49
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
    }

    /** A map from the FQ class name to its corresponding vertex. Use {@code mapKey(...)} to locate the key. */
    private final Map<String, ClassGraph.Vertex<ClassOrInterfaceDeclaration>> classDeclarationMap = new HashMap<>();
    private final Map<String, ClassGraph.Vertex<? extends TypeDeclaration<?>>> classDeclarationMap = new HashMap<>();
    /** A map from the field name to its corresponding vertex. Use {@code mapKey(...)} to locate the key. */
    private final Map<String, ClassGraph.Vertex<FieldDeclaration>> fieldDeclarationMap = new HashMap<>();
    /** A map from the method's signature to its corresponding vertex. Use {@code mapKey(...)} to locate the key. */
@@ -46,10 +46,15 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
        super(null, null, false);
    }

    public Collection<ClassGraph.Vertex<? extends TypeDeclaration<?>>> typeVertices() {
        return classDeclarationMap.values();
    }

    /** Locates the vertex that represents a given class or interface declaration.
     *  If the vertex is not contained in the graph, {@code null} will be returned. */
    @SuppressWarnings("unchecked")
    protected Vertex<ClassOrInterfaceDeclaration> findClassVertex(ClassOrInterfaceDeclaration declaration) {
        return classDeclarationMap.get(mapKey(declaration));
        return (Vertex<ClassOrInterfaceDeclaration>) classDeclarationMap.get(mapKey(declaration));
    }

    /** Whether this graph contains the given type as a vertex. */
@@ -77,14 +82,14 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG

    /** @see #findClassField(ResolvedType,String) */
    @SuppressWarnings("unchecked")
    public Optional<FieldDeclaration> findClassField(Vertex<ClassOrInterfaceDeclaration> vertex, String fieldName) {
    public Optional<FieldDeclaration> findClassField(Vertex<? extends TypeDeclaration<?>> vertex, String fieldName) {
        var field = vertex.getDeclaration().getFieldByName(fieldName);
        if (field.isPresent())
            return field;
        return incomingEdgesOf(vertex).stream()
                .filter(ClassArc.Extends.class::isInstance)
                .map(this::getEdgeSource)
                .map(v -> (Vertex<ClassOrInterfaceDeclaration>) v)
                .map(v -> (Vertex<? extends TypeDeclaration<?>>) v)
                .findAny()
                .flatMap(parent -> findClassField(parent, fieldName));
    }
@@ -104,8 +109,11 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
    }

    /** @see #subclassesOf(ClassOrInterfaceDeclaration) */
    protected Set<ClassOrInterfaceDeclaration> subclassesOf(Vertex<ClassOrInterfaceDeclaration> v) {
        return subclassesStreamOf(v)
    @SuppressWarnings("unchecked")
    protected Set<ClassOrInterfaceDeclaration> subclassesOf(Vertex<? extends TypeDeclaration<?>> v) {
        if (v.getDeclaration() instanceof EnumDeclaration)
            return Collections.emptySet();
        return subclassesStreamOf((Vertex<ClassOrInterfaceDeclaration>) v)
                .map(Vertex::getDeclaration)
                .map(ClassOrInterfaceDeclaration.class::cast)
                .collect(Collectors.toSet());
@@ -164,7 +172,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG

    public Optional<ObjectTree> generateObjectTreeForType(ResolvedType type) {
        if (type.isReferenceType()) {
            Vertex<ClassOrInterfaceDeclaration> v = classDeclarationMap.get(mapKey(type.asReferenceType()));
            Vertex<? extends TypeDeclaration<?>> v = classDeclarationMap.get(mapKey(type.asReferenceType()));
            if (v != null)
                return Optional.of(generateObjectTreeFor(v));
        }
@@ -179,13 +187,13 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
        return generateObjectTreeFor(classDeclarationMap.get(mapKey(type)));
    }

    protected ObjectTree generateObjectTreeFor(Vertex<ClassOrInterfaceDeclaration> classVertex) {
    protected ObjectTree generateObjectTreeFor(Vertex<? extends TypeDeclaration<?>> classVertex) {
        if (classVertex == null)
            return new ObjectTree();
        return generatePolyObjectTreeFor(classVertex, new ObjectTree(), ObjectTree.ROOT_NAME, 0);
    }

    protected ObjectTree generatePolyObjectTreeFor(Vertex<ClassOrInterfaceDeclaration> classVertex, ObjectTree tree, String level, int depth) {
    protected ObjectTree generatePolyObjectTreeFor(Vertex<? extends TypeDeclaration<?>> classVertex, ObjectTree tree, String level, int depth) {
        if (depth >= StaticConfig.K_LIMIT)
            return tree;
        Set<ClassOrInterfaceDeclaration> types = subclassesOf(classVertex);
@@ -193,7 +201,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
            generateObjectTreeFor(classVertex, tree, level, depth);
        } else {
            for (ClassOrInterfaceDeclaration type : types) {
                Vertex<ClassOrInterfaceDeclaration> subclassVertex = classDeclarationMap.get(mapKey(type));
                Vertex<? extends TypeDeclaration<?>> subclassVertex = classDeclarationMap.get(mapKey(type));
                if (!findAllFieldsOf(subclassVertex).isEmpty()) {
                    ObjectTree newType = tree.addType(ASTUtils.resolvedTypeDeclarationToResolvedType(type.resolve()), level);
                    generateObjectTreeFor(subclassVertex, tree, level + '.' + newType.getMemberNode().getLabel(), depth);
@@ -203,8 +211,8 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
        return tree;
    }

    protected void generateObjectTreeFor(Vertex<ClassOrInterfaceDeclaration> classVertex, ObjectTree tree, String level, int depth) {
        Map<String, Vertex<ClassOrInterfaceDeclaration>> classFields = findAllFieldsOf(classVertex);
    protected void generateObjectTreeFor(Vertex<? extends TypeDeclaration<?>> classVertex, ObjectTree tree, String level, int depth) {
        Map<String, Vertex<? extends TypeDeclaration<?>>> classFields = findAllFieldsOf(classVertex);
        for (var entry : classFields.entrySet()) {
            tree.addField(level + '.' + entry.getKey());
            if (entry.getValue() != null)
@@ -212,16 +220,17 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
        }
    }

    protected Map<String, Vertex<ClassOrInterfaceDeclaration>> findAllFieldsOf(Vertex<ClassOrInterfaceDeclaration> classVertex) {
        assert !classVertex.declaration.asClassOrInterfaceDeclaration().isInterface();
        ClassOrInterfaceDeclaration clazz = classVertex.getDeclaration().asClassOrInterfaceDeclaration();
        Map<String, Vertex<ClassOrInterfaceDeclaration>> fieldMap = new HashMap<>();
        while (clazz != null) {
            for (FieldDeclaration field : clazz.getFields()) {
    protected Map<String, Vertex<? extends TypeDeclaration<?>>> findAllFieldsOf(Vertex<? extends TypeDeclaration<?>> classVertex) {
        TypeDeclaration<?> type = classVertex.getDeclaration();
        assert !type.isClassOrInterfaceDeclaration() ||
                !type.asClassOrInterfaceDeclaration().isInterface();
        Map<String, Vertex<? extends TypeDeclaration<?>>> fieldMap = new HashMap<>();
        while (type != null) {
            for (FieldDeclaration field : type.getFields()) {
                for (VariableDeclarator var : field.getVariables()) {
                    if (fieldMap.containsKey(var.getNameAsString()))
                        continue;
                    Vertex<ClassOrInterfaceDeclaration> v = null;
                    Vertex<? extends TypeDeclaration<?>> v = null;
                    if (var.getType().isClassOrInterfaceType()) {
                        try {
                            v = classDeclarationMap.get(mapKey(var.getType().asClassOrInterfaceType().resolve()));
@@ -231,10 +240,11 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
                    fieldMap.put(var.getNameAsString(), v);
                }
            }
            Optional<ClassOrInterfaceDeclaration> parent = parentOf(clazz);
            if (parent.isEmpty())
                break;
            clazz = parent.get();
            if (type.isClassOrInterfaceDeclaration()) {
                type = parentOf(type.asClassOrInterfaceDeclaration()).orElse(null);
            } else {
                type = null;
            }
        }
        return fieldMap;
    }
@@ -253,7 +263,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
        return built;
    }

    protected String mapKey(ClassOrInterfaceDeclaration n) {
    protected String mapKey(TypeDeclaration<?> n) {
        return n.getFullyQualifiedName().orElseThrow();
    }

@@ -265,11 +275,11 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
        return n.getQualifiedName();
    }

    protected String mapKey(CallableDeclaration<?> declaration, ClassOrInterfaceDeclaration clazz) {
    protected String mapKey(CallableDeclaration<?> declaration, TypeDeclaration<?> clazz) {
        return clazz.getFullyQualifiedName().orElseThrow() + "." + declaration.getSignature();
    }

    protected String mapKey(FieldDeclaration declaration, ClassOrInterfaceDeclaration clazz) {
    protected String mapKey(FieldDeclaration declaration, TypeDeclaration<?> clazz) {
        return clazz.getFullyQualifiedName().orElseThrow() + "." + declaration;
    }

@@ -277,7 +287,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
     * in the given list of compilation units. */
    protected void buildVertices(NodeList<CompilationUnit> arg) {
        arg.accept(new VoidVisitorAdapter<Void>() {
            private final Deque<ClassOrInterfaceDeclaration> classStack = new LinkedList<>();
            private final Deque<TypeDeclaration<?>> typeStack = new LinkedList<>();
//            QUESTIONS & LACKS:
//              1) Is it necessary to include something apart from class vertices?
//              2) Private classes inside other classes?
@@ -285,49 +295,56 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG

            @Override
            public void visit(ClassOrInterfaceDeclaration n, Void arg) {
                classStack.push(n);
                addClassDeclaration(n);
                typeStack.push(n);
                addTypeDeclaration(n);
                super.visit(n, arg);
                classStack.pop();
                typeStack.pop();
            }

            @Override
            public void visit(EnumDeclaration n, Void arg) {
                typeStack.push(n);
                addTypeDeclaration(n);
                super.visit(n, arg);
                typeStack.pop();
            }

            @Override
            public void visit(FieldDeclaration n, Void arg) {
                assert classStack.peek() != null;
                addFieldDeclaration(n, classStack.peek());
                assert typeStack.peek() != null;
                addFieldDeclaration(n, typeStack.peek());
            }

            @Override
            public void visit(MethodDeclaration n, Void arg) {
                assert classStack.peek() != null;
                addCallableDeclaration(n, classStack.peek());
                assert typeStack.peek() != null;
                addCallableDeclaration(n, typeStack.peek());
            }

            @Override
            public void visit(ConstructorDeclaration n, Void arg) {
                assert classStack.peek() != null;
                addCallableDeclaration(n, classStack.peek());
                assert typeStack.peek() != null;
                addCallableDeclaration(n, typeStack.peek());
            }
        }, null);
    }

    /** Add a class declaration vertex to the class graph */
    protected void addClassDeclaration(ClassOrInterfaceDeclaration n) {
        ClassGraph.Vertex<ClassOrInterfaceDeclaration> v = new ClassGraph.Vertex<>(n);
        // Required string to match ClassOrInterfaceType and ClassOrInterfaceDeclaration. QualifiedName Not Valid
    /** Add a type declaration vertex to the class graph, to represent classes and enums. */
    protected void addTypeDeclaration(TypeDeclaration<?> n) {
        ClassGraph.Vertex<TypeDeclaration<?>> v = new ClassGraph.Vertex<>(n);
        classDeclarationMap.put(mapKey(n), v);
        addVertex(v);
    }

    /** Add a field declaration vertex to the class graph */
    protected void addFieldDeclaration(FieldDeclaration n, ClassOrInterfaceDeclaration c){
    protected void addFieldDeclaration(FieldDeclaration n, TypeDeclaration<?> c){
        ClassGraph.Vertex<FieldDeclaration> v = new ClassGraph.Vertex<>(n);
        fieldDeclarationMap.put(mapKey(n, c), v);
        addVertex(v);
    }

    /** Add a method/constructor declaration vertex to the class graph */
    protected void addCallableDeclaration(CallableDeclaration<?> n, ClassOrInterfaceDeclaration c){
    protected void addCallableDeclaration(CallableDeclaration<?> n, TypeDeclaration<?> c){
        assert n instanceof ConstructorDeclaration || n instanceof MethodDeclaration;
        ClassGraph.Vertex<CallableDeclaration<?>> v = new ClassGraph.Vertex<>(n);
        methodDeclarationMap.put(mapKey(n, c), v);
@@ -343,7 +360,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
            @Override
            public void visit(ClassOrInterfaceDeclaration n, Void arg) {
                classStack.push(n);
                Vertex<ClassOrInterfaceDeclaration> v = classDeclarationMap.get(mapKey(n));
                var v = classDeclarationMap.get(mapKey(n));
                addClassEdges(v);
                super.visit(n, arg);
                classStack.pop();
@@ -353,7 +370,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
            public void visit(FieldDeclaration n, Void arg) {
                ClassOrInterfaceDeclaration clazz = classStack.peek();
                assert clazz != null;
                Vertex<ClassOrInterfaceDeclaration> c = classDeclarationMap.get(mapKey(clazz));
                var c = classDeclarationMap.get(mapKey(clazz));
                Vertex<FieldDeclaration> v = fieldDeclarationMap.get(mapKey(n, clazz));
                addEdge(c, v, new ClassArc.Member());
            }
@@ -362,7 +379,7 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
            public void visit(MethodDeclaration n, Void arg) {
                ClassOrInterfaceDeclaration clazz = classStack.peek();
                assert clazz != null;
                Vertex<ClassOrInterfaceDeclaration> c = classDeclarationMap.get(mapKey(clazz));
                var c = classDeclarationMap.get(mapKey(clazz));
                Vertex<CallableDeclaration<?>> v = methodDeclarationMap.get(mapKey(n, clazz));
                addEdge(c, v, new ClassArc.Member());
            }
@@ -371,20 +388,23 @@ public class ClassGraph extends DirectedPseudograph<ClassGraph.Vertex<?>, ClassG
            public void visit(ConstructorDeclaration n, Void arg) {
                ClassOrInterfaceDeclaration clazz = classStack.peek();
                assert clazz != null;
                Vertex<ClassOrInterfaceDeclaration> c = classDeclarationMap.get(mapKey(clazz));
                var c = classDeclarationMap.get(mapKey(clazz));
                Vertex<CallableDeclaration<?>> v = methodDeclarationMap.get(mapKey(n, clazz));
                addEdge(c, v, new ClassArc.Member());
            }
        }, null);
    }

    protected void addClassEdges(Vertex<ClassOrInterfaceDeclaration> v) {
        v.declaration.getExtendedTypes().forEach(p -> {
    protected void addClassEdges(Vertex<? extends TypeDeclaration<?>> v) {
        if (v.declaration instanceof EnumDeclaration)
            return; // nothing to do, it is final and cannot extend nor implement user-defined types
        ClassOrInterfaceDeclaration c = (ClassOrInterfaceDeclaration) v.declaration;
        c.getExtendedTypes().forEach(p -> {
            Vertex<?> source = classDeclarationMap.get(mapKey(p.resolve()));
            if (source != null && containsVertex(v))
                addEdge(source, v, new ClassArc.Extends());
        });
        v.declaration.getImplementedTypes().forEach(p -> {
        c.getImplementedTypes().forEach(p -> {
            Vertex<?> source = classDeclarationMap.get(mapKey(p.resolve()));
            if (source != null && containsVertex(v))
                addEdge(source, v, new ClassArc.Implements());
+7 −1
Original line number Diff line number Diff line
@@ -205,8 +205,14 @@ public class ExpressionObjectTreeFinder {
            public void visit(FieldAccessExpr n, String arg) {
                if (!arg.isEmpty())
                    arg = "." + arg;
                if (n.resolve().isEnumConstant()) {
                    var vaOpt = locateVariableAction(n.getScope(), va -> va.getName().equals(n.getScope().toString()));
                    if (vaOpt.isEmpty()) throw new IllegalStateException("Could not find USE(" + n.getScope().toString() + ")");
                    list.add(new Pair<>(vaOpt.get(), n.getNameAsString() + arg));
                } else {
                    n.getScope().accept(this, n.getNameAsString() + arg);
                }
            }

            @Override
            public void visit(ObjectCreationExpr n, String arg) {
+7 −2
Original line number Diff line number Diff line
@@ -46,6 +46,11 @@ public class GraphNodeContentVisitor<A> extends VoidVisitorAdapter<A> {
        n.getParameter().accept(this, arg);
    }

    @Override
    public void visit(ClassOrInterfaceDeclaration n, A arg) {
        // A node representing a class or interface declaration has no relevant elements to be visited.
    }

    @Override
    public void visit(ConstructorDeclaration n, A arg) {
        // A node representing a constructor declaration has no relevant elements to be visited.
@@ -68,12 +73,12 @@ public class GraphNodeContentVisitor<A> extends VoidVisitorAdapter<A> {

    @Override
    public void visit(EnumConstantDeclaration n, A arg) {
        throw new UnsupportedOperationException();
        n.getArguments().accept(this, arg);
    }

    @Override
    public void visit(EnumDeclaration n, A arg) {
        throw new UnsupportedOperationException();
        // This node should not contain other elements
    }

    @Override
+68 −3
Original line number Diff line number Diff line
@@ -3,17 +3,20 @@ package es.upv.mist.slicing.graphs.jsysdg;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.CallableDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.*;
import com.github.javaparser.ast.visitor.ModifierVisitor;
import com.github.javaparser.ast.visitor.Visitable;
import es.upv.mist.slicing.arcs.pdg.StructuralArc;
import es.upv.mist.slicing.graphs.ClassGraph;
import es.upv.mist.slicing.graphs.augmented.PSDG;
import es.upv.mist.slicing.graphs.cfg.CFG;
import es.upv.mist.slicing.graphs.exceptionsensitive.ESSDG;
import es.upv.mist.slicing.graphs.exceptionsensitive.ExceptionSensitiveCallConnector;
import es.upv.mist.slicing.graphs.pdg.PDG;
import es.upv.mist.slicing.nodes.GraphNode;
import es.upv.mist.slicing.nodes.VariableAction;
import es.upv.mist.slicing.nodes.io.FormalIONode;
import es.upv.mist.slicing.nodes.oo.MemberNode;
import es.upv.mist.slicing.slicing.JSysDGSlicingAlgorithm;
import es.upv.mist.slicing.slicing.SlicingAlgorithm;
import es.upv.mist.slicing.utils.NodeHashSet;
@@ -41,6 +44,12 @@ public class JSysDG extends ESSDG {
            super.build(nodeList);
        }

        @Override
        protected void createClassGraph(NodeList<CompilationUnit> nodeList) {
            super.createClassGraph(nodeList);
            insertTypeNodes();
        }

        /** Create implicit constructors, and store them in a set so that they may be built with implicit nodes. */
        protected void insertImplicitConstructors(NodeList<CompilationUnit> nodeList) {
            nodeList.accept(new ModifierVisitor<>() {
@@ -72,11 +81,67 @@ public class JSysDG extends ESSDG {
        @Override
        protected void connectCalls() {
            new JSysCallConnector(JSysDG.this).connectAllCalls(callGraph);
            connectEnumToFormalIn();
        }

        protected void connectEnumToFormalIn() {
            for (GraphNode<?> g1 : vertexSet()) {
                if (!(g1.getAstNode() instanceof EnumDeclaration))
                    continue;
                VariableAction a1 = g1.getLastVariableAction();
                for (GraphNode<?> g2 : vertexSet()) {
                    if (g2 instanceof FormalIONode) {
                        FormalIONode fIn = (FormalIONode) g2;
                        if (fIn.isInput() && fIn.getVariableName().equals(a1.getName()))
                            a1.applySDGTreeConnection(JSysDG.this, g2.getLastVariableAction());
                    }
                }
            }
        }

        @Override
        protected void createSummaryArcs() {
            new SummaryArcAnalyzer(JSysDG.this, callGraph).analyze();
        }

        /** Adds type nodes (classes, interfaces, enums) to the SDG, along with their static fields. */
        protected void insertTypeNodes() {
            for (ClassGraph.Vertex<? extends TypeDeclaration<?>> cgVertex : ClassGraph.getInstance().typeVertices()) {
                String kind;
                if (cgVertex.getDeclaration() instanceof EnumDeclaration) {
                    kind = "enum";
                } else if (cgVertex.getDeclaration() instanceof ClassOrInterfaceDeclaration) {
                    if (((ClassOrInterfaceDeclaration) cgVertex.getDeclaration()).isInterface())
                        kind = "interface";
                    else
                        kind = "class";
                } else {
                    throw new IllegalStateException("Invalid kind of type node");
                }

                String typeName = cgVertex.getDeclaration().getNameAsString();
                GraphNode<?> typeNode = addVertex(kind + " " + typeName, cgVertex.getDeclaration());
                VariableAction typeDef = new VariableAction.Definition(VariableAction.DeclarationType.TYPE, typeName, typeNode);
                typeNode.addVariableAction(typeDef);

                for (FieldDeclaration fieldDecl : cgVertex.getDeclaration().getFields())
                    if (fieldDecl.isStatic())
                        for (VariableDeclarator vd : fieldDecl.getVariables())
                            typeNode.getLastVariableAction().getObjectTree().addStaticField(vd.getNameAsString(), fieldDecl);

                // Enums have additional static fields: their entries or constants
                if (cgVertex.getDeclaration() instanceof EnumDeclaration)
                    for (EnumConstantDeclaration ecDecl : ((EnumDeclaration) cgVertex.getDeclaration()).getEntries())
                        typeDef.getObjectTree().addStaticField(ecDecl.getNameAsString(), ecDecl);

                // Copy object tree nodes to the SDG
                addVertex(typeDef.getObjectTree().getMemberNode());
                addEdge(typeNode, typeDef.getObjectTree().getMemberNode(), new StructuralArc());
                for (MemberNode memberNode : typeDef.getObjectTree().nodeIterable()) {
                    addVertex(memberNode);
                    addEdge(memberNode.getParent(), memberNode, new StructuralArc());
                }
            }
        }
    }
}
+7 −0
Original line number Diff line number Diff line
package es.upv.mist.slicing.nodes;

import com.github.javaparser.ast.Node;
import com.github.javaparser.resolution.types.ResolvedType;
import es.upv.mist.slicing.nodes.oo.MemberNode;
import es.upv.mist.slicing.nodes.oo.PolyMemberNode;
@@ -141,6 +142,12 @@ public class ObjectTree implements Cloneable {
        return childrenMap.computeIfAbsent(fieldName, f -> new ObjectTree(f, this));
    }

    public ObjectTree addStaticField(String fieldName, Node node) {
        if (fieldName.contains(".") || fieldName.isBlank())
            throw new IllegalArgumentException("field name must not include dots or be blank!");
        return childrenMap.computeIfAbsent(fieldName, f -> new ObjectTree(new MemberNode(fieldName, node, memberNode)));
    }

    /** Similar to {@link #addField(String)}, but may be called at any level
     *  and the argument must not contain the root variable. */
    private ObjectTree addNonRootField(String members) {
Loading