/*
* Ada Sonar Plugin
* Copyright (C) 2010 Akram Ben Aissi
* 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 org.sonar.plugins.ada.gnat.metric;
import static org.sonar.api.measures.CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION;
import static org.sonar.api.measures.CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.CoreMetrics;
import org.sonar.api.measures.Metric;
import org.sonar.api.measures.PersistenceMode;
import org.sonar.api.measures.RangeDistributionBuilder;
import org.sonar.api.resources.Project;
import org.sonar.api.utils.SonarException;
import org.sonar.plugins.ada.Ada;
import org.sonar.plugins.ada.ResourcesBag;
import org.sonar.plugins.ada.core.AdaFile;
import org.sonar.plugins.ada.gnat.metric.xml.FileNode;
import org.sonar.plugins.ada.gnat.metric.xml.GlobalNode;
import org.sonar.plugins.ada.gnat.metric.xml.MetricNode;
import org.sonar.plugins.ada.gnat.metric.xml.UnitNode;
import org.sonar.plugins.ada.lexer.AdaSourceCode;
import org.sonar.plugins.ada.lexer.Node;
import org.sonar.plugins.ada.lexer.PageLexer;
import org.sonar.plugins.ada.lexer.PageLineCounter;
import org.sonar.plugins.ada.lexer.PageScanner;
/**
* @author Akram Ben Aissi
*/
public class GnatMetricSensor implements Sensor {
private static final String GNAT_METRIC_LSLOC = "lsloc";
private static final String GNAT_METRIC_GENERIC_PACKAGE = "generic package";
private static final String GNAT_METRIC_PACKAGE_BODY = "package body";
private static final String GNAT_METRIC_ALL_STMTS = "all_stmts";
private static final String GNAT_METRIC_COMMENT_PERCENTAGE = "comment_percentage";
private static final String GNAT_METRIC_EOL_COMMENTS = "eol_comments";
private static final String GNAT_METRIC_CYCLOMATIC_COMPLEXITY = "cyclomatic_complexity";
private static final String GNAT_METRIC_CODE_LINES = "code_lines";
private static final String GNAT_METRIC_COMMENT_LINES = "comment_lines";
private static final String GNAT_METRIC_ALL_LINES = "all_lines";
private static final Logger LOG = LoggerFactory.getLogger(GnatMetricSensor.class);
private final static Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = { 1, 2, 4, 6, 8, 10, 12 };
private final static Number[] CLASSES_DISTRIB_BOTTOM_LIMITS = { 0, 5, 10, 20, 30, 60, 90 };
private final static Map<String, Metric> METRICS_BY_TYPE_MAP = new HashMap<String, Metric>();
static {
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_ALL_LINES, CoreMetrics.LINES);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_CODE_LINES, CoreMetrics.NCLOC);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_COMMENT_LINES, CoreMetrics.COMMENT_LINES);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_COMMENT_PERCENTAGE, CoreMetrics.COMMENT_LINES_DENSITY);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_ALL_STMTS, CoreMetrics.STATEMENTS);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_CYCLOMATIC_COMPLEXITY, CoreMetrics.COMPLEXITY);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_PACKAGE_BODY, CoreMetrics.CLASSES);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_GENERIC_PACKAGE, CoreMetrics.CLASSES);
METRICS_BY_TYPE_MAP.put(GNAT_METRIC_LSLOC, CoreMetrics.NCLOC);
// METRICS_BY_TYPE_MAP.put("blank_lines", CoreMetrics.COMMENT_BLANK_LINES);
// METRICS_BY_TYPE_MAP.put(GNAT_METRIC_EOL_COMMENTS, CoreMetrics.COMMENT_LINES);
}
private GnatMetricExecutor executor;
private GnatMetricResultsParser parser;
private PageLexer lexer;
private Project project;
private PageScanner scanner;
private PageLineCounter pageLineCounter;
private ResourcesBag<AdaFile> resourcesBag;
private Set<Metric> metrics;
/**
* @param executor
* @param parser
*/
public GnatMetricSensor(Project project, GnatMetricExecutor executor, GnatMetricResultsParser parser, PageLexer lexer,
PageScanner scanner, PageLineCounter pageLineCounter) {
super();
this.project = project;
this.executor = executor;
this.parser = parser;
this.lexer = lexer;
this.scanner = scanner;
this.pageLineCounter = pageLineCounter;
resourcesBag = new ResourcesBag<AdaFile>();
metrics = getMetrics();
}
/**
* @see org.sonar.api.batch.Sensor#analyse(org.sonar.api.resources.Project, org.sonar.api.batch.SensorContext)
*/
public void analyse(Project project, SensorContext context) {
scan(project);
execute(project, context);
saveMeasures(context);
}
/**
* @param project
*/
private void scan(Project project) {
for (File file : project.getFileSystem().getSourceFiles(Ada.INSTANCE)) {
try {
AdaFile resource = AdaFile.fromIOFile(file, project.getFileSystem().getSourceDirs(), false);
List<Node> nodeList = lexer.parse(new FileReader(file));
AdaSourceCode sourceCode = new AdaSourceCode(resource);
scanner.scan(nodeList, sourceCode);
pageLineCounter.count(nodeList, sourceCode);
} catch (FileNotFoundException e) {
LOG.error("Cannot read project file " + file.getAbsolutePath(), e);
}
}
}
/**
* @param project
* @param context
*/
private void execute(Project project, SensorContext context) {
try {
executor.execute();
File reportFile = executor.getConfiguration().getReportFile();
GlobalNode node = parser.parse(reportFile);
for (FileNode file : node.getFiles()) {
AdaFile currentResourceFile = AdaFile.fromAbsolutePath(file.getName(), project);
collectFileMeasures(context, file, currentResourceFile);
}
} catch (SonarException e) {
LOG.error("Error occured while launching gnat metric sensor", e);
}
}
/**
* @return the metrics that we want to be saved by this sensor.
*/
private Set<Metric> getMetrics() {
Set<Metric> metrics = new HashSet<Metric>();
metrics.add(CoreMetrics.LINES);
metrics.add(CoreMetrics.NCLOC);
metrics.add(CoreMetrics.FUNCTIONS);
metrics.add(CoreMetrics.COMMENT_LINES);
metrics.add(CoreMetrics.COMPLEXITY);
// metrics.add(CoreMetrics.FILES);
// metrics.add(CoreMetrics.CLASSES);
return metrics;
}
/**
* Collect measures.
*
* @param reportFile
* the report xml
* @throws FileNotFoundException
* the file not found exception
* @throws ParseException
* the parse exception
*/
protected void collectMeasures(SensorContext context, File reportFile) throws FileNotFoundException, ParseException {
GlobalNode globalNode = parser.parse(reportFile);
for (FileNode fileNode : globalNode.getFiles()) {
String fileName = fileNode.getName();
AdaFile currentResourceFile = AdaFile.fromAbsolutePath(fileName, project);
if (currentResourceFile != null) {
collectFileMeasures(context, fileNode, currentResourceFile);
} else {
LOG.warn("The following file doesn't belong to current project sources or tests : " + fileName);
}
}
}
/**
* Saves all the measure contained in the resourceBag used for this analysis.
*
* @throws ParseException
*/
private void saveMeasures(SensorContext context) {
LOG.info("Saving measures...");
for (AdaFile resource : resourcesBag.getResources()) {
for (Metric metric : resourcesBag.getMetrics(resource)) {
if (metrics.contains(metric)) {
Double measure = resourcesBag.getMeasure(metric, resource);
saveMeasure(context, resource, metric, measure);
}
}
}
}
/**
*
* @see org.sonar.api.batch.CheckProject#shouldExecuteOnProject(org.sonar.api.resources.Project)
*/
public boolean shouldExecuteOnProject(Project project) {
return Ada.INSTANCE.equals(project.getLanguage());
}
/**
* Saves on measure in the context. One value is associated with a metric and a resource.
*
* @param resource
* Can be a AdaFile or a AdaDirectory
* @param metric
* the metric evaluated
* @param measure
* the corresponding value
*/
private void saveMeasure(SensorContext context, AdaFile resource, Metric metric, Double measure) {
if (LOG.isDebugEnabled()) {
LOG.debug("Saving " + metric.getName() + " for resource " + resource.getKey() + " with value " + measure);
}
context.saveMeasure(resource, metric, measure);
}
/** If the given value is not null, the metric, resource and value will be associated */
private void addMeasure(AdaFile file, Metric metric, Double value) {
if (value != null) {
resourcesBag.add(value, metric, file);
}
}
/**
* Collect the fiven php file measures and launches {@see #collectClassMeasures(ClassNode, AdaFile)} for all its descendant. Indeed even
* if it's not a good practice it isn't illegal to have more than one public class in one php file.
*
* @param file
* the php file
* @param fileNode
* the node representing the file in the report file.
*/
private void collectFileMeasures(SensorContext context, FileNode fileNode, AdaFile file) {
for (MetricNode metricNode : fileNode.getMetrics()) {
addMeasure(file, CoreMetrics.FILES, 1.0);
String metricName = metricNode.getName();
Metric metric = METRICS_BY_TYPE_MAP.get(metricName);
if (metric != null) {
addMeasureIfNecessary(file, metric, metricNode.getValue());
}
}
List<UnitNode> adaPackages = new ArrayList<UnitNode>();
List<UnitNode> adaFunctionsOrProcedures = new ArrayList<UnitNode>();
List<UnitNode> units = fileNode.getUnits();
extractUnits(adaPackages, adaFunctionsOrProcedures, units);
// for all class in this file
RangeDistributionBuilder ccd = new RangeDistributionBuilder(CLASS_COMPLEXITY_DISTRIBUTION, CLASSES_DISTRIB_BOTTOM_LIMITS);
RangeDistributionBuilder mcd = new RangeDistributionBuilder(FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTIONS_DISTRIB_BOTTOM_LIMITS);
for (UnitNode adaProcedures : adaFunctionsOrProcedures) {
collectFunctionsMeasures(adaProcedures, file, mcd, ccd);
}
for (UnitNode adaPackage : adaPackages) {
collectPackagesMeasures(adaPackage, file, ccd);
// ccd.add(adaPackage.getComplexity());
}
context.saveMeasure(file, ccd.build().setPersistenceMode(PersistenceMode.MEMORY));
context.saveMeasure(file, mcd.build().setPersistenceMode(PersistenceMode.MEMORY));
}
/**
* @param file
* @param adaPackages
* @param adaFunctionsOrProcedures
* @param units
*/
private void extractUnits(List<UnitNode> adaPackages, List<UnitNode> adaFunctionsOrProcedures, List<UnitNode> units) {
for (UnitNode unit : units) {
String kind = unit.getKind();
if (adaPackages != null && (GNAT_METRIC_PACKAGE_BODY.equals(kind) || "package".equals(kind))) {
adaPackages.add(unit);
}
if (adaFunctionsOrProcedures != null && ("procedure body".equals(kind) || "function body".equals(kind))) {
adaFunctionsOrProcedures.add(unit);
}
if (unit.getUnits() != null) {
extractUnits(adaPackages, adaFunctionsOrProcedures, unit.getUnits());
}
}
}
/**
* Collects the given function measures.
*
* @param ccd
*/
private void collectFunctionsMeasures(UnitNode unitNode, AdaFile file, RangeDistributionBuilder mcd, RangeDistributionBuilder ccd) {
Map<String, Double> metrics = getMetricsMap(unitNode);
addMeasureIfNecessary(file, CoreMetrics.LINES, metrics.get(GNAT_METRIC_ALL_LINES));
addMeasureIfNecessary(file, CoreMetrics.COMMENT_LINES, metrics.get(GNAT_METRIC_COMMENT_LINES) + metrics.get(GNAT_METRIC_EOL_COMMENTS));
addMeasureIfNecessary(file, CoreMetrics.NCLOC, metrics.get(GNAT_METRIC_CODE_LINES));
addMeasure(file, CoreMetrics.FUNCTIONS, 1.0);
Double cyclomaticComplexity = metrics.get(GNAT_METRIC_CYCLOMATIC_COMPLEXITY);
addMeasure(file, CoreMetrics.COMPLEXITY, cyclomaticComplexity);
mcd.add(cyclomaticComplexity);
}
/** */
private Map<String, Double> getMetricsMap(UnitNode unitNode) {
Map<String, Double> metricsMap = new HashMap<String, Double>();
for (MetricNode metric : unitNode.getMetrics()) {
metricsMap.put(metric.getName(), metric.getValue());
}
return metricsMap;
}
/**
* Adds the measure if the given metrics isn't already present on this resource.
*
* @param file
* @param metric
* @param value
*/
private void addMeasureIfNecessary(AdaFile file, Metric metric, double value) {
Double measure = resourcesBag.getMeasure(metric, file);
if (measure == null || measure == 0) {
resourcesBag.add(value, metric, file);
}
}
/**
* Collects the given class measures and launches {@see #collectFunctionMeasures(MethodNode, AdaFile)} for all its descendant.
*
* @param file
* the php related file
* @param packageNode
* representing the class in the report file
* @param ccd
*/
private void collectPackagesMeasures(UnitNode packageNode, AdaFile file, RangeDistributionBuilder ccd) {
Map<String, Double> metrics = getMetricsMap(packageNode);
addMeasure(file, CoreMetrics.CLASSES, 1.0);
addMeasureIfNecessary(file, CoreMetrics.LINES, metrics.get(GNAT_METRIC_ALL_LINES));
// addMeasureIfNecessary(file, CoreMetrics.COMMENT_LINES, classNode.getCommentLineNumber());
// addMeasureIfNecessary(file, CoreMetrics.NCLOC, classNode.getCodeLinesNumber());
// for all methods in this package
List<UnitNode> onePackage = new ArrayList<UnitNode>();
onePackage.add(packageNode);
List<UnitNode> functionsAndProcedures = new ArrayList<UnitNode>();
extractUnits(null, functionsAndProcedures, onePackage);
for (UnitNode functionOrProcedure : functionsAndProcedures) {
// collectMethodMeasures(methodNode, file);
Map<String, Double> functionMetrics = getMetricsMap(functionOrProcedure);
ccd.add(functionMetrics.get(GNAT_METRIC_CYCLOMATIC_COMPLEXITY));
}
}
}