/* * FindBugs - Find bugs in Java programs * Copyright (C) 2003-2005 William Pugh * 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.File; import java.io.IOException; import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import org.dom4j.DocumentException; import edu.umd.cs.findbugs.AppVersion; import edu.umd.cs.findbugs.BugCollection; import edu.umd.cs.findbugs.BugDesignation; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.BugRanker; import edu.umd.cs.findbugs.ClassAnnotation; import edu.umd.cs.findbugs.DetectorFactoryCollection; import edu.umd.cs.findbugs.FindBugs; import edu.umd.cs.findbugs.PackageStats; import edu.umd.cs.findbugs.PackageStats.ClassStats; import edu.umd.cs.findbugs.SloppyBugComparator; import edu.umd.cs.findbugs.SortedBugCollection; import edu.umd.cs.findbugs.SystemProperties; import edu.umd.cs.findbugs.VersionInsensitiveBugComparator; import edu.umd.cs.findbugs.config.CommandLine; import edu.umd.cs.findbugs.model.MovedClassMap; /** * Java main application to compute update a historical bug collection with * results from another build/analysis. * * @author William Pugh */ public class Update { static final boolean doMatchFixedBugs = SystemProperties.getBoolean("findbugs.matchFixedBugs", true); static final int maxResurrection = SystemProperties.getInt("findbugs.maxResurrection", 90); /** * */ private static final String USAGE = "Usage: " + Update.class.getName() + " [options] data1File data2File data3File ... "; private final Map<BugInstance, BugInstance> mapFromNewToOldBug = new IdentityHashMap<BugInstance, BugInstance>(); private final Set<String> resurrected = new HashSet<String>(); private final Map<BugInstance, Void> matchedOldBugs = new IdentityHashMap<BugInstance, Void>(); boolean noPackageMoves = false; boolean useAnalysisTimes = false; boolean noResurrections = false; boolean preciseMatch = false; boolean sloppyMatch = false; boolean precisePriorityMatch = false; int mostRecent = -1; int maxRank = BugRanker.VISIBLE_RANK_MAX; class UpdateCommandLine extends CommandLine { boolean overrideRevisionNames = false; String outputFilename; boolean withMessages = false; UpdateCommandLine() { addSwitch("-overrideRevisionNames", "override revision names for each version with names computed filenames"); addSwitch("-noPackageMoves", "if a class seems to have moved from one package to another, treat warnings in that class as two seperate warnings"); addSwitch("-noResurrections", "if an issue had been detected in two versions but not in an intermediate version, record as two separate issues"); addSwitch("-preciseMatch", "require bug patterns to match precisely"); addSwitch("-precisePriorityMatch", "only consider two warnings to be the same if their priorities match exactly"); addSwitch("-sloppyMatch", "very relaxed matching of bugs"); addOption("-output", "output file", "explicit filename for merged results (standard out used if not specified)"); addOption("-maxRank", "max rank", "maximum rank for issues to store"); addSwitch("-quiet", "don't generate any outout to standard out unless there is an error"); addSwitch("-useAnalysisTimes", "use analysis timestamp rather than code timestamp in history"); addSwitch("-withMessages", "Add bug description"); addOption("-onlyMostRecent", "number", "only use the last # input files"); } @Override protected void handleOption(String option, String optionExtraPart) throws IOException { if (option.equals("-overrideRevisionNames")) { if (optionExtraPart.length() == 0) overrideRevisionNames = true; else overrideRevisionNames = Boolean.parseBoolean(optionExtraPart); } else if (option.equals("-noPackageMoves")) { if (optionExtraPart.length() == 0) noPackageMoves = true; else noPackageMoves = Boolean.parseBoolean(optionExtraPart); } else if (option.equals("-noResurrections")) { if (optionExtraPart.length() == 0) noResurrections = true; else noResurrections = Boolean.parseBoolean(optionExtraPart); } else if (option.equals("-preciseMatch")) { preciseMatch = true; } else if (option.equals("-sloppyMatch")) { sloppyMatch = true; } else if (option.equals("-precisePriorityMatch")) { versionInsensitiveBugComparator.setComparePriorities(true); fuzzyBugPatternMatcher.setComparePriorities(true); precisePriorityMatch = true; } else if (option.equals("-quiet")) verbose = false; else if (option.equals("-useAnalysisTimes")) useAnalysisTimes = true; else if (option.equals("-withMessages")) withMessages = true; else throw new IllegalArgumentException("no option " + option); } @Override protected void handleOptionWithArgument(String option, String argument) throws IOException { if (option.equals("-output")) outputFilename = argument; else if (option.equals("-maxRank")) { maxRank = Integer.parseInt(argument); } else if (option.equals("-onlyMostRecent")) { mostRecent = Integer.parseInt(argument); } else throw new IllegalArgumentException("Can't handle option " + option); } } VersionInsensitiveBugComparator versionInsensitiveBugComparator = new VersionInsensitiveBugComparator(); VersionInsensitiveBugComparator fuzzyBugPatternMatcher = new VersionInsensitiveBugComparator(); { fuzzyBugPatternMatcher.setExactBugPatternMatch(false); } HashSet<String> sourceFilesInCollection(BugCollection collection) { HashSet<String> result = new HashSet<String>(); for (PackageStats pStats : collection.getProjectStats().getPackageStats()) { for (ClassStats cStats : pStats.getClassStats()) { result.add(cStats.getSourceFile()); } } return result; } public void removeBaselineBugs(BugCollection baselineCollection, BugCollection bugCollection) { matchBugs(baselineCollection, bugCollection); matchBugs(SortedBugCollection.BugInstanceComparator.instance, baselineCollection, bugCollection); matchBugs(versionInsensitiveBugComparator, baselineCollection, bugCollection); for (Iterator<BugInstance> i = bugCollection.getCollection().iterator(); i.hasNext();) { BugInstance bug = i.next(); if (matchedOldBugs.containsKey(bug)) i.remove(); } } public BugCollection mergeCollections(BugCollection origCollection, BugCollection newCollection, boolean copyDeadBugs, boolean incrementalAnalysis) { for (BugInstance b : newCollection) if (b.isDead()) throw new IllegalArgumentException("Can't merge bug collections if the newer collection contains dead bugs: " + b); mapFromNewToOldBug.clear(); matchedOldBugs.clear(); BugCollection resultCollection = newCollection.createEmptyCollectionWithMetadata(); // Previous sequence number long lastSequence = origCollection.getSequenceNumber(); // The AppVersion history is retained from the orig collection, // adding an entry for the sequence/timestamp of the current state // of the orig collection. resultCollection.clearAppVersions(); for (Iterator<AppVersion> i = origCollection.appVersionIterator(); i.hasNext();) { AppVersion appVersion = i.next(); resultCollection.addAppVersion((AppVersion) appVersion.clone()); } AppVersion origCollectionVersion = origCollection.getCurrentAppVersion(); AppVersion origCollectionVersionClone = new AppVersion(lastSequence); origCollectionVersionClone.setTimestamp(origCollectionVersion.getTimestamp()); origCollectionVersionClone.setReleaseName(origCollectionVersion.getReleaseName()); origCollectionVersionClone.setNumClasses(origCollection.getProjectStats().getNumClasses()); origCollectionVersionClone.setCodeSize(origCollection.getProjectStats().getCodeSize()); resultCollection.addAppVersion(origCollectionVersionClone); // We assign a sequence number to the new collection as one greater than // the original collection. long currentSequence = origCollection.getSequenceNumber() + 1; resultCollection.setSequenceNumber(currentSequence); int oldBugs = 0; matchBugs(origCollection, newCollection); if (sloppyMatch) matchBugs(new SloppyBugComparator(), origCollection, newCollection); int newlyDeadBugs = 0; int persistantBugs = 0; int addedBugs = 0; int addedInNewCode = 0; int deadBugInDeadCode = 0; HashSet<String> analyzedSourceFiles = sourceFilesInCollection(newCollection); // Copy unmatched bugs if (copyDeadBugs || incrementalAnalysis) for (BugInstance bug : origCollection.getCollection()) if (!matchedOldBugs.containsKey(bug)) if (bug.isDead()) { oldBugs++; BugInstance newBug = (BugInstance) bug.clone(); resultCollection.add(newBug, false); } else { newlyDeadBugs++; BugInstance newBug = (BugInstance) bug.clone(); ClassAnnotation classBugFoundIn = bug.getPrimaryClass(); String className = classBugFoundIn.getClassName(); String sourceFile = classBugFoundIn.getSourceFileName(); boolean fixed = sourceFile != null && analyzedSourceFiles.contains(sourceFile) || newCollection.getProjectStats().getClassStats(className) != null; if (fixed) { if (!copyDeadBugs) continue; newBug.setRemovedByChangeOfPersistingClass(true); newBug.setLastVersion(lastSequence); } else { deadBugInDeadCode++; if (!incrementalAnalysis) newBug.setLastVersion(lastSequence); } if (newBug.isDead() && newBug.getFirstVersion() > newBug.getLastVersion()) throw new IllegalStateException("Illegal Version range: " + newBug.getFirstVersion() + ".." + newBug.getLastVersion()); resultCollection.add(newBug, false); } // Copy matched bugs for (BugInstance bug : newCollection.getCollection()) { BugInstance newBug = (BugInstance) bug.clone(); if (mapFromNewToOldBug.containsKey(bug)) { BugInstance origWarning = mapFromNewToOldBug.get(bug); mergeBugHistory(origWarning, newBug); // handle getAnnotationText()/setAnnotationText() and // designation key BugDesignation designation = newBug.getUserDesignation(); if (designation != null) designation.merge(origWarning.getUserDesignation()); else newBug.setUserDesignation(origWarning.getUserDesignation()); // clone?? persistantBugs++; } else { newBug.setFirstVersion(lastSequence + 1); addedBugs++; ClassAnnotation classBugFoundIn = bug.getPrimaryClass(); String className = classBugFoundIn.getClassName(); if (origCollection.getProjectStats().getClassStats(className) != null) { newBug.setIntroducedByChangeOfExistingClass(true); // System.out.println("added bug to existing code " + // newBug.getUniqueId() + " : " + newBug.getAbbrev() + " in // " + classBugFoundIn); } else addedInNewCode++; } if (newBug.isDead()) throw new IllegalStateException("Illegal Version range: " + newBug.getFirstVersion() + ".." + newBug.getLastVersion()); int oldSize = resultCollection.getCollection().size(); resultCollection.add(newBug, false); int newSize = resultCollection.getCollection().size(); if (newSize != oldSize + 1) { System.out.println("Failed to add bug" + newBug.getMessage()); } } if (false && verbose) { System.out.println(origCollection.getCollection().size() + " orig bugs, " + newCollection.getCollection().size() + " new bugs"); System.out.println("Bugs: " + oldBugs + " old, " + deadBugInDeadCode + " in removed code, " + (newlyDeadBugs - deadBugInDeadCode) + " died, " + persistantBugs + " persist, " + addedInNewCode + " in new code, " + (addedBugs - addedInNewCode) + " added"); System.out.println(resultCollection.getCollection().size() + " resulting bugs"); } return resultCollection; } /** * @param newCollection */ private void discardUnwantedBugs(BugCollection newCollection) { BugRanker.trimToMaxRank(newCollection, maxRank); if (sloppyMatch) { TreeSet<BugInstance> sloppyUnique = new TreeSet<BugInstance>(new SloppyBugComparator()); for(Iterator<BugInstance> i = newCollection.iterator(); i.hasNext(); ) if (!sloppyUnique.add(i.next())) i.remove(); } } private static int size(BugCollection b) { int count = 0; for (Iterator<BugInstance> i = b.iterator(); i.hasNext();) { i.next(); count++; } return count; } /** * @param origCollection * @param newCollection */ private void matchBugs(BugCollection origCollection, BugCollection newCollection) { matchBugs(SortedBugCollection.BugInstanceComparator.instance, origCollection, newCollection); mapFromNewToOldBug.clear(); matchedOldBugs.clear(); matchBugs(versionInsensitiveBugComparator, origCollection, newCollection); matchBugs(versionInsensitiveBugComparator, origCollection, newCollection, MatchOldBugs.IF_CLASS_NOT_SEEN_UNTIL_NOW); if (doMatchFixedBugs) matchBugs(versionInsensitiveBugComparator, origCollection, newCollection, MatchOldBugs.ALWAYS); if (!preciseMatch) matchBugs(fuzzyBugPatternMatcher, origCollection, newCollection); if (!noPackageMoves) { VersionInsensitiveBugComparator movedBugComparator = new VersionInsensitiveBugComparator(); MovedClassMap movedClassMap = new MovedClassMap(origCollection, newCollection).execute(); if (!movedClassMap.isEmpty()) { movedBugComparator.setClassNameRewriter(movedClassMap); movedBugComparator.setComparePriorities(precisePriorityMatch); matchBugs(movedBugComparator, origCollection, newCollection); if (!preciseMatch) { movedBugComparator.setExactBugPatternMatch(false); matchBugs(movedBugComparator, origCollection, newCollection); } } if (false) System.out.println("Matched old bugs: " + matchedOldBugs.size()); } } boolean verbose = true; public static String[] getFilePathParts(String filePath) { String regex = (File.separatorChar == '\\' ? "\\\\" : File.separator); return filePath.split(regex); } public static void main(String[] args) throws IOException, DocumentException { FindBugs.setNoAnalysis(); new Update().doit(args); } public void doit(String[] args) throws IOException, DocumentException { DetectorFactoryCollection.instance(); UpdateCommandLine commandLine = new UpdateCommandLine(); int argCount = commandLine.parse(args, 1, Integer.MAX_VALUE, USAGE); if (commandLine.outputFilename == null) verbose = false; if (mostRecent > 0) { argCount = Math.max(argCount, args.length - mostRecent); } String[] firstPathParts = getFilePathParts(args[argCount]); int commonPrefix = firstPathParts.length; for (int i = argCount + 1; i <= (args.length - 1); i++) { commonPrefix = Math.min(commonPrefix, lengthCommonPrefix(firstPathParts, getFilePathParts(args[i]))); } String origFilename = args[argCount++]; BugCollection origCollection; origCollection = new SortedBugCollection(); if (verbose) System.out.println("Starting with " + origFilename); while (true) try { while (true) { File f = new File(origFilename); if (f.length() > 0) break; if (verbose) System.out.println("Empty input file: " + f); origFilename = args[argCount++]; } origCollection.readXML(origFilename); break; } catch (Exception e) { if (verbose) { System.out.println("Error reading " + origFilename); e.printStackTrace(System.out); } origFilename = args[argCount++]; } if (commandLine.overrideRevisionNames || origCollection.getReleaseName() == null || origCollection.getReleaseName().length() == 0) { if (commonPrefix >= firstPathParts.length) { // This should only happen if either // // (1) there is only one input file, or // (2) all of the input files have the same name // // In either case, make the release name the same // as the file part of the input file(s). commonPrefix = firstPathParts.length - 1; } origCollection.setReleaseName(firstPathParts[commonPrefix]); if (useAnalysisTimes) origCollection.setTimestamp(origCollection.getAnalysisTimestamp()); } for (BugInstance bug : origCollection.getCollection()) if (bug.getLastVersion() >= 0 && bug.getFirstVersion() > bug.getLastVersion()) throw new IllegalStateException("Illegal Version range: " + bug.getFirstVersion() + ".." + bug.getLastVersion()); discardUnwantedBugs(origCollection); while (argCount <= (args.length - 1)) { BugCollection newCollection = new SortedBugCollection(); String newFilename = args[argCount++]; if (verbose) System.out.println("Merging " + newFilename); try { File f = new File(newFilename); if (f.length() == 0) { if (verbose) System.out.println("Empty input file: " + f); continue; } newCollection.readXML(newFilename); if (commandLine.overrideRevisionNames || newCollection.getReleaseName() == null || newCollection.getReleaseName().length() == 0) newCollection.setReleaseName(getFilePathParts(newFilename)[commonPrefix]); if (useAnalysisTimes) newCollection.setTimestamp(newCollection.getAnalysisTimestamp()); discardUnwantedBugs(newCollection); origCollection = mergeCollections(origCollection, newCollection, true, false); } catch (IOException e) { IOException e2 = new IOException("Error parsing " + newFilename); e2.initCause(e); if (verbose) e2.printStackTrace(); throw e2; } catch (DocumentException e) { DocumentException e2 = new DocumentException("Error parsing " + newFilename); e2.initCause(e); if (verbose) e2.printStackTrace(); throw e2; } } if (false) for (Iterator<BugInstance> i = origCollection.iterator(); i.hasNext();) { if (!resurrected.contains(i.next().getInstanceKey())) i.remove(); } origCollection.setWithMessages(commandLine.withMessages); if (commandLine.outputFilename != null) { if (verbose) System.out.println("Writing " + commandLine.outputFilename); origCollection.writeXML(commandLine.outputFilename); } else origCollection.writeXML(System.out); } private static int lengthCommonPrefix(String[] string, String[] string2) { int maxLength = Math.min(string.length, string2.length); for (int result = 0; result < maxLength; result++) if (!string[result].equals(string2[result])) return result; return maxLength; } private static void mergeBugHistory(BugInstance older, BugInstance newer) { newer.setFirstVersion(older.getFirstVersion()); newer.setIntroducedByChangeOfExistingClass(older.isIntroducedByChangeOfExistingClass()); } enum MatchOldBugs { IF_LIVE, IF_CLASS_NOT_SEEN_UNTIL_NOW, ALWAYS; boolean match(BugInstance b) { switch (this) { case ALWAYS: return true; case IF_CLASS_NOT_SEEN_UNTIL_NOW: return !b.isDead() || b.isRemovedByChangeOfPersistingClass(); case IF_LIVE: return !b.isDead(); } throw new IllegalStateException(); } } private void matchBugs(Comparator<BugInstance> bugInstanceComparator, BugCollection origCollection, BugCollection newCollection) { matchBugs(bugInstanceComparator, origCollection, newCollection, MatchOldBugs.IF_LIVE); } private void matchBugs(Comparator<BugInstance> bugInstanceComparator, BugCollection origCollection, BugCollection newCollection, MatchOldBugs matchOld) { TreeMap<BugInstance, LinkedList<BugInstance>> set = new TreeMap<BugInstance, LinkedList<BugInstance>>( bugInstanceComparator); int oldBugs = 0; int newBugs = 0; int matchedBugs = 0; for (BugInstance bug : origCollection.getCollection()) if (!matchedOldBugs.containsKey(bug)) { if (matchOld.match(bug)) { oldBugs++; LinkedList<BugInstance> q = set.get(bug); if (q == null) { q = new LinkedList<BugInstance>(); set.put(bug, q); } q.add(bug); } } long newVersion = origCollection.getCurrentAppVersion().getSequenceNumber() + 1; for (BugInstance bug : newCollection.getCollection()) if (!mapFromNewToOldBug.containsKey(bug)) { newBugs++; LinkedList<BugInstance> q = set.get(bug); if (q == null) continue; for (Iterator<BugInstance> i = q.iterator(); i.hasNext();) { BugInstance matchedBug = i.next(); if (matchedBug.isDead()) { if (noResurrections || matchedBug.isRemovedByChangeOfPersistingClass() && newVersion - matchedBug.getLastVersion() > maxResurrection) continue; resurrected.add(bug.getInstanceKey()); // System.out.println("in version " + // newCollection.getReleaseName()); // System.out.println(" resurrected " + // bug.getMessageWithoutPrefix()); } matchedBugs++; mapFromNewToOldBug.put(bug, matchedBug); matchedOldBugs.put(matchedBug, null); i.remove(); if (q.isEmpty()) set.remove(bug); break; } } } }