org.summer.ana.CyclomaticComplexityPrinter.java Source code

Java tutorial

Introduction

Here is the source code for org.summer.ana.CyclomaticComplexityPrinter.java

Source

/***
 * Summer: An enhanced, non-invasive, and easy-to-use IoC container and
 * LTW-AOP framework enabling brand-new mocking capabilities in combination
 * with a lightweight rule engine and general meta expression language purely
 * written in Java.
 * It provides component composition at run-time as well as brand-new mocking
 * capabilities that are also applicable to binary third-party libraries, to name
 * only two of all its features.
 * There are barely limitations due to the lack of
 * Java in adding and removing fields and methods to and from a class at run-time.
 *
 * Copyright (C) 2011-2013  Sandro Sebastian Koll
 *
 * This file is part of Summer.
 *
 * Summer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Summer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Summer. If not, see <http://www.gnu.org/licenses/>.
 */
package org.summer.ana;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import org.objectweb.asm.tree.analysis.Frame;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
import java.io.File;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * @author Sandro Sebastian Koll
 */
public class CyclomaticComplexityPrinter extends ClassVisitor {
    private static final MathContext mc = new MathContext(3, RoundingMode.HALF_UP);
    private static boolean skipTrivialMethods = false;
    private static int ccLowerBoundToBeNonTrivial = 2;
    private static int ccUpperBoundToBeNonCritical = 10;

    public static void main(String[] args) {
        if (args == null || args.length < 2)
            printHelp();
        for (int i = 0; i < args.length; ++i) {
            if (args[i] == null)
                printHelp();
            switch (args[i]) {
            case "-s":
                skipTrivialMethods = true;
                break;
            case "-l":
                ccLowerBoundToBeNonTrivial = Integer.parseInt(args[++i].trim());
                break;
            case "-u":
                ccUpperBoundToBeNonCritical = Integer.parseInt(args[++i].trim());
                break;
            case "-m":
                StringTokenizer st = new StringTokenizer(args[++i].trim(), ",", false);
                while (st.hasMoreTokens()) {
                    baseDirs.add(getPath(st.nextToken()));
                }
                break;
            case "-e":
                st = new StringTokenizer(args[++i].trim(), ",", false);
                while (st.hasMoreTokens()) {
                    excludes.add(Paths.get(st.nextToken()));
                }
                break;
            case "-o":
                outputXML = getPath(args[++i].trim());
                break;
            default:
                printHelp();
            }
        }
        if (outputXML == null)
            printHelp();
        for (Path path : baseDirs) {
            if (!Files.exists(path)) {
                System.out.println("ERROR: Path \"" + path + "\" not found.");
                printHelp();
            }
        }
        try {
            writer = XMLOutputFactory.newInstance().createXMLStreamWriter(Files.newOutputStream(outputXML),
                    "UTF-8");
            writer.writeStartDocument();
            writer.writeStartElement("results");
            writer.writeStartElement("header");
            writer.writeStartElement("skip-trivial-methods");
            writer.writeCharacters("" + skipTrivialMethods);
            writer.writeEndElement();
            writer.writeStartElement("lower-bound-to-be-non-trivial");
            writer.writeCharacters("" + ccLowerBoundToBeNonTrivial);
            writer.writeEndElement();
            writer.writeStartElement("upper-bound-to-be-non-critical");
            writer.writeCharacters("" + ccUpperBoundToBeNonCritical);
            writer.writeEndElement();
            writer.writeEndElement();
            writer.writeStartElement("modules");
            for (Path path : baseDirs) {
                writer.writeStartElement("module");
                String module = path.toString();
                writer.writeAttribute("name", module);
                printModuleCCs(path);
                writer.writeEndElement();
            }
            writer.writeEndElement();
            writer.writeStartElement("critical-methods");
            int index;
            for (int i = 0; i < criticalMethods.size(); ++i) {
                writer.writeStartElement("critical-method" + (i + 1));
                index = criticalMethods.get(i).indexOf(" = ");
                writer.writeAttribute("cc", criticalMethods.get(i).substring(index + 3));
                writer.writeCharacters(criticalMethods.get(i).substring(0, index));
                writer.writeEndElement();
            }
            writer.writeEndElement();
            writer.writeStartElement("total-stats");
            writer.writeAttribute("classCount", "" + totalClassCount);
            writer.writeAttribute("methodCount", totalMethodCount == 0 ? "-" : "" + totalMethodCount);
            writer.writeAttribute("sumCC", totalCCSum == 0 ? "-" : "" + totalCCSum);
            writer.writeAttribute("avgCC", "" + (totalCCSum == 0 || totalMethodCount == 0 ? "-"
                    : new BigDecimal((double) totalCCSum / (double) totalMethodCount).round(mc)));
            writer.writeEndElement();
            writer.writeEndElement();
            writer.writeEndDocument();
            writer.flush();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private static void printHelp() {
        System.out
                .println("Usage: java.exe -jar \"summer-1.0.0-all.jar\" -cc <required-options> <optional-options>");
        System.out.println("<required-options>:");
        System.out.println("    -m = comma separated list of the modules (maven projects)");
        System.out.println("    -o = XML file with the cyclomatic complexity stats of all available methods");
        System.out.println("<optional-options>:");
        System.out.println(
                "    -e = comma separated list of packages or classes to exclude from the ones specified by -m");
        System.out.println("    -s = skip trivial methods (no arguments)");
        System.out.println(
                "    -l = lower bound of a method's cyclomatic complexity to be non trivial (positive integer, defaults to 2)");
        System.out.println(
                "    -u = upper bound of a method's cyclomatic complexity to be non critical (positive integer, defaults to 10)");
        System.out.println("\nExample:");
        System.out.println(
                "java -jar \"summer-1.0.0-all.jar\" -cc -s -l 2 -u 10 -m \"c:/First Project,c:/Second Project\" -e \"org.mycompany.util\" -o \"c:/result.xml\"");
        System.exit(0);
    }

    private static Path getPath(String file) {
        Path path = new File(file).toPath().normalize();
        if (!path.isAbsolute())
            path = Paths.get(System.getProperty("user.dir")).resolve(path);
        return path;
    }

    private static void printModuleCCs(Path module) throws Exception {
        moduleClassCount = 0;
        moduleMethodCount = 0;
        moduleCCSum = 0;
        writer.writeStartElement("classes");
        printDirCCs(module);
        writer.writeEndElement();
        writer.writeStartElement("module-stats");
        writer.writeAttribute("classCount", "" + moduleClassCount);
        writer.writeAttribute("methodCount", "" + moduleMethodCount);
        writer.writeAttribute("sumCC", moduleCCSum == 0 ? "-" : "" + moduleCCSum);
        writer.writeAttribute("avgCC", "" + (moduleCCSum == 0 || moduleMethodCount == 0 ? "-"
                : new BigDecimal((double) moduleCCSum / (double) moduleMethodCount).round(mc)));
        writer.writeEndElement();
    }

    private static void printDirCCs(Path dir) throws Exception {
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
            outer: for (Path path : stream) {
                if (Files.isDirectory(path)) {
                    for (Path exclude : excludes) {
                        if (path.toString().contains(exclude.toString()))
                            continue outer;
                    }
                    printDirCCs(path);
                } else if (path.getFileName().toString().toLowerCase(Locale.getDefault()).endsWith(".class"))
                    printClassCCs(path);
            }
        }
    }

    private static void printClassCCs(Path classFile) throws Exception {
        String className = null;
        for (int i = 0; i < classFile.getNameCount(); ++i) {
            if (classFile.getName(i).toString().equals("classes")) {
                className = classFile.subpath(i + 1, classFile.getNameCount()).toString();
                break;
            }
        }
        if (className == null) {
            System.out.println("No classes directory found.");
            printHelp();
        }
        className = className.replace('\\', '.');
        className = className.substring(0, className.length() - 6);
        classMethodCount = 0;
        classCCSum = 0;
        ClassReader cr = new ClassReader(className);
        cr.accept(new CyclomaticComplexityPrinter(), 0);
        if (classMethodCount > 0) {
            writer.writeStartElement("class");
            writer.writeAttribute("name", className);
            writer.writeAttribute("methodCount", classMethodCount == 0 ? "-" : "" + classMethodCount);
            writer.writeAttribute("sumCC", classCCSum == 0 ? "-" : "" + classCCSum);
            writer.writeAttribute("avgCC", "" + (classCCSum == 0 || classMethodCount == 0 ? "-"
                    : (new BigDecimal((double) classCCSum / (double) classMethodCount).round(mc))));
            writer.writeEndElement();
        }
    }

    public CyclomaticComplexityPrinter() {
        super(Opcodes.ASM4);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName,
            String[] interfaces) {
        this.className = name;
        isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
        if (!isInterface) {
            ++totalClassCount;
            ++moduleClassCount;
        }
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (!isInterface)
            return new CyclomaticComplexityPrinter.MethodAnalyzer(className, access, name, desc, null);
        return null;
    }

    private class MethodAnalyzer extends MethodVisitor {

        public MethodAnalyzer(String owner, int access, String name, String desc, MethodVisitor mv) {
            super(Opcodes.ASM4, new MethodNode(access, name, desc, null, null));
            this.owner = owner;
            this.methodName = name;
        }

        @Override
        public void visitEnd() {
            try {
                int cc = getCyclomaticComplexity(owner, (MethodNode) mv);
                if (!skipTrivialMethods || cc >= ccLowerBoundToBeNonTrivial) {
                    classCCSum += cc;
                    moduleCCSum += cc;
                    totalCCSum += cc;
                    ++classMethodCount;
                    ++moduleMethodCount;
                    ++totalMethodCount;
                    if (cc > ccUpperBoundToBeNonCritical) {
                        String desc = className.replace('/', '.') + "#" + methodName + " = " + cc;
                        boolean added = false;
                        for (int i = 0; i < criticalMethods.size(); ++i) {
                            String m = criticalMethods.get(i);
                            int mCC = Integer.parseInt(m.substring(m.indexOf(" = ") + 3));
                            if (mCC > cc) {
                                criticalMethods.add(i, desc);
                                added = true;
                                break;
                            }
                        }
                        if (!added)
                            criticalMethods.add(desc);
                    }
                }
            } catch (AnalyzerException ex) {
                ex.printStackTrace();
            }
        }

        private int getCyclomaticComplexity(String owner, MethodNode mn) throws AnalyzerException {
            org.objectweb.asm.tree.analysis.Analyzer ana = new org.objectweb.asm.tree.analysis.Analyzer(
                    new BasicInterpreter()) {
                @Override
                protected Frame newFrame(int nLocals, int nStack) {
                    return new CyclomaticComplexityPrinter.FrameNode(nLocals, nStack);
                }

                @Override
                protected Frame newFrame(Frame src) {
                    return new CyclomaticComplexityPrinter.FrameNode(src);
                }

                @Override
                protected void newControlFlowEdge(int src, int dst) {
                    ((CyclomaticComplexityPrinter.FrameNode) getFrames()[src]).successors
                            .add((CyclomaticComplexityPrinter.FrameNode) getFrames()[dst]);
                }
            };
            ana.analyze(owner, mn);
            Frame[] frames = ana.getFrames();
            int edges = 0;
            int nodes = 0;
            for (int i = 0; i < frames.length; ++i) {
                if (frames[i] != null) {
                    edges += ((CyclomaticComplexityPrinter.FrameNode) frames[i]).successors.size();
                    ++nodes;
                }
            }
            return edges - nodes + 2;
        }

        private String owner;
        private String methodName;
    }

    private static class FrameNode extends Frame {
        public FrameNode(int nLocals, int nStack) {
            super(nLocals, nStack);
        }

        public FrameNode(Frame src) {
            super(src);
        }

        protected Set<CyclomaticComplexityPrinter.FrameNode> successors = new HashSet<>();
    }

    private static Path outputXML;
    private static XMLStreamWriter writer;
    private static List<Path> baseDirs = new LinkedList<>();
    private static List<Path> excludes = new LinkedList<>();
    private static int totalClassCount = 0;
    private static int totalMethodCount = 0;
    private static int totalCCSum = 0;
    private static int moduleClassCount;
    private static int moduleMethodCount;
    private static int moduleCCSum;
    private static int classMethodCount;
    private static int classCCSum;
    private static List<String> criticalMethods = new LinkedList<>();
    private String className;
    private boolean isInterface;
}