All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.plugins.groovy.GroovySensor Maven / Gradle / Ivy

The newest version!
/*
 * Sonar Groovy Plugin
 * Copyright (C) 2010-2016 SonarSource SA
 * mailto:contact AT sonarsource DOT com
 *
 * 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  02110-1301, USA.
 */
package org.sonar.plugins.groovy;

import org.apache.commons.io.FileUtils;
import org.codehaus.groovy.antlr.GroovySourceToken;
import org.codehaus.groovy.antlr.parser.GroovyLexer;
import org.codehaus.groovy.antlr.parser.GroovyTokenTypes;
import org.gmetrics.result.MetricResult;
import org.gmetrics.result.MutableMapMetricResult;
import org.gmetrics.result.NumberMetricResult;
import org.gmetrics.result.SingleNumberMetricResult;
import org.gmetrics.resultsnode.ClassResultsNode;
import org.gmetrics.resultsnode.PackageResultsNode;
import org.gmetrics.resultsnode.ResultsNode;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputComponent;
import org.sonar.api.batch.fs.InputDir;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.measure.Metric;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.ce.measure.RangeDistributionBuilder;
import org.sonar.api.config.Settings;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.FileLinesContext;
import org.sonar.api.measures.FileLinesContextFactory;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.plugins.groovy.foundation.Groovy;
import org.sonar.plugins.groovy.foundation.GroovyFileSystem;
import org.sonar.plugins.groovy.foundation.GroovyHighlighterAndTokenizer;
import org.sonar.plugins.groovy.gmetrics.GMetricsSourceAnalyzer;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import groovyjarjarantlr.Token;
import groovyjarjarantlr.TokenStream;
import groovyjarjarantlr.TokenStreamException;

public class GroovySensor implements Sensor {
  private static final Logger LOG = Loggers.get(GroovySensor.class);

  private static final String CYCLOMATIC_COMPLEXITY_METRIC_NAME = "CyclomaticComplexity";
  private static final String EFFERENT_COUPLING_METRIC_NAME = "EfferentCoupling";
  private static final String AFFERENT_COUPLING_METRIC_NAME = "AfferentCoupling";

  private static final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = {1, 2, 4, 6, 8, 10, 12};
  private static final Number[] FILES_DISTRIB_BOTTOM_LIMITS = {0, 5, 10, 20, 30, 60, 90};

  private static final Set EMPTY_COMMENT_LINES = Arrays.stream(new String[] {"/**", "/*", "*", "*/", "//"}).collect(Collectors.toSet());

  private final Settings settings;
  private final FileLinesContextFactory fileLinesContextFactory;
  private final GroovyFileSystem groovyFileSystem;

  private int loc = 0;
  private int comments = 0;
  private int currentLine = 0;
  private FileLinesContext fileLinesContext;

  public GroovySensor(Settings settings, FileLinesContextFactory fileLinesContextFactory, FileSystem fileSystem) {
    this.settings = settings;
    this.fileLinesContextFactory = fileLinesContextFactory;
    this.groovyFileSystem = new GroovyFileSystem(fileSystem);
  }

  @Override
  public void describe(SensorDescriptor descriptor) {
    descriptor.onlyOnLanguage(Groovy.KEY).name(this.toString());
  }

  @Override
  public void execute(SensorContext context) {
    if (groovyFileSystem.hasGroovyFiles()) {
      List inputFiles = groovyFileSystem.sourceInputFiles();
      computeBaseMetrics(context, inputFiles);
      computeGroovyMetrics(context, inputFiles);
      highlightFiles(context, groovyFileSystem.groovyInputFiles());
    }
  }

  private static void computeGroovyMetrics(SensorContext context, List inputFiles) {
    GMetricsSourceAnalyzer metricsAnalyzer = new GMetricsSourceAnalyzer(context.fileSystem(), inputFiles);

    metricsAnalyzer.analyze();

    for (Entry> entry : metricsAnalyzer.resultsByFile().asMap().entrySet()) {
      processFile(context, entry.getKey(), entry.getValue());
    }

    for (Entry entry : metricsAnalyzer.resultsByPackage().entrySet()) {
      processPackage(context, entry.getKey(), entry.getValue().getMetricResults());
    }
  }

  private static void processFile(SensorContext context, InputFile sonarFile, Collection results) {
    int classes = 0;
    int methods = 0;
    int complexity = 0;
    int complexityInFunctions = 0;

    RangeDistributionBuilder functionsComplexityDistribution = new RangeDistributionBuilder(FUNCTIONS_DISTRIB_BOTTOM_LIMITS);

    for (ClassResultsNode result : results) {
      classes += 1;

      for (ResultsNode resultsNode : result.getChildren().values()) {
        methods += 1;
        Optional cyclomaticComplexity = getCyclomaticComplexity(resultsNode.getMetricResults());
        if (cyclomaticComplexity.isPresent()) {
          int value = (Integer) ((SingleNumberMetricResult) cyclomaticComplexity.get()).getNumber();
          functionsComplexityDistribution.add(value);
          complexityInFunctions += value;
        }
      }

      Optional cyclomaticComplexity = getCyclomaticComplexity(result.getMetricResults());
      if (cyclomaticComplexity.isPresent()) {
        int value = (Integer) ((NumberMetricResult) cyclomaticComplexity.get()).getValues().get("total");
        complexity += value;
      }
    }

    saveMetric(context, sonarFile, CoreMetrics.FILES, 1);
    saveMetric(context, sonarFile, CoreMetrics.CLASSES, classes);
    saveMetric(context, sonarFile, CoreMetrics.FUNCTIONS, methods);
    saveMetric(context, sonarFile, CoreMetrics.COMPLEXITY, complexity);
    saveMetric(context, sonarFile, CoreMetrics.COMPLEXITY_IN_CLASSES, complexity);
    saveMetric(context, sonarFile, CoreMetrics.COMPLEXITY_IN_FUNCTIONS, complexityInFunctions);
    saveMetric(context, sonarFile, CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION, functionsComplexityDistribution.build());

    RangeDistributionBuilder fileComplexityDistribution = new RangeDistributionBuilder(FILES_DISTRIB_BOTTOM_LIMITS);
    fileComplexityDistribution.add(complexity);
    saveMetric(context, sonarFile, CoreMetrics.FILE_COMPLEXITY_DISTRIBUTION, fileComplexityDistribution.build());
  }

  private static Optional getCyclomaticComplexity(List metricResults) {
    return metricResults
      .stream()
      .filter(metricResult -> CYCLOMATIC_COMPLEXITY_METRIC_NAME.equals(metricResult.getMetric().getName()))
      .findAny();
  }

  private static void processPackage(SensorContext context, InputDir inputDir, List metricResults) {
    for (MetricResult metricResult : metricResults) {
      org.gmetrics.metric.Metric metric = metricResult.getMetric();
      String metricName = metric.getName();
      if (EFFERENT_COUPLING_METRIC_NAME.equals(metricName)) {
        MutableMapMetricResult result = (MutableMapMetricResult) metricResult;
        saveMetric(context, inputDir, GroovyMetrics.EFFERENT_COUPLING_TOTAL, getTotalValue(result));
        saveMetric(context, inputDir, GroovyMetrics.EFFERENT_COUPLING_AVERAGE, getAverageValue(result));
      } else if (AFFERENT_COUPLING_METRIC_NAME.equals(metricName)) {
        MutableMapMetricResult result = (MutableMapMetricResult) metricResult;
        saveMetric(context, inputDir, GroovyMetrics.AFFERENT_COUPLING_TOTAL, getTotalValue(result));
        saveMetric(context, inputDir, GroovyMetrics.AFFERENT_COUPLING_AVERAGE, getAverageValue(result));
      }
    }
  }

  private static Integer getTotalValue(MutableMapMetricResult result) {
    return (Integer) result.getAt("total");
  }

  private static double getAverageValue(MutableMapMetricResult result) {
    Object avg = result.getAt("average");
    BigDecimal avgValue = (avg instanceof Integer) ? new BigDecimal((Integer) avg) : (BigDecimal) avg;
    return avgValue.doubleValue();
  }

  private void computeBaseMetrics(SensorContext context, List inputFiles) {
    for (InputFile groovyFile : inputFiles) {
      computeBaseMetrics(context, groovyFile);
    }
  }

  private void computeBaseMetrics(SensorContext context, InputFile groovyFile) {
    File file = groovyFile.file();
    if (file.exists()) {
      loc = 0;
      comments = 0;
      currentLine = 0;
      fileLinesContext = fileLinesContextFactory.createFor(groovyFile);
      Charset encoding = context.fileSystem().encoding();
      try (InputStreamReader streamReader = new InputStreamReader(new FileInputStream(file), encoding)) {
        List lines = FileUtils.readLines(file, encoding);
        GroovyLexer groovyLexer = new GroovyLexer(streamReader);
        groovyLexer.setWhitespaceIncluded(true);
        TokenStream tokenStream = groovyLexer.plumb();
        Token token = tokenStream.nextToken();
        Token nextToken = tokenStream.nextToken();
        while (nextToken.getType() != Token.EOF_TYPE) {
          handleToken(token, nextToken.getLine(), lines);
          token = nextToken;
          nextToken = tokenStream.nextToken();
        }
        handleToken(token, nextToken.getLine(), lines);
        saveMetric(context, groovyFile, CoreMetrics.LINES, nextToken.getLine());
        saveMetric(context, groovyFile, CoreMetrics.NCLOC, loc);
        saveMetric(context, groovyFile, CoreMetrics.COMMENT_LINES, comments);
      } catch (TokenStreamException e) {
        LOG.error("Unexpected token when lexing file : " + file.getName(), e);
      } catch (IOException e) {
        LOG.error("Unable to read file: " + file.getName(), e);
      }
      fileLinesContext.save();
    }
  }

  private static void highlightFiles(SensorContext context, List inputFiles) {
    for (InputFile inputFile : inputFiles) {
      new GroovyHighlighterAndTokenizer(inputFile).processFile(context);
    }
  }

  private static  void saveMetric(SensorContext context, InputComponent inputComponent, Metric metric, T value) {
    context.newMeasure()
      .withValue(value)
      .forMetric(metric)
      .on(inputComponent)
      .save();
  }

  private void handleToken(Token token, int nextTokenLine, List lines) {
    int tokenType = token.getType();
    int tokenLine = token.getLine();
    if (isComment(tokenType)) {
      if (isNotHeaderComment(tokenLine)) {
        comments += nextTokenLine - tokenLine + 1 - numberEmptyLines(token, lines);
      }
      for (int commentLineNb = tokenLine; commentLineNb <= nextTokenLine; commentLineNb++) {
        fileLinesContext.setIntValue(CoreMetrics.COMMENT_LINES_DATA_KEY, commentLineNb, 1);
      }
    } else if (isNotWhitespace(tokenType) && tokenLine != currentLine) {
      loc++;
      fileLinesContext.setIntValue(CoreMetrics.NCLOC_DATA_KEY, tokenLine, 1);
      currentLine = tokenLine;
    }
  }

  private int numberEmptyLines(Token token, List lines) {
    List relatedLines = getLinesFromToken(lines, (GroovySourceToken) token);
    long emptyLines = relatedLines.stream().map(String::trim).filter(EMPTY_COMMENT_LINES::contains).count();
    return (int) emptyLines;
  }

  private static List getLinesFromToken(List lines, GroovySourceToken gst) {
    List newLines = new ArrayList<>(lines.subList(gst.getLine() - 1, gst.getLineLast()));

    int lastLineIndex = newLines.size() - 1;
    String lastLine = newLines.get(lastLineIndex).substring(0, gst.getColumnLast() - 1);
    newLines.set(lastLineIndex, lastLine);

    String firstLine = newLines.get(0).substring(gst.getColumn() - 1);
    newLines.set(0, firstLine);

    return newLines;
  }

  private boolean isNotHeaderComment(int tokenLine) {
    return !(tokenLine == 1 && settings.getBoolean(GroovyPlugin.IGNORE_HEADER_COMMENTS));
  }

  private static boolean isNotWhitespace(int tokenType) {
    return !(tokenType == GroovyTokenTypes.WS ||
      tokenType == GroovyTokenTypes.STRING_NL ||
      tokenType == GroovyTokenTypes.ONE_NL || tokenType == GroovyTokenTypes.NLS);
  }

  private static boolean isComment(int tokenType) {
    return tokenType == GroovyTokenTypes.SL_COMMENT || tokenType == GroovyTokenTypes.SH_COMMENT || tokenType == GroovyTokenTypes.ML_COMMENT;
  }

  @Override
  public String toString() {
    return getClass().getSimpleName();
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy