/* * FindBugs - Find bugs in Java programs * Copyright (C) 2003-2005, University of Maryland * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package edu.umd.cs.findbugs.workflow; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.Map.Entry; import org.dom4j.DocumentException; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.FindBugs; import edu.umd.cs.findbugs.FuzzyBugComparator; import edu.umd.cs.findbugs.Project; import edu.umd.cs.findbugs.SloppyBugComparator; import edu.umd.cs.findbugs.SortedBugCollection; import edu.umd.cs.findbugs.VersionInsensitiveBugComparator; import edu.umd.cs.findbugs.WarningComparator; import edu.umd.cs.findbugs.config.CommandLine; import edu.umd.cs.findbugs.model.MovedClassMap; import edu.umd.cs.findbugs.util.Util; /** * Analyze bug results to find new, fixed, and retained bugs * between versions of the same program. Uses VersionInsensitiveBugComparator * (or FuzzyBugComparator) * to determine when two BugInstances are the "same". * The new BugCollection returned is a deep copy of one of the input collections * (depending on the operation performed), with only a subset of the original * BugInstances retained. Because it is a deep copy, it may be freely modified. * * @author David Hovemeyer */ @Deprecated public class BugHistory { private static final boolean DEBUG = false; private static class BugCollectionAndProject { SortedBugCollection bugCollection; public BugCollectionAndProject(SortedBugCollection bugCollection) { this.bugCollection = bugCollection; } /** * @return Returns the bugCollection. */ public SortedBugCollection getBugCollection() { return bugCollection; } /** * @return Returns the project. */ public Project getProject() { return bugCollection.getProject(); } } /** * Cache of BugCollections and Projects for when we're operating in bulk mode. * If the pairs of files form a chronological sequence, then we won't have to * repeatedly perform I/O. */ private static class BugCollectionAndProjectCache extends LinkedHashMap<String,BugCollectionAndProject> { private static final long serialVersionUID = 1L; // 2 should be sufficient if the pairs are sorted private static final int CACHE_SIZE = 5; /* (non-Javadoc) * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry) */ @Override protected boolean removeEldestEntry(Entry<String, BugCollectionAndProject> eldest) { return size() > CACHE_SIZE; } /** * Fetch an entry, reading it if necessary. * * @param fileName file to get * @return the BugCollectionAndProject for the file * @throws IOException * @throws DocumentException */ public BugCollectionAndProject fetch(String fileName) throws IOException, DocumentException { BugCollectionAndProject result = get(fileName); if (result == null) { SortedBugCollection bugCollection = readCollection(fileName); result = new BugCollectionAndProject(bugCollection); put(fileName, result); } return result; } } /** * A set operation between two bug collections. */ public interface SetOperation { /** * Perform the set operation. * * @param result Set to put the resulting BugInstances in * @param origCollection original BugCollection * @param newCollection new BugCollection * @return the input bug collection the results are taken from */ public SortedBugCollection perform(Set<BugInstance> result, SortedBugCollection origCollection, SortedBugCollection newCollection); } /** * Get the warnings which were <em>added</em>, * meaning that they were not part of the original BugCollection. * The BugInstances returned are from the new BugCollection. */ public static final SetOperation ADDED_WARNINGS = new SetOperation(){ public SortedBugCollection perform(Set<BugInstance> result, SortedBugCollection origCollection, SortedBugCollection newCollection) { result.addAll(newCollection.getCollection()); // Get shared instances List<BugInstance> inBoth = getSharedInstances(result, origCollection); // Remove the shared instances from the result removeBugInstances(result, inBoth); return newCollection; } }; /** * Get the warnings which were <em>retained</em>, * meaning that they occur in both the original and new BugCollections. * The BugInstances returned are from the new BugCollection. */ public static final SetOperation RETAINED_WARNINGS = new SetOperation(){ public SortedBugCollection perform(Set<BugInstance> result, SortedBugCollection origCollection, SortedBugCollection newCollection) { result.addAll(newCollection.getCollection()); if (DEBUG) System.out.println(result.size() + " instances initially"); // Get shared instances List<BugInstance> inBoth = getSharedInstances(result, origCollection); // Replace instances with only those shared replaceBugInstances(result, inBoth); if (DEBUG) System.out.println(result.size() + " after retaining new instances"); return newCollection; } }; /** * Get the warnings which were <em>removed</em>, * meaning that they occur in the original BugCollection but not in * the new BugCollection. * The BugInstances returned are from the original BugCollection. */ public static final SetOperation REMOVED_WARNINGS = new SetOperation(){ public SortedBugCollection perform(Set<BugInstance> result, SortedBugCollection origCollection, SortedBugCollection newCollection) { result.addAll(origCollection.getCollection()); // Get shared instances List<BugInstance> inBoth = getSharedInstances(result, newCollection); // Remove shared instances removeBugInstances(result, inBoth); return origCollection; } }; private SortedBugCollection origCollection, newCollection; private SortedBugCollection resultCollection; private SortedBugCollection originator; private WarningComparator comparator; /** * Contructor. * * @param origCollection the original BugCollection * @param newCollection the new BugCollection */ public BugHistory(SortedBugCollection origCollection, SortedBugCollection newCollection) { this.origCollection = origCollection; this.newCollection = newCollection; } /** * Get the Comparator used to compare BugInstances from different BugCollections. */ public WarningComparator getComparator() { return comparator; } /** * @param comparator The comparator to set. */ public void setComparator(WarningComparator comparator) { this.comparator = comparator; } /** * Perform a SetOperation. * * @param operation the SetOperation * @return the BugCollection resulting from performing the SetOperation */ public SortedBugCollection performSetOperation(SetOperation operation) { // Create a result set which uses the version-insensitive/fuzzy bug comparator. // This will help figure out which bug instances are the "same" // between versions. TreeSet<BugInstance> result = new TreeSet<BugInstance>(getComparator()); // Perform the operation, keeping track of which input BugCollection // should be cloned for metadata. originator = operation.perform(result, origCollection, newCollection); // Clone the actual BugInstances selected by the set operation. Collection<BugInstance> selected = new LinkedList<BugInstance>(); SortedBugCollection.cloneAll(selected, result); // Duplicate the collection from which the results came, // in order to copy all metadata, such as analysis errors, // class/method hashes, etc. SortedBugCollection resultCollection = originator.duplicate(); // Replace with just the cloned instances of the subset selected by the set operation. resultCollection.clearBugInstances(); resultCollection.addAll(selected); this.resultCollection = resultCollection; return resultCollection; } /** * @return Returns the originator. */ public SortedBugCollection getOriginator() { return originator; } /** * @return Returns the origCollection. */ public SortedBugCollection getOrigCollection() { return origCollection; } /** * @return Returns the newCollection. */ public SortedBugCollection getNewCollection() { return newCollection; } /** * @return Returns the result. */ public SortedBugCollection getResultCollection() { return resultCollection; } public void writeResultCollection(Project origProject, Project newProject, OutputStream outputStream) throws IOException { getResultCollection().writeXML( outputStream); } /** * Get instances shared between given Set and BugCollection. * The Set is queried for membership, because it has a special Comparator * which can match BugInstances from different versions. * * @param result the Set * @param collection the BugCollection * @return List of shared instances */ private static List<BugInstance> getSharedInstances(Set<BugInstance> result, SortedBugCollection collection) { List<BugInstance> inBoth = new LinkedList<BugInstance>(); for (Iterator<BugInstance> i = collection.iterator(); i.hasNext();) { BugInstance origBugInstance = i.next(); if (result.contains(origBugInstance)) { inBoth.add(origBugInstance); } } return inBoth; } /** * Replace all of the BugInstances in given Set with the given Collection. * * @param dest the Set to replace the instances of * @param source the Collection containing the instances to put in the Set */ private static void replaceBugInstances(Set<BugInstance> dest, Collection<BugInstance> source) { dest.clear(); dest.addAll(source); } /** * Remove bug instances from Set. * * @param result the Set * @param toRemove Collection of BugInstances to remove */ private static void removeBugInstances(Set<BugInstance> result, Collection<BugInstance> toRemove) { for (BugInstance aToRemove : toRemove) { result.remove(aToRemove); } } private static final int VERSION_INSENSITIVE_COMPARATOR = 0; private static final int FUZZY_COMPARATOR = 1; private static final int SLOPPY_COMPARATOR = 2; private static class BugHistoryCommandLine extends CommandLine { private int comparatorType = VERSION_INSENSITIVE_COMPARATOR; private boolean count; private String opName; private SetOperation setOp; private String listFile; private String outputDir; private boolean verbose; public BugHistoryCommandLine() { addSwitch("-fuzzy", "use fuzzy warning matching"); addSwitch("-sloppy", "use sloppy warning matching"); addSwitch("-added", "compute added warnings"); addSwitch("-new", "same as \"-added\" switch"); addSwitch("-removed", "compute removed warnings"); addSwitch("-fixed", "same as \"-removed\" switch"); addSwitch("-retained", "compute retained warnings"); addSwitch("-count", "just print warning count"); addOption("-bulk", "file of csv xml file pairs", "bulk mode, output written to v2-OP.xml"); addOption("-outputDir", "output dir", "output directory for bulk mode (optional)"); addSwitch("-verbose", "verbose output for bulk mode"); } /* (non-Javadoc) * @see edu.umd.cs.findbugs.config.CommandLine#handleOption(java.lang.String, java.lang.String) */ @Override protected void handleOption(String option, String optionExtraPart) throws IOException { if (option.equals("-fuzzy")) { comparatorType = FUZZY_COMPARATOR; } else if (option.equals("-sloppy")) { comparatorType = SLOPPY_COMPARATOR; } else if (option.equals("-added") || option.equals("-new")) { opName = option; setOp = ADDED_WARNINGS; } else if (option.equals("-removed") || option.equals("-fixed")) { opName = option; setOp = REMOVED_WARNINGS; } else if (option.equals("-retained")) { opName = option; setOp = RETAINED_WARNINGS; } else if (option.equals("-count")) { count = true; } else if (option.equals("-verbose")) { verbose = true; } else { throw new IllegalArgumentException("Unknown option: " + option); } } /* (non-Javadoc) * @see edu.umd.cs.findbugs.config.CommandLine#handleOptionWithArgument(java.lang.String, java.lang.String) */ @Override protected void handleOptionWithArgument(String option, String argument) throws IOException { if (option.equals("-bulk")) { listFile = argument; } else if (option.equals("-outputDir")) { outputDir = argument; } else { throw new IllegalArgumentException("Unknown option: " + option); } } /** * @return Returns the comparatorType. */ public int getComparatorType() { return comparatorType; } /** * @return true if we should just output the delta */ public boolean isCount() { return count; } /** * @return Returns the opName. */ public String getOpName() { return opName; } /** * @return Returns the set operation to apply. */ public SetOperation getSetOp() { return setOp; } /** * @return Returns the listFile. */ public String getListFile() { return listFile; } /** * @return Returns the outputDir. */ public String getOutputDir() { return outputDir; } /** * @return Returns the verbose. */ public boolean isVerbose() { return verbose; } public void configure(BugHistory bugHistory, SortedBugCollection origCollection, SortedBugCollection newCollection) { // Create comparator WarningComparator comparator; switch (getComparatorType()) { case VERSION_INSENSITIVE_COMPARATOR: comparator = new VersionInsensitiveBugComparator(); break; case FUZZY_COMPARATOR: FuzzyBugComparator fuzzy = new FuzzyBugComparator(); fuzzy.registerBugCollection(origCollection); fuzzy.registerBugCollection(newCollection); comparator = fuzzy; break; case SLOPPY_COMPARATOR: comparator = new SloppyBugComparator(); break; default: throw new IllegalStateException(); } // Handle renamed classes MovedClassMap classNameRewriter = new MovedClassMap(origCollection, newCollection).execute(); comparator.setClassNameRewriter(classNameRewriter); bugHistory.setComparator(comparator); } public BugHistory createAndExecute( String origFile, String newFile, Project origProject, Project newProject) throws IOException, DocumentException { SortedBugCollection origCollection = readCollection(origFile); SortedBugCollection newCollection = readCollection(newFile); return createAndExecute(origCollection, newCollection, origProject, newProject); } public BugHistory createAndExecute( SortedBugCollection origCollection, SortedBugCollection newCollection, Project origProject, Project newProject) { BugHistory bugHistory = new BugHistory(origCollection, newCollection); configure(bugHistory, origCollection, newCollection); // We can ignore the return value because it will be accessible by calling getResult() bugHistory.performSetOperation(getSetOp()); return bugHistory; } public String getBulkOutputFileName(String fileName) { File file = new File(fileName); String filePart = file.getName(); int ext = filePart.lastIndexOf('.'); if (ext < 0 ) { filePart = filePart + getOpName(); } else { filePart = filePart.substring(0, ext) + getOpName() + filePart.substring(ext); } String dirPart = (getOutputDir() != null) ? getOutputDir() : file.getParent(); File outputFile = new File(dirPart, filePart); return outputFile.getPath(); } } private static SortedBugCollection readCollection(String fileName) throws IOException, DocumentException { SortedBugCollection result = new SortedBugCollection(); result.readXML(fileName); return result; } public static void main(String[] argv) throws Exception { FindBugs.setNoAnalysis(); BugHistoryCommandLine commandLine = new BugHistoryCommandLine(); int argCount = commandLine.parse(argv); if (commandLine.getSetOp() == null) { System.err.println("No set operation specified"); printUsage(); System.exit(1); } if (commandLine.getListFile() != null) { if (argv.length != argCount) { printUsage(); } runBulk(commandLine); } else{ if (argv.length - argCount != 2) { printUsage(); } String origFile = argv[argCount++]; String newFile = argv[argCount++]; runSinglePair(commandLine, origFile, newFile); } } private static void runBulk(BugHistoryCommandLine commandLine) throws FileNotFoundException, IOException, DocumentException { BufferedReader reader; if (commandLine.getListFile().equals("-")) { reader = new BufferedReader(new InputStreamReader(System.in)); } else { reader = new BufferedReader(Util.getFileReader(commandLine.getListFile())); } int missing = 0; try { BugCollectionAndProjectCache cache = new BugCollectionAndProjectCache(); String csvRecord; while ((csvRecord = reader.readLine()) != null) { csvRecord = csvRecord.trim(); String[] tuple = csvRecord.split(","); if (tuple.length < 2) continue; String origFile = tuple[0]; String newFile = tuple[1]; BugCollectionAndProject orig; BugCollectionAndProject next; try { orig = cache.fetch(origFile); next = cache.fetch(newFile); } catch (RuntimeException e) { throw e; } catch (Exception e ) { System.err.println("Warning: error reading bug collection: " + e.toString()); ++missing; continue; } if (commandLine.isVerbose()) { System.out.print("Computing delta from " + origFile + " to " + newFile + "..."); System.out.flush(); } BugHistory bugHistory = commandLine.createAndExecute( orig.getBugCollection(), next.getBugCollection(), orig.getProject(), next.getProject()); String outputFile = commandLine.getBulkOutputFileName(newFile); if (commandLine.isVerbose()) { System.out.print("Writing " + outputFile + "..."); System.out.flush(); } bugHistory.writeResultCollection(orig.getProject(), next.getProject(), new BufferedOutputStream(new FileOutputStream(outputFile))); if (commandLine.isVerbose()) { System.out.println("done"); } } } finally { reader.close(); } if (missing > 0) { System.err.println(missing + " pairs skipped because of missing files"); } } private static void runSinglePair(BugHistoryCommandLine commandLine, String origFile, String newFile) throws IOException, DocumentException { Project origProject = new Project(); Project newProject = new Project(); BugHistory bugHistory = commandLine.createAndExecute(origFile, newFile, origProject, newProject); if (commandLine.isCount()) { System.out.println(bugHistory.getResultCollection().getCollection().size()); } else { OutputStream outputStream = System.out; bugHistory.writeResultCollection(origProject, newProject, outputStream); } } /** * Print usage and exit. */ private static void printUsage() { System.err.println("Usage: " + BugHistory.class.getName() + " [options] <operation> <old results> <new results>"); new BugHistoryCommandLine().printUsage(System.err); System.exit(1); } } // vim:ts=4