From 7d1c24822fc9181607b22ec3c15fb72728bcdf73 Mon Sep 17 00:00:00 2001 From: Simo Kutlin Date: Thu, 19 Oct 2017 15:10:15 +0200 Subject: [PATCH] Adding comment lines and comment density metrics. --- .../sonar/CombinedCoverageSensor.java | 36 +++-- .../com/pablissimo/sonar/CommentSensor.java | 14 ++ .../pablissimo/sonar/CommentSensorImpl.java | 141 ++++++++++++++++++ .../pablissimo/sonar/TypeScriptPlugin.java | 1 + .../sonar/CombinedCoverageSensorTest.java | 47 +++--- .../sonar/CommentSensorImplTest.java | 72 +++++++++ src/test/resources/loc/blockcomments1.txt | 4 +- src/test/resources/loc/blockcomments2.txt | 3 +- src/test/resources/loc/blockcomments3.txt | 7 +- src/test/resources/loc/linecomments.txt | 8 +- 10 files changed, 295 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/pablissimo/sonar/CommentSensor.java create mode 100644 src/main/java/com/pablissimo/sonar/CommentSensorImpl.java create mode 100644 src/test/java/com/pablissimo/sonar/CommentSensorImplTest.java diff --git a/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java b/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java index 6fd1fcf..3000fa6 100644 --- a/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java +++ b/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java @@ -1,25 +1,28 @@ package com.pablissimo.sonar; -import java.util.Map; -import java.util.Set; - import org.sonar.api.batch.fs.InputFile; 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.measures.CoreMetrics; + +import java.util.Map; +import java.util.Set; public class CombinedCoverageSensor implements Sensor { private LOCSensor locSensor; + private CommentSensor commentSensor; private TsCoverageSensor coverageSensor; - - public CombinedCoverageSensor(LOCSensor locSensor, TsCoverageSensor coverageSensor) { + + public CombinedCoverageSensor(LOCSensor locSensor, CommentSensor commentSensor, TsCoverageSensor coverageSensor) { this.locSensor = locSensor; + this.commentSensor = commentSensor; this.coverageSensor = coverageSensor; } - + @Override public void describe(SensorDescriptor descriptor) { - descriptor.name("Combined LCOV and LOC sensor"); + descriptor.name("Combined LCOV, LOC and comments"); descriptor.onlyOnLanguage(TypeScriptLanguage.LANGUAGE_KEY); } @@ -28,9 +31,24 @@ public void execute(SensorContext context) { // First - LOC everything up, as we'll need LOC for uncovered lines metrics Map> nonCommentLinesByFile = this.locSensor.execute(context); - // Now the LCOV pass can properly handle files that don't appear in + // Now the LCOV pass can properly handle files that don't appear in // configuration and set lines-to-cover values as required this.coverageSensor.execute(context, nonCommentLinesByFile); + + // Last but not least - get those comments and compute comment density + Map> commentedLinesByFile = this.commentSensor.execute(context); + + for (InputFile inputFile : nonCommentLinesByFile.keySet()) { + Double commentDensity = 0.0; + Double commentLines = new Double(commentedLinesByFile.get(inputFile).size()); + Double nonCommentLines = new Double(nonCommentLinesByFile.get(inputFile).size()); + if (commentedLinesByFile.containsKey(inputFile)) { + commentDensity = commentLines / (commentLines + nonCommentLines) * 100; + } + context.newMeasure().forMetric(CoreMetrics.COMMENT_LINES_DENSITY).on(inputFile).withValue(commentDensity).save(); + + } + } - + } diff --git a/src/main/java/com/pablissimo/sonar/CommentSensor.java b/src/main/java/com/pablissimo/sonar/CommentSensor.java new file mode 100644 index 0000000..c283bdb --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/CommentSensor.java @@ -0,0 +1,14 @@ +package com.pablissimo.sonar; + +import java.util.Map; +import java.util.Set; + +import org.sonar.api.batch.BatchSide; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; + +@BatchSide +@FunctionalInterface +public interface CommentSensor { + Map> execute(SensorContext ctx); +} diff --git a/src/main/java/com/pablissimo/sonar/CommentSensorImpl.java b/src/main/java/com/pablissimo/sonar/CommentSensorImpl.java new file mode 100644 index 0000000..808f5dd --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/CommentSensorImpl.java @@ -0,0 +1,141 @@ +package com.pablissimo.sonar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.measures.CoreMetrics; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class CommentSensorImpl implements CommentSensor { + private static final Logger LOG = LoggerFactory.getLogger(CommentSensorImpl.class); + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + private BufferedReader getReaderFromFile(InputFile inputFile) throws FileNotFoundException { + return new BufferedReader(new FileReader(inputFile.file())); + } + + private Set getCommentLineNumbers(InputFile inputFile) { + HashSet toReturn = new HashSet<>(); + + int lineNumber = 0; + + BufferedReader br; + try { + br = this.getReaderFromFile(inputFile); + + boolean isEOF; + boolean isCommentOpen = false; + boolean isACommentLine; + do { + String line = br.readLine(); + lineNumber++; + + if (line != null) { + isACommentLine = isCommentOpen; + line = line.trim(); + + if (isCommentOpen) { + if (line.contains("*/")) { + isCommentOpen = false; + isACommentLine = false; + } else if (line.equals("*")) { + isACommentLine = false; + } + } else { + if (line.startsWith("/*")) { + if (line.contains("*/")) { + isCommentOpen = false; + } else { + isCommentOpen = true; + } + if (line.equals("/*") || line.endsWith("/*")) { + isACommentLine = false; + } else { + isACommentLine = true; + } + } else if (line.startsWith("*")) { + if (line.equals("*")) { + isACommentLine = false; + } else { + isACommentLine = true; + } + } else if (line.startsWith("*/")) { + isCommentOpen = false; + isACommentLine = false; + } else if (line.contains("//") && !line.equals("//")) { + isACommentLine = true; + } else if (line.contains("/*")) { + if (line.contains("*/")) { + isCommentOpen = false; + + if (line.substring(line.indexOf("/*") + 1, line.indexOf("*/") - 1).trim().length() > 0) { + isACommentLine = true; + } + } else { + isCommentOpen = true; + } + } + + +// if (line.contains("/*")) { +// if (line.contains("*/")) { +// isCommentOpen = false; +// } else { +// isCommentOpen = true; +// } +// isACommentLine = true; +// } + } + isEOF = true; + line = line.replaceAll("\\n|\\t|\\s", ""); + if (isACommentLine) { + toReturn.add(lineNumber); + } + } else { + isEOF = false; + } + } while (isEOF); + + br.close(); + + } catch (FileNotFoundException e) { + LOG.error("File not found", e); + } catch (IOException e) { + LOG.error("Error while reading BufferedReader", e); + } + + return toReturn; + } + + @Override + public Map> execute(SensorContext ctx) { + HashMap> toReturn = new HashMap<>(); + + Iterable affectedFiles = + ctx + .fileSystem() + .inputFiles(ctx.fileSystem().predicates().hasLanguage(TypeScriptLanguage.LANGUAGE_KEY)); + + for (InputFile inputFile : affectedFiles) { + Set commentLineNumbers = this.getCommentLineNumbers(inputFile); + toReturn.put(inputFile, commentLineNumbers); + + ctx.newMeasure().forMetric(CoreMetrics.COMMENT_LINES).on(inputFile).withValue(commentLineNumbers.size()).save(); + } + + return toReturn; + } +} diff --git a/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java b/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java index f70f53a..081501f 100644 --- a/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java +++ b/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java @@ -193,6 +193,7 @@ public void define(Context ctx) { ctx.addExtension(TsLintExecutorImpl.class); ctx.addExtension(TsLintParserImpl.class); ctx.addExtension(LOCSensorImpl.class); + ctx.addExtension(CommentSensorImpl.class); ctx.addExtension(TsCoverageSensorImpl.class); } } diff --git a/src/test/java/com/pablissimo/sonar/CombinedCoverageSensorTest.java b/src/test/java/com/pablissimo/sonar/CombinedCoverageSensorTest.java index deaedfd..2283129 100644 --- a/src/test/java/com/pablissimo/sonar/CombinedCoverageSensorTest.java +++ b/src/test/java/com/pablissimo/sonar/CombinedCoverageSensorTest.java @@ -16,54 +16,63 @@ public class CombinedCoverageSensorTest { LOCSensor locSensor; + CommentSensor commentSensor; TsCoverageSensor coverageSensor; - + SensorContextTester sensorContext; - + CombinedCoverageSensor sensor; - + @Before public void setUp() throws Exception { this.sensorContext = SensorContextTester.create(new File("")); this.locSensor = mock(LOCSensor.class); this.coverageSensor = mock(TsCoverageSensor.class); - - this.sensor = new CombinedCoverageSensor(this.locSensor, this.coverageSensor); + this.commentSensor = mock(CommentSensor.class); + + this.sensor = new CombinedCoverageSensor(this.locSensor, this.commentSensor, this.coverageSensor); } - + @Test public void describe_setsName() { DefaultSensorDescriptor desc = new DefaultSensorDescriptor(); - + this.sensor.describe(desc); - - assertEquals(desc.name(), "Combined LCOV and LOC sensor"); + + assertEquals(desc.name(), "Combined LCOV, LOC and comments"); } - + @Test public void describe_setsLanguage() { DefaultSensorDescriptor desc = new DefaultSensorDescriptor(); - + this.sensor.describe(desc); - + assertEquals(TypeScriptLanguage.LANGUAGE_KEY, desc.languages().iterator().next()); } - + @Test public void execute_callsLocSensor() { this.sensor.execute(this.sensorContext); - + verify(this.locSensor, times(1)).execute(eq(this.sensorContext)); } - + + @Test + public void execute_callsCommentSensor() { + this.sensor.execute(this.sensorContext); + + verify(this.commentSensor, times(1)).execute(eq(this.sensorContext)); + } + @Test public void execute_callsCoverageSensorWithLocSensorOutput() { Map> locOutput = new HashMap>(); - + when(this.locSensor.execute(eq(this.sensorContext))).thenReturn(locOutput); - + this.sensor.execute(this.sensorContext); - - verify(this.coverageSensor, times(1)).execute(eq(this.sensorContext), eq(locOutput)); + + verify(this.coverageSensor, times(1)).execute(eq(this.sensorContext), eq(locOutput)); } } diff --git a/src/test/java/com/pablissimo/sonar/CommentSensorImplTest.java b/src/test/java/com/pablissimo/sonar/CommentSensorImplTest.java new file mode 100644 index 0000000..175ecd6 --- /dev/null +++ b/src/test/java/com/pablissimo/sonar/CommentSensorImplTest.java @@ -0,0 +1,72 @@ +package com.pablissimo.sonar; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.FileNotFoundException; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.measures.CoreMetrics; + +public class CommentSensorImplTest { + CommentSensor sensor; + + SensorContextTester sensorContext; + + @Before + public void setUp() throws Exception { + this.sensorContext = SensorContextTester.create(new File("")); + this.sensor = new CommentSensorImpl(); + } + + @Test + public void toStringReturnsClassName() { + assertEquals("CommentSensorImpl", new CommentSensorImpl().toString()); + } + + @Test + public void basicBlockCommentsDiscounted() throws FileNotFoundException { + assertLineCount("blockcomments1", 1); + } + + @Test + public void blockCommentsNotConfusedWithNestedComments() throws FileNotFoundException { + assertLineCount("blockcomments2", 6); + } + + @Test + public void linesEndingWithABlockCommentStillCounted() throws FileNotFoundException { + assertLineCount("blockcomments3", 2); + } + + @Test + public void oneLineBlockCommentsDoNotConfuseCounting() throws FileNotFoundException { + assertLineCount("blockcomments4", 1); + } + + @Test + public void oneLineBlockCommentAtEndOfRealLineShouldNotConsiderNextLinesAsComments() throws FileNotFoundException { + assertLineCount("blockcomments5", 1); + } + + @Test + public void lineLevelCommentsAndWhitespaceHandledCorrectly() throws FileNotFoundException { + assertLineCount("linecomments", 5); + } + + private DefaultInputFile resource(String relativePath) { + return new DefaultInputFile("", relativePath).setLanguage(TypeScriptLanguage.LANGUAGE_KEY); + } + + private void assertLineCount(String testName, Integer expected) throws FileNotFoundException { + DefaultInputFile resource = resource("src/test/resources/loc/" + testName + ".txt"); + this.sensorContext.fileSystem().add(resource); + + this.sensor.execute(this.sensorContext); + + assertEquals(expected, this.sensorContext.measure(resource.key(), CoreMetrics.COMMENT_LINES).value()); + } +} diff --git a/src/test/resources/loc/blockcomments1.txt b/src/test/resources/loc/blockcomments1.txt index c4b7a0b..920986f 100644 --- a/src/test/resources/loc/blockcomments1.txt +++ b/src/test/resources/loc/blockcomments1.txt @@ -1,5 +1,5 @@ /* - Block comments shouldn't count towards line count - expected 2 lines + Block comments should count towards line count - expect 2 code and 1 comment lines */ module Whatever { -} \ No newline at end of file +} diff --git a/src/test/resources/loc/blockcomments2.txt b/src/test/resources/loc/blockcomments2.txt index 6a625ed..4777531 100644 --- a/src/test/resources/loc/blockcomments2.txt +++ b/src/test/resources/loc/blockcomments2.txt @@ -4,6 +4,7 @@ // like this nor by unexpected extra comment block openings /* like this + should expect six comment lines */ module Whatever { - } \ No newline at end of file + } diff --git a/src/test/resources/loc/blockcomments3.txt b/src/test/resources/loc/blockcomments3.txt index 115c16b..4e52c9b 100644 --- a/src/test/resources/loc/blockcomments3.txt +++ b/src/test/resources/loc/blockcomments3.txt @@ -1,4 +1,5 @@ var x = 1; /* -this block comment started somewhere unusual but shouldn't -confuse things any - expecting one line -*/ \ No newline at end of file +* this block comment started somewhere unusual but shouldn't confuse things neither +* +* than a blank comment line - expecting one code line and two comment lines +*/ diff --git a/src/test/resources/loc/linecomments.txt b/src/test/resources/loc/linecomments.txt index cd29e15..6c03be0 100644 --- a/src/test/resources/loc/linecomments.txt +++ b/src/test/resources/loc/linecomments.txt @@ -1,8 +1,8 @@ -// This line shouldn't be counted +// This line shouldn't be counted as code // Nor should this one // Nor this -var x = "this one should, though."; // but not get confused by this +var x = "this one should"; // both as code and comment -// Whitespace above shouldn't count -x += "but this should"; +// Whitespace above shouldn't count neither as code nor as comment +x += "but this should be counted as code again";