com.revitasinc.sonar.dp.batch.DefectPredictionSensor.java Source code

Java tutorial

Introduction

Here is the source code for com.revitasinc.sonar.dp.batch.DefectPredictionSensor.java

Source

/*
 * Defect Prediction Plugin
 * Copyright (C) 2013 Revitas, Inc.
 * dev@sonar.codehaus.org
 *
 * This program 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 3 of the License, or (at your option) any later version.
 *
 * This program 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 program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
 */
package com.revitasinc.sonar.dp.batch;

import com.revitasinc.sonar.dp.DefectPredictionMetrics;
import com.revitasinc.sonar.dp.DefectPredictionPlugin;
import com.revitasinc.sonar.dp.batch.parser.ScmLogParserFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.Sensor;
import org.sonar.api.batch.SensorContext;
import org.sonar.api.measures.Measure;
import org.sonar.api.resources.Project;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

/**
 * Assigns a score to each source file in the project and collects scores for
 * those files most likely to contain more defects.
 *
 * @author John Amos (jamos@revitasinc.com)
 */
public class DefectPredictionSensor implements Sensor {
    private static final double TOP_SCORE = 100.0;

    private static Logger logger = LoggerFactory.getLogger(DefectPredictionSensor.class);

    public boolean shouldExecuteOnProject(Project project) {
        // This sensor is executed on all projects
        return true;
    }

    public void analyse(Project project, SensorContext sensorContext) {
        try {
            String command = (String) project.getProperty(DefectPredictionPlugin.COMMAND);
            if (StringUtils.isBlank(command)) {
                logger.error(
                        "Please specify an SCM Command in Configuration > General Settings > Defect Prediction");
            } else {
                saveScores(project,
                        ScmLogParserFactory.getParser(command).parse(new File(getSourcePath(project)), command),
                        sensorContext,
                        doubleFromProperty(project, DefectPredictionPlugin.AUTHOR_CHANGE_WEIGHT,
                                DefectPredictionPlugin.DEFAULT_AUTHOR_CHANGE_WEIGHT),
                        doubleFromProperty(project, DefectPredictionPlugin.LINES_CHANGED_WEIGHT,
                                DefectPredictionPlugin.DEFAULT_LINES_CHANGED_WEIGHT),
                        doubleFromProperty(project, DefectPredictionPlugin.TIME_DECAY_EXPONENT,
                                DefectPredictionPlugin.DEFAULT_TIME_DECAY_EXPONENT),
                        (String) project.getProperty(DefectPredictionPlugin.FILE_NAME_REGEX),
                        (String) project.getProperty(DefectPredictionPlugin.COMMENT_REGEX));
            }
        } catch (Exception e) {
            logger.error(e.getClass().getName() + " - " + e.getMessage(), e);
        }
    }

    @Override
    public String toString() {
        return "DefectPredictionSensor";
    }

    /**
     * Reads the revision data from the scm repository to calculate a score for
     * each file and saves a Measure for each of the top scoring files. Calculates
     * and saves the score for each file using the code from
     * https://github.com/igrigorik/bugspots/blob/master/lib/bugspots/scanner.rb
     * <p>
     * t = 1 - ((Time.now - fix.date).to_f / (Time.now - fixes.last.date)) <br>
     * hotspots[file] += 1/(1+Math.exp((-12*t)+12))
     *
     * @param project
     * @param map
     * @param sensorContext
     * @param authorChangeWeight
     * @param lineWeight
     * @param timeDecay
     */
    void saveScores(Project project, Map<String, List<RevisionInfo>> map, SensorContext sensorContext,
            double authorChangeWeight, double lineWeight, double timeDecay, String fileNameRegex,
            String commentRegex) {
        final long startTime = System.currentTimeMillis();
        Set<FileScore> set = new TreeSet<FileScore>();
        double projectDuration = startTime - getEarliestCommit(map);
        for (Entry<String, List<RevisionInfo>> entry : map.entrySet()) {
            if (!isFileIncluded(project, entry.getKey(), fileNameRegex)) {
                continue;
            }
            double score = 0.0;
            String lastAuthor = null;
            for (RevisionInfo revision : entry.getValue()) {
                if (isRevisionIncluded(revision, commentRegex)) {
                    double authorChange = (lastAuthor != null && !lastAuthor.equals(revision.getAuthor()))
                            ? authorChangeWeight
                            : 1.0;
                    double ti = 1.0 - ((startTime - revision.getDate().getTime()) / projectDuration);
                    score += authorChange * (lineWeight * revision.getAddedLineCount())
                            / (1 + Math.exp((-timeDecay * ti) + timeDecay));
                    lastAuthor = revision.getAuthor();
                }
            }
            set.add(new FileScore(entry.getKey(), score));
        }
        set = normalizeScores(set);
        ScoreHolder.buildScoreMap(set);
        buildSortedMeasure(set, sensorContext);
        writeToFile(set);
    }

    /**
     * Writes the supplied set to a file in the current working directory.
     * 
     * @param set
     */
    private void writeToFile(Set<FileScore> set) {
        try {
            FileUtils.writeLines(new File("defect-prediction.txt"), set);
        } catch (IOException e) {
            logger.error("", e);
        }
    }

    /**
     * @param project
     * @param fileName
     * @param regex
     * @return true if the supplied fileName is included in the results, false
     *         otherwise
     */
    private boolean isFileIncluded(Project project, String fileName, String regex) {
        boolean result = (StringUtils.isEmpty(regex) || fileName == null || fileName.matches(regex));
        if (result && fileName != null) {
            String[] exclusions = project.getExclusionPatterns();
            if (exclusions != null) {
                for (String exclusion : exclusions) {
                    if (fileName.matches(".*" + exclusion.replace("*", ".*"))) {
                        result = false;
                        break;
                    }
                }
            }
        }
        return result;
    }

    /**
     * @param revision
     * @param commentRegex
     * @return true if the supplied RevisionInfo should be included in the score
     */
    private boolean isRevisionIncluded(RevisionInfo revision, String commentRegex) {
        return revision.getAuthor() != null && (commentRegex == null || revision.getComment() == null
                || revision.getComment().matches(commentRegex));
    }

    /**
     * @param map
     * @return the earliest commit for all files in the supplied revision history
     *         (can be used as the project start date)
     */
    private long getEarliestCommit(Map<String, List<RevisionInfo>> map) {
        long result = System.currentTimeMillis();
        for (List<RevisionInfo> list : map.values()) {
            for (RevisionInfo rev : list) {
                if (rev.getDate().getTime() < result) {
                    result = rev.getDate().getTime();
                }
            }
        }
        return result;
    }

    /**
     * Normalizes the score for every file in the repository so that the top score
     * is TOP_SCORE.
     *
     * @param set
     * @return
     */
    private Set<FileScore> normalizeScores(Set<FileScore> set) {
        Set<FileScore> newSet = new TreeSet<FileScore>();
        if (set != null && !set.isEmpty()) {
            double topRawScore = set.iterator().next().getScore();
            for (FileScore score : set) {
                newSet.add(new FileScore(score.getPath(), (score.getScore() / topRawScore) * TOP_SCORE));
            }
        } else {
            logger.warn("There were no source files analyzed by " + getClass().getSimpleName()
                    + ".  You may need to change the Source Path in Configuration > General Settings > Defect Prediction.");
        }
        return newSet;
    }

    /**
     * @param project
     * @param key
     * @param defaultValue
     * @return a double value from the property with the supplied key
     */
    private double doubleFromProperty(Project project, String key, String defaultValue) {
        double result = Double.parseDouble(defaultValue);
        String property = (String) project.getProperty(key);
        if (!StringUtils.isBlank(property)) {
            try {
                result = Double.parseDouble(property);
            } catch (Exception e) {
                logger.warn("The property \"" + key
                        + "\" is not set to a valid number in Configuration > System > General Settings > "
                        + DefectPredictionPlugin.PLUGIN_NAME + ".  Using " + defaultValue + " instead.");
            }
        }
        return result;
    }

    /**
     * Saves one Measure for each of the top-scoring files.
     *
     * @param set
     * @param sensorContext
     */
    private void buildSortedMeasure(Set<FileScore> set, SensorContext sensorContext) {
        int i = 0;
        for (FileScore score : set) {
            if (i >= DefectPredictionMetrics.TOP_FILES_METRICS.size()) {
                break;
            }
            String value = String.format("%.2f", score.getScore()) + "#"
                    + score.getPath().substring(score.getPath().lastIndexOf('/') + 1);
            logger.debug("Top defects measure value: \"" + value + "\"");
            sensorContext.saveMeasure(new Measure(DefectPredictionMetrics.TOP_FILES_METRICS.get(i), value));
            i++;
        }
    }

    /**
     * @param project
     * @return the path to the source code for the supplied project
     */
    private String getSourcePath(Project project) {
        String result = project.getFileSystem().getBasedir().getPath();
        logger.info("Project basedir = \"" + result + "\"");
        if (result != null) {
            String subfolder = (String) project.getProperty(DefectPredictionPlugin.SOURCE_PATH);
            if (subfolder != null) {
                // absolute path
                if (subfolder.startsWith("/") || subfolder.substring(1).startsWith(":\\")) {
                    result = subfolder;
                } else {
                    result = new File(result, subfolder).getPath();
                }
            }
        }
        return result;
    }
}