diff --git a/.gitignore b/.gitignore index b9e6089..8be16ef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ target/ META-INF/ bin/ build.properties - +.idea/ +*.iml diff --git a/README.md b/README.md index 014e524..cf0db1b 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,7 @@ this will run all metrics. At the very least, Saikuro/Cane and Hotspots metrics * Code Violations ##Giving Credit -The github project [pica/ruby-sonar-plugin](https://github.com/pica/ruby-sonar-plugin), is where the ruby-sonar-plugin started, rather than reinvent the wheel, we thought it better to enhance it. -We used that plugin as a starting point for basic stats, then, updated the references to their latest versions and added additional metrics like line-by-line code coverage and code complexity. - -We referenced the [javascript sonar plugin](https://github.com/SonarCommunity/sonar-javascript) and the [php sonar plugin](https://github.com/SonarCommunity/sonar-php) for complexity and coverage implementation. -Our complexity sensor and code coverage sensor borrow heavily from the javascript plugin's equivalent sensors. +This is forked from GoDaddy-Hosting/ruby-sonar-plugin.git, enhanced it by adding sensors for design violations provides by roodi. Fixed bugs to make it compatible with different types of simplecov-rcov reports, populate complexity. ##Tool Versions This plugin has been tested with the following dependency versions diff --git a/pom.xml b/pom.xml index 8d368d6..dec4d4f 100755 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ UTF-8 - 4.5.2 + 5.0.1 1.6 0.7.4.201502262128 @@ -24,18 +24,37 @@ snakeyaml 1.16 - org.codehaus.sonar sonar-plugin-api ${sonar.buildVersion} + + org.apache.lucene + lucene-analyzers-common + 4.3.0 + + + org.apache.lucene + lucene-queryparser + 4.3.0 + + + org.apache.lucene + lucene-core + 4.3.0 + org.codehaus.sonar sonar-batch ${sonar.buildVersion} + + + com.googlecode.json-simple + json-simple + + - org.codehaus.sonar @@ -55,12 +74,6 @@ 3.3.1 test - - com.googlecode.json-simple - json-simple - 1.1.1 - - org.easytesting fest-assert @@ -83,7 +96,11 @@ commons-configuration 1.9 - + + com.googlecode.json-simple + json-simple + 1.1.1 + @@ -91,7 +108,7 @@ org.codehaus.sonar sonar-packaging-maven-plugin - 1.7 + 1.8 true com.godaddy.sonar.ruby.RubyPlugin @@ -201,7 +218,7 @@ - + f diff --git a/src/main/java/com/godaddy/sonar/ruby/RubyPlugin.java b/src/main/java/com/godaddy/sonar/ruby/RubyPlugin.java index 435bdd2..8809583 100755 --- a/src/main/java/com/godaddy/sonar/ruby/RubyPlugin.java +++ b/src/main/java/com/godaddy/sonar/ruby/RubyPlugin.java @@ -4,6 +4,10 @@ import java.util.Arrays; import java.util.List; +import com.godaddy.sonar.ruby.metricfu.MetricfuRoodiYamlParserImpl; +import com.godaddy.sonar.ruby.metricfu.rules.RoodiRuleParser; +import com.godaddy.sonar.ruby.metricfu.rules.RoodiSensor; +import com.godaddy.sonar.ruby.metricfu.rules.RubyRuleRepository; import org.sonar.api.CoreProperties; import org.sonar.api.Properties; import org.sonar.api.PropertyType; @@ -32,13 +36,14 @@ public final class RubyPlugin extends SonarPlugin public List getExtensions() { List extensions = new ArrayList(); - extensions.add(Ruby.class); - extensions.add(SimpleCovRcovSensor.class); - extensions.add(SimpleCovRcovJsonParserImpl.class); - extensions.add(MetricfuComplexityYamlParserImpl.class); - extensions.add(RubySourceCodeColorizer.class); - extensions.add(RubySensor.class); - extensions.add(MetricfuComplexitySensor.class); + + extensions.add(Ruby.class); + extensions.add(SimpleCovRcovSensor.class); + extensions.add(SimpleCovRcovJsonParserImpl.class); + extensions.add(MetricfuComplexityYamlParserImpl.class); + extensions.add(RubySourceCodeColorizer.class); + extensions.add(RubySensor.class); + extensions.add(MetricfuComplexitySensor.class); // Profiles extensions.add(SonarWayProfile.class); @@ -70,13 +75,20 @@ public List getExtensions() .subCategory("Ruby Coverage") .name("MetricFu Complexity Metric") .description("Type of complexity, Saikuro or Cane") - .defaultValue("Saikuro") + .defaultValue("Cane") .onQualifiers(Qualifiers.PROJECT) .type(PropertyType.SINGLE_SELECT_LIST) .options(options) .build(); extensions.add(ComplexityMetric); + extensions.add(RubyRuleRepository.class); + + extensions.add(MetricfuRoodiYamlParserImpl.class); + extensions.add(RoodiSensor.class); +// extensions.add(MetricfuDuplicationSensor.class); +// extensions.add(MetricfuDuplicationYamlParserImpl.class); + return extensions; } } diff --git a/src/main/java/com/godaddy/sonar/ruby/RubySensor.java b/src/main/java/com/godaddy/sonar/ruby/RubySensor.java index 02dd819..62128e5 100755 --- a/src/main/java/com/godaddy/sonar/ruby/RubySensor.java +++ b/src/main/java/com/godaddy/sonar/ruby/RubySensor.java @@ -12,7 +12,6 @@ import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.batch.fs.FilePredicate; -import org.sonar.api.batch.fs.FilePredicates; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.config.Settings; diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensor.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensor.java index 81c07d9..0d0cde7 100755 --- a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensor.java +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensor.java @@ -53,6 +53,7 @@ public boolean shouldExecuteOnProject(Project project) return fs.hasFiles(fs.predicates().hasLanguage("ruby")); } + @Override public void analyse(Project project, SensorContext context) { File report = pathResolver.relativeFile(fs.baseDir(), reportPath); @@ -100,7 +101,7 @@ private void analyzeFile(InputFile inputFile, SensorContext sensorContext, File fileComplexity += function.getComplexity(); LOG.info("File complexity " + fileComplexity); } - + sensorContext.saveMeasure(inputFile, CoreMetrics.COMPLEXITY, Double.valueOf(fileComplexity)); RangeDistributionBuilder fileDistribution = new RangeDistributionBuilder(CoreMetrics.FILE_COMPLEXITY_DISTRIBUTION, FILES_DISTRIB_BOTTOM_LIMITS); fileDistribution.add(Double.valueOf(fileComplexity)); @@ -115,6 +116,7 @@ private void analyzeFile(InputFile inputFile, SensorContext sensorContext, File { functionDistribution.add(Double.valueOf(function.getComplexity())); } + System.out.println("Analyzeds for : "+ inputFile); sensorContext.saveMeasure(inputFile, functionDistribution.build().setPersistenceMode(PersistenceMode.MEMORY)); } -} +} \ No newline at end of file diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexityYamlParserImpl.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexityYamlParserImpl.java index f012895..7742a9a 100755 --- a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexityYamlParserImpl.java +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexityYamlParserImpl.java @@ -19,6 +19,8 @@ public class MetricfuComplexityYamlParserImpl implements MetricfuComplexityYamlParser { + + private Map metricfuResult; private static final Logger LOG = LoggerFactory .getLogger(MetricfuComplexityYamlParser.class); @@ -44,10 +46,12 @@ public List parseFunctions(String fileNameFromModule, File results // } Yaml yaml = new Yaml(); - Map metricfuResult = new HashMap(); try { // metricfuResult = (Map) yaml.loadAs(fileString, Map.class); - metricfuResult = (Map) yaml.load(resultsStream); + if(metricfuResult == null) { + metricfuResult = new HashMap(); + metricfuResult = (Map) yaml.load(resultsStream); + } Map saikuroResult = (Map) metricfuResult.get(":saikuro"); Map caneResult = (Map) metricfuResult.get(":cane"); diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationSensor.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationSensor.java new file mode 100644 index 0000000..1c6ac27 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationSensor.java @@ -0,0 +1,76 @@ +package com.godaddy.sonar.ruby.metricfu; + +import com.godaddy.sonar.ruby.RubyPlugin; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.Sensor; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.duplication.DuplicationBuilder; +import org.sonar.api.config.Settings; +import org.sonar.api.resources.Project; +import org.sonar.api.scan.filesystem.PathResolver; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Created by akash.v on 22/04/16. + */ +public class MetricfuDuplicationSensor implements Sensor { + + private static final Logger LOG = LoggerFactory + .getLogger(MetricfuComplexitySensor.class); + + private MetricfuDuplicationYamlParserImpl metricfuDuplicationYamlParserImpl; + private Settings settings; + private FileSystem fs; + + private static final Number[] FILES_DISTRIB_BOTTOM_LIMITS = { 0, 5, 10, 20, 30, 60, 90 }; + private static final Number[] FUNCTIONS_DISTRIB_BOTTOM_LIMITS = { 1, 2, 4, 6, 8, 10, 12, 20, 30 }; + + private String reportPath = "tmp/metric_fu/report.yml"; + private PathResolver pathResolver; + + public MetricfuDuplicationSensor(Settings settings, FileSystem fs, + PathResolver pathResolver, + MetricfuDuplicationYamlParserImpl metricfuDuplicationYamlParserImpl) { + this.settings = settings; + this.fs = fs; + this.metricfuDuplicationYamlParserImpl = metricfuDuplicationYamlParserImpl; + this.pathResolver = pathResolver; + String reportpath_prop = settings.getString(RubyPlugin.METRICFU_REPORT_PATH_PROPERTY); + if (null != reportpath_prop) { + this.reportPath = reportpath_prop; + } + } + + @Override + public void analyse(Project project, SensorContext sensorContext) { + LOG.info("Analysing Duplications."); + File report = pathResolver.relativeFile(fs.baseDir(), reportPath); + LOG.info("Calling analyse for report results: " + report.getPath()); + if (!report.isFile()) { + LOG.warn("MetricFu report not found at {}", report); + return; + } + + List sourceFiles = Lists.newArrayList(fs.inputFiles(fs.predicates().hasLanguage("ruby"))); + + try { + metricfuDuplicationYamlParserImpl.parse(sourceFiles, report); + } catch (IOException e) { + LOG.error("Parsing duplications failed:"); + e.printStackTrace(); + } + } + + + @Override + public boolean shouldExecuteOnProject(Project project) { + return fs.hasFiles(fs.predicates().hasLanguage("ruby")); + } +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationYamlParser.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationYamlParser.java new file mode 100644 index 0000000..b2a29d2 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationYamlParser.java @@ -0,0 +1,17 @@ +package com.godaddy.sonar.ruby.metricfu; + +import org.sonar.api.BatchExtension; +import org.sonar.api.batch.fs.InputFile; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Created by akash.v on 25/04/16. + */ +public interface MetricfuDuplicationYamlParser extends BatchExtension { + + public void parse(List inputFiles, File resultsFile) throws IOException; + +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationYamlParserImpl.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationYamlParserImpl.java new file mode 100644 index 0000000..67027bd --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuDuplicationYamlParserImpl.java @@ -0,0 +1,77 @@ +package com.godaddy.sonar.ruby.metricfu; + +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.fs.InputFile; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by akash.v on 22/04/16. + */ +public class MetricfuDuplicationYamlParserImpl implements MetricfuDuplicationYamlParser { + + private static final Logger LOG = LoggerFactory + .getLogger(MetricfuComplexityYamlParser.class); + + @SuppressWarnings("unchecked") + @Override + public void parse(List inputFiles, File resultsFile) throws IOException + { + InputStream resultsStream = new FileInputStream(resultsFile); + + LOG.debug("MetricfuDuplicationYamlParserImpl: Start start parse of metrics_fu YAML"); + + Yaml yaml = new Yaml(); + Map> metricfuResult = new HashMap(); + try { + metricfuResult = (Map>) yaml.load(resultsStream); + + ArrayList> flayResult = metricfuResult.get(":flay").get(":matches"); + + analyzeFlay(inputFiles, flayResult); + + } catch (Exception e) { + LOG.error(Throwables.getStackTraceAsString(e)); + throw new IOException("Failure parsing YAML results", e); + } + + } + + private void analyzeFlay(List inputFiles, ArrayList> flayResult) { + Map indexedInputFiles = index(inputFiles); + for(Map duplicate : flayResult){ + if(((String)duplicate.get(":reason")).contains("IDENTICAL")){ + ArrayList> matches = (ArrayList>) duplicate.get(":matches"); +// DuplicationBuilder duplicationBuilder = new DefaultDuplicationBuilder(indexedInputFiles.get(matches.get(0).get(":name"))); + LOG.info("Identical Code Found: {}", matches ); + for(Map match : matches ){ +// duplicationBuilder.isDuplicatedBy(indexedInputFiles.get(match.get(":name")), Integer.parseInt(match.get(":line")), 1); + + } + } + } + + } + + private Map index(List inputFiles) { + Map indexedFiles= Maps.newConcurrentMap(); + + for(InputFile inputFile : inputFiles){ + indexedFiles.put(inputFile.relativePath(), inputFile); + } + + return indexedFiles; + } +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuRoodiYamlParser.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuRoodiYamlParser.java new file mode 100644 index 0000000..9647273 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuRoodiYamlParser.java @@ -0,0 +1,16 @@ +package com.godaddy.sonar.ruby.metricfu; + +import com.godaddy.sonar.ruby.metricfu.rules.RoodiProblem; +import org.sonar.api.BatchExtension; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.List; + +/** + * Created by akash.v on 27/04/16. + */ +public interface MetricfuRoodiYamlParser extends BatchExtension{ + + public List parse(String fileName, File resultsFile) throws FileNotFoundException; +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuRoodiYamlParserImpl.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuRoodiYamlParserImpl.java new file mode 100644 index 0000000..6956c9c --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/MetricfuRoodiYamlParserImpl.java @@ -0,0 +1,52 @@ +package com.godaddy.sonar.ruby.metricfu; + +import com.godaddy.sonar.ruby.metricfu.rules.RoodiProblem; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import java.io.*; +import java.util.List; +import java.util.Map; + +/** + * Created by akash.v on 27/04/16. + */ +public class MetricfuRoodiYamlParserImpl implements MetricfuRoodiYamlParser { + private static final Logger LOG = LoggerFactory + .getLogger(MetricfuRoodiYamlParser.class); + private static Map metricfuResult; + @Override + public List parse(String fileName, File resultsFile) throws FileNotFoundException { + + InputStream resultsStream = new FileInputStream(resultsFile); + LOG.debug("MetricfuRoodiYamlParserImpl Start start parse of metrics_fu YAML"); + List roodiProblems = Lists.newArrayList(); + Yaml yaml = new Yaml(); + + try { + if(metricfuResult == null) + metricfuResult = (Map) yaml.load(resultsStream); + Map> roodiResult = (Map>) metricfuResult.get(":roodi"); + + for(Map problemMap: (List >) roodiResult.get(":problems")){ + String file = problemMap.get(":file"); + if(fileName.equals(file)) { + roodiProblems.add(getProblem(problemMap)); + + } + } + + } catch (Exception e) { + LOG.error("Failure parsing YAML results {}", Throwables.getStackTraceAsString(e)); + } + + return roodiProblems; + } + + private RoodiProblem getProblem(Map problem) { + return new RoodiProblem(problem.get(":file"), Integer.parseInt(problem.get(":line")), problem.get(":problem")); + } +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/InMemoryRuleStore.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/InMemoryRuleStore.java new file mode 100644 index 0000000..aa309f4 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/InMemoryRuleStore.java @@ -0,0 +1,100 @@ +package com.godaddy.sonar.ruby.metricfu.rules; + + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.core.SimpleAnalyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.*; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.index.*; +import org.apache.lucene.queries.mlt.MoreLikeThis; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.*; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.LockObtainFailedException; +import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.util.Version; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + + +/** + * Created by akash.v on 06/05/16. + */ +public class InMemoryRuleStore { + + private Directory ramDirectory; + private StandardAnalyzer analyzer; + private IndexWriterConfig config; + private IndexWriter indexWriter; + private IndexReader reader; + + public InMemoryRuleStore(){ + analyzer = new StandardAnalyzer(Version.LUCENE_43); + config = new IndexWriterConfig(Version.LUCENE_43 , analyzer); + config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); + + ramDirectory = new RAMDirectory(); + + } + + public void addRule(String ruleId, String discription){ + try { + indexWriter = new IndexWriter(ramDirectory, config); + FieldType type = new FieldType(); + type.setIndexed(true); + type.setStored(true); + type.setStoreTermVectors(true); + Document doc = new Document(); + doc.add(new StringField("ruleId", ruleId, Store.YES)); + doc.add(new Field("description", discription, type)); + indexWriter.addDocument(doc); + indexWriter.commit(); + indexWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public String findRule(String searchForSimilar) { + try { + if(reader ==null){ + reader = DirectoryReader.open(ramDirectory); + } + IndexSearcher indexSearcher = new IndexSearcher(reader); + + MoreLikeThis mlt = new MoreLikeThis(reader); + mlt.setAnalyzer(analyzer); + mlt.setFieldNames(new String[]{"description"}); + mlt.setMinTermFreq(0); + mlt.setMinDocFreq(0); + + Reader sReader = new StringReader(searchForSimilar); + Query query = mlt.like(sReader, null); + TopDocs topDocs = indexSearcher.search(query, 1); + if(topDocs.scoreDocs.length > 0) + return indexSearcher.doc(topDocs.scoreDocs[0].doc).get("ruleId"); + + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + public void close() { + try { + reader.close(); + ramDirectory.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} + diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiProblem.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiProblem.java new file mode 100644 index 0000000..ed3e394 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiProblem.java @@ -0,0 +1,25 @@ +package com.godaddy.sonar.ruby.metricfu.rules; + +/** + * Created by akash.v on 27/04/16. + */ +public class RoodiProblem { + + public String file; + + public int line; + + public String problem; + + public RoodiProblem(String file, int line, String problem){ + this.file = file; + this.line = line; + this.problem = problem; + } + + + public int getLine() { + return line; + } + +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiRule.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiRule.java new file mode 100644 index 0000000..7092087 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiRule.java @@ -0,0 +1,13 @@ +package com.godaddy.sonar.ruby.metricfu.rules; + +/** + * Created by akash.v on 02/05/16. + */ +public class RoodiRule { + String key; + String name; + String description; + String severity; + String debtRemediationFunctionOffset; + String match; +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiRuleParser.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiRuleParser.java new file mode 100644 index 0000000..fb7c673 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiRuleParser.java @@ -0,0 +1,52 @@ +package com.godaddy.sonar.ruby.metricfu.rules; + +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import java.util.List; +import java.util.Map; + +/** + * Created by akash.v on 02/05/16. + */ +public class RoodiRuleParser { + + private static final String RULES_FILE = "rules.yml"; + private Yaml yaml; + private List roodiRules; + + private static final Logger LOG = LoggerFactory + .getLogger(RoodiRuleParser.class); + + public RoodiRuleParser(){ + yaml = new Yaml(); + parse(); + } + + public List parse(){ + + if(roodiRules == null) { + roodiRules = Lists.newArrayList(); + + for(Map p: (List>) yaml.load(getClass().getResourceAsStream(RULES_FILE))){ + roodiRules.add(ruleFor(p)); + } + } + + return roodiRules; + } + + private RoodiRule ruleFor(Map p) { + RoodiRule r = new RoodiRule(); + r.key = (String) p.get("key"); + r.description = (String) p.get("description"); + r.name = (String) p.get("name"); + r.match = (String) p.get("match"); + r.severity = (String) p.get("severity"); + r.debtRemediationFunctionOffset = (String) p.get("debtRemediationFunctionOffset"); + + return r; + } +} diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiSensor.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiSensor.java new file mode 100644 index 0000000..4e82ce1 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RoodiSensor.java @@ -0,0 +1,113 @@ +package com.godaddy.sonar.ruby.metricfu.rules; + +import com.godaddy.sonar.ruby.RubyPlugin; +import com.godaddy.sonar.ruby.metricfu.MetricfuRoodiYamlParser; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.Sensor; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.component.ResourcePerspectives; +import org.sonar.api.config.Settings; +import org.sonar.api.issue.Issuable; +import org.sonar.api.resources.Project; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.scan.filesystem.PathResolver; + +import java.io.File; +import java.io.FileNotFoundException; + +import java.util.List; + +/** + * Created by akash.v on 27/04/16. + */ +public class RoodiSensor implements Sensor { + + private static final Logger LOG = LoggerFactory + .getLogger(RoodiSensor.class); + private MetricfuRoodiYamlParser metricfuRoodiYamlParser; + private ActiveRules activeRules; + private ResourcePerspectives resourcePerspectives; + private Settings settings; + private FileSystem fs; + private String reportPath = "tmp/metric_fu/report.yml"; + private PathResolver pathResolver; + private RoodiRuleParser roodiRuleParser; + private InMemoryRuleStore inMemoryRuleStore; + + public RoodiSensor(Settings settings, FileSystem fs, ActiveRules activeRules, ResourcePerspectives resourcePerspectives, PathResolver pathResolver, MetricfuRoodiYamlParser metricfuRoodiYamlParser) { + + this.settings = settings; + this.fs = fs; + this.activeRules = activeRules; + this.resourcePerspectives = resourcePerspectives; + this.pathResolver = pathResolver; + String reportpath_prop = settings.getString(RubyPlugin.METRICFU_REPORT_PATH_PROPERTY); + this.metricfuRoodiYamlParser = metricfuRoodiYamlParser; + inMemoryRuleStore = new InMemoryRuleStore(); + if (null != reportpath_prop) { + this.reportPath = reportpath_prop; + } + this.roodiRuleParser = new RoodiRuleParser(); + for(RoodiRule roodiRule: roodiRuleParser.parse()){ + inMemoryRuleStore.addRule(roodiRule.key, roodiRule.match); + } + } + + + @Override + public void analyse(Project project, SensorContext sensorContext) { + File report = pathResolver.relativeFile(fs.baseDir(), reportPath); + LOG.info("Calling analyse for report results: " + report.getPath()); + if (!report.isFile()) { + LOG.warn("MetricFu report not found at {}", report); + return; + } + + List sourceFiles = Lists.newArrayList(fs.inputFiles(fs.predicates().hasLanguage("ruby"))); + + for (InputFile inputFile : sourceFiles) + { + LOG.debug("analyzing functions for Issues in the file: " + inputFile.file().getName()); + try + { + analyzeFile(inputFile, sensorContext, report); + } catch (Exception e) + { + LOG.error("Can not analyze the file " + inputFile.absolutePath() + " for issues", e); + } finally { + inMemoryRuleStore.close(); + } + } + } + + private void analyzeFile(InputFile inputFile, SensorContext sensorContext, File report) throws FileNotFoundException { + List issues= metricfuRoodiYamlParser.parse(inputFile.relativePath(), report); + for (RoodiProblem roodiProblem: issues) { + if (inMemoryRuleStore.findRule(roodiProblem.problem) != null) { + RuleKey ruleKey = RuleKey.of(RubyRuleRepository.REPOSITORY_KEY, inMemoryRuleStore.findRule(roodiProblem.problem)); + LOG.info("Rule Key: {}", ruleKey); + Issuable issuable = resourcePerspectives.as(Issuable.class, inputFile); + if (issuable != null) { + issuable.addIssue(issuable.newIssueBuilder() + .ruleKey(ruleKey) + .line(roodiProblem.getLine()) + .message(roodiProblem.problem) + .build()); + LOG.info("adding issue."); + } + } else { + LOG.warn("Ruby rule '{}' is unknown in Sonar", roodiProblem.problem); + } + } + } + + @Override + public boolean shouldExecuteOnProject(Project project) { + return fs.hasFiles(fs.predicates().hasLanguage("ruby")); + } +} \ No newline at end of file diff --git a/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RubyRuleRepository.java b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RubyRuleRepository.java new file mode 100644 index 0000000..fa9d007 --- /dev/null +++ b/src/main/java/com/godaddy/sonar/ruby/metricfu/rules/RubyRuleRepository.java @@ -0,0 +1,41 @@ +package com.godaddy.sonar.ruby.metricfu.rules; + +import com.godaddy.sonar.ruby.core.Ruby; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.server.rule.RulesDefinition; + +import javax.annotation.ParametersAreNonnullByDefault; + + +/** + * Created by akash.v on 27/04/16. + */ +public class RubyRuleRepository implements RulesDefinition { + + public static final String REPOSITORY_NAME = "Roodi"; + public static final String REPOSITORY_KEY = REPOSITORY_NAME; + RoodiRuleParser roodiRuleParser; + + public RubyRuleRepository(){ + this.roodiRuleParser = new RoodiRuleParser(); + } + + @ParametersAreNonnullByDefault + public void define(Context context) { + NewRepository repository = context + .createRepository(REPOSITORY_KEY, Ruby.KEY) + .setName(REPOSITORY_NAME); + + for(RoodiRule rule : roodiRuleParser.parse()){ + NewRule newRule = repository.createRule(rule.key) + .setName(rule.name) + .setHtmlDescription(rule.description) + .setStatus(RuleStatus.READY) + .setSeverity(rule.severity); + newRule.setDebtSubCharacteristic(SubCharacteristics.LOGIC_RELIABILITY) + .setDebtRemediationFunction(newRule.debtRemediationFunctions().constantPerIssue(rule.debtRemediationFunctionOffset)); + } + + repository.done(); + } +} diff --git a/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovJsonParserImpl.java b/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovJsonParserImpl.java index 16e198e..b87485c 100755 --- a/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovJsonParserImpl.java +++ b/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovJsonParserImpl.java @@ -2,18 +2,22 @@ import java.io.File; import java.io.IOException; + +import java.util.Collection; import java.util.Map; -import org.apache.commons.io.FileUtils; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.measures.CoverageMeasuresBuilder; -import com.godaddy.sonar.ruby.simplecovrcov.SimpleCovRcovJsonParser; import com.google.common.collect.Maps; +import org.sonar.api.measures.Measure; +import org.sonar.core.measure.db.MeasureDto; +import org.sonar.squid.api.SourceCode; public class SimpleCovRcovJsonParserImpl implements SimpleCovRcovJsonParser { @@ -26,11 +30,9 @@ public Map parse(File file) throws IOException File fileToFindCoverage = file; String fileString = FileUtils.readFileToString(fileToFindCoverage, "UTF-8"); - JSONObject resultJsonObject = (JSONObject) JSONValue.parse(fileString); - JSONObject coverageJsonObj = (JSONObject) ((JSONObject) resultJsonObject.get("RSpec")).get("coverage"); + JSONObject coverageJsonObj = (JSONObject) ((JSONObject) resultJsonObject.get(resultJsonObject.keySet().iterator().next())).get("coverage"); - // for each file in the coverage report for (int j = 0; j < coverageJsonObj.keySet().size(); j++) { CoverageMeasuresBuilder fileCoverage = CoverageMeasuresBuilder.create(); diff --git a/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovSensor.java b/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovSensor.java index d42ffe7..3f15717 100755 --- a/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovSensor.java +++ b/src/main/java/com/godaddy/sonar/ruby/simplecovrcov/SimpleCovRcovSensor.java @@ -55,7 +55,7 @@ public SimpleCovRcovSensor(Settings settings, FileSystem fs, this.reportPath = reportpath_prop; } } - + @Override public boolean shouldExecuteOnProject(Project project) { // return Ruby.KEY.equals(fs.languages()); @@ -63,6 +63,7 @@ public boolean shouldExecuteOnProject(Project project) return fs.hasFiles(fs.predicates().hasLanguage("ruby")); } + @Override public void analyse(Project project, SensorContext context) { File report = pathResolver.relativeFile(fs.baseDir(), reportPath); diff --git a/src/main/resources/com/godaddy/sonar/ruby/metricfu/rules/rules.yml b/src/main/resources/com/godaddy/sonar/ruby/metricfu/rules/rules.yml new file mode 100644 index 0000000..447857c --- /dev/null +++ b/src/main/resources/com/godaddy/sonar/ruby/metricfu/rules/rules.yml @@ -0,0 +1,66 @@ +--- +- + key: "R0001" + name: "Case statement is missing an else clause" + description: "Case statement is missing an else clause" + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 5min" + match: "Case statement is missing an else clause." +- + key: "R0002" + name: "Class should have 300 or less lines." + description: "Class should have 300 or less number of lines." + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 2h 5min" + match: "Class \"abc\" has 123 lines. It should have 300 or less" +- + key: "R0003" + name: "Block cyclomatic complexity should be 4 or less" + description: "Block cyclomatic complexity should be 4 or less." + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 20min" + match: "Block cyclomatic complexity is 123 It should be 4 or less." +- + key: "R0004" + name: "Method cyclomatic complexity should be 8 or less." + description: "Method cyclomatic complexity should be 8 or less." + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 20min" + match: "Method name \"abc\" cyclomatic complexity is 123 It should be 8 or less." +- + key: "R0005" + name: "Don't use 'for' loops. Use Enumerable.each instead." + description: "Don't use 'for' loops. Use Enumerable.each instead." + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 10min" + match: "Don't use 'for' loops. Use Enumerable.each instead." +- + key: "R0006" + name: "Method should have 20 or less lines." + description: "Method should have 20 or less lines." + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 12min" + match: "Method \"abc\" has 12 lines. It should have 20 or less." +- + key: "R0007" + name: "Method name should match pattern /^[_a-z<>=\\[\\]|+-\\/\\*`]+[_a-z0-9_<>=~@\\[\\]]*[=!\\?]?$/" + description: "Method name should match pattern /^[_a-z<>=\\[\\]|+-\\/\\*`]+[_a-z0-9_<>=~@\\[\\]]*[=!\\?]?$/" + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 5min" + match: "Method name \"abc\" should match pattern abc" +- + key: "R0008" + name: "Method should have 5 or less parameters." + description: "Method should have 5 or less parameters." + severity: "MINOR" + status: "READY" + debtRemediationFunctionOffset: "0d 0h 15min" + match: "Method name \"abc\" has 123 parameters. It should have 5 or less" + diff --git a/src/test/java/com/godaddy/sonar/ruby/RubySensorTest.java b/src/test/java/com/godaddy/sonar/ruby/RubySensorTest.java deleted file mode 100755 index 7aa6c4e..0000000 --- a/src/test/java/com/godaddy/sonar/ruby/RubySensorTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.godaddy.sonar.ruby; - -import static org.easymock.EasyMock.eq; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import org.easymock.EasyMock; -import org.easymock.IMocksControl; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.batch.fs.FilePredicate; -import org.sonar.api.batch.fs.FilePredicates; -import org.sonar.api.batch.fs.FileSystem; -import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.batch.fs.internal.DefaultInputFile; -import org.sonar.api.config.Settings; -import org.sonar.api.measures.Measure; -import org.sonar.api.measures.Metric; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.Resource; - -import com.godaddy.sonar.ruby.core.LanguageRuby; - -public class RubySensorTest { - public static String INPUT_SOURCE_DIR = "src/test/resources/test-data"; - public static String INPUT_SOURCE_FILE = "src/test/resources/test-data/hello_world.rb"; - - private IMocksControl mocksControl; - private SensorContext sensorContext; - private Project project; - private List sourceDirs; - private List files; - - private Settings settings; - private FileSystem fs; - private FilePredicates filePredicates; - private FilePredicate filePredicate; - - @Before - public void setUp() throws Exception { - mocksControl = EasyMock.createControl(); - fs = mocksControl.createMock(FileSystem.class); - filePredicates = mocksControl.createMock(FilePredicates.class); - filePredicate = mocksControl.createMock(FilePredicate.class); - - project = new Project("test project"); - settings = new Settings(); - project.setLanguage(LanguageRuby.INSTANCE); - - sensorContext = mocksControl.createMock(SensorContext.class); - - sourceDirs = new ArrayList(); - sourceDirs.add(new File(INPUT_SOURCE_DIR)); - files = new ArrayList(); - files.add(new File(INPUT_SOURCE_FILE)); - - } - - @After - public void tearDown() throws Exception { - } - - @Test - public void testRubySensor() { - RubySensor sensor = new RubySensor(settings, fs); - assertNotNull(sensor); - } - - @Test - public void testShouldExecuteOnProject() { - RubySensor sensor = new RubySensor(settings, fs); - - expect(fs.predicates()).andReturn(filePredicates).times(1); - expect(fs.hasFiles(isA(FilePredicate.class))).andReturn(true).times(1); - expect(filePredicates.hasLanguage(eq("ruby"))).andReturn(filePredicate).times(1); - mocksControl.replay(); - - sensor.shouldExecuteOnProject(project); - - mocksControl.verify(); - } - - @Test - public void testAnalyse() { - RubySensor sensor = new RubySensor(settings, fs); - - Measure measure = new Measure(); - List inputFiles = new ArrayList(); - File aFile = new File(INPUT_SOURCE_FILE); - DefaultInputFile difFile = new DefaultInputFile(aFile.getPath()); - difFile.setFile(aFile); - - inputFiles.add(difFile); - - expect(sensorContext.saveMeasure(isA(InputFile.class), isA(Metric.class), isA(Double.class))).andReturn(measure).times(4); - expect(sensorContext.saveMeasure(isA(Resource.class), isA(Metric.class), isA(Double.class))).andReturn(measure).times(1); - expect(fs.predicates()).andReturn(filePredicates).times(1); - expect(filePredicates.hasLanguage(eq("ruby"))).andReturn(filePredicate).times(1); - expect(fs.inputFiles(isA(FilePredicate.class))).andReturn((Iterable) inputFiles).times(1); - expect(fs.encoding()).andReturn(StandardCharsets.UTF_8).times(1); - - mocksControl.replay(); - - sensor.analyse(project, sensorContext); - mocksControl.verify(); - } - - @Test - public void testToString() { - RubySensor sensor = new RubySensor(settings, fs); - String result = sensor.toString(); - assertEquals("RubySensor", result); - } -} diff --git a/src/test/java/com/godaddy/sonar/ruby/core/RubyFileTest.java b/src/test/java/com/godaddy/sonar/ruby/core/RubyFileTest.java deleted file mode 100755 index d4ed81f..0000000 --- a/src/test/java/com/godaddy/sonar/ruby/core/RubyFileTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.godaddy.sonar.ruby.core; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.batch.fs.internal.DefaultInputFile; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.resources.Scopes; - -public class RubyFileTest { - protected final static String SOURCE_FILE = "/path/to/source/file.rb"; - - protected RubyFile rubyFile; - - @Before - public void setUp() { - File file = new File(SOURCE_FILE); - List sourceDirs = new ArrayList(); - -// fs.add(new DefaultInputFile(file.getPath()).setAbsolutePath(file.getAbsolutePath()).setType(Type.MAIN).setLanguage(Java.KEY)); - File aSrcDir = new File("/path/to/source"); -// sourceDirs.add(new DefaultInputFile("/path/to/source")); - sourceDirs.add(new DefaultInputFile(aSrcDir.getPath()).setAbsolutePath(file.getParent())); - - rubyFile = new RubyFile(file, sourceDirs); - } - - @After - public void tearDown() { - - } - - - - @Test(expected=IllegalArgumentException.class) - public void testRubyFileWithNullFile() { - new RubyFile(null, new ArrayList()); - } - - @Test - public void testRubyFileWithNullSourceDirs() { - File file = new File(SOURCE_FILE); - rubyFile = new RubyFile(file, null); - assertEquals("[default].file", rubyFile.getKey()); - } - - @Test - public void testGetParent() { - RubyPackage parent = rubyFile.getParent(); - assertEquals("source", parent.getKey()); - } - - @Test - public void testGetDescription() { - assertNull(rubyFile.getDescription()); - } - - @Test - public void testGetLanguage() { - assertEquals(Ruby.INSTANCE, rubyFile.getLanguage()); - } - - @Test - public void testGetName() { - assertEquals("file", rubyFile.getName()); - } - - @Test - public void testGetLongName() { - assertEquals("source.file", rubyFile.getLongName()); - } - - @Test - public void testGetScope() { - assertEquals(Scopes.FILE, rubyFile.getScope()); - } - - @Test - public void testGetQualifier() { - assertEquals(Qualifiers.CLASS, rubyFile.getQualifier()); - } - - @Test - public void testMatchFilePatternString() { - assertTrue(rubyFile.matchFilePattern("source.file.rb")); - } - -// @Test -// public void testToString() { -// System.out.println(rubyFile.toString()); -// assertTrue(rubyFile.toString().contains("key=source.file,package=source,longName=source.file")); -// } - -} diff --git a/src/test/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensorTest.java b/src/test/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensorTest.java deleted file mode 100755 index 9a2b122..0000000 --- a/src/test/java/com/godaddy/sonar/ruby/metricfu/MetricfuComplexitySensorTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.godaddy.sonar.ruby.metricfu; - -import static org.easymock.EasyMock.expect; -import static org.junit.Assert.assertNotNull; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.configuration.Configuration; -import org.easymock.EasyMock; - -import static org.easymock.EasyMock.isA; -import static org.easymock.EasyMock.eq; - -import org.easymock.IMocksControl; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.CoreProperties; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.batch.fs.FileSystem; -import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.config.Settings; -import org.sonar.api.measures.Measure; -import org.sonar.api.measures.Metric; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.Resource; -import org.sonar.api.scan.filesystem.FileQuery; -import org.sonar.api.scan.filesystem.ModuleFileSystem; -import org.sonar.api.scan.filesystem.PathResolver; - -import com.godaddy.sonar.ruby.RubySensor; -import com.godaddy.sonar.ruby.core.LanguageRuby; -import com.godaddy.sonar.ruby.core.RubyFile; - - -public class MetricfuComplexitySensorTest -{ - private IMocksControl mocksControl; - - private PathResolver pathResolver; - private SensorContext sensorContext; - private MetricfuComplexityYamlParser metricfuComplexityYamlParser; - private MetricfuComplexitySensor metricfuComplexitySensor; - private Configuration config; - private Project project; - - private Settings settings; - private FileSystem fs; - - @Before - public void setUp() throws Exception - { - mocksControl = EasyMock.createControl(); - pathResolver = mocksControl.createMock(PathResolver.class); - fs = mocksControl.createMock(FileSystem.class); - metricfuComplexityYamlParser = mocksControl.createMock(MetricfuComplexityYamlParser.class); - settings = new Settings(); - - metricfuComplexitySensor = new MetricfuComplexitySensor(settings, fs, pathResolver, metricfuComplexityYamlParser); - config = mocksControl.createMock(Configuration.class); - expect(config.getString("sonar.language", "java")).andStubReturn("ruby"); - - project = new Project("test project"); - project.setLanguage(LanguageRuby.INSTANCE); - project.setConfiguration(config); - - } - - @Test - public void testConstructor() - { - assertNotNull(metricfuComplexitySensor); - } - - @Test - public void testAnalyse() throws IOException - { - List sourceFiles= new ArrayList(); - List sourceDirs = new ArrayList(); - - sourceDirs.add(new File("lib")); - sourceFiles.add(new File("lib/some_path/foo_bar.rb")); - - sensorContext = mocksControl.createMock(SensorContext.class); - List functions = new ArrayList(); - functions.add(new RubyFunction("validate", 5, 10)); - - expect(fs.baseDir()).andReturn(new File("bar")); - expect(pathResolver.relativeFile(isA(File.class),isA(String.class))).andReturn(new File("foo")); - mocksControl.replay(); - - metricfuComplexitySensor.analyse(project, sensorContext); - mocksControl.verify(); - } -}