001: /*
002: * FindBugs - Find bugs in Java programs
003: * Copyright (C) 2003-2005 William Pugh
004: * This library is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 2.1 of the License, or (at your option) any later version.
008: *
009: * This library is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018: package edu.umd.cs.findbugs.workflow;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.util.Comparator;
023: import java.util.HashMap;
024: import java.util.HashSet;
025: import java.util.Iterator;
026: import java.util.LinkedList;
027: import java.util.TreeMap;
028:
029: import org.dom4j.DocumentException;
030:
031: import edu.umd.cs.findbugs.AppVersion;
032: import edu.umd.cs.findbugs.BugCollection;
033: import edu.umd.cs.findbugs.BugDesignation;
034: import edu.umd.cs.findbugs.BugInstance;
035: import edu.umd.cs.findbugs.ClassAnnotation;
036: import edu.umd.cs.findbugs.DetectorFactoryCollection;
037: import edu.umd.cs.findbugs.PackageStats;
038: import edu.umd.cs.findbugs.Project;
039: import edu.umd.cs.findbugs.SortedBugCollection;
040: import edu.umd.cs.findbugs.TigerSubstitutes;
041: import edu.umd.cs.findbugs.VersionInsensitiveBugComparator;
042: import edu.umd.cs.findbugs.PackageStats.ClassStats;
043: import edu.umd.cs.findbugs.config.CommandLine;
044: import edu.umd.cs.findbugs.model.MovedClassMap;
045:
046: /**
047: * Java main application to compute update a historical bug collection with
048: * results from another build/analysis.
049: *
050: * @author William Pugh
051: */
052:
053: public class Update {
054:
055: /**
056: *
057: */
058: private static final String USAGE = "Usage: "
059: + Update.class.getName()
060: + " [options] data1File data2File data3File ... ";
061:
062: private HashMap<BugInstance, BugInstance> mapFromNewToOldBug = new HashMap<BugInstance, BugInstance>();
063:
064: private HashSet<BugInstance> matchedOldBugs = new HashSet<BugInstance>();
065:
066: boolean noPackageMoves = false;
067:
068: boolean preciseMatch = false;
069: boolean precisePriorityMatch = false;
070: int mostRecent = -1;
071:
072: class UpdateCommandLine extends CommandLine {
073: boolean overrideRevisionNames = false;
074:
075: String outputFilename;
076: boolean withMessages = false;
077:
078: UpdateCommandLine() {
079: addSwitch("-overrideRevisionNames",
080: "override revision names for each version with names computed filenames");
081: addSwitch(
082: "-noPackageMoves",
083: "if a class seems to have moved from one package to another, treat warnings in that class as two seperate warnings");
084: addSwitch("-preciseMatch",
085: "require bug patterns to match precisely");
086: addSwitch("-precisePriorityMatch",
087: "only consider two warnings to be the same if their priorities match exactly");
088: addOption("-output", "output file",
089: "explicit filename for merged results (standard out used if not specified)");
090: addSwitch("-quiet",
091: "don't generate any outout to standard out unless there is an error");
092: addSwitch("-withMessages", "Add bug description");
093: addOption("-onlyMostRecent", "number",
094: "only use the last # input files");
095:
096: }
097:
098: @Override
099: protected void handleOption(String option,
100: String optionExtraPart) throws IOException {
101: if (option.equals("-overrideRevisionNames")) {
102: if (optionExtraPart.length() == 0)
103: overrideRevisionNames = true;
104: else
105: overrideRevisionNames = TigerSubstitutes
106: .parseBoolean(optionExtraPart);
107: } else if (option.equals("-noPackageMoves")) {
108: if (optionExtraPart.length() == 0)
109: noPackageMoves = true;
110: else
111: noPackageMoves = TigerSubstitutes
112: .parseBoolean(optionExtraPart);
113: } else if (option.equals("-preciseMatch")) {
114: preciseMatch = true;
115: } else if (option.equals("-precisePriorityMatch")) {
116: versionInsensitiveBugComparator
117: .setComparePriorities(true);
118: fuzzyBugPatternMatcher.setComparePriorities(true);
119: precisePriorityMatch = true;
120: } else if (option.equals("-quiet"))
121: verbose = false;
122: else if (option.equals("-withMessages"))
123: withMessages = true;
124: else
125: throw new IllegalArgumentException("no option "
126: + option);
127:
128: }
129:
130: @Override
131: protected void handleOptionWithArgument(String option,
132: String argument) throws IOException {
133: if (option.equals("-output"))
134: outputFilename = argument;
135: else if (option.equals("-onlyMostRecent")) {
136: mostRecent = Integer.parseInt(argument);
137: } else
138: throw new IllegalArgumentException(
139: "Can't handle option " + option);
140:
141: }
142:
143: }
144:
145: VersionInsensitiveBugComparator versionInsensitiveBugComparator = new VersionInsensitiveBugComparator();
146:
147: VersionInsensitiveBugComparator fuzzyBugPatternMatcher = new VersionInsensitiveBugComparator();
148: {
149: fuzzyBugPatternMatcher.setExactBugPatternMatch(false);
150: }
151:
152: HashSet<String> sourceFilesInCollection(BugCollection collection) {
153: HashSet<String> result = new HashSet<String>();
154: for (PackageStats pStats : collection.getProjectStats()
155: .getPackageStats()) {
156: for (ClassStats cStats : pStats.getClassStats()) {
157: result.add(cStats.getSourceFile());
158: }
159: }
160: return result;
161: }
162:
163: public void removeBaselineBugs(BugCollection baselineCollection,
164: BugCollection bugCollection) {
165:
166: matchBugs(baselineCollection, bugCollection);
167: matchBugs(SortedBugCollection.BugInstanceComparator.instance,
168: baselineCollection, bugCollection);
169: matchBugs(versionInsensitiveBugComparator, baselineCollection,
170: bugCollection);
171: for (Iterator<BugInstance> i = bugCollection.getCollection()
172: .iterator(); i.hasNext();) {
173: BugInstance bug = i.next();
174: if (matchedOldBugs.contains(bug))
175: i.remove();
176: }
177:
178: }
179:
180: public BugCollection mergeCollections(BugCollection origCollection,
181: BugCollection newCollection, boolean copyDeadBugs,
182: boolean incrementalAnalysis) {
183:
184: mapFromNewToOldBug.clear();
185:
186: matchedOldBugs.clear();
187: BugCollection resultCollection = newCollection
188: .createEmptyCollectionWithMetadata();
189: // Previous sequence number
190: long lastSequence = origCollection.getSequenceNumber();
191: // The AppVersion history is retained from the orig collection,
192: // adding an entry for the sequence/timestamp of the current state
193: // of the orig collection.
194: resultCollection.clearAppVersions();
195: for (Iterator<AppVersion> i = origCollection
196: .appVersionIterator(); i.hasNext();) {
197: AppVersion appVersion = i.next();
198: resultCollection.addAppVersion((AppVersion) appVersion
199: .clone());
200: }
201: AppVersion origCollectionVersion = origCollection
202: .getCurrentAppVersion();
203: AppVersion origCollectionVersionClone = new AppVersion(
204: lastSequence);
205: origCollectionVersionClone.setTimestamp(origCollectionVersion
206: .getTimestamp());
207: origCollectionVersionClone.setReleaseName(origCollectionVersion
208: .getReleaseName());
209: origCollectionVersionClone.setNumClasses(origCollection
210: .getProjectStats().getNumClasses());
211: origCollectionVersionClone.setCodeSize(origCollection
212: .getProjectStats().getCodeSize());
213:
214: resultCollection.addAppVersion(origCollectionVersionClone);
215:
216: // We assign a sequence number to the new collection as one greater than
217: // the original collection.
218: long currentSequence = origCollection.getSequenceNumber() + 1;
219: resultCollection.setSequenceNumber(currentSequence);
220:
221: int oldBugs = 0;
222: // move all inactive bugs
223: if (copyDeadBugs)
224: for (BugInstance bug : origCollection.getCollection())
225: if (bug.getLastVersion() != -1) {
226: oldBugs++;
227: BugInstance newBug = (BugInstance) bug.clone();
228: resultCollection.add(newBug, false);
229: }
230:
231: matchBugs(origCollection, newCollection);
232:
233: // matchBugs(new SloppyBugComparator(), origCollection, newCollection);
234:
235: int newlyDeadBugs = 0;
236: int persistantBugs = 0;
237: int addedBugs = 0;
238: int addedInNewCode = 0;
239: int deadBugInDeadCode = 0;
240:
241: HashSet<String> analyzedSourceFiles = sourceFilesInCollection(newCollection);
242: // Copy unmatched bugs
243: if (copyDeadBugs || incrementalAnalysis)
244: for (BugInstance bug : origCollection.getCollection())
245: if (!matchedOldBugs.contains(bug)
246: && bug.getLastVersion() == -1) {
247: newlyDeadBugs++;
248:
249: BugInstance newBug = (BugInstance) bug.clone();
250:
251: ClassAnnotation classBugFoundIn = bug
252: .getPrimaryClass();
253: String className = classBugFoundIn.getClassName();
254: String sourceFile = classBugFoundIn
255: .getSourceFileName();
256: boolean removed = sourceFile != null
257: && analyzedSourceFiles.contains(sourceFile)
258: || newCollection.getProjectStats()
259: .getClassStats(className) != null;
260: if (removed) {
261: if (!copyDeadBugs)
262: continue;
263: newBug
264: .setRemovedByChangeOfPersistingClass(true);
265: newBug.setLastVersion(lastSequence);
266: } else {
267: deadBugInDeadCode++;
268: if (!incrementalAnalysis)
269: newBug.setLastVersion(lastSequence);
270: }
271:
272: if (newBug.getLastVersion() != -1
273: && newBug.getFirstVersion() > newBug
274: .getLastVersion())
275: throw new IllegalStateException(
276: "Illegal Version range: "
277: + newBug.getFirstVersion()
278: + ".."
279: + newBug.getLastVersion());
280: resultCollection.add(newBug, false);
281: }
282: // Copy matched bugs
283: for (BugInstance bug : newCollection.getCollection()) {
284: BugInstance newBug = (BugInstance) bug.clone();
285: if (mapFromNewToOldBug.containsKey(bug)) {
286: BugInstance origWarning = mapFromNewToOldBug.get(bug);
287: assert origWarning.getLastVersion() == -1;
288:
289: copyBugHistory(origWarning, newBug);
290: // handle getAnnotationText()/setAnnotationText() and
291: // designation key
292: BugDesignation designation = newBug
293: .getUserDesignation();
294: if (designation != null)
295: designation.merge(origWarning.getUserDesignation());
296: else
297: newBug.setUserDesignation(origWarning
298: .getUserDesignation()); // clone??
299:
300: persistantBugs++;
301: } else {
302: newBug.setFirstVersion(lastSequence + 1);
303: addedBugs++;
304:
305: ClassAnnotation classBugFoundIn = bug.getPrimaryClass();
306:
307: String className = classBugFoundIn.getClassName();
308: if (origCollection.getProjectStats().getClassStats(
309: className) != null) {
310: newBug.setIntroducedByChangeOfExistingClass(true);
311: // System.out.println("added bug to existing code " +
312: // newBug.getUniqueId() + " : " + newBug.getAbbrev() + " in
313: // " + classBugFoundIn);
314: } else
315: addedInNewCode++;
316: }
317: assert newBug.getLastVersion() == -1;
318: if (newBug.getLastVersion() != -1)
319: throw new IllegalStateException(
320: "Illegal Version range: "
321: + newBug.getFirstVersion() + ".."
322: + newBug.getLastVersion());
323: int oldSize = resultCollection.getCollection().size();
324: resultCollection.add(newBug, false);
325: int newSize = resultCollection.getCollection().size();
326: if (newSize != oldSize + 1) {
327: System.out.println("Failed to add bug #"
328: + newBug.getUniqueId() + " : "
329: + newBug.getMessage());
330: }
331: }
332: if (false && verbose) {
333: System.out.println(origCollection.getCollection().size()
334: + " orig bugs, "
335: + newCollection.getCollection().size()
336: + " new bugs");
337: System.out.println("Bugs: " + oldBugs + " old, "
338: + deadBugInDeadCode + " in removed code, "
339: + (newlyDeadBugs - deadBugInDeadCode) + " died, "
340: + persistantBugs + " persist, " + addedInNewCode
341: + " in new code, " + (addedBugs - addedInNewCode)
342: + " added");
343: System.out.println(resultCollection.getCollection().size()
344: + " resulting bugs");
345: }
346: return resultCollection;
347:
348: }
349:
350: /**
351: * @param origCollection
352: * @param newCollection
353: */
354: private void matchBugs(BugCollection origCollection,
355: BugCollection newCollection) {
356: matchBugs(SortedBugCollection.BugInstanceComparator.instance,
357: origCollection, newCollection);
358:
359: mapFromNewToOldBug.clear();
360: matchedOldBugs.clear();
361:
362: matchBugs(versionInsensitiveBugComparator, origCollection,
363: newCollection);
364: if (!preciseMatch) {
365: matchBugs(fuzzyBugPatternMatcher, origCollection,
366: newCollection);
367: }
368: if (!noPackageMoves) {
369: VersionInsensitiveBugComparator movedBugComparator = new VersionInsensitiveBugComparator();
370: MovedClassMap movedClassMap = new MovedClassMap(
371: origCollection, newCollection).execute();
372: if (!movedClassMap.isEmpty()) {
373: movedBugComparator.setClassNameRewriter(movedClassMap);
374: movedBugComparator
375: .setComparePriorities(precisePriorityMatch);
376: matchBugs(movedBugComparator, origCollection,
377: newCollection);
378: if (!preciseMatch) {
379: movedBugComparator.setExactBugPatternMatch(false);
380: matchBugs(movedBugComparator, origCollection,
381: newCollection);
382: }
383: }
384: }
385: }
386:
387: boolean verbose = true;
388:
389: public static String[] getFilePathParts(String filePath) {
390: String regex = (File.separatorChar == '\\' ? "\\\\"
391: : File.separator);
392: return filePath.split(regex);
393: }
394:
395: public static void main(String[] args) throws IOException,
396: DocumentException {
397: new Update().doit(args);
398: }
399:
400: public void doit(String[] args) throws IOException,
401: DocumentException {
402:
403: DetectorFactoryCollection.instance();
404: UpdateCommandLine commandLine = new UpdateCommandLine();
405: int argCount = commandLine.parse(args, 1, Integer.MAX_VALUE,
406: USAGE);
407:
408: if (commandLine.outputFilename == null)
409: verbose = false;
410: String[] firstPathParts = getFilePathParts(args[argCount]);
411: int commonPrefix = firstPathParts.length;
412: for (int i = argCount + 1; i <= (args.length - 1); i++) {
413:
414: commonPrefix = Math.min(commonPrefix, lengthCommonPrefix(
415: firstPathParts, getFilePathParts(args[i])));
416: }
417:
418: if (mostRecent > 0) {
419: argCount = Math.max(argCount, args.length - mostRecent);
420: }
421: String origFilename = args[argCount++];
422: Project project = new Project();
423: BugCollection origCollection;
424: origCollection = new SortedBugCollection();
425: if (verbose)
426: System.out.println("Starting with " + origFilename);
427:
428: origCollection.readXML(origFilename, project);
429:
430: if (commandLine.overrideRevisionNames
431: || origCollection.getReleaseName() == null
432: || origCollection.getReleaseName().length() == 0) {
433:
434: if (commonPrefix >= firstPathParts.length) {
435: // This should only happen if either
436: //
437: // (1) there is only one input file, or
438: // (2) all of the input files have the same name
439: //
440: // In either case, make the release name the same
441: // as the file part of the input file(s).
442: commonPrefix = firstPathParts.length - 1;
443: }
444:
445: origCollection.setReleaseName(firstPathParts[commonPrefix]);
446: }
447:
448: for (BugInstance bug : origCollection.getCollection())
449: if (bug.getLastVersion() >= 0
450: && bug.getFirstVersion() > bug.getLastVersion())
451: throw new IllegalStateException(
452: "Illegal Version range: "
453: + bug.getFirstVersion() + ".."
454: + bug.getLastVersion());
455:
456: while (argCount <= (args.length - 1)) {
457:
458: BugCollection newCollection = new SortedBugCollection();
459:
460: String newFilename = args[argCount++];
461: if (verbose)
462: System.out.println("Merging " + newFilename);
463: project = new Project();
464: try {
465: File f = new File(newFilename);
466: if (f.length() == 0) {
467: if (verbose)
468: System.out.println("Empty input file: " + f);
469: continue;
470: }
471: newCollection.readXML(newFilename, project);
472:
473: if (commandLine.overrideRevisionNames
474: || newCollection.getReleaseName() == null
475: || newCollection.getReleaseName().length() == 0)
476: newCollection
477: .setReleaseName(getFilePathParts(newFilename)[commonPrefix]);
478:
479: origCollection = mergeCollections(origCollection,
480: newCollection, true, false);
481: } catch (IOException e) {
482: if (verbose)
483: System.out.println(e);
484: else
485: throw e;
486: }
487: }
488:
489: origCollection.setWithMessages(commandLine.withMessages);
490: if (commandLine.outputFilename != null)
491: origCollection
492: .writeXML(commandLine.outputFilename, project);
493: else
494: origCollection.writeXML(System.out, project);
495:
496: }
497:
498: private static int lengthCommonPrefix(String[] string,
499: String[] string2) {
500: int maxLength = Math.min(string.length, string2.length);
501: for (int result = 0; result < maxLength; result++)
502: if (!string[result].equals(string2[result]))
503: return result;
504: return maxLength;
505: }
506:
507: private static void copyBugHistory(BugInstance src, BugInstance dest) {
508:
509: dest.setFirstVersion(src.getFirstVersion());
510: dest.setLastVersion(src.getLastVersion());
511: dest.setIntroducedByChangeOfExistingClass(src
512: .isIntroducedByChangeOfExistingClass());
513: dest.setRemovedByChangeOfPersistingClass(src
514: .isRemovedByChangeOfPersistingClass());
515: }
516:
517: private void matchBugs(
518: Comparator<BugInstance> bugInstanceComparator,
519: BugCollection origCollection, BugCollection newCollection) {
520:
521: TreeMap<BugInstance, LinkedList<BugInstance>> set = new TreeMap<BugInstance, LinkedList<BugInstance>>(
522: bugInstanceComparator);
523: int oldBugs = 0;
524: int newBugs = 0;
525: int matchedBugs = 0;
526: for (BugInstance bug : origCollection.getCollection())
527: if (bug.getLastVersion() == -1
528: && !matchedOldBugs.contains(bug)) {
529: oldBugs++;
530: LinkedList<BugInstance> q = set.get(bug);
531: if (q == null) {
532: q = new LinkedList<BugInstance>();
533: set.put(bug, q);
534: }
535: q.add(bug);
536: }
537: for (BugInstance bug : newCollection.getCollection())
538: if (!mapFromNewToOldBug.containsKey(bug)) {
539: newBugs++;
540: LinkedList<BugInstance> q = set.get(bug);
541: if (q != null && !q.isEmpty()) {
542: matchedBugs++;
543: BugInstance matchedBug = q.removeFirst();
544: mapFromNewToOldBug.put(bug, matchedBug);
545: matchedOldBugs.add(matchedBug);
546: }
547: }
548: if (false && verbose)
549: System.out.println("matched " + matchedBugs + " of "
550: + oldBugs + "o/" + newBugs + "n bugs using "
551: + bugInstanceComparator.getClass().getName());
552: }
553:
554: }
|