From 1c2ac3a5b6239a30a8b18df987869066fe95fa23 Mon Sep 17 00:00:00 2001 From: Pablissimo Date: Sun, 30 Oct 2016 14:13:55 +0000 Subject: [PATCH] SonarQube 6.* support, and fixing various minor Linux issues --- .travis.yml | 11 +- README.md | 28 +- pom.xml | 48 ++- .../sonar/CombinedCoverageSensor.java | 37 ++ .../java/com/pablissimo/sonar/LCOVParser.java | 7 +- .../com/pablissimo/sonar/LCOVParserImpl.java | 320 +++++++++++------- .../java/com/pablissimo/sonar/LOCSensor.java | 115 +------ .../com/pablissimo/sonar/LOCSensorImpl.java | 114 +++++++ .../com/pablissimo/sonar/PathResolver.java | 7 + .../pablissimo/sonar/PathResolverImpl.java | 50 +++ .../pablissimo/sonar/TsCoverageSensor.java | 168 +-------- .../sonar/TsCoverageSensorImpl.java | 146 ++++++++ .../pablissimo/sonar/TsLintExecutorImpl.java | 29 +- .../com/pablissimo/sonar/TsLintSensor.java | 164 ++++----- .../pablissimo/sonar/TsRulesDefinition.java | 35 +- .../pablissimo/sonar/TypeScriptPlugin.java | 23 +- .../sonar/TypeScriptRuleProfile.java | 7 +- .../pablissimo/sonar/model/TsLintRule.java | 8 +- .../pablissimo/sonar/LOCSensorImplTest.java | 73 ++++ .../com/pablissimo/sonar/LOCSensorTest.java | 139 -------- .../sonar/TsCoverageSensorImplTest.java | 139 ++++++++ .../sonar/TsCoverageSensorTest.java | 180 ---------- .../sonar/TsLintExecutorImplTest.java | 11 +- .../pablissimo/sonar/TsLintSensorTest.java | 313 ++++++----------- .../sonar/TsRulesDefinitionTest.java | 47 +-- .../sonar/TypeScriptPluginTest.java | 13 +- .../sonar/model/TsLintRuleTest.java | 7 +- src/test/resources/existing.ts | 1 + 28 files changed, 1101 insertions(+), 1139 deletions(-) create mode 100644 src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java create mode 100644 src/main/java/com/pablissimo/sonar/LOCSensorImpl.java create mode 100644 src/main/java/com/pablissimo/sonar/PathResolver.java create mode 100644 src/main/java/com/pablissimo/sonar/PathResolverImpl.java create mode 100644 src/main/java/com/pablissimo/sonar/TsCoverageSensorImpl.java create mode 100644 src/test/java/com/pablissimo/sonar/LOCSensorImplTest.java delete mode 100644 src/test/java/com/pablissimo/sonar/LOCSensorTest.java create mode 100644 src/test/java/com/pablissimo/sonar/TsCoverageSensorImplTest.java delete mode 100644 src/test/java/com/pablissimo/sonar/TsCoverageSensorTest.java create mode 100644 src/test/resources/existing.ts diff --git a/.travis.yml b/.travis.yml index 3303829..4b6bcd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,13 @@ language: java +jdk: + - oraclejdk8 + +addons: + artifacts: + s3_region: "us-west-2" + paths: + - $(ls ./target/*.jar | tr "\n" ":") + target_paths: builds/$TRAVIS_BRANCH/$TRAVIS_BUILD_NUMBER after_success: - - mvn clean cobertura:cobertura coveralls:report \ No newline at end of file + - mvn cobertura:cobertura coveralls:report diff --git a/README.md b/README.md index 6206eb1..8ff9bff 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ SonarQube plugin for TypeScript files ##Demos A live deployed demo hitting a few large open-source TypeScript projects can be found here: -https://sonar.pablissimo.com +[https://sonar.pablissimo.com](https://sonar.pablissimo.com). Suggestions for more projects (or ones with easy-to-gather code coverage info) appreciated! @@ -26,44 +26,38 @@ Suggestions for more projects (or ones with easy-to-gather code coverage info) a ##Overview -This is a **not even alpha-level yet** SonarQube plugin for analysing projects with TypeScript content that supports: +This is plugin for SonarQube 5.6+ for analysing projects with TypeScript content that supports: * TsLint for code quality information * Importing LCOV files for unit test coverage information * NCLOC metric generation It's unfinished in the following respects: -* Plug-in code quality needs improved * Incomplete unit test coverage of the plugin -* Exceptionally little error handling +* No support for code duplication metrics It's presented only for the interested, and the brave. -###Breaking change in 0.2### -To more easily support changes to the rules TsLint understands, the plugin no longer generates a TsLint configuration file for you but instead you must now specify your own using the sonar.ts.tslintconfigpath configuration property (either in the web interface, or in your sonar-project.properties file). - ##Requirements -* Java 1.7+ -* SonarQube 4.4+ (may or may not work with others) +* Java 1.8+ +* SonarQube 5.4 LTS+ * TsLint 2.4.0+ -The plugin has so far *only been tested on Windows* and it'll be no surprise if it fails on Linux just now. - ##Building * Download the source * Build with maven, *mvn clean && mvn install* ##Installation * Install Node.js -* Install TsLint (2.4.0+) with *npm install -g tslint* -* Find the path to TsLint and copy it - will be similar to *C:\Users\\[Username]\AppData\Roaming\npm\node_modules\tslint\bin\tslint* on Windows -* Copy .jar file from target/ after build to SonarQube extensions folder +* Install TsLint (2.4.0+) with `npm install -g tslint`, or ensure it is installed locally against your project + * If you're installing globally, find the path to TsLint and copy it - will be similar to ```C:\Users\\[Username]\AppData\Roaming\npm\node_modules\tslint\bin\tslint``` on Windows +* Copy .jar file (from ```target/``` after build, or downloaded from [Releases page](https://github.com/Pablissimo/SonarTsPlugin/releases)) to SonarQube extensions folder * Restart SonarQube server * Browse to SonarQube web interface, login as Admin, hit up Settings * Find the TypeScript tab, paste in the TsLint path * Hit the Rules tab, then the TsLint rule set, then apply it to your project - alter rule activation as required * Make sure you have a ```tslint.json``` file next to ```sonar-project.properties```, or specify its path using the ```sonar.ts.tslintconfigpath``` setting * If LCOV data available, add *sonar.ts.lcov.reportpath=lcov.dat* to your sonar-project.properties file (replace lcov.dat with your lcov output, will be sought relative to the sonar-project.properties file) -* Run sonar-runner +* Run ```sonar-runner``` or ```sonar-scanner``` * TsLint rule breaches should be shown in the web view ##Configuration @@ -87,7 +81,7 @@ The plugin has so far *only been tested on Windows* and it'll be no surprise if sonar.ts.tslintpathRecommendedPath to the installed copy of TsLint to use - see note below -sonar.ts.tslintconfigpathMandatoryPath to the tslint.json file that configures the rules to be used in linting +sonar.ts.tslintconfigpathRecommendedPath to the tslint.json file that configures the rules to be used in linting - see note below sonar.ts.excludetypedefinitionfilesOptionalExcludes .d.ts files from analysis, defaults to true sonar.ts.forceZeroCoverageOptionalForces code coverage percentage to zero when no report is supplied, defaults to false sonar.ts.ignoreNotFoundOptionalDon't set code coverage percentage to zero when file is not found in report, defaults to false @@ -104,6 +98,8 @@ By default, SonarTsPlugin will look for a version of TsLint installed locally wi If analysis is failing, run ```sonar-runner``` with the ```-X -e``` options for more diagnostic information, including a note of where the plugin is searching for ```tslint```. Bear in mind that if running on a build server, the account running the build will need access to the path to ```tslint```. +By default, SonarTsPlugin will look for a TsLint configuration file called tslint.json next to the sonar-project.properties file. You can override this using the ```sonar.ts.tslintconfigpath``` configuration setting if this isn't the case for your project. + ## TsLint Custom Rules To present custom TsLint rules in SonarQube analysis, you can provide a configuration that maps the TsLint rules from your `sonar.ts.tslintrulesdir` diff --git a/pom.xml b/pom.xml index 662b3b5..acaf070 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.pablissimo.sonar sonar-typescript-plugin sonar-plugin - 0.9-SNAPSHOT + 0.94-SNAPSHOT TypeScript Analyse TypeScript projects @@ -29,7 +29,7 @@ scm:git:git@github.com:Pablissimo/SonarTsPlugin.git scm:git:git@github.com:Pablissimo/SonarTsPlugin.git https://github.com/Pablissimo/SonarTsPlugin - 0.9 + 1.0 Github @@ -42,14 +42,15 @@ UTF-8 - - 4.4 - 1.7 + + 5.6 + 1.8 + 1.21 - org.codehaus.sonar + org.sonarsource.sonarqube sonar-plugin-api ${sonar.buildVersion} provided @@ -60,14 +61,35 @@ gson 2.3 + + + commons-io + commons-io + 2.4 + - org.codehaus.sonar + org.sonarsource.sonarqube sonar-testing-harness ${sonar.buildVersion} test + + org.sonarsource.sslr + sslr-testing-harness + ${sslr.version} + + + commons-lang + commons-lang + 2.6 + + + commons-collections + commons-collections + 3.0 + junit junit @@ -97,9 +119,9 @@ - org.codehaus.sonar + org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin - 1.3 + 1.16 true typescript @@ -113,6 +135,12 @@ ${jdk.min.version} ${jdk.min.version} + + true + + -Xlint:all + -Werror + @@ -136,7 +164,7 @@ org.codehaus.mojo cobertura-maven-plugin - 2.6 + 2.7 xml diff --git a/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java b/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java new file mode 100644 index 0000000..bae76d4 --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/CombinedCoverageSensor.java @@ -0,0 +1,37 @@ +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; + +public class CombinedCoverageSensor implements Sensor { + + protected LOCSensor getLOCSensor() { + return new LOCSensorImpl(); + } + + protected TsCoverageSensor getCoverageSensor() { + return new TsCoverageSensorImpl(); + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Combined LCOV and LOC sensor"); + descriptor.onlyOnLanguage(TypeScriptLanguage.LANGUAGE_KEY); + } + + @Override + public void execute(SensorContext context) { + // First - LOC everything up, as we'll need LOC for uncovered lines metrics + Map> nonCommentLinesByFile = getLOCSensor().execute(context); + + // Now the LCOV pass can properly handle files that don't appear in + // configuration and set lines-to-cover values as required + getCoverageSensor().execute(context, nonCommentLinesByFile); + } + +} diff --git a/src/main/java/com/pablissimo/sonar/LCOVParser.java b/src/main/java/com/pablissimo/sonar/LCOVParser.java index 8a62ba9..18bb5fc 100644 --- a/src/main/java/com/pablissimo/sonar/LCOVParser.java +++ b/src/main/java/com/pablissimo/sonar/LCOVParser.java @@ -4,9 +4,10 @@ import java.util.List; import java.util.Map; -import org.sonar.api.measures.CoverageMeasuresBuilder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.coverage.NewCoverage; public interface LCOVParser { - Map parseFile(File file); - Map parse(List lines); + Map parseFile(File file); + Map parse(List lines); } diff --git a/src/main/java/com/pablissimo/sonar/LCOVParserImpl.java b/src/main/java/com/pablissimo/sonar/LCOVParserImpl.java index 7582465..d743f96 100644 --- a/src/main/java/com/pablissimo/sonar/LCOVParserImpl.java +++ b/src/main/java/com/pablissimo/sonar/LCOVParserImpl.java @@ -1,7 +1,9 @@ /* + * Pretty heavily modified version of the existing SonarQube LCOV parser implementation + * * SonarQube JavaScript Plugin - * Copyright (C) 2011 SonarSource and Eriks Nukis - * dev@sonar.codehaus.org + * Copyright (C) 2011-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 @@ -13,157 +15,223 @@ * 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 + * 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 com.pablissimo.sonar; -import com.google.common.base.Objects; -import com.google.common.collect.Maps; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.measures.CoverageMeasuresBuilder; -import org.sonar.api.utils.SonarException; - import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; + +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.coverage.CoverageType; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; /** * http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php */ -public final class LCOVParserImpl implements LCOVParser { +public class LCOVParserImpl implements LCOVParser { + + private static final String SF = "SF:"; + private static final String DA = "DA:"; + private static final String BRDA = "BRDA:"; + + private final Map coverageByFile; + private final SensorContext context; + private final List unresolvedPaths = new ArrayList(); + + private static final Logger LOG = Loggers.get(LCOVParser.class); + + private LCOVParserImpl(List lines, SensorContext context) { + this.context = context; + this.coverageByFile = parse(lines); + } + + static LCOVParser create(SensorContext context, File... files) { + final List lines = new LinkedList<>(); + for(File file: files) { + try { + lines.addAll(Files.lines(file.toPath()).collect(Collectors.toList())); + } catch (IOException e) { + throw new IllegalArgumentException("Could not read content from file: " + file, e); + } + } + + return new LCOVParserImpl(lines, context); + } + + Map coverageByFile() { + return coverageByFile; + } + + List unresolvedPaths() { + return unresolvedPaths; + } + + public Map parseFile(File file) { + final List lines; + try + { + lines = Files.readAllLines(file.toPath()); + } + catch (IOException e) + { + throw new IllegalArgumentException("Could not read content from file: " + file, e); + } + + return parse(lines); + } + + public Map parse(List lines) { + final Map files = new HashMap(); + FileData fileData = null; + + for (String line : lines) { + if (line.startsWith(SF)) { + // SF: + fileData = loadCurrentFileData(files, line); + } else if (fileData != null) { + if (line.startsWith(DA)) { + // DA:,[,] + String execution = line.substring(DA.length()); + String executionCount = execution.substring(execution.indexOf(',') + 1); + String lineNumber = execution.substring(0, execution.indexOf(',')); + + try { + fileData.addLine(Integer.valueOf(lineNumber), Integer.valueOf(executionCount)); + } catch (IllegalArgumentException e) { + logWrongDataWarning("DA", lineNumber, e); + } + } else if (line.startsWith(BRDA)) { + // BRDA:,,, + String[] tokens = line.substring(BRDA.length()).trim().split(","); + String lineNumber = tokens[0]; + String branchNumber = tokens[1] + tokens[2]; + String taken = tokens[3]; + + try { + fileData.addBranch(Integer.valueOf(lineNumber), branchNumber, "-".equals(taken) ? 0 : Integer.valueOf(taken)); + } catch (IllegalArgumentException e) { + logWrongDataWarning("BRDA", lineNumber, e); + } + } + } - private static final Logger LOG = LoggerFactory.getLogger(LCOVParserImpl.class); + } - private static final String SF = "SF:"; - private static final String DA = "DA:"; - private static final String BRDA = "BRDA:"; + Map coveredFiles = new HashMap(); + for (Map.Entry e : files.entrySet()) { + NewCoverage newCoverage = context.newCoverage().onFile(e.getKey()).ofType(CoverageType.UNIT); + e.getValue().save(newCoverage); + coveredFiles.put(e.getKey(), newCoverage); + } + return coveredFiles; + } + + private static void logWrongDataWarning(String dataType, String lineNumber, IllegalArgumentException e) { + LOG.warn(String.format("Problem during processing LCOV report: can't save %s data for line %s (%s).", dataType, lineNumber, e.getMessage())); + } + + @CheckForNull + private FileData loadCurrentFileData(final Map files, String line) { + String filePath = line.substring(SF.length()); + FileData fileData = null; + // some tools (like Istanbul, Karma) provide relative paths, so let's consider them relative to project directory + InputFile inputFile = context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath)); + if (inputFile != null) { + fileData = files.get(inputFile); + if (fileData == null) { + fileData = new FileData(inputFile); + files.put(inputFile, fileData); + } + } else { + unresolvedPaths.add(filePath); + } + return fileData; + } - private final File moduleBaseDir; + private static class FileData { + /** + * line number -> branch number -> taken + */ + private Map> branches = new HashMap>(); - public LCOVParserImpl(File moduleBaseDir) { - this.moduleBaseDir = moduleBaseDir; - } + /** + * line number -> execution count + */ + private Map hits = new HashMap(); - public Map parseFile(File file) { - final List lines; - try { - lines = FileUtils.readLines(file); - } catch (IOException e) { - throw new SonarException("Could not read content from file: " + file, e); - } - return parse(lines); + /** + * Number of lines in the file + * Required to check if line exist in a file, see {@link #checkLine(Integer)} + */ + private final int linesInFile; + + private final String filename; + private static final String WRONG_LINE_EXCEPTION_MESSAGE = "Line with number %s doesn't belong to file %s"; + + FileData(InputFile inputFile) { + linesInFile = inputFile.lines(); + filename = inputFile.relativePath(); } - public Map parse(List lines) { - final Map files = Maps.newHashMap(); - FileData fileData = new FileData(); - - for (String line : lines) { - if (line.startsWith(SF)) { - // SF: - String filePath = line.substring(SF.length()); - - // some tools (like Istanbul, Karma) provide relative paths, so let's consider them relative to project directory - try { - filePath = this.getIOFile(moduleBaseDir, filePath).getCanonicalPath(); - } catch (IOException e) { - filePath = ""; - LOG.error("Unable to retreive coverage info for file {}, because: {}", filePath, e); - } - - fileData = files.get(filePath); - if (fileData == null) { - fileData = new FileData(); - files.put(filePath, fileData); - } - - } else if (line.startsWith(DA)) { - // DA:,[,] - String execution = line.substring(DA.length()); - String executionCount = execution.substring(execution.indexOf(',') + 1); - String lineNumber = execution.substring(0, execution.indexOf(',')); - - fileData.addLine(Integer.valueOf(lineNumber), Integer.valueOf(executionCount)); - - } else if (line.startsWith(BRDA)) { - // BRDA:,,, - String[] tokens = line.substring(BRDA.length()).trim().split(","); - String lineNumber = tokens[0]; - String branchNumber = tokens[1] + tokens[2]; - String taken = tokens[3]; - - fileData.addBranch(Integer.valueOf(lineNumber), branchNumber, "-".equals(taken) ? 0 : Integer.valueOf(taken)); - } - } + void addBranch(Integer lineNumber, String branchNumber, Integer taken) { + checkLine(lineNumber); - Map coveredFiles = Maps.newHashMap(); - for (Map.Entry e : files.entrySet()) { - coveredFiles.put(e.getKey(), e.getValue().convert()); - } - return coveredFiles; + Map branchesForLine = branches.get(lineNumber); + if (branchesForLine == null) { + branchesForLine = new HashMap(); + branches.put(lineNumber, branchesForLine); + } + Integer currentValue = branchesForLine.get(branchNumber); + branchesForLine.put(branchNumber, (currentValue == null ? 0 : currentValue) + taken); } - /** - * Returns a java.io.File for the given path. - * If path is not absolute, returns a File with module base directory as parent path. - */ - protected File getIOFile(File baseDir, String path) { - File file = new File(path); - if (!file.isAbsolute()) { - file = new File(baseDir, path); - } + void addLine(Integer lineNumber, Integer executionCount) { + checkLine(lineNumber); - return file; + Integer currentValue = hits.get(lineNumber); + if (currentValue == null) { + currentValue = 0; + } + + hits.put(lineNumber, currentValue + executionCount); } - private static class FileData { - /** - * line number -> branch number -> taken - */ - private Map> branches = Maps.newHashMap(); - - /** - * line number -> execution count - */ - private Map hits = Maps.newHashMap(); - - public void addBranch(Integer lineNumber, String branchNumber, Integer taken) { - Map branchesForLine = branches.get(lineNumber); - if (branchesForLine == null) { - branchesForLine = Maps.newHashMap(); - branches.put(lineNumber, branchesForLine); - } - Integer currentValue = branchesForLine.get(branchNumber); - branchesForLine.put(branchNumber, Objects.firstNonNull(currentValue, 0) + taken); + void save(NewCoverage newCoverage) { + for (Map.Entry e : hits.entrySet()) { + newCoverage.lineHits(e.getKey(), e.getValue()); + } + for (Map.Entry> e : branches.entrySet()) { + int conditions = e.getValue().size(); + int covered = 0; + for (Integer taken : e.getValue().values()) { + if (taken > 0) { + covered++; + } } - public void addLine(Integer lineNumber, Integer executionCount) { - Integer currentValue = hits.get(lineNumber); - hits.put(lineNumber, Objects.firstNonNull(currentValue, 0) + executionCount); - } + newCoverage.conditions(e.getKey(), conditions, covered); + } + } - public CoverageMeasuresBuilder convert() { - CoverageMeasuresBuilder result = CoverageMeasuresBuilder.create(); - for (Map.Entry e : hits.entrySet()) { - result.setHits(e.getKey(), e.getValue()); - } - for (Map.Entry> e : branches.entrySet()) { - int conditions = e.getValue().size(); - int covered = 0; - for (Integer taken : e.getValue().values()) { - if (taken > 0) { - covered++; - } - } - result.setConditions(e.getKey(), conditions, covered); - } - return result; - } + private void checkLine(Integer lineNumber) { + if (lineNumber < 1 || lineNumber > linesInFile) { + throw new IllegalArgumentException(String.format(WRONG_LINE_EXCEPTION_MESSAGE, lineNumber, filename)); + } } + } } diff --git a/src/main/java/com/pablissimo/sonar/LOCSensor.java b/src/main/java/com/pablissimo/sonar/LOCSensor.java index c23f678..a14b40b 100644 --- a/src/main/java/com/pablissimo/sonar/LOCSensor.java +++ b/src/main/java/com/pablissimo/sonar/LOCSensor.java @@ -1,116 +1,11 @@ package com.pablissimo.sonar; -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; +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.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Measure; -import org.sonar.api.resources.Project; +import org.sonar.api.batch.sensor.SensorContext; -public class LOCSensor implements Sensor { - private static final Logger LOG = LoggerFactory.getLogger(LOCSensor.class); - - private FileSystem fileSystem; - - /** - * Use of IoC to get Settings and FileSystem - */ - public LOCSensor(FileSystem fs) { - this.fileSystem = fs; - } - - public boolean shouldExecuteOnProject(Project project) { - // This sensor is executed only when there are TypeScript files - return fileSystem.hasFiles(fileSystem.predicates().hasLanguage("ts")); - } - - public void analyse(Project project, SensorContext sensorContext) { - // This sensor count the Line of source code in every .ts file - - for (InputFile inputFile : fileSystem.inputFiles(fileSystem.predicates().hasLanguage( - "ts"))) { - int value = this.getNonCommentLineCount(inputFile); - sensorContext.saveMeasure(inputFile, new Measure( - CoreMetrics.NCLOC, (double) value)); - } - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - protected BufferedReader getReaderFromFile(InputFile inputFile) throws FileNotFoundException { - return new BufferedReader(new FileReader(inputFile.file())); - } - - private int getNonCommentLineCount(InputFile inputFile) { - int value = 0; - BufferedReader br; - try { - br = this.getReaderFromFile(inputFile); - - boolean isEOF; - boolean isCommentOpen = false; - boolean isACommentLine; - do { - - String line = br.readLine(); - if (line != null) { - isACommentLine = isCommentOpen; - line = line.trim(); - - if (isCommentOpen) { - if (line.contains("*/")) { - isCommentOpen = false; - isACommentLine = true; - } - } else { - if (line.startsWith("//")) { - isACommentLine = true; - } - if (line.startsWith("/*")) { - if (line.contains("*/")) { - isCommentOpen = false; - } else { - isCommentOpen = true; - } - isACommentLine = true; - - } else if (line.contains("/*")) { - if (line.contains("*/")) { - isCommentOpen = false; - } else { - isCommentOpen = true; - } - isACommentLine = false; - } - } - isEOF = true; - line = line.replaceAll("\\n|\\t|\\s", ""); - if ((!line.equals("")) && !isACommentLine) { - value++; - } - } 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 value; - } +public interface LOCSensor { + Map> execute(SensorContext ctx); } diff --git a/src/main/java/com/pablissimo/sonar/LOCSensorImpl.java b/src/main/java/com/pablissimo/sonar/LOCSensorImpl.java new file mode 100644 index 0000000..51c2859 --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/LOCSensorImpl.java @@ -0,0 +1,114 @@ +package com.pablissimo.sonar; + +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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.measures.CoreMetrics; + +public class LOCSensorImpl implements LOCSensor { + private static final Logger LOG = LoggerFactory.getLogger(LOCSensorImpl.class); + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + protected BufferedReader getReaderFromFile(InputFile inputFile) throws FileNotFoundException { + return new BufferedReader(new FileReader(inputFile.file())); + } + + private Set getNonCommentLineNumbers(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 = true; + } + } else { + if (line.startsWith("//")) { + isACommentLine = true; + } + if (line.startsWith("/*")) { + if (line.contains("*/")) { + isCommentOpen = false; + } else { + isCommentOpen = true; + } + isACommentLine = true; + + } else if (line.contains("/*")) { + if (line.contains("*/")) { + isCommentOpen = false; + } else { + isCommentOpen = true; + } + isACommentLine = false; + } + } + isEOF = true; + line = line.replaceAll("\\n|\\t|\\s", ""); + if ((!line.equals("")) && !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; + } + + 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 nonCommentLineNumbers = this.getNonCommentLineNumbers(inputFile); + toReturn.put(inputFile, nonCommentLineNumbers); + + ctx.newMeasure().forMetric(CoreMetrics.NCLOC).on(inputFile).withValue(nonCommentLineNumbers.size()).save(); + } + + return toReturn; + } +} diff --git a/src/main/java/com/pablissimo/sonar/PathResolver.java b/src/main/java/com/pablissimo/sonar/PathResolver.java new file mode 100644 index 0000000..632ab57 --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/PathResolver.java @@ -0,0 +1,7 @@ +package com.pablissimo.sonar; + +import org.sonar.api.batch.sensor.SensorContext; + +public interface PathResolver { + String getPath(SensorContext context, String settingKey, String defaultValue); +} diff --git a/src/main/java/com/pablissimo/sonar/PathResolverImpl.java b/src/main/java/com/pablissimo/sonar/PathResolverImpl.java new file mode 100644 index 0000000..19faddb --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/PathResolverImpl.java @@ -0,0 +1,50 @@ +package com.pablissimo.sonar; + +import java.io.File; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.utils.System2; + +public class PathResolverImpl implements PathResolver { + private static final Logger LOG = LoggerFactory.getLogger(TsLintExecutorImpl.class); + + @Override + public String getPath(SensorContext context, String settingKey, String defaultValue) { + // Prefer the specified path + String toReturn = context.settings().getString(settingKey); + + // Fall back to a file system search if null or doesn't exist + if (toReturn == null || toReturn.isEmpty()) { + LOG.debug("Path " + settingKey + " not specified, falling back to " + defaultValue); + toReturn = defaultValue; + } + else { + LOG.debug("Found " + settingKey + " Lint path to be '" + toReturn + "'"); + } + + return getAbsolutePath(context, toReturn); + } + + String getAbsolutePath(SensorContext context, String toReturn) { + if (toReturn != null) { + File candidateFile = new java.io.File(toReturn); + if (!candidateFile.isAbsolute()) { + candidateFile = new java.io.File(context.fileSystem().baseDir().getAbsolutePath(), toReturn); + } + + if (!doesFileExist(candidateFile)) { + return null; + } + + return candidateFile.getAbsolutePath(); + } + + return null; + } + + boolean doesFileExist(File f) { + return f.exists(); + } +} diff --git a/src/main/java/com/pablissimo/sonar/TsCoverageSensor.java b/src/main/java/com/pablissimo/sonar/TsCoverageSensor.java index cfdbaa9..3ed99fd 100644 --- a/src/main/java/com/pablissimo/sonar/TsCoverageSensor.java +++ b/src/main/java/com/pablissimo/sonar/TsCoverageSensor.java @@ -1,167 +1,11 @@ -/* - * Slightly modified version of SonarQube JavaScript Plugin - * Copyright (C) 2011 SonarSource and Eriks Nukis - * 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 com.pablissimo.sonar; -import org.apache.commons.lang.StringUtils; -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.FilePredicates; -import org.sonar.api.batch.fs.FileSystem; -import org.sonar.api.config.Settings; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.CoverageMeasuresBuilder; -import org.sonar.api.measures.Measure; -import org.sonar.api.measures.PropertiesBuilder; -import org.sonar.api.resources.Project; - -import java.io.File; import java.util.Map; +import java.util.Set; -public class TsCoverageSensor implements Sensor { - - private static final Logger LOG = LoggerFactory.getLogger(TsCoverageSensor.class); - - private final FileSystem moduleFileSystem; - private final FilePredicates filePredicates; - private final Settings settings; - - public TsCoverageSensor(FileSystem moduleFileSystem, Settings settings) { - this.moduleFileSystem = moduleFileSystem; - this.filePredicates = moduleFileSystem.predicates(); - this.settings = settings; - } - - public boolean shouldExecuteOnProject(Project project) { - return moduleFileSystem.files(this.filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY)).iterator().hasNext(); - } - - public void analyse(Project project, SensorContext context) { - if (isLCOVReportProvided()) { - saveMeasureFromLCOVFile(project, context); - - } else if (isForceZeroCoverageActivated()) { - saveZeroValueForAllFiles(project, context); - } - - // Else, nothing to do, there will be no coverage information for JavaScript files. - } - - protected void saveZeroValueForAllFiles(Project project, SensorContext context) { - for (File file : moduleFileSystem.files(this.filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { - saveZeroValueForResource(this.fileFromIoFile(file, project), context); - } - } - - protected void saveMeasureFromLCOVFile(Project project, SensorContext context) { - String providedPath = settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH); - File lcovFile = getIOFile(moduleFileSystem.baseDir(), providedPath); - - if (!lcovFile.isFile()) { - LOG.warn("No coverage information will be saved because LCOV file cannot be analysed. Provided LCOV file path: {}", providedPath); - return; - } - - LOG.info("Analysing {}", lcovFile); - - LCOVParser parser = getParser(moduleFileSystem.baseDir()); - Map coveredFiles = parser.parseFile(lcovFile); - - final boolean ignoreNotFound = isIgnoreNotFoundActivated(); - - for (File file : moduleFileSystem.files(this.filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { - try { - CoverageMeasuresBuilder fileCoverage = coveredFiles.get(file.getAbsolutePath()); - org.sonar.api.resources.File resource = this.fileFromIoFile(file, project); - - if (fileCoverage != null) { - for (Measure measure : fileCoverage.createMeasures()) { - context.saveMeasure(resource, measure); - } - } else if (!ignoreNotFound) { - // colour all lines as not executed - saveZeroValueForResource(resource, context); - } - } catch (Exception e) { - LOG.error("Problem while calculating coverage for " + file.getAbsolutePath(), e); - } - } - } - - protected org.sonar.api.resources.File fileFromIoFile(java.io.File file, Project project) { - return org.sonar.api.resources.File.fromIOFile(file, project); - } - - protected LCOVParser getParser(File baseDirectory) { - return new LCOVParserImpl(baseDirectory); - } - - private void saveZeroValueForResource(org.sonar.api.resources.File resource, SensorContext context) { - PropertiesBuilder lineHitsData = new PropertiesBuilder<>(CoreMetrics.COVERAGE_LINE_HITS_DATA); - - Measure measure = context.getMeasure(resource, CoreMetrics.LINES); - - if(measure == null){ - LOG.info("measure == null for {}", resource.getPath()); - return; - } - - for (int x = 1; x < measure.getIntValue(); x++) { - lineHitsData.add(x, 0); - } - - // use non comment lines of code for coverage calculation - Measure ncloc = context.getMeasure(resource, CoreMetrics.NCLOC); - context.saveMeasure(resource, lineHitsData.build()); - context.saveMeasure(resource, CoreMetrics.LINES_TO_COVER, ncloc.getValue()); - context.saveMeasure(resource, CoreMetrics.UNCOVERED_LINES, ncloc.getValue()); - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - private boolean isForceZeroCoverageActivated() { - return settings.getBoolean(TypeScriptPlugin.SETTING_FORCE_ZERO_COVERAGE); - } - - private boolean isIgnoreNotFoundActivated() { - return settings.getBoolean(TypeScriptPlugin.SETTING_IGNORE_NOT_FOUND); - } - - private boolean isLCOVReportProvided() { - return StringUtils.isNotBlank(settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)); - } - - /** - * Returns a java.io.File for the given path. - * If path is not absolute, returns a File with module base directory as parent path. - */ - public File getIOFile(File baseDir, String path) { - File file = new File(path); - if (!file.isAbsolute()) { - file = new File(baseDir, path); - } +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; - return file; - } -} +public interface TsCoverageSensor { + public abstract void execute(SensorContext ctx, Map> nonCommentLineNumbersByFile); +} \ No newline at end of file diff --git a/src/main/java/com/pablissimo/sonar/TsCoverageSensorImpl.java b/src/main/java/com/pablissimo/sonar/TsCoverageSensorImpl.java new file mode 100644 index 0000000..13920a4 --- /dev/null +++ b/src/main/java/com/pablissimo/sonar/TsCoverageSensorImpl.java @@ -0,0 +1,146 @@ +/* + * Slightly modified version of SonarQube JavaScript Plugin + * Copyright (C) 2011 SonarSource and Eriks Nukis + * 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 com.pablissimo.sonar; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.coverage.CoverageType; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.measures.CoreMetrics; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class TsCoverageSensorImpl implements TsCoverageSensor { + + private static final Logger LOG = LoggerFactory.getLogger(TsCoverageSensorImpl.class); + + public TsCoverageSensorImpl() { + } + + private void saveZeroValueForAllFiles(SensorContext context, Map> nonCommentLineNumbersByFile) { + for (InputFile inputFile : context.fileSystem().inputFiles(context.fileSystem().predicates().hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { + saveZeroValue(inputFile, context, nonCommentLineNumbersByFile.get(inputFile)); + } + } + + private void saveZeroValue(InputFile inputFile, SensorContext context, Set nonCommentLineNumbers) { + NewCoverage newCoverage = + context + .newCoverage() + .ofType(CoverageType.UNIT) + .onFile(inputFile); + + if (nonCommentLineNumbers != null) { + for (Integer nonCommentLineNumber : nonCommentLineNumbers) { + newCoverage.lineHits(nonCommentLineNumber, 0); + } + } + else { + for (int i = 1; i <= inputFile.lines(); i++) { + newCoverage.lineHits(i, 0); + } + } + + newCoverage.save(); + } + + protected void saveMeasureFromLCOVFile(SensorContext context, Map> nonCommentLineNumbersByFile) { + String providedPath = context.settings().getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH); + File lcovFile = getIOFile(context.fileSystem().baseDir(), providedPath); + + if (!lcovFile.isFile()) { + LOG.warn("No coverage information will be saved because LCOV file cannot be analysed. Provided LCOV file path: {}", providedPath); + return; + } + + LOG.info("Analysing {}", lcovFile); + + LCOVParser parser = getParser(context, new File[]{ lcovFile }); + Map coveredFiles = parser.parseFile(lcovFile); + + final boolean ignoreNotFound = isIgnoreNotFoundActivated(context); + + for (InputFile file : context.fileSystem().inputFiles(context.fileSystem().predicates().hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { + try { + NewCoverage fileCoverage = coveredFiles.get(file); + + if (fileCoverage != null) { + fileCoverage.save(); + } + else if (!ignoreNotFound) { + // colour all lines as not executed + LOG.debug("Default value of zero will be saved for file: {}", file.relativePath()); + LOG.debug("Because was not present in LCOV report."); + saveZeroValue(file, context, nonCommentLineNumbersByFile.get(file)); + } + } catch (Exception e) { + LOG.error("Problem while calculating coverage for " + file.absolutePath(), e); + } + } + } + + protected LCOVParser getParser(SensorContext context, File[] files) { + return LCOVParserImpl.create(context, files); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + private boolean isForceZeroCoverageActivated(SensorContext ctx) { + return ctx.settings().getBoolean(TypeScriptPlugin.SETTING_FORCE_ZERO_COVERAGE); + } + + private boolean isIgnoreNotFoundActivated(SensorContext ctx) { + return ctx.settings().getBoolean(TypeScriptPlugin.SETTING_IGNORE_NOT_FOUND); + } + + private boolean isLCOVReportProvided(SensorContext ctx) { + return StringUtils.isNotBlank(ctx.settings().getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)); + } + + public File getIOFile(File baseDir, String path) { + File file = new File(path); + if (!file.isAbsolute()) { + file = new File(baseDir, path); + } + + return file; + } + + public void execute(SensorContext ctx, Map> nonCommentLineNumbersByFile) { + if (nonCommentLineNumbersByFile == null) { + nonCommentLineNumbersByFile = new HashMap>(); + } + + if (isLCOVReportProvided(ctx)) { + saveMeasureFromLCOVFile(ctx, nonCommentLineNumbersByFile); + } else if (isForceZeroCoverageActivated(ctx)) { + saveZeroValueForAllFiles(ctx, nonCommentLineNumbersByFile); + } + } +} diff --git a/src/main/java/com/pablissimo/sonar/TsLintExecutorImpl.java b/src/main/java/com/pablissimo/sonar/TsLintExecutorImpl.java index ed5d8d2..955ed26 100644 --- a/src/main/java/com/pablissimo/sonar/TsLintExecutorImpl.java +++ b/src/main/java/com/pablissimo/sonar/TsLintExecutorImpl.java @@ -5,6 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.api.utils.System2; import org.sonar.api.utils.command.Command; import org.sonar.api.utils.command.CommandExecutor; import org.sonar.api.utils.command.StreamConsumer; @@ -16,23 +17,41 @@ public class TsLintExecutorImpl implements TsLintExecutor { private StringBuilder stdOut; private StringBuilder stdErr; - private static Command getBaseCommand(String pathToTsLint, String configFile, String rulesDir) { + private boolean mustQuoteSpaceContainingPaths = false; + + public TsLintExecutorImpl(System2 system) { + this.mustQuoteSpaceContainingPaths = system.isOsWindows(); + } + + public String preparePath(String path) { + if (path == null) { + return null; + } + else if (path.contains(" ") && this.mustQuoteSpaceContainingPaths) { + return '"' + path + '"'; + } + else { + return path; + } + } + + private Command getBaseCommand(String pathToTsLint, String configFile, String rulesDir) { Command command = Command .create("node") - .addArgument('"' + pathToTsLint + '"') + .addArgument(this.preparePath(pathToTsLint)) .addArgument("--format") .addArgument("json"); if (rulesDir != null && rulesDir.length() > 0) { command .addArgument("--rules-dir") - .addArgument('"' + rulesDir + '"'); + .addArgument(this.preparePath(rulesDir)); } command .addArgument("--config") - .addArgument('"' + configFile + '"') + .addArgument(this.preparePath(configFile)) .setNewShell(false); return command; @@ -50,7 +69,7 @@ public String execute(String pathToTsLint, String configFile, String rulesDir, L int currentBatchLength = 0; for (int i = 0; i < files.size(); i++) { - String nextPath = '"' + files.get(i).trim() + '"'; + String nextPath = this.preparePath(files.get(i).trim()); // +1 for the space we'll be adding between filenames if (currentBatchLength + nextPath.length() + 1 > availableForBatching) { diff --git a/src/main/java/com/pablissimo/sonar/TsLintSensor.java b/src/main/java/com/pablissimo/sonar/TsLintSensor.java index 23b4d69..e4c8ccd 100644 --- a/src/main/java/com/pablissimo/sonar/TsLintSensor.java +++ b/src/main/java/com/pablissimo/sonar/TsLintSensor.java @@ -3,21 +3,17 @@ import com.pablissimo.sonar.model.TsLintIssue; 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.FilePredicates; -import org.sonar.api.batch.fs.FileSystem; -import org.sonar.api.component.ResourcePerspectives; +import org.sonar.api.batch.rule.ActiveRule; +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.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.batch.fs.InputFile; import org.sonar.api.config.Settings; -import org.sonar.api.issue.Issuable; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.Resource; import org.sonar.api.rule.RuleKey; -import org.sonar.api.rules.Rule; -import org.sonar.api.rules.RuleFinder; -import org.sonar.api.rules.RuleQuery; +import org.sonar.api.utils.System2; -import java.io.File; import java.util.*; public class TsLintSensor implements Sensor { @@ -27,68 +23,43 @@ public class TsLintSensor implements Sensor { private static final Logger LOG = LoggerFactory.getLogger(TsLintExecutorImpl.class); private Settings settings; - private FileSystem fileSystem; - private FilePredicates filePredicates; - private ResourcePerspectives perspectives; - private RuleFinder ruleFinder; + private System2 system; - public TsLintSensor(Settings settings, FileSystem fileSystem, ResourcePerspectives perspectives, RuleFinder ruleFinder) { + public TsLintSensor(Settings settings, System2 system) { this.settings = settings; - this.fileSystem = fileSystem; - this.filePredicates = fileSystem.predicates(); - this.perspectives = perspectives; - this.ruleFinder = ruleFinder; + this.system = system; } - - public boolean shouldExecuteOnProject(Project project) { - return hasFilesToAnalyze(); + + protected PathResolver getPathResolver() { + return new PathResolverImpl(); } - private boolean hasFilesToAnalyze() { - return fileSystem.files(this.filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY)).iterator().hasNext(); + protected TsLintExecutor getTsLintExecutor() { + return new TsLintExecutorImpl(this.system); } - - private String getPath(String settingKey, String defaultValue) { - // Prefer the specified path - String toReturn = settings.getString(settingKey); - - // Fall back to a file system search if null or doesn't exist - if (toReturn == null || toReturn.isEmpty()) { - LOG.debug("Path " + settingKey + " not specified, falling back to " + defaultValue); - toReturn = defaultValue; - } - else { - LOG.debug("Found " + settingKey + " Lint path to be '" + toReturn + "'"); - } - - return getAbsolutePath(toReturn); + + protected TsLintParser getTsLintParser() { + return new TsLintParserImpl(); } - - protected String getAbsolutePath(String toReturn) { - if (toReturn != null) { - File candidateFile = new java.io.File(toReturn); - if (!candidateFile.isAbsolute()) { - candidateFile = new java.io.File(this.fileSystem.baseDir().getAbsolutePath(), toReturn); - } - - if (!doesFileExist(candidateFile)) { - return null; - } - return candidateFile.getAbsolutePath(); - } - - return null; + protected TsRulesDefinition getTsRulesDefinition() { + return new TsRulesDefinition(this.settings); } - - protected boolean doesFileExist(File f) { - return f.exists(); + + @Override + public void describe(SensorDescriptor desc) { + desc + .name("Linting sensor for TypeScript files") + .onlyOnLanguage(TypeScriptLanguage.LANGUAGE_KEY); } - public void analyse(Project project, SensorContext context) { - String pathToTsLint = this.getPath(TypeScriptPlugin.SETTING_TS_LINT_PATH, TSLINT_FALLBACK_PATH); - String pathToTsLintConfig = this.getPath(TypeScriptPlugin.SETTING_TS_LINT_CONFIG_PATH, CONFIG_FILENAME); - String rulesDir = this.getPath(TypeScriptPlugin.SETTING_TS_LINT_RULES_DIR, null); + @Override + public void execute(SensorContext ctx) { + PathResolver resolver = getPathResolver(); + + String pathToTsLint = resolver.getPath(ctx, TypeScriptPlugin.SETTING_TS_LINT_PATH, TSLINT_FALLBACK_PATH); + String pathToTsLintConfig = resolver.getPath(ctx, TypeScriptPlugin.SETTING_TS_LINT_CONFIG_PATH, CONFIG_FILENAME); + String rulesDir = resolver.getPath(ctx, TypeScriptPlugin.SETTING_TS_LINT_RULES_DIR, null); Integer tsLintTimeoutMs = Math.max(5000, settings.getInt(TypeScriptPlugin.SETTING_TS_LINT_TIMEOUT)); @@ -106,22 +77,21 @@ else if (pathToTsLintConfig == null) { boolean skipTypeDefFiles = settings.getBoolean(TypeScriptPlugin.SETTING_EXCLUDE_TYPE_DEFINITION_FILES); - RuleQuery ruleQuery = RuleQuery.create().withRepositoryKey(TsRulesDefinition.REPOSITORY_NAME); - Collection allRules = this.ruleFinder.findAll(ruleQuery); + Collection allRules = ctx.activeRules().findByRepository(TsRulesDefinition.REPOSITORY_NAME); HashSet ruleNames = new HashSet<>(); - for (Rule rule : allRules) { - ruleNames.add(rule.getKey()); + for (ActiveRule rule : allRules) { + ruleNames.add(rule.ruleKey().rule()); } List paths = new ArrayList(); - HashMap fileMap = new HashMap(); + HashMap fileMap = new HashMap(); - for (File file : fileSystem.files(this.filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { - if (skipTypeDefFiles && file.getName().toLowerCase().endsWith("." + TypeScriptLanguage.LANGUAGE_DEFINITION_EXTENSION)) { + for (InputFile file : ctx.fileSystem().inputFiles(ctx.fileSystem().predicates().hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { + if (skipTypeDefFiles && file.file().getName().toLowerCase().endsWith("." + TypeScriptLanguage.LANGUAGE_DEFINITION_EXTENSION)) { continue; } - String pathAdjusted = file.getAbsolutePath().replace('\\', '/'); + String pathAdjusted = file.absolutePath().replace('\\', '/'); paths.add(pathAdjusted); fileMap.put(pathAdjusted, file); } @@ -148,10 +118,8 @@ else if (pathToTsLintConfig == null) { continue; } - File file = fileMap.get(filePath); - Resource resource = this.getFileFromIOFile(file, project); - Issuable issuable = perspectives.as(Issuable.class, resource); - + InputFile file = fileMap.get(filePath); + for (TsLintIssue issue : batchIssues) { // Make sure the rule we're violating is one we recognise - if not, we'll // fall back to the generic 'tslint-issue' rule @@ -160,32 +128,30 @@ else if (pathToTsLintConfig == null) { ruleName = TsRulesDefinition.TSLINT_UNKNOWN_RULE.key; } - issuable.addIssue - ( - issuable - .newIssueBuilder() - .line(issue.getStartPosition().getLine() + 1) + NewIssue newIssue = + ctx + .newIssue() + .forRule(RuleKey.of(TsRulesDefinition.REPOSITORY_NAME, ruleName)); + + NewIssueLocation newIssueLocation = + newIssue + .newLocation() + .on(file) .message(issue.getFailure()) - .ruleKey(RuleKey.of(TsRulesDefinition.REPOSITORY_NAME, ruleName)) - .build() - ); + .at(file.selectLine(issue.getStartPosition().getLine() + 1)) + /*.at( + file + .newRange( + issue.getStartPosition().getLine(), + issue.getStartPosition().getCharacter(), + issue.getEndPosition().getLine(), + issue.getEndPosition().getCharacter() + ) + )*/; + + newIssue.at(newIssueLocation); + newIssue.save(); } } } - - protected org.sonar.api.resources.File getFileFromIOFile(File file, Project project) { - return org.sonar.api.resources.File.fromIOFile(file, project); - } - - protected TsLintExecutor getTsLintExecutor() { - return new TsLintExecutorImpl(); - } - - protected TsLintParser getTsLintParser() { - return new TsLintParserImpl(); - } - - protected TsRulesDefinition getTsRulesDefinition() { - return new TsRulesDefinition(this.settings); - } } diff --git a/src/main/java/com/pablissimo/sonar/TsRulesDefinition.java b/src/main/java/com/pablissimo/sonar/TsRulesDefinition.java index 183bb8c..6aec863 100644 --- a/src/main/java/com/pablissimo/sonar/TsRulesDefinition.java +++ b/src/main/java/com/pablissimo/sonar/TsRulesDefinition.java @@ -6,6 +6,7 @@ import org.sonar.api.config.Settings; import org.sonar.api.rule.RuleStatus; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.api.server.rule.RulesDefinition; @@ -22,7 +23,7 @@ public class TsRulesDefinition implements RulesDefinition { public static final String DEFAULT_RULE_DESCRIPTION = "No description provided for this TsLint rule"; public static final String DEFAULT_RULE_DEBT_SCALAR = "0min"; public static final String DEFAULT_RULE_DEBT_OFFSET = "0min"; - public static final String DEFAULT_RULE_DEBT_TYPE = SubCharacteristics.ARCHITECTURE_RELIABILITY; + public static final String DEFAULT_RULE_DEBT_TYPE = RuleType.CODE_SMELL.name(); private static final String CORE_RULES_CONFIG_RESOURCE_PATH = "/tslint/tslint-rules.properties"; @@ -98,7 +99,7 @@ public static void loadRules(InputStream stream, List rulesCollectio String debtRemediationFunction = properties.getProperty(propKey + ".debtFunc", null); String debtRemediationScalar = properties.getProperty(propKey + ".debtScalar", DEFAULT_RULE_DEBT_SCALAR); String debtRemediationOffset = properties.getProperty(propKey + ".debtOffset", DEFAULT_RULE_DEBT_OFFSET); - String debtCharacteristic = properties.getProperty(propKey + ".debtType", DEFAULT_RULE_DEBT_TYPE); + String debtType = properties.getProperty(propKey + ".debtType", DEFAULT_RULE_DEBT_TYPE); TsLintRule tsRule = null; @@ -114,7 +115,7 @@ public static void loadRules(InputStream stream, List rulesCollectio debtRemediationFunctionEnum, debtRemediationScalar, debtRemediationOffset, - debtCharacteristic + debtType ); } @@ -140,12 +141,13 @@ public int compare(TsLintRule r1, TsLintRule r2) { } private void createRule(NewRepository repository, TsLintRule tsRule) { - NewRule sonarRule = repository - .createRule(tsRule.key) - .setName(tsRule.name) - .setSeverity(tsRule.severity) - .setHtmlDescription(tsRule.htmlDescription) - .setStatus(RuleStatus.READY); + NewRule sonarRule = + repository + .createRule(tsRule.key) + .setName(tsRule.name) + .setSeverity(tsRule.severity) + .setHtmlDescription(tsRule.htmlDescription) + .setStatus(RuleStatus.READY); if (tsRule.hasDebtRemediation) { DebtRemediationFunction debtRemediationFn = null; @@ -167,8 +169,21 @@ private void createRule(NewRepository repository, TsLintRule tsRule) { } sonarRule.setDebtRemediationFunction(debtRemediationFn); - sonarRule.setDebtSubCharacteristic(tsRule.debtCharacteristic); } + + RuleType type = null; + + if (tsRule.debtType != null && RuleType.names().contains(tsRule.debtType)) { + // Try and parse it as a new-style rule type (since 5.5 SQALE's been replaced + // with something simpler, and there's really only three buckets) + type = RuleType.valueOf(tsRule.debtType); + } + + if (type == null) { + type = RuleType.CODE_SMELL; + } + + sonarRule.setType(type); } public void define(Context context) { diff --git a/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java b/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java index d58cf8b..79d48e1 100644 --- a/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java +++ b/src/main/java/com/pablissimo/sonar/TypeScriptPlugin.java @@ -1,8 +1,5 @@ package com.pablissimo.sonar; -import java.util.Arrays; -import java.util.List; - import org.sonar.api.*; @Properties({ @@ -88,7 +85,7 @@ global = false ) }) -public class TypeScriptPlugin extends SonarPlugin { +public class TypeScriptPlugin implements Plugin { public static final String SETTING_EXCLUDE_TYPE_DEFINITION_FILES = "sonar.ts.excludetypedefinitionfiles"; public static final String SETTING_FORCE_ZERO_COVERAGE = "sonar.ts.forceZeroCoverage"; public static final String SETTING_IGNORE_NOT_FOUND = "sonar.ts.ignoreNotFound"; @@ -99,15 +96,13 @@ public class TypeScriptPlugin extends SonarPlugin { public static final String SETTING_LCOV_REPORT_PATH = "sonar.ts.lcov.reportpath"; public static final String SETTING_TS_RULE_CONFIGS = "sonar.ts.ruleconfigs"; - public List getExtensions() { - return Arrays.asList - ( - TypeScriptRuleProfile.class, - TypeScriptLanguage.class, - TsLintSensor.class, - LOCSensor.class, - TsCoverageSensor.class, - TsRulesDefinition.class - ); + @Override + public void define(Context ctx) { + ctx + .addExtension(TypeScriptRuleProfile.class) + .addExtension(TypeScriptLanguage.class) + .addExtension(TsLintSensor.class) + .addExtension(CombinedCoverageSensor.class) + .addExtension(TsRulesDefinition.class); } } diff --git a/src/main/java/com/pablissimo/sonar/TypeScriptRuleProfile.java b/src/main/java/com/pablissimo/sonar/TypeScriptRuleProfile.java index b4bed5c..118cf51 100644 --- a/src/main/java/com/pablissimo/sonar/TypeScriptRuleProfile.java +++ b/src/main/java/com/pablissimo/sonar/TypeScriptRuleProfile.java @@ -1,8 +1,6 @@ package com.pablissimo.sonar; import com.pablissimo.sonar.model.TsLintRule; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.sonar.api.profiles.ProfileDefinition; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.rules.Rule; @@ -11,8 +9,6 @@ public class TypeScriptRuleProfile extends ProfileDefinition { public static final String PROFILE_NAME = "tslint"; - private static final Logger LOG = LoggerFactory.getLogger(TypeScriptRuleProfile.class); - @Override public RulesProfile createProfile(ValidationMessages validation) { RulesProfile profile = RulesProfile.create("TsLint", TypeScriptLanguage.LANGUAGE_KEY); @@ -21,8 +17,9 @@ public RulesProfile createProfile(ValidationMessages validation) { activateRule(profile, TsRulesDefinition.TSLINT_UNKNOWN_RULE.key); - for (TsLintRule coreRule : rules.getCoreRules()) + for (TsLintRule coreRule : rules.getCoreRules()) { activateRule(profile, coreRule.key); + } return profile; } diff --git a/src/main/java/com/pablissimo/sonar/model/TsLintRule.java b/src/main/java/com/pablissimo/sonar/model/TsLintRule.java index 2b55f79..8ac00bc 100644 --- a/src/main/java/com/pablissimo/sonar/model/TsLintRule.java +++ b/src/main/java/com/pablissimo/sonar/model/TsLintRule.java @@ -12,7 +12,7 @@ public class TsLintRule { public final DebtRemediationFunction.Type debtRemediationFunction; public final String debtRemediationScalar; public final String debtRemediationOffset; - public final String debtCharacteristic; + public final String debtType; public TsLintRule( String key, @@ -28,7 +28,7 @@ public TsLintRule( this.debtRemediationFunction = DebtRemediationFunction.Type.CONSTANT_ISSUE; this.debtRemediationScalar = "0min"; this.debtRemediationOffset = "0min"; - this.debtCharacteristic = null; + this.debtType = null; } public TsLintRule( @@ -39,7 +39,7 @@ public TsLintRule( DebtRemediationFunction.Type debtRemediationFunction, String debtRemediationScalar, String debtRemediationOffset, - String debtCharacteristic + String debtType ) { this.key = key; this.severity = severity; @@ -50,6 +50,6 @@ public TsLintRule( this.debtRemediationFunction = debtRemediationFunction; this.debtRemediationScalar = debtRemediationScalar; this.debtRemediationOffset = debtRemediationOffset; - this.debtCharacteristic = debtCharacteristic; + this.debtType = debtType; } } diff --git a/src/test/java/com/pablissimo/sonar/LOCSensorImplTest.java b/src/test/java/com/pablissimo/sonar/LOCSensorImplTest.java new file mode 100644 index 0000000..1a43890 --- /dev/null +++ b/src/test/java/com/pablissimo/sonar/LOCSensorImplTest.java @@ -0,0 +1,73 @@ +package com.pablissimo.sonar; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +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 LOCSensorImplTest { + LOCSensor sensor; + + SensorContextTester sensorContext; + + @Before + public void setUp() throws Exception { + this.sensorContext = SensorContextTester.create(new File("")); + this.sensor = spy(new LOCSensorImpl()); + } + + @Test + public void toStringReturnsClassName() { + assertEquals("LOCSensorImpl", new LOCSensorImpl().toString()); + } + + @Test + public void basicBlockCommentsDiscounted() throws FileNotFoundException { + assertLineCount("blockcomments1", 2); + } + + @Test + public void blockCommentsNotConfusedWithNestedComments() throws FileNotFoundException { + assertLineCount("blockcomments2", 2); + } + + @Test + public void linesEndingWithABlockCommentStillCounted() throws FileNotFoundException { + assertLineCount("blockcomments3", 1); + } + + @Test + public void oneLineBlockCommentsDoNotConfuseCounting() throws FileNotFoundException { + assertLineCount("blockcomments4", 1); + } + + @Test + public void oneLineBlockCommentAtEndOfRealLineShouldNotConsiderNextLinesAsComments() throws FileNotFoundException { + assertLineCount("blockcomments5", 2); + } + + @Test + public void lineLevelCommentsAndWhitespaceHandledCorrectly() throws FileNotFoundException { + assertLineCount("linecomments", 2); + } + + 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.NCLOC).value()); + } +} diff --git a/src/test/java/com/pablissimo/sonar/LOCSensorTest.java b/src/test/java/com/pablissimo/sonar/LOCSensorTest.java deleted file mode 100644 index da34ba3..0000000 --- a/src/test/java/com/pablissimo/sonar/LOCSensorTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.pablissimo.sonar; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -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.measures.CoreMetrics; -import org.sonar.api.measures.Measure; - -public class LOCSensorTest { - FileSystem fileSystem; - FilePredicates filePredicates; - FilePredicate predicate; - - List files; - File file; - - LOCSensor sensor; - - SensorContext sensorContext; - - @Before - public void setUp() throws Exception { - this.file = mock(File.class); - doReturn(true).when(this.file).isFile(); - - this.files = new ArrayList(Arrays.asList(new File[] { - this.file - })); - - ArrayList inputFiles = new ArrayList(); - inputFiles.add(mock(InputFile.class)); - - this.fileSystem = mock(FileSystem.class); - this.predicate = mock(FilePredicate.class); - when(fileSystem.files(this.predicate)).thenReturn(this.files); - when(fileSystem.inputFiles(this.predicate)).thenReturn(inputFiles); - - this.sensorContext = mock(SensorContext.class); - - this.filePredicates = mock(FilePredicates.class); - when(this.fileSystem.predicates()).thenReturn(this.filePredicates); - when(filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY)).thenReturn(this.predicate); - when(fileSystem.hasFiles(this.predicate)).thenReturn(true); - - this.sensor = spy(new LOCSensor(this.fileSystem)); - } - - @Test - public void shouldExecuteOnProject_ReturnsTrue_WhenAnyTsFiles() { - assertTrue(this.sensor.shouldExecuteOnProject(null)); - } - - @Test - public void shouldExecuteOnProject_ReturnsFalse_WhenNoTsFiles() { - when(fileSystem.hasFiles(this.predicate)).thenReturn(false); - assertFalse(this.sensor.shouldExecuteOnProject(null)); - } - - @Test - public void getStringReturnsClassName() { - assertEquals("LOCSensor", new LOCSensor(this.fileSystem).toString()); - } - - @Test - public void basicBlockCommentsDiscounted() throws FileNotFoundException { - assertLineCount("blockcomments1", 2); - } - - @Test - public void blockCommentsNotConfusedWithNestedComments() throws FileNotFoundException { - assertLineCount("blockcomments2", 2); - } - - @Test - public void linesEndingWithABlockCommentStillCounted() throws FileNotFoundException { - assertLineCount("blockcomments3", 1); - } - - @Test - public void oneLineBlockCommentsDoNotConfuseCounting() throws FileNotFoundException { - assertLineCount("blockcomments4", 1); - } - - @Test - public void oneLineBlockCommentAtEndOfRealLineShouldNotConsiderNextLinesAsComments() throws FileNotFoundException { - assertLineCount("blockcomments5", 2); - } - - @Test - public void lineLevelCommentsAndWhitespaceHandledCorrectly() throws FileNotFoundException { - assertLineCount("linecomments", 2); - } - - private void assertLineCount(String testName, int expected) throws FileNotFoundException { - ClassLoader loader = getClass().getClassLoader(); - URL resource = loader.getResource("loc/" + testName + ".txt"); - InputStream stream = loader.getResourceAsStream("loc/" + testName + ".txt"); - this.file = new File(resource.getFile()); - - doReturn(new BufferedReader(new InputStreamReader(stream))).when(this.sensor).getReaderFromFile(any(InputFile.class)); - - final List> capturedMeasures = new ArrayList>(); - Answer captureMeasure = new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - capturedMeasures.add((Measure) invocation.getArguments()[1]); - return null; - } - }; - - when(this.sensorContext.saveMeasure(any(InputFile.class), any(Measure.class))).then(captureMeasure); - - this.sensor.analyse(null, this.sensorContext); - - assertEquals(1, capturedMeasures.size()); - assertEquals(CoreMetrics.NCLOC, capturedMeasures.get(0).getMetric()); - assertEquals(expected, (int) capturedMeasures.get(0).getIntValue()); - } -} diff --git a/src/test/java/com/pablissimo/sonar/TsCoverageSensorImplTest.java b/src/test/java/com/pablissimo/sonar/TsCoverageSensorImplTest.java new file mode 100644 index 0000000..f405f1e --- /dev/null +++ b/src/test/java/com/pablissimo/sonar/TsCoverageSensorImplTest.java @@ -0,0 +1,139 @@ +package com.pablissimo.sonar; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.sensor.coverage.CoverageType; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +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.CoreMetrics; + +public class TsCoverageSensorImplTest { + TsCoverageSensorImpl sensor; + Settings settings; + SensorContextTester context; + LCOVParser parser; + + DefaultInputFile file; + File lcovFile; + + @Before + public void setUp() throws Exception { + this.file = new DefaultInputFile("", "src/test/existing.ts").setLanguage(TypeScriptLanguage.LANGUAGE_KEY); + this.file.setLines(5); + + this.settings = mock(Settings.class); + when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn("lcovpath"); + + this.parser = mock(LCOVParser.class); + + this.sensor = spy(new TsCoverageSensorImpl()); + this.context = SensorContextTester.create(new File("")); + + this.context.fileSystem().add(this.file); + this.context.setSettings(this.settings); + + this.lcovFile = mock(File.class); + when(this.lcovFile.isFile()).thenReturn(true); + doReturn(this.lcovFile).when(this.sensor).getIOFile(any(File.class), eq("lcovpath")); + doReturn(this.parser).when(this.sensor).getParser(eq(this.context), any(File[].class)); + } + + @Test + public void savesZeroCoverage_IfNoReportAndSettingEnabled() { + when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn(""); + when(this.settings.getBoolean(TypeScriptPlugin.SETTING_FORCE_ZERO_COVERAGE)).thenReturn(true); + + this.sensor.execute(this.context, null); + + for (int i = 1; i <= this.file.lines(); i++) { + assertEquals((Integer) 0, this.context.lineHits(this.file.key(), CoverageType.UNIT, i)); + } + } + + @Test + public void savesNothing_IfNoReportAndSettingDisabled() { + when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn(""); + when(this.settings.getBoolean(TypeScriptPlugin.SETTING_FORCE_ZERO_COVERAGE)).thenReturn(false); + + this.sensor.execute(this.context, null); + + for (int i = 1; i <= this.file.lines(); i++) { + assertNull(this.context.lineHits(this.file.key(), CoverageType.UNIT, i)); + } + } + + @Test + public void doesNotCallParser_WhenNoLCOVPathSupplied() { + when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn(""); + + this.sensor.execute(this.context, null); + verify(this.parser, never()).parseFile(any(java.io.File.class)); + } + + @Test + public void savesZeroCoverage_IfParserOutputsNothingForFile() { + this.sensor.execute(this.context, null); + + for (int i = 1; i <= this.file.lines(); i++) { + assertEquals((Integer) 0, this.context.lineHits(this.file.key(), CoverageType.UNIT, i)); + } + } + + @Test + public void usesNonCommentLinesSetForLinesToCoverMetrics_IfSettingZeroCoverage() { + Map> nonCommentLineNumbersByFile = new HashMap>(); + HashSet nonCommentLineNumbers = new HashSet(); + nonCommentLineNumbers.add(1); + nonCommentLineNumbers.add(3); + nonCommentLineNumbers.add(5); + + nonCommentLineNumbersByFile.put(this.file, nonCommentLineNumbers); + + this.sensor.execute(this.context, nonCommentLineNumbersByFile); + + // Expect lines 1, 3 and 5 to have zero coverage... + assertEquals((Integer) 0, this.context.lineHits(this.file.key(), CoverageType.UNIT, 1)); + assertEquals((Integer) 0, this.context.lineHits(this.file.key(), CoverageType.UNIT, 3)); + assertEquals((Integer) 0, this.context.lineHits(this.file.key(), CoverageType.UNIT, 5)); + + // and lines 2 and 5 to not have any coverage since they're not counted as code + // according to our supplied map + assertNull(this.context.lineHits(this.file.key(), CoverageType.UNIT, 2)); + assertNull(this.context.lineHits(this.file.key(), CoverageType.UNIT, 4)); + } + + @Test + public void savesNoCoverage_IfNotFoundFilesAreIgnored() { + when(this.settings.getBoolean(TypeScriptPlugin.SETTING_IGNORE_NOT_FOUND)).thenReturn(true); + this.sensor.execute(this.context, null); + + for (int i = 1; i <= this.file.lines(); i++) { + assertNull(this.context.lineHits(this.file.key(), CoverageType.UNIT, i)); + } + } + + @Test + public void savesCoverage_IfParserOutputHasDetailsForFile() { + HashMap allFilesCoverage = new HashMap(); + NewCoverage fileCoverage = spy(this.context.newCoverage()); + allFilesCoverage.put(this.file, fileCoverage); + + when(this.parser.parseFile(this.lcovFile)).thenReturn(allFilesCoverage); + + this.sensor.execute(this.context, null); + + verify(fileCoverage, times(1)).save(); + } +} diff --git a/src/test/java/com/pablissimo/sonar/TsCoverageSensorTest.java b/src/test/java/com/pablissimo/sonar/TsCoverageSensorTest.java deleted file mode 100644 index 93fd908..0000000 --- a/src/test/java/com/pablissimo/sonar/TsCoverageSensorTest.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.pablissimo.sonar; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -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.config.Settings; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.CoverageMeasuresBuilder; -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 edu.emory.mathcs.backport.java.util.Arrays; - -public class TsCoverageSensorTest { - TsCoverageSensor sensor; - Settings settings; - FilePredicates filePredicates; - FileSystem fileSystem; - SensorContext context; - List files; - org.sonar.api.resources.File sonarFile; - File file; - FilePredicate predicate; - LCOVParser parser; - - @Before - public void setUp() throws Exception { - this.file = mock(File.class); - doReturn(true).when(this.file).isFile(); - - this.files = new ArrayList(Arrays.asList(new File[] { - this.file - })); - - this.fileSystem = mock(FileSystem.class); - this.predicate = mock(FilePredicate.class); - when(fileSystem.files(this.predicate)).thenReturn(this.files); - - this.settings = mock(Settings.class); - when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn("lcovpath"); - - this.filePredicates = mock(FilePredicates.class); - when(this.fileSystem.predicates()).thenReturn(this.filePredicates); - when(filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY)).thenReturn(this.predicate); - - this.sonarFile = mock(org.sonar.api.resources.File.class); - - this.parser = mock(LCOVParser.class); - - this.sensor = spy(new TsCoverageSensor(fileSystem, settings)); - doReturn(this.sonarFile).when(this.sensor).fileFromIoFile(any(java.io.File.class), any(Project.class)); - doReturn(this.file).when(this.sensor).getIOFile(any(java.io.File.class), any(String.class)); - doReturn(this.parser).when(this.sensor).getParser(any(File.class)); - this.context = mock(SensorContext.class); - } - - @Test - public void shouldExecuteOnProject_ReturnsTrue_WhenAnyTsFiles() { - assertTrue(this.sensor.shouldExecuteOnProject(null)); - } - - @Test - public void shouldExecuteOnProject_ReturnsFalse_WhenNoTsFiles() { - when(fileSystem.files(this.predicate)).thenReturn(new ArrayList()); - assertFalse(this.sensor.shouldExecuteOnProject(null)); - } - - @Test - public void savesZeroValues_IfNoReportAndSettingEnabled() { - when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn(""); - when(this.settings.getBoolean(TypeScriptPlugin.SETTING_FORCE_ZERO_COVERAGE)).thenReturn(true); - - Measure linesMeasure = mock(Measure.class); - when(this.context.getMeasure(eq(this.sonarFile), eq(CoreMetrics.LINES))).thenReturn(linesMeasure); - - Measure nclocLines = mock(Measure.class); - when(nclocLines.getIntValue()).thenReturn(5); - when(nclocLines.getValue()).thenReturn(5.0); - when(this.context.getMeasure(eq(this.sonarFile), eq(CoreMetrics.NCLOC))).thenReturn(nclocLines); - - this.sensor.analyse(mock(Project.class), this.context); - verify(context).saveMeasure(eq(this.sonarFile), any(Measure.class)); - verify(context).saveMeasure(eq(this.sonarFile), eq(CoreMetrics.LINES_TO_COVER), eq(5.0)); - verify(context).saveMeasure(eq(this.sonarFile), eq(CoreMetrics.UNCOVERED_LINES), eq(5.0)); - } - - @Test - public void savesNothing_IfNoReportAndSettingDisabled() { - when(this.settings.getString(TypeScriptPlugin.SETTING_LCOV_REPORT_PATH)).thenReturn(""); - when(this.settings.getBoolean(TypeScriptPlugin.SETTING_FORCE_ZERO_COVERAGE)).thenReturn(false); - this.sensor.analyse(mock(Project.class), this.context); - verify(context, never()).saveMeasure(any(Resource.class), any(Measure.class)); - verify(context, never()).saveMeasure(any(Resource.class), any(Metric.class), any(Double.class)); - } - - @Test - public void doesNotCallParser_WhenNoLCOVPathSupplied() { - doReturn(false).when(this.file).isFile(); - this.sensor.analyse(mock(Project.class), this.context); - verify(this.parser, never()).parseFile(any(java.io.File.class)); - } - - @Test - public void savesZeroCoverage_IfParserOutputsNothingForFile() { - Measure linesMeasure = mock(Measure.class); - when(this.context.getMeasure(eq(this.sonarFile), eq(CoreMetrics.LINES))).thenReturn(linesMeasure); - - Measure nclocLines = mock(Measure.class); - when(nclocLines.getIntValue()).thenReturn(5); - when(nclocLines.getValue()).thenReturn(5.0); - when(this.context.getMeasure(eq(this.sonarFile), eq(CoreMetrics.NCLOC))).thenReturn(nclocLines); - - this.sensor.analyse(mock(Project.class), this.context); - verify(context).saveMeasure(eq(this.sonarFile), any(Measure.class)); - verify(context).saveMeasure(eq(this.sonarFile), eq(CoreMetrics.LINES_TO_COVER), eq(5.0)); - verify(context).saveMeasure(eq(this.sonarFile), eq(CoreMetrics.UNCOVERED_LINES), eq(5.0)); - } - - @Test - public void savesNoCoverage_IfNotFoundFilesAreIgnored() { - when(this.settings.getBoolean(TypeScriptPlugin.SETTING_IGNORE_NOT_FOUND)).thenReturn(true); - - Measure linesMeasure = mock(Measure.class); - when(this.context.getMeasure(eq(this.sonarFile), eq(CoreMetrics.LINES))).thenReturn(linesMeasure); - - Measure nclocLines = mock(Measure.class); - when(nclocLines.getIntValue()).thenReturn(5); - when(nclocLines.getValue()).thenReturn(5.0); - when(this.context.getMeasure(eq(this.sonarFile), eq(CoreMetrics.NCLOC))).thenReturn(nclocLines); - - this.sensor.analyse(mock(Project.class), this.context); - verify(context, never()).saveMeasure(eq(this.sonarFile), any(Measure.class)); - verify(context, never()).saveMeasure(eq(this.sonarFile), eq(CoreMetrics.LINES_TO_COVER), any(Double.class)); - verify(context, never()).saveMeasure(eq(this.sonarFile), eq(CoreMetrics.UNCOVERED_LINES), any(Double.class)); - } - - @Test - public void savesCoverage_IfParserOutputHasDetailsForFile() { - when(this.file.getAbsolutePath()).thenReturn("path"); - - Map coverage = new HashMap(); - CoverageMeasuresBuilder builder = CoverageMeasuresBuilder.create(); - builder.setHits(1, 1); - builder.setConditions(1, 1, 1); - - coverage.put("path", builder); - - when(this.parser.parseFile(any(File.class))).thenReturn(coverage); - - final List measuresSaved = new ArrayList(); - Answer measureTracker = new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - measuresSaved.add((Measure) invocation.getArguments()[1]); - return null; - } - }; - - when(this.context.saveMeasure(eq(this.sonarFile), any(Measure.class))).then(measureTracker); - this.sensor.analyse(mock(Project.class), this.context); - - assertEquals(7, measuresSaved.size()); - } -} diff --git a/src/test/java/com/pablissimo/sonar/TsLintExecutorImplTest.java b/src/test/java/com/pablissimo/sonar/TsLintExecutorImplTest.java index fd439fc..430d699 100644 --- a/src/test/java/com/pablissimo/sonar/TsLintExecutorImplTest.java +++ b/src/test/java/com/pablissimo/sonar/TsLintExecutorImplTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.sonar.api.utils.System2; import org.sonar.api.utils.command.Command; import org.sonar.api.utils.command.CommandExecutor; import org.sonar.api.utils.command.StreamConsumer; @@ -20,10 +21,14 @@ public class TsLintExecutorImplTest { TsLintExecutorImpl executorImpl; CommandExecutor commandExecutor; + System2 system; + @Before public void setUp() throws Exception { + this.system = mock(System2.class); + this.commandExecutor = mock(CommandExecutor.class); - this.executorImpl = spy(new TsLintExecutorImpl()); + this.executorImpl = spy(new TsLintExecutorImpl(this.system)); when(this.executorImpl.createExecutor()).thenReturn(this.commandExecutor); } @@ -49,7 +54,7 @@ public Integer answer(InvocationOnMock invocation) throws Throwable { Command theCommand = capturedCommands.get(0); long theTimeout = capturedTimeouts.get(0); - assertEquals("node \"path/to/tslint\" --format json --rules-dir \"path/to/rules\" --config \"path/to/config\" \"path/to/file\" \"path/to/another\"", theCommand.toCommandLine()); + assertEquals("node path/to/tslint --format json --rules-dir path/to/rules --config path/to/config path/to/file path/to/another", theCommand.toCommandLine()); // Expect one timeout period per file processed assertEquals(2 * 40000, theTimeout); } @@ -105,7 +110,7 @@ public void BatchesExecutions_IfTooManyFilesForCommandLine() { String firstBatch = "first batch"; while (currentLength + 12 < TsLintExecutorImpl.MAX_COMMAND_LENGTH - standardCmdLength) { filenames.add(firstBatch); - currentLength += firstBatch.length() + 3; // 1 for the space, 2 for the quotes + currentLength += firstBatch.length() + 1; // 1 for the space } filenames.add("second batch"); diff --git a/src/test/java/com/pablissimo/sonar/TsLintSensorTest.java b/src/test/java/com/pablissimo/sonar/TsLintSensorTest.java index 220ede9..f157c76 100644 --- a/src/test/java/com/pablissimo/sonar/TsLintSensorTest.java +++ b/src/test/java/com/pablissimo/sonar/TsLintSensorTest.java @@ -5,286 +5,167 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -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.component.ResourcePerspectives; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor; +import org.sonar.api.batch.sensor.internal.SensorContextTester; import org.sonar.api.config.Settings; -import org.sonar.api.issue.Issuable; -import org.sonar.api.issue.Issuable.IssueBuilder; -import org.sonar.api.issue.Issue; -import org.sonar.api.resources.Project; -import org.sonar.api.rules.RuleFinder; import com.pablissimo.sonar.model.TsLintIssue; import com.pablissimo.sonar.model.TsLintPosition; -import org.sonar.api.server.debt.DebtRemediationFunction; -import org.sonar.api.server.rule.RulesDefinition; + +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.System2; public class TsLintSensorTest { Settings settings; - FileSystem fileSystem; - ResourcePerspectives perspectives; - RuleFinder ruleFinder; - FilePredicates filePredicates; - FilePredicate predicate; - Issuable issuable; - IssueBuilder issueBuilder; - - List files; - File file; - org.sonar.api.resources.File sonarFile; + + DefaultInputFile file; TsLintExecutor executor; TsLintParser parser; TsLintSensor sensor; + SensorContextTester context; + + PathResolver resolver; + HashMap fakePathResolutions; + + System2 system; + @Before public void setUp() throws Exception { + this.fakePathResolutions = new HashMap(); + this.fakePathResolutions.put(TypeScriptPlugin.SETTING_TS_LINT_PATH, "/path/to/tslint"); + this.fakePathResolutions.put(TypeScriptPlugin.SETTING_TS_LINT_CONFIG_PATH, "/path/to/tslint.json"); + this.fakePathResolutions.put(TypeScriptPlugin.SETTING_TS_LINT_RULES_DIR, "/path/to/rules"); + this.settings = mock(Settings.class); - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_PATH)).thenReturn("/path/to/tslint"); - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_CONFIG_PATH)).thenReturn("/path/to/tslint.json"); + this.system = mock(System2.class); + when(this.settings.getInt(TypeScriptPlugin.SETTING_TS_LINT_TIMEOUT)).thenReturn(45000); - when(this.settings.getKeysStartingWith(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS)) - .thenReturn(new ArrayList() {{ - add(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg1.name"); - add(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg1.config"); - add(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg2.name"); - add(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg2.config"); - add(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg3.name"); - add(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg3.config"); - }}); - - // config with one disabled rule - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg1.config")) - .thenReturn( - "custom-rule-1=false\n" + - "custom-rule-1.name=test rule #1\n" + - "custom-rule-1.severity=MAJOR\n" + - "custom-rule-1.description=#1 description\n" + - "\n" - ); - - // config with a basic rule (no debt settings) - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg2.config")) - .thenReturn( - "custom-rule-2=true\n" + - "custom-rule-2.name=test rule #2\n" + - "custom-rule-2.severity=MINOR\n" + - "custom-rule-2.description=#2 description\n" + - "\n" - ); - - // config with a advanced rules (including debt settings) - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_RULE_CONFIGS + ".cfg3.config")) - .thenReturn( - "custom-rule-3=true\n" + - "custom-rule-3.name=test rule #3\n" + - "custom-rule-3.severity=INFO\n" + - "custom-rule-3.description=#3 description\n" + - "custom-rule-3.debtFunc=" + DebtRemediationFunction.Type.CONSTANT_ISSUE + "\n" + - "custom-rule-3.debtScalar=15min\n" + - "custom-rule-3.debtOffset=1min\n" + - "\n" + - "custom-rule-4=true\n" + - "custom-rule-4.name=test rule #4\n" + - "custom-rule-4.severity=MINOR\n" + - "custom-rule-4.description=#4 description\n" + - "custom-rule-4.debtFunc=" + DebtRemediationFunction.Type.LINEAR + "\n" + - "custom-rule-4.debtScalar=5min\n" + - "custom-rule-4.debtOffset=2h\n" + - "custom-rule-4.debtType=" + RulesDefinition.SubCharacteristics.EXCEPTION_HANDLING + "\n" + - "\n" + - "custom-rule-5=true\n" + - "custom-rule-5.name=test rule #5\n" + - "custom-rule-5.severity=MAJOR\n" + - "custom-rule-5.description=#5 description\n" + - "custom-rule-5.debtFunc=" + DebtRemediationFunction.Type.LINEAR_OFFSET + "\n" + - "custom-rule-5.debtScalar=30min\n" + - "custom-rule-5.debtOffset=15min\n" + - "custom-rule-5.debtType=" + RulesDefinition.SubCharacteristics.HARDWARE_RELATED_PORTABILITY + "\n" + - "\n" - ); - - this.fileSystem = mock(FileSystem.class); - this.perspectives = mock(ResourcePerspectives.class); - this.issuable = mock(Issuable.class); - this.issueBuilder = mock(IssueBuilder.class, RETURNS_DEEP_STUBS); - when(this.issuable.newIssueBuilder()).thenReturn(this.issueBuilder); - doReturn(this.issuable).when(this.perspectives).as(eq(Issuable.class), any(org.sonar.api.resources.File.class)); - this.ruleFinder = mock(RuleFinder.class); - - this.file = mock(File.class); - doReturn(true).when(this.file).isFile(); - doReturn("/path/to/file").when(this.file).getAbsolutePath(); - - this.files = new ArrayList(Arrays.asList(new File[] { - this.file - })); - - this.predicate = mock(FilePredicate.class); - when(fileSystem.files(this.predicate)).thenReturn(this.files); - when(fileSystem.baseDir()).thenReturn(new File("/path/to/base/of/project/")); - - this.filePredicates = mock(FilePredicates.class); - when(this.fileSystem.predicates()).thenReturn(this.filePredicates); - when(filePredicates.hasLanguage(TypeScriptLanguage.LANGUAGE_KEY)).thenReturn(this.predicate); - - this.sonarFile = mock(org.sonar.api.resources.File.class); - this.executor = mock(TsLintExecutor.class); this.parser = mock(TsLintParser.class); - this.sensor = spy(new TsLintSensor(settings, fileSystem, perspectives, ruleFinder)); - doReturn(this.sonarFile).when(this.sensor).getFileFromIOFile(eq(this.file), any(Project.class)); + this.resolver = mock(PathResolver.class); + this.sensor = spy(new TsLintSensor(settings, this.system)); + + this.file = new DefaultInputFile("", "path/to/file") + .setLanguage(TypeScriptLanguage.LANGUAGE_KEY) + .setLines(1) + .setLastValidOffset(999) + .setOriginalLineOffsets(new int[] { 5 }); + doReturn(this.executor).when(this.sensor).getTsLintExecutor(); doReturn(this.parser).when(this.sensor).getTsLintParser(); - doReturn(true).when(this.sensor).doesFileExist(any(File.class)); + doReturn(this.resolver).when(this.sensor).getPathResolver(); - // For now, pretend all paths are absolute - when(this.sensor.getAbsolutePath(any(String.class))).thenAnswer(new Answer() { - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - return (String) invocation.getArguments()[0]; - } - }); - } - - @After - public void tearDown() throws Exception { + this.context = SensorContextTester.create(new File("")); + this.context.fileSystem().add(this.file); + + ActiveRulesBuilder rulesBuilder = new ActiveRulesBuilder(); + rulesBuilder.create(RuleKey.of(TsRulesDefinition.REPOSITORY_NAME, "rule name")).activate(); + + this.context.setActiveRules(rulesBuilder.build()); + + // Pretend all paths are absolute + Answer lookUpFakePath = new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + return fakePathResolutions.get(invocation.getArgumentAt(1, String.class)); + } + }; + + doAnswer(lookUpFakePath).when(this.resolver).getPath(any(SensorContext.class), any(String.class), any(String.class)); } @Test - public void shouldExecuteOnProject_ReturnsTrue_WhenAnyTsFiles() { - assertTrue(this.sensor.shouldExecuteOnProject(null)); + public void describe_setsName() { + DefaultSensorDescriptor desc = new DefaultSensorDescriptor(); + this.sensor.describe(desc); + + assertNotNull(desc.name()); } - + @Test - public void shouldExecuteOnProject_ReturnsFalse_WhenNoTsFiles() { - when(fileSystem.files(this.predicate)).thenReturn(new ArrayList()); - assertFalse(this.sensor.shouldExecuteOnProject(null)); + public void describe_setsLanguage() { + DefaultSensorDescriptor desc = new DefaultSensorDescriptor(); + this.sensor.describe(desc); + + assertEquals(TypeScriptLanguage.LANGUAGE_KEY, desc.languages().iterator().next()); } - + @Test - public void analyse_addsIssues() { + public void execute_addsIssues() { TsLintIssue issue = new TsLintIssue(); issue.setFailure("failure"); issue.setRuleName("rule name"); - issue.setName("/path/to/file"); + issue.setName(this.file.absolutePath().replace("\\", "/")); TsLintPosition startPosition = new TsLintPosition(); - startPosition.setLine(1); + startPosition.setLine(0); issue.setStartPosition(startPosition); TsLintIssue[][] issues = new TsLintIssue[][] { new TsLintIssue[] { issue } }; - - final List capturedIssues = new ArrayList(); - Answer captureIssue = new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - capturedIssues.add((Issue) invocation.getArguments()[0]); - return null; - } - }; - - when(this.issuable.addIssue(any(Issue.class))).then(captureIssue); - when(parser.parse(any(String.class))).thenReturn(issues); - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); - - assertEquals(1, capturedIssues.size()); + + when(this.parser.parse(any(String.class))).thenReturn(issues); + this.sensor.execute(this.context); + + assertEquals(1, this.context.allIssues().size()); + assertEquals("rule name", this.context.allIssues().iterator().next().ruleKey().rule()); } @Test - public void analyse_doesNothingWhenNotConfigured() throws IOException { - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_PATH)).thenReturn(null); - - when(this.fileSystem.files(any(FilePredicate.class))).thenReturn(new ArrayList()); - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); + public void execute_doesNothingWhenNotConfigured() throws IOException { + this.fakePathResolutions.remove(TypeScriptPlugin.SETTING_TS_LINT_PATH); - verify(this.issuable, never()).addIssue(any(Issue.class)); + this.sensor.execute(this.context); + + verify(this.executor, times(0)).execute(any(String.class), any(String.class), any(String.class), any(List.class), any(Integer.class)); + + assertEquals(0, this.context.allIssues().size()); } - + @Test - public void analyse_doesNothingWhenNoConfigPathset() throws IOException { - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_CONFIG_PATH)).thenReturn(null); - when(this.fileSystem.files(any(FilePredicate.class))).thenReturn(new ArrayList()); - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); - - verify(this.issuable, never()).addIssue(any(Issue.class)); + public void execute_doesNothingWhenNoConfigPathset() throws IOException { + this.fakePathResolutions.remove(TypeScriptPlugin.SETTING_TS_LINT_CONFIG_PATH); + + this.sensor.execute(this.context); + + verify(this.executor, times(0)).execute(any(String.class), any(String.class), any(String.class), any(List.class), any(Integer.class)); + + assertEquals(0, this.context.allIssues().size()); } @Test - public void analyse_callsExecutorWithSuppliedTimeout() throws IOException { - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); + public void execute_callsExecutorWithSuppliedTimeout() throws IOException { + this.sensor.execute(this.context); verify(this.executor, times(1)).execute(any(String.class), any(String.class), any(String.class), any(List.class), eq(45000)); } @Test - public void analyze_callsExecutorWithAtLeast5000msTimeout() throws IOException { + public void execute_callsExecutorWithAtLeast5000msTimeout() throws IOException { when(this.settings.getInt(TypeScriptPlugin.SETTING_TS_LINT_TIMEOUT)).thenReturn(-500); - - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); + + this.sensor.execute(this.context); verify(this.executor, times(1)).execute(any(String.class), any(String.class), any(String.class), any(List.class), eq(5000)); } @Test - public void check_getExecutor() - { - TsLintSensor sensor = new TsLintSensor(settings, fileSystem, perspectives, ruleFinder); - TsLintExecutor executor = sensor.getTsLintExecutor(); - assertNotNull(executor); - } - - @Test - public void check_getParser() - { - TsLintSensor sensor = new TsLintSensor(settings, fileSystem, perspectives, ruleFinder); - TsLintParser parser = sensor.getTsLintParser(); - assertNotNull(parser); - } - - @Test - public void check_getTsRulesDefinition() - { - TsLintSensor sensor = new TsLintSensor(settings, fileSystem, perspectives, ruleFinder); - TsRulesDefinition rulesDef = sensor.getTsRulesDefinition(); - assertNotNull(rulesDef); - } - - @Test - public void analyse_usesServerConfiguredTsLintPath_whenSet() { - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_PATH)).thenReturn("/path/to/tslint"); - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); - - verify(this.executor).execute(eq("/path/to/tslint"), any(String.class), any(String.class), any(List.class), any(Integer.class)); - } - - @Test - public void analyse_fallsBackToDefaultTsLintPath_whenNoServerConfiguration() { - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_PATH)).thenReturn(null); - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); - - verify(this.executor).execute(eq(TsLintSensor.TSLINT_FALLBACK_PATH), any(String.class), any(String.class), any(List.class), any(Integer.class)); - } - - @Test - public void analyse_fallsBackToDefaultTsLintPath_whenServerConfigurationNonNullButEmpty() { - when(this.settings.getString(TypeScriptPlugin.SETTING_TS_LINT_PATH)).thenReturn(""); - this.sensor.analyse(mock(Project.class), mock(SensorContext.class)); + public void execute_callsExecutorWithConfiguredPaths() { + this.sensor.execute(this.context); - verify(this.executor).execute(eq(TsLintSensor.TSLINT_FALLBACK_PATH), any(String.class), any(String.class), any(List.class), any(Integer.class)); + verify(this.executor, times(1)).execute(eq("/path/to/tslint"), eq("/path/to/tslint.json"), eq("/path/to/rules"), any(List.class), any(Integer.class)); } } diff --git a/src/test/java/com/pablissimo/sonar/TsRulesDefinitionTest.java b/src/test/java/com/pablissimo/sonar/TsRulesDefinitionTest.java index 47c22a1..93f00ba 100644 --- a/src/test/java/com/pablissimo/sonar/TsRulesDefinitionTest.java +++ b/src/test/java/com/pablissimo/sonar/TsRulesDefinitionTest.java @@ -5,8 +5,8 @@ import org.junit.Test; import org.sonar.api.config.Settings; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; -import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.server.rule.RulesDefinition.Context; import org.sonar.api.server.rule.RulesDefinition.Rule; @@ -71,6 +71,7 @@ public void setUp() throws Exception { "custom-rule-3.debtFunc=" + DebtRemediationFunction.Type.CONSTANT_ISSUE + "\n" + "custom-rule-3.debtScalar=15min\n" + "custom-rule-3.debtOffset=1min\n" + + "custom-rule-3.debtType=INVALID_TYPE_GOES_HERE\n" + "\n" + "custom-rule-4=true\n" + "custom-rule-4.name=test rule #4\n" + @@ -79,7 +80,7 @@ public void setUp() throws Exception { "custom-rule-4.debtFunc=" + DebtRemediationFunction.Type.LINEAR + "\n" + "custom-rule-4.debtScalar=5min\n" + "custom-rule-4.debtOffset=2h\n" + - "custom-rule-4.debtType=" + RulesDefinition.SubCharacteristics.EXCEPTION_HANDLING + "\n" + + "custom-rule-4.debtType=" + RuleType.BUG.name() + "\n" + "\n" + "custom-rule-5=true\n" + "custom-rule-5.name=test rule #5\n" + @@ -88,7 +89,7 @@ public void setUp() throws Exception { "custom-rule-5.debtFunc=" + DebtRemediationFunction.Type.LINEAR_OFFSET + "\n" + "custom-rule-5.debtScalar=30min\n" + "custom-rule-5.debtOffset=15min\n" + - "custom-rule-5.debtType=" + RulesDefinition.SubCharacteristics.HARDWARE_RELATED_PORTABILITY + "\n" + + "custom-rule-5.debtType=" + RuleType.VULNERABILITY.name() + "\n" + "\n" ); @@ -510,7 +511,7 @@ public void ConfiguresAdditionalRules() { assertEquals(Severity.MINOR, rule2.severity()); assertEquals("#2 description", rule2.htmlDescription()); assertEquals(null, rule2.debtRemediationFunction()); - assertEquals(null, rule2.debtSubCharacteristic()); + assertEquals(RuleType.CODE_SMELL, rule2.type()); // cfg3 Rule rule3 = getRule("custom-rule-3"); @@ -522,13 +523,11 @@ public void ConfiguresAdditionalRules() { DebtRemediationFunction.Type.CONSTANT_ISSUE, rule3.debtRemediationFunction().type() ); - assertEquals(null, rule3.debtRemediationFunction().coefficient()); - assertEquals("15min", rule3.debtRemediationFunction().offset()); - assertEquals( - TsRulesDefinition.DEFAULT_RULE_DEBT_TYPE, - rule3.debtSubCharacteristic() - ); + assertEquals(null, rule3.debtRemediationFunction().gapMultiplier()); + assertEquals("15min", rule3.debtRemediationFunction().baseEffort()); + assertEquals(RuleType.CODE_SMELL, rule3.type()); + // cfg4 Rule rule4 = getRule("custom-rule-4"); assertNotNull(rule4); assertEquals("test rule #4", rule4.name()); @@ -538,28 +537,20 @@ public void ConfiguresAdditionalRules() { DebtRemediationFunction.Type.LINEAR, rule4.debtRemediationFunction().type() ); - assertEquals("5min", rule4.debtRemediationFunction().coefficient()); - assertEquals(null, rule4.debtRemediationFunction().offset()); - assertEquals( - RulesDefinition.SubCharacteristics.EXCEPTION_HANDLING, - rule4.debtSubCharacteristic() - ); + assertEquals("5min", rule4.debtRemediationFunction().gapMultiplier()); + assertEquals(null, rule4.debtRemediationFunction().baseEffort()); + assertEquals(RuleType.BUG, rule4.type()); + // cfg5 Rule rule5 = getRule("custom-rule-5"); assertNotNull(rule5); assertEquals("test rule #5", rule5.name()); assertEquals(Severity.MAJOR, rule5.severity()); assertEquals("#5 description", rule5.htmlDescription()); - assertEquals( - DebtRemediationFunction.Type.LINEAR_OFFSET, - rule5.debtRemediationFunction().type() - ); - assertEquals("30min", rule5.debtRemediationFunction().coefficient()); - assertEquals("15min", rule5.debtRemediationFunction().offset()); - assertEquals( - RulesDefinition.SubCharacteristics.HARDWARE_RELATED_PORTABILITY, - rule5.debtSubCharacteristic() - ); + assertEquals(RuleType.VULNERABILITY, rule5.type()); + + assertEquals("30min", rule5.debtRemediationFunction().gapMultiplier()); + assertEquals("15min", rule5.debtRemediationFunction().baseEffort()); } @Test @@ -597,8 +588,4 @@ public void CheckCustomRulesConfigNotProvided() { private Rule getRule(String name) { return this.context.repository(TsRulesDefinition.REPOSITORY_NAME).rule(name); } - - private RulesDefinition.Param getParam(Rule rule, String name) { - return rule.param(name); - } } diff --git a/src/test/java/com/pablissimo/sonar/TypeScriptPluginTest.java b/src/test/java/com/pablissimo/sonar/TypeScriptPluginTest.java index fa27c7b..db4c112 100644 --- a/src/test/java/com/pablissimo/sonar/TypeScriptPluginTest.java +++ b/src/test/java/com/pablissimo/sonar/TypeScriptPluginTest.java @@ -8,12 +8,15 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; +import org.codehaus.plexus.context.DefaultContext; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.sonar.api.Plugin; import org.sonar.api.Properties; import org.sonar.api.Property; import org.sonar.api.PropertyType; +import org.sonar.api.SonarQubeVersion; public class TypeScriptPluginTest { TypeScriptPlugin plugin; @@ -29,13 +32,17 @@ public void tearDown() throws Exception { @Test public void advertisesAppropriateExtensions() { - List extensions = this.plugin.getExtensions(); + Plugin.Context context = new Plugin.Context(SonarQubeVersion.V5_6); - assertEquals(6, extensions.size()); + this.plugin.define(context); + + List extensions = context.getExtensions(); + + assertEquals(5, extensions.size()); assertTrue(extensions.contains(TypeScriptRuleProfile.class)); assertTrue(extensions.contains(TypeScriptLanguage.class)); assertTrue(extensions.contains(TsLintSensor.class)); - assertTrue(extensions.contains(TsCoverageSensor.class)); + assertTrue(extensions.contains(CombinedCoverageSensor.class)); assertTrue(extensions.contains(TsRulesDefinition.class)); } diff --git a/src/test/java/com/pablissimo/sonar/model/TsLintRuleTest.java b/src/test/java/com/pablissimo/sonar/model/TsLintRuleTest.java index aa5ee34..629595d 100644 --- a/src/test/java/com/pablissimo/sonar/model/TsLintRuleTest.java +++ b/src/test/java/com/pablissimo/sonar/model/TsLintRuleTest.java @@ -2,6 +2,7 @@ import org.junit.Test; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; import org.sonar.api.server.debt.DebtRemediationFunction; import org.sonar.api.server.rule.RulesDefinition; @@ -25,7 +26,7 @@ public void ruleWithoutDebtRemediation() { assertEquals(DebtRemediationFunction.Type.CONSTANT_ISSUE, rule.debtRemediationFunction); assertEquals("0min", rule.debtRemediationScalar); assertEquals("0min", rule.debtRemediationOffset); - assertEquals(null, rule.debtCharacteristic); + assertEquals(null, rule.debtType); } @Test @@ -38,7 +39,7 @@ public void ruleWithDebtRemediation() { DebtRemediationFunction.Type.LINEAR_OFFSET, "1min", "2min", - RulesDefinition.SubCharacteristics.COMPILER_RELATED_PORTABILITY + RuleType.CODE_SMELL.name() ); assertEquals("key", rule.key); @@ -49,6 +50,6 @@ public void ruleWithDebtRemediation() { assertEquals(DebtRemediationFunction.Type.LINEAR_OFFSET, rule.debtRemediationFunction); assertEquals("1min", rule.debtRemediationScalar); assertEquals("2min", rule.debtRemediationOffset); - assertEquals(RulesDefinition.SubCharacteristics.COMPILER_RELATED_PORTABILITY, rule.debtCharacteristic); + assertEquals(RuleType.CODE_SMELL.name(), rule.debtType); } } diff --git a/src/test/resources/existing.ts b/src/test/resources/existing.ts new file mode 100644 index 0000000..9505b7a --- /dev/null +++ b/src/test/resources/existing.ts @@ -0,0 +1 @@ +// I am a TypeScript file, but without much of interest within it