CodeDependencyCollector.java

/*
 *  Copyright (c) 2001-2024, Jean Tessier
 *  All rights reserved.
 *  
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  
 *      * Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *  
 *      * Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *  
 *      * Neither the name of Jean Tessier nor the names of his contributors
 *        may be used to endorse or promote products derived from this software
 *        without specific prior written permission.
 *  
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jeantessier.dependency;

import java.util.*;

import org.apache.logging.log4j.*;

import com.jeantessier.classreader.*;

/**
 * <p>Traverses a Classfile and extracts dependencies from its code.</p>
 *
 * <p>Known limitations:</p>
 * <ul>
 *     <li>Does not see dependencies on <code>static final</code> constants of
 *         primitive types or String</li>
 *     <li>Does not look at local variables</li>
 * </ul>
 */
public class CodeDependencyCollector extends com.jeantessier.classreader.VisitorBase {
    private final NodeFactory factory;
    private final SelectionCriteria filterCriteria;

    private Node current;

    private final Collection<DependencyListener> dependencyListeners = new HashSet<>();

    public CodeDependencyCollector() {
        this(new NodeFactory());
    }

    public CodeDependencyCollector(NodeFactory factory) {
        this(factory, new ComprehensiveSelectionCriteria());
    }

    public CodeDependencyCollector(NodeFactory factory, SelectionCriteria filterCriteria) {
        this.factory = factory;
        this.filterCriteria = filterCriteria;
    }

    public NodeFactory getFactory() {
        return factory;
    }

    private Node getCurrent() {
        return current;
    }

    /**
     * Visible for testing only
     */
    void setCurrent(Node current) {
        this.current = current;
    }

    public void visitClassfile(Classfile classfile) {
        ClassNode currentClass = getFactory().createClass(classfile.getClassName(), true);
        setCurrent(currentClass);

        fireBeginClass(classfile.getClassName());

        if (classfile.hasSuperclass()) {
            Class_info superclass = classfile.getRawSuperclass();
            superclass.accept(this);
            currentClass.addParent(getFactory().createClass(superclass.getName()));
        }

        for (Class_info class_info : classfile.getAllInterfaces()) {
            class_info.accept(this);
            currentClass.addParent(getFactory().createClass(class_info.getName()));
        }

        super.visitClassfile(classfile);

        fireEndClass(classfile.getClassName());
    }

    protected void visitClassfileAttributes(Classfile classfile) {
        setCurrent(getFactory().createClass(classfile.getClassName()));
        super.visitClassfileAttributes(classfile);
    }

    public void visitClass_info(Class_info entry) {
        String classname = entry.getName();
        LogManager.getLogger(getClass()).debug("VisitClass_info():");
        LogManager.getLogger(getClass()).debug("    name = \"{}\"", classname);

        if (entry.getRawName().getValue().startsWith("[")) {
            processDescriptor(entry.getRawName().getValue());
        } else {
            processClassName(classname);
        }

        super.visitClass_info(entry);
    }

    public void visitFieldRef_info(FieldRef_info entry) {
        LogManager.getLogger(getClass()).debug("VisitFieldRef_info():");
        LogManager.getLogger(getClass()).debug("    class = \"{}\"", () -> entry.getClassName());
        LogManager.getLogger(getClass()).debug("    name = \"{}\"", () -> entry.getRawNameAndType().getName());
        LogManager.getLogger(getClass()).debug("    type = \"{}\"", () -> entry.getRawNameAndType().getType());

        String name = entry.getFullUniqueName();
        if (filterCriteria.isMatchingFeatures() && filterCriteria.matchesFeatureName(name)) {
            Node other = getFactory().createFeature(name);
            getCurrent().addDependency(other);
            LogManager.getLogger(getClass()).info("FieldRef_info dependency: {} --> {}", getCurrent(), other);
            fireDependency(getCurrent(), other);
        }

        processDescriptor(entry.getRawNameAndType().getType());

        super.visitFieldRef_info(entry);
    }

    public void visitMethodRef_info(MethodRef_info entry) {
        LogManager.getLogger(getClass()).debug("VisitMethodRef_info():");
        LogManager.getLogger(getClass()).debug("    class = \"{}\"", () -> entry.getClassName());
        LogManager.getLogger(getClass()).debug("    name = \"{}\"", () -> entry.getRawNameAndType().getName());
        LogManager.getLogger(getClass()).debug("    type = \"{}\"", () -> entry.getRawNameAndType().getType());

        if (!entry.isStaticInitializer()) {
            String name = entry.getFullUniqueName();
            if (filterCriteria.isMatchingFeatures() && filterCriteria.matchesFeatureName(name)) {
                Node other  = getFactory().createFeature(name);
                getCurrent().addDependency(other);
                LogManager.getLogger(getClass()).info("MethodRef_info dependency: {} --> {}", getCurrent(), other);
                fireDependency(getCurrent(), other);
            }

            processDescriptor(entry.getRawNameAndType().getType());
        }

        super.visitMethodRef_info(entry);
    }

    public void visitInterfaceMethodRef_info(InterfaceMethodRef_info entry) {
        LogManager.getLogger(getClass()).debug("VisitInterfaceMethodRef_info():");
        LogManager.getLogger(getClass()).debug("    class = \"{}\"", () -> entry.getClassName());
        LogManager.getLogger(getClass()).debug("    name = \"{}\"", () -> entry.getRawNameAndType().getName());
        LogManager.getLogger(getClass()).debug("    type = \"{}\"", () -> entry.getRawNameAndType().getType());

        String name = entry.getFullUniqueName();
        if (filterCriteria.isMatchingFeatures() && filterCriteria.matchesFeatureName(name)) {
            Node other  = getFactory().createFeature(name);
            getCurrent().addDependency(other);
            LogManager.getLogger(getClass()).info("InterfaceMethodRef_info dependency: {} --> {}", getCurrent(), other);
            fireDependency(getCurrent(), other);
        }

        processDescriptor(entry.getRawNameAndType().getType());

        super.visitInterfaceMethodRef_info(entry);
    }

    public void visitField_info(Field_info entry) {
        LogManager.getLogger(getClass()).debug("VisitField_info():");
        LogManager.getLogger(getClass()).debug("    name = \"{}\"", () -> entry.getName());
        LogManager.getLogger(getClass()).debug("    descriptor = \"{}\"", () -> entry.getDescriptor());

        setCurrent(getFactory().createFeature(entry.getFullUniqueName(), true));

        processDescriptor(entry.getDescriptor());

        super.visitField_info(entry);
    }

    public void visitMethod_info(Method_info entry) {
        LogManager.getLogger(getClass()).debug("VisitMethod_info():");
        LogManager.getLogger(getClass()).debug("    name = \"{}\"", () -> entry.getName());
        LogManager.getLogger(getClass()).debug("    descriptor = \"{}\"", () -> entry.getDescriptor());

        setCurrent(getFactory().createFeature(entry.getFullUniqueName(), true));

        processDescriptor(entry.getDescriptor());

        super.visitMethod_info(entry);
    }

    public void visitInstruction(Instruction helper) {
        LogManager.getLogger(getClass()).debug("VisitInstruction() ...");

        /*
         *  We can skip the "new" (0xbb) instruction as it is always
         *  followed by a call to the constructor method.
         */

        switch (helper.getOpcode()) {
            case 0x12: // ldc
            case 0x13: // ldc_w
            case 0xb2: // getstatic
            case 0xb3: // putstatic
            case 0xb4: // getfield
            case 0xb5: // putfield
            case 0xb6: // invokevirtual
            case 0xb7: // invokespecial
            case 0xb8: // invokestatic
            case 0xb9: // invokeinterface
            // case 0xbb: // new
            case 0xbd: // anewarray
            case 0xc0: // checkcast
            case 0xc1: // instanceof
            case 0xc5: // multianewarray
                helper.getIndexedConstantPoolEntry().accept(this);
                break;
            case 0xba: // invokedynamic
                helper.getDynamicConstantPoolEntries().forEach(entry -> entry.accept(this));
                break;
            default:
                // Do nothing
                break;
        }

        super.visitInstruction(helper);
    }

    public void visitExceptionHandler(ExceptionHandler helper) {
        LogManager.getLogger(getClass()).debug("{}VisitExceptionHandler(): {}", getClass().getName(), helper);

        if (helper.hasCatchType()) {
            helper.getRawCatchType().accept(this);
        }

        super.visitExceptionHandler(helper);
    }

    public void visitAnnotation(Annotation helper) {
        processClassName(helper.getType());

        super.visitAnnotation(helper);
    }

    public void visitEnumElementValue(EnumElementValue helper) {
        String signature = helper.getTypeName() + "." + helper.getConstName();
        if (filterCriteria.isMatchingFeatures() && filterCriteria.matchesFeatureName(signature)) {
            Node other = getFactory().createFeature(signature);
            getCurrent().addDependency(other);
            LogManager.getLogger(getClass()).info("EnumElementValue dependency: {} --> {}", getCurrent(), other);
            fireDependency(getCurrent(), other);
        }

        super.visitEnumElementValue(helper);
    }

    public void visitClassElementValue(ClassElementValue helper) {
        processClassName(helper.getClassInfo());

        super.visitClassElementValue(helper);
    }

    private void processDescriptor(String str) {
        int currentPos = 0;
        int startPos;
        int endPos;

        while ((startPos = str.indexOf('L', currentPos)) != -1) {
            if ((endPos = str.indexOf(';', startPos)) != -1) {
                processClassName(ClassNameHelper.path2ClassName(str.substring(startPos + 1, endPos)));
                currentPos = endPos + 1;
            } else {
                currentPos = startPos + 1;
            }
        }
    }

    private void processClassName(String classname) {
        if (filterCriteria.isMatchingClasses() && filterCriteria.matchesClassName(classname)) {
            LogManager.getLogger(getClass()).debug("    Adding \"{}\"", classname);
            Node other = getFactory().createClass(classname);
            getCurrent().addDependency(other);
            LogManager.getLogger(getClass()).info("Class_info dependency: {} --> {}", getCurrent(), other);
            fireDependency(getCurrent(), other);
        }
    }

    public void addDependencyListener(DependencyListener listener) {
        dependencyListeners.add(listener);
    }

    public void removeDependencyListener(DependencyListener listener) {
        dependencyListeners.remove(listener);
    }

    protected void fireBeginSession() {
        DependencyEvent event = new DependencyEvent(this);
        dependencyListeners.forEach(listener -> listener.beginSession(event));
    }

    protected void fireBeginClass(String classname) {
        DependencyEvent event = new DependencyEvent(this, classname);
        dependencyListeners.forEach(listener -> listener.beginClass(event));
    }

    protected void fireDependency(Node dependent, Node dependable) {
        DependencyEvent event = new DependencyEvent(this, dependent, dependable);
        dependencyListeners.forEach(listener -> listener.dependency(event));
    }

    protected void fireEndClass(String classname) {
        DependencyEvent event = new DependencyEvent(this, classname);
        dependencyListeners.forEach(listener -> listener.endClass(event));
    }

    protected void fireEndSession() {
        DependencyEvent event = new DependencyEvent(this);
        dependencyListeners.forEach(listener -> listener.endSession(event));
    }
}