/* * Sonar PHP Plugin * Copyright (C) 2010 Sonar PHP Plugin * 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.php.phpdepend; import static org.sonar.api.measures.CoreMetrics.CLASSES; import static org.sonar.api.measures.CoreMetrics.COMMENT_LINES; import static org.sonar.api.measures.CoreMetrics.COMPLEXITY; import static org.sonar.api.measures.CoreMetrics.FILES; import static org.sonar.api.measures.CoreMetrics.FUNCTIONS; import static org.sonar.api.measures.CoreMetrics.LINES; import static org.sonar.api.measures.CoreMetrics.NCLOC; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.BatchExtension; 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.resources.ResourceUtils; import org.sonar.api.utils.SonarException; import org.sonar.plugins.php.api.PhpFile; import org.sonar.plugins.php.phpdepend.xml.ClassNode; import org.sonar.plugins.php.phpdepend.xml.FileNode; import org.sonar.plugins.php.phpdepend.xml.FunctionNode; import org.sonar.plugins.php.phpdepend.xml.MethodNode; import org.sonar.plugins.php.phpdepend.xml.MetricsNode; import org.sonar.plugins.php.phpdepend.xml.PackageNode; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; /** * The PhpDependResultsParser par pdepend reports files and associate measures with metrics and resources. */ public class PhpDependResultsParser implements BatchExtension { private static final Logger LOG = LoggerFactory.getLogger(PhpDependResultsParser.class); private static final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = { 1, 2, 4, 6, 8, 10, 12 }; private static final Number[] CLASSES_DISTRIB_BOTTOM_LIMITS = { 0, 5, 10, 20, 30, 60, 90 }; /** * The context. */ private SensorContext context; /** * The metrics. */ private Set<Metric> metrics; /** * The project. */ private Project project; /** * Resources bag to store metrics and their values. */ private ResourcesBag resourcesBag; private boolean measureUnitTests = true; /** * Instantiates a new php depend results parser. * * @param config * the config * @param context * the context */ public PhpDependResultsParser(Project project, SensorContext context) { this.project = project; this.context = context; this.resourcesBag = new ResourcesBag(); this.metrics = getMetrics(); } /** * Returns true if unit tests are counted when measuring complexity and LOC. * * @return true if unit tests are counted when measuring complexity and LOC. */ public boolean isMeasureUnitTests() { return measureUnitTests; } /** * Sets whether unit tests are counted when measuring complexity and LOC. * * @param measureUnitTests * true if unit tests are counted when measuring complexity and LOC. */ public void setMeasureUnitTests(boolean measureUnitTests) { this.measureUnitTests = measureUnitTests; } /** * If the given value is not null, the metric, resource and value will be associated * * @param file * the file * @param metric * the metric * @param value * the value */ private void addMeasure(PhpFile file, Metric metric, Double value) { if (value != null) { resourcesBag.add(value, metric, file); } } /** * Adds the measure if the given metrics isn't already present on this resource. * * @param file * @param metric * @param value */ private void addMeasureIfNecessary(PhpFile 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, PhpFile)} for all its descendant. * * @param file * the php related file * @param classNode * representing the class in the report file * @param methodComplexityDistribution */ private void collectClassMeasures(ClassNode classNode, PhpFile file, RangeDistributionBuilder methodComplexityDistribution) { addMeasureIfNecessary(file, LINES, classNode.getLinesNumber()); addMeasureIfNecessary(file, COMMENT_LINES, classNode.getCommentLineNumber()); addMeasureIfNecessary(file, NCLOC, classNode.getCodeLinesNumber()); // Adds one class to this file addMeasure(file, CLASSES, 1.0); // for all methods in this class. List<MethodNode> methodes = classNode.getMethodes(); if (methodes != null && !methodes.isEmpty()) { for (MethodNode methodNode : methodes) { collectMethodMeasures(methodNode, file); methodComplexityDistribution.add(methodNode.getComplexity()); } } } /** * Collects the given function measures. * * @param file * the php related file * @param functionNode * representing the class in the report file * @param methodComplexityDistribution */ private void collectFunctionsMeasures(FunctionNode functionNode, PhpFile file, RangeDistributionBuilder methodComplexityDistribution) { addMeasureIfNecessary(file, LINES, functionNode.getLinesNumber()); addMeasureIfNecessary(file, COMMENT_LINES, functionNode.getCommentLineNumber()); addMeasureIfNecessary(file, NCLOC, functionNode.getCodeLinesNumber()); // Adds one class to this file addMeasure(file, FUNCTIONS, 1.0); addMeasure(file, COMPLEXITY, functionNode.getComplexity()); methodComplexityDistribution.add(functionNode.getComplexity()); } /** * Collect the fiven php file measures and launches {@see #collectClassMeasures(ClassNode, PhpFile)} 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(FileNode fileNode, PhpFile file) { addMeasure(file, LINES, fileNode.getLinesNumber()); addMeasure(file, CoreMetrics.NCLOC, fileNode.getCodeLinesNumber()); addMeasure(file, CoreMetrics.COMMENT_LINES, fileNode.getCommentLineNumber()); // Adds one file to this php file addMeasure(file, CoreMetrics.FILES, 1.0); // for all class in this file RangeDistributionBuilder classComplexityDistribution = new RangeDistributionBuilder(CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION, CLASSES_DISTRIB_BOTTOM_LIMITS); RangeDistributionBuilder methodComplexityDistribution = new RangeDistributionBuilder(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION, FUNCTIONS_DISTRIB_BOTTOM_LIMITS); if (fileNode.getClasses() != null) { for (ClassNode classNode : fileNode.getClasses()) { collectClassMeasures(classNode, file, methodComplexityDistribution); classComplexityDistribution.add(classNode.getComplexity()); }// for all class in this file } if (fileNode.getFunctions() != null) { for (FunctionNode funcNode : fileNode.getFunctions()) { collectFunctionsMeasures(funcNode, file, methodComplexityDistribution); } } // String fileName = fileNode.getFileName(); context.saveMeasure(file, classComplexityDistribution.build().setPersistenceMode(PersistenceMode.MEMORY)); context.saveMeasure(file, methodComplexityDistribution.build().setPersistenceMode(PersistenceMode.MEMORY)); } /** * Collect function measures. * * @param file * the file * @param methodNode * the method node */ private void collectMethodMeasures(MethodNode methodNode, PhpFile file) { // Adds one method to this file addMeasure(file, CoreMetrics.FUNCTIONS, 1.0); addMeasure(file, CoreMetrics.COMPLEXITY, methodNode.getComplexity()); } /** * Collect measures. * * @param reportXml * the report xml * @throws FileNotFoundException * the file not found exception * @throws ParseException * the parse exception */ protected void collectMeasures(File reportXml) throws FileNotFoundException, ParseException { MetricsNode metricsNode = getMetrics(reportXml); List<FileNode> files = metricsNode.getFiles(); for (FileNode fileNode : files) { String fileName = fileNode.getFileName(); PhpFile currentResourceFile = PhpFile.getInstance(project).fromAbsolutePath(fileName, project); if (currentResourceFile != null) { if (measureUnitTests || !ResourceUtils.isUnitTestClass(currentResourceFile)) { collectFileMeasures(fileNode, currentResourceFile); } } else { LOG.warn("The following file doesn't belong to current project sources or tests : " + fileName); } } saveMeasures(); } /** * Gets the metrics. * * @return the metrics */ private Set<Metric> getMetrics() { Set<Metric> metricsNode = new HashSet<Metric>(); metricsNode.add(LINES); metricsNode.add(NCLOC); metricsNode.add(FUNCTIONS); metricsNode.add(COMMENT_LINES); metricsNode.add(FILES); metricsNode.add(COMPLEXITY); metricsNode.add(CLASSES); return metricsNode; } /** * Gets the metrics. * * @param report * the report * @return the metrics */ private MetricsNode getMetrics(File report) { InputStream inputStream = null; try { XStream xstream = new XStream(); // Migration Sonar 2.2 xstream.setClassLoader(getClass().getClassLoader()); xstream.processAnnotations(MetricsNode.class); xstream.processAnnotations(PackageNode.class); xstream.processAnnotations(FileNode.class); xstream.processAnnotations(ClassNode.class); xstream.processAnnotations(FunctionNode.class); xstream.processAnnotations(MethodNode.class); inputStream = new FileInputStream(report); return (MetricsNode) xstream.fromXML(inputStream); } catch (XStreamException e) { throw new SonarException("PDepend report isn't valid: " + report.getName(), e); } catch (IOException e) { throw new SonarException("Can't read report : " + report.getName(), e); } finally { IOUtils.closeQuietly(inputStream); } } /** * Parses the pdepend report file. */ public void parse(File reportXml) { // If no files can be found, plugin will stop normally only logging the // error if ( !reportXml.exists()) { LOG.error("Result file not found : " + reportXml.getAbsolutePath() + ". Plugin will stop"); return; } try { LOG.info("Collecting measures..."); collectMeasures(reportXml); } catch (Exception e) { LOG.error("Report file is invalid or can't be found, plugin will stop.", e); throw new SonarException(e); } } /** * Saves on measure in the context. One value is associated with a metric and a resource. * * @param resource * Can be a PhpFile or a PhpPackage * @param metric * the metric evaluated * @param measure * the corresponding value */ private void saveMeasure(PhpFile 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); } /** * Saves all the measure contained in the resourceBag used for this analysis. * * @throws ParseException */ private void saveMeasures() { LOG.info("Saving measures..."); for (PhpFile resource : resourcesBag.getResources()) { for (Metric metric : resourcesBag.getMetrics(resource)) { if (metrics.contains(metric)) { Double measure = resourcesBag.getMeasure(metric, resource); saveMeasure(resource, metric, measure); } } } } }