diff --git a/CHANGELOG.md b/CHANGELOG.md index a7cf170..8c8ffdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # SonarQube Plugin for Flutter / Dart +## 0.3.2 + +#### Breaking + +- None. + +#### Experimental + +- None. + +#### Enhancements + +- Allow re-using an existing dartanalyzer report with `sonar.dart.analysis.reportPath` (thanks to [Peter Leibiger](https://github.com/kuhnroyal)) +- Add missing dart keywords `extension`, `on`, `mixin` (thanks to [Peter Leibiger](https://github.com/kuhnroyal)) +- Add pedantic 1.9.0 profile (thanks to [Daniel Morawetz](https://github.com/dmorawetz)) +- Faster analysis with 'flutter analyze' and support for different analysis modes with `sonar.flutter.analyzer.mode` (thanks to [Marc Reichelt](https://github.com/mreichelt)) + +#### Bug Fixes + +- Ensure analyzer encoding is UTF-8 (thanks to [Daniel Morawetz](https://github.com/dmorawetz)) + + ## 0.3.1 #### Breaking diff --git a/README.md b/README.md index 87e3acc..9bc6b68 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,19 @@ sonar.projectVersion=1.0 # Use commas to specify more than one folder. sonar.sources=lib sonar.tests=test - + # Encoding of the source code. Default is default system encoding. sonar.sourceEncoding=UTF-8 + +# Allows reuse of an existing analyzer report +# sonar.dart.analysis.reportPath= + +# Analyzer mode +# Can be: +# - flutter (flutter analyze) - default +# - dart (dart analyze) +# - legacy (dartanalyzer) +# sonar.flutter.analyzer.mode= ``` *For a complete list of available options, please refer to the [SonarQube documentation](https://docs.sonarqube.org/latest/analysis/analysis-parameters/).* @@ -84,10 +94,11 @@ Use the following commands from the root folder to start an analysis: ```console # Download dependencies flutter pub get -# Run tests -flutter test --machine > tests.output -# Compute coverage (--machine and --coverage cannot be run at once...) -flutter test --coverage +# Run tests with User feedback (in case some test are failing) +flutter test +# Run tests without user feedback regeneration tests.output and coverage/lcov.info +flutter test --machine --coverage > tests.output + # Run the analysis and publish to the SonarQube server sonar-scanner ``` diff --git a/dart-lang/pom.xml b/dart-lang/pom.xml index 57104c3..9307f67 100644 --- a/dart-lang/pom.xml +++ b/dart-lang/pom.xml @@ -3,7 +3,7 @@ sonar-flutter fr.insideapp.sonarqube - 0.3.1 + 0.3.2 4.0.0 diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/DartSensor.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/DartSensor.java index 6aaa9f2..abef2ed 100644 --- a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/DartSensor.java +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/DartSensor.java @@ -45,6 +45,7 @@ public class DartSensor implements Sensor { private static final Logger LOGGER = LoggerFactory.getLogger(DartSensor.class); private static final int EXECUTOR_TIMEOUT = 10000; public static final String DART_ANALYSIS_USE_EXISTING_OPTIONS_KEY = "sonar.dart.analysis.useExistingOptions"; + public static final String DART_ANALYSIS_USE_EXISTING_REPORT_PATH_KEY = "sonar.dart.analysis.reportPath"; @Override public void describe(SensorDescriptor sensorDescriptor) { diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/SourceLinesProvider.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/SourceLinesProvider.java index 493f506..d9c3a6a 100644 --- a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/SourceLinesProvider.java +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/SourceLinesProvider.java @@ -59,7 +59,7 @@ public SourceLine[] getLines(final InputStream inputStream, final Charset charse } sourceLines.add(new SourceLine(totalLines, count, global - count, global)); } catch (final Throwable e) { - LOGGER.warn("Error occured reading file", e); + LOGGER.warn("Error occurred reading file", e); } return sourceLines.toArray(new SourceLine[0]); diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/DartProfilePedantic190.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/DartProfilePedantic190.java new file mode 100644 index 0000000..03d866d --- /dev/null +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/DartProfilePedantic190.java @@ -0,0 +1,42 @@ +/* + * SonarQube Flutter Plugin + * Copyright (C) 2020 inside|app + * contact@insideapp.fr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package fr.insideapp.sonarqube.dart.lang.issues; + +import fr.insideapp.sonarqube.dart.lang.Dart; +import fr.insideapp.sonarqube.dart.lang.issues.dartanalyzer.DartAnalyzerRulesDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; + +public class DartProfilePedantic190 implements BuiltInQualityProfilesDefinition { + + private static final Logger LOGGER = LoggerFactory.getLogger(DartProfile.class); + public static final String DARTANALYZER_PROFILE_PATH = "fr/insideapp/sonarqube/dart/dartanalyzer/profile-pedantic.1.9.0.xml"; + + @Override + public void define(BuiltInQualityProfilesDefinition.Context context) { + + // dartanalyzer profile + LOGGER.info("Creating dartanalyzer pedantic 1.9.0 Profile"); + NewBuiltInQualityProfile nbiqp = context.createBuiltInQualityProfile("dartanalyzer pedantic 1.9.0", Dart.KEY); + XmlProfileParser.parse(DARTANALYZER_PROFILE_PATH, nbiqp); + nbiqp.done(); + } +} diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/XmlProfileParser.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/XmlProfileParser.java index bc53661..df2e009 100644 --- a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/XmlProfileParser.java +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/XmlProfileParser.java @@ -48,18 +48,23 @@ public static void parse(String path, NewBuiltInQualityProfile profile) { String repositoryKey = getChildContent(ruleElement, "repositoryKey"); String key = getChildContent(ruleElement, "key"); - NewBuiltInActiveRule newActiveRule = profile.activateRule(repositoryKey, key); + try { + NewBuiltInActiveRule newActiveRule = profile.activateRule(repositoryKey, key); - NodeList parameterNodeList = ruleElement.getElementsByTagName("parameter"); - XmlFile.asList(parameterNodeList).forEach(parameter -> { - Element parameterElement = (Element) parameter; - String paramKey = getChildContent(parameterElement, "key"); - String paramValue = getChildContent(parameterElement, "value"); - newActiveRule.overrideParam(paramKey, paramValue); - }); + NodeList parameterNodeList = ruleElement.getElementsByTagName("parameter"); + XmlFile.asList(parameterNodeList).forEach(parameter -> { + Element parameterElement = (Element) parameter; + String paramKey = getChildContent(parameterElement, "key"); + String paramValue = getChildContent(parameterElement, "value"); + newActiveRule.overrideParam(paramKey, paramValue); + }); - Optional priority = getOptionalChildContent(ruleElement, "priority"); - priority.ifPresent(newActiveRule::overrideSeverity); + Optional priority = getOptionalChildContent(ruleElement, "priority"); + priority.ifPresent(newActiveRule::overrideSeverity); + } catch (IllegalArgumentException e) { + // ignore, if rule was already registered in another profile + } + }); } catch (IOException e) { diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/AnalyzerMode.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/AnalyzerMode.java new file mode 100644 index 0000000..4f271d0 --- /dev/null +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/AnalyzerMode.java @@ -0,0 +1,29 @@ +/* + * SonarQube Flutter Plugin + * Copyright (C) 2020 inside|app + * contact@insideapp.fr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package fr.insideapp.sonarqube.dart.lang.issues.dartanalyzer; + +public enum AnalyzerMode { + flutter, + dart, + legacy; + + public static final AnalyzerMode defaultMode = flutter; +} diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParser.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParser.java index db0027b..176ee4a 100644 --- a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParser.java +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParser.java @@ -31,7 +31,7 @@ public List parse(String input) { List issues = new ArrayList<>(); String[] lines = input.split(System.getProperty("line.separator")); - Pattern pattern = Pattern.compile("(error|hint|lint)(.*)(-|•)(.*)(-|•)(.*):(.*):(.*)(-|•)(.*)"); + Pattern pattern = Pattern.compile("(hint|lint|info|warning|error)(.*)(-|•)(.*)(-|•)(.*):(.*):(.*)(-|•)(.*)"); for (int i = 0; i < lines.length; i++) { Matcher matcher = pattern.matcher(lines[i]); while (matcher.find()) { diff --git a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerSensor.java b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerSensor.java index 1cf7d4e..16d8ab6 100644 --- a/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerSensor.java +++ b/dart-lang/src/main/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerSensor.java @@ -23,6 +23,7 @@ import com.google.common.io.Resources; import fr.insideapp.sonarqube.dart.lang.Dart; import fr.insideapp.sonarqube.dart.lang.DartSensor; +import org.apache.commons.lang.NotImplementedException; import org.buildobjects.process.ProcBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,42 +47,99 @@ import java.util.Set; import java.util.stream.Collectors; -public class DartAnalyzerSensor implements Sensor { - private static final Logger LOGGER = LoggerFactory.getLogger(DartAnalyzerSensor.class); - private static final String ANALYZER_COMMAND = System.getProperty("os.name").toUpperCase().contains("WINDOWS") - ? "dartanalyzer.bat" - : "dartanalyzer"; - private static final int ANALYZER_TIMEOUT = 10 * 60 * 1000; - private static final String ANALYSIS_OPTIONS_FILENAME = "analysis_options.yaml"; - private static final String ANALYSIS_OPTIONS_FILE = "/fr/insideapp/sonarqube/dart/dartanalyzer/analysis_options.yaml"; - private static final Integer PAGE_SIZE = 50; - private boolean useExistingAnalysisOptions; - - @Override - public void describe(SensorDescriptor sensorDescriptor) { - sensorDescriptor.onlyOnLanguage(Dart.KEY).name("dartanalyzer sensor").onlyOnFileType(Type.MAIN); - } +import static java.util.Arrays.asList; - @Override - public void execute(SensorContext sensorContext) { - try { - verifyIfDartAnalyzerExists(); - - selectOptionFileToUse(sensorContext); - - recordIssues(sensorContext, buildIssues(getFilesWithAbsolutePath(sensorContext))); - - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - } finally { - if (!useExistingAnalysisOptions) { - restoreCurrentAnalysisOptionsFile(sensorContext); - } - } - } - private void selectOptionFileToUse(SensorContext sensorContext) throws IOException { - useExistingAnalysisOptions = getUseExistingAnalysisOptions(sensorContext); +public class DartAnalyzerSensor implements Sensor { + private static final Logger LOGGER = LoggerFactory.getLogger(DartAnalyzerSensor.class); + private static final String LEGACY_COMMAND = System.getProperty("os.name").toUpperCase().contains("WINDOWS") + ? "dartanalyzer.bat" + : "dartanalyzer"; + private static final String FLUTTER_COMMAND = System.getProperty("os.name").toUpperCase().contains("WINDOWS") + ? "flutter.bat" + : "flutter"; + private static final String DART_COMMAND = System.getProperty("os.name").toUpperCase().contains("WINDOWS") + ? "dart.bat" + : "dart"; + private static final int ANALYZER_TIMEOUT = 10 * 60 * 1000; + private static final String ANALYSIS_OPTIONS_FILENAME = "analysis_options.yaml"; + private static final String ANALYSIS_OPTIONS_FILE = "/fr/insideapp/sonarqube/dart/dartanalyzer/analysis_options.yaml"; + private static final Integer PAGE_SIZE = 50; + private boolean useExistingAnalysisOptions; + public static final String FLUTTER_ANALYZER_MODE = "sonar.flutter.analyzer.mode"; + public static final List FLUTTER_ANALYZER_MODE_OPTIONS = asList(AnalyzerMode.values()); + + @Override + public void describe(SensorDescriptor sensorDescriptor) { + sensorDescriptor.onlyOnLanguage(Dart.KEY).name("dartanalyzer sensor").onlyOnFileType(Type.MAIN); + } + + @Override + public void execute(SensorContext sensorContext) { + + AnalyzerMode analyzerMode = getAnalyzerMode(sensorContext); + LOGGER.info("Chosen analyzer mode: {}", analyzerMode); + + switch (analyzerMode) { + case legacy: + try { + + verifyIfDartAnalyzerExists(); + + selectOptionFileToUse(sensorContext); + + recordIssues(sensorContext, buildIssues(getFilesWithAbsolutePath(sensorContext))); + + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } finally { + if (!useExistingAnalysisOptions) { + restoreCurrentAnalysisOptionsFile(sensorContext); + } + } + break; + + case flutter: + recordIssuesFromAnalyzer(sensorContext, FLUTTER_COMMAND); + break; + + case dart: + recordIssuesFromAnalyzer(sensorContext, DART_COMMAND); + break; + + default: + throw new NotImplementedException(); + } + } + + private void recordIssuesFromAnalyzer(SensorContext sensorContext, String analyzerCommand) { + try { + final List issues = getIssuesFromAnalyzer(analyzerCommand); + recordIssues(sensorContext, issues); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + } + + private List getIssuesFromAnalyzer(String analyzerCommand) throws IOException { + try { + LOGGER.info("Running '{} analyze'...", analyzerCommand); + String output = new ProcBuilder(analyzerCommand, "analyze") + .withTimeoutMillis(ANALYZER_TIMEOUT) + .ignoreExitStatus() + .run() + .getOutputString(); + + List issues = new DartAnalyzerReportParser().parse(output); + LOGGER.info("Found issues: {}", issues.size()); + return issues; + } catch (Exception e) { + throw new IOException(e); + } + } + + private void selectOptionFileToUse(SensorContext sensorContext) throws IOException { + useExistingAnalysisOptions = getUseExistingAnalysisOptions(sensorContext); // Usage of existing option is required but file is missing // Or usage of existing option is not required @@ -90,199 +148,207 @@ private void selectOptionFileToUse(SensorContext sensorContext) throws IOExcepti } } - private void useDefaultAnalysisOptionsFile(SensorContext sensorContext) throws IOException { - LOGGER.debug("Either {} option is not set to true or {} file does not exists, use default analysis options instead", - DartSensor.DART_ANALYSIS_USE_EXISTING_OPTIONS_KEY, ANALYSIS_OPTIONS_FILENAME); - - useExistingAnalysisOptions = false; - - this.saveCurrentAnalysisOptionsFile(sensorContext); - this.createAnalysisOptionsFile(sensorContext); - } + private void useDefaultAnalysisOptionsFile(SensorContext sensorContext) throws IOException { + LOGGER.debug("Either {} option is not set to true or {} file does not exists, use default analysis options instead", + DartSensor.DART_ANALYSIS_USE_EXISTING_OPTIONS_KEY, ANALYSIS_OPTIONS_FILENAME); - private Boolean getUseExistingAnalysisOptions(SensorContext sensorContext) { - return sensorContext.config().getBoolean(DartSensor.DART_ANALYSIS_USE_EXISTING_OPTIONS_KEY).orElse(false); - } + useExistingAnalysisOptions = false; - private List buildIssues(List filesWithAbsolutePath) - throws IOException { - Set issues = new HashSet<>(); + this.saveCurrentAnalysisOptionsFile(sensorContext); + this.createAnalysisOptionsFile(sensorContext); + } - for (String paginatedFileBatch : getPaginatedFilesPaths(filesWithAbsolutePath)) { + private AnalyzerMode getAnalyzerMode(SensorContext sensorContext) { + return sensorContext.config() + .get(FLUTTER_ANALYZER_MODE) + .map(AnalyzerMode::valueOf) + .orElse(AnalyzerMode.defaultMode); + } - LOGGER.debug("Current file batch: {}", paginatedFileBatch); + private Boolean getUseExistingAnalysisOptions(SensorContext sensorContext) { + return sensorContext.config().getBoolean(DartSensor.DART_ANALYSIS_USE_EXISTING_OPTIONS_KEY).orElse(false); + } - try { - String output = new ProcBuilder(ANALYZER_COMMAND) - .withArgs(paginatedFileBatch.split(" ")) - .withTimeoutMillis(ANALYZER_TIMEOUT) - //.withExpectedExitStatuses(0, 1, 2, 3) - .ignoreExitStatus() - .run() - .getOutputString(); + private List buildIssues(List filesWithAbsolutePath) + throws IOException { + Set issues = new HashSet<>(); - issues.addAll(new DartAnalyzerReportParser().parse(output)); - } catch (Exception e) { - throw new IOException(e); - } + for (String paginatedFileBatch : getPaginatedFilesPaths(filesWithAbsolutePath)) { + LOGGER.debug("Current file batch: {}", paginatedFileBatch); - } - LOGGER.debug("Found issues: {}", issues.size()); - List result = new ArrayList<>(); - result.addAll(issues); - return result; - } + try { + byte[] outputBytes = new ProcBuilder(LEGACY_COMMAND) + .withArgs(paginatedFileBatch.split(" ")) + .withTimeoutMillis(ANALYZER_TIMEOUT) + //.withExpectedExitStatuses(0, 1, 2, 3) + .ignoreExitStatus() + .run() + .getOutputBytes(); - private List getPaginatedFilesPaths(List filesWithAbsolutePath) { - List paginated = new ArrayList(); + String output = new String(outputBytes, "UTF-8"); + issues.addAll(new DartAnalyzerReportParser().parse(output)); + } catch (Exception e) { + throw new IOException(e); + } - LOGGER.debug("Paging the files to execute the analyzer..."); - Integer pagingStart = 0; - Integer pagingRemainder = getPaginationRemainder(filesWithAbsolutePath); - Integer totalPages = getPaginationTotalPages(filesWithAbsolutePath, PAGE_SIZE, pagingRemainder); + } + LOGGER.debug("Found issues: {}", issues.size()); + List result = new ArrayList<>(); + result.addAll(issues); + return result; + } - for (int i = 0; i < totalPages; i++) { - LOGGER.debug("Current index: {}", i); + private List getPaginatedFilesPaths(List filesWithAbsolutePath) { + List paginated = new ArrayList(); - LOGGER.debug("Current paging start: {}", pagingStart); + LOGGER.debug("Paging the files to execute the analyzer..."); - Integer pagingEnd = getPagingEnd(pagingStart, pagingRemainder, totalPages, i); + Integer pagingStart = 0; + Integer pagingRemainder = getPaginationRemainder(filesWithAbsolutePath); + Integer totalPages = getPaginationTotalPages(filesWithAbsolutePath, PAGE_SIZE, pagingRemainder); - paginated.add(getFilesPathsSplitBySpace(filesWithAbsolutePath, pagingStart, pagingEnd)); + for (int i = 0; i < totalPages; i++) { + LOGGER.debug("Current index: {}", i); - pagingStart += PAGE_SIZE; - } - return paginated; - } + LOGGER.debug("Current paging start: {}", pagingStart); - private Integer getPagingEnd(Integer pagingStart, Integer pagingRemainder, Integer totalPages, int i) { - Integer pagingEnd = pagingStart + PAGE_SIZE; - if (isLastPage(pagingRemainder, totalPages, i)) { - pagingEnd = pagingStart + pagingRemainder; - } - LOGGER.debug("Current paging end: {}", pagingEnd); - return pagingEnd; - } - - private String getFilesPathsSplitBySpace(List filesWithAbsolutePath, Integer pagingStart, - Integer pagingEnd) { - return filesWithAbsolutePath.subList(pagingStart, pagingEnd).stream().collect(Collectors.joining(" ")); - } - - private boolean isLastPage(Integer pagingRemainder, Integer totalPages, int i) { - return pagingRemainder != 0 && i + 1 == totalPages; - } - - private Integer getPaginationRemainder(List filesWithAbsolutePath) { - Integer remainder = filesWithAbsolutePath.size() % PAGE_SIZE; - - LOGGER.debug("Paging remainder: {}", remainder); - - return remainder; - } - - private Integer getPaginationTotalPages(List filesWithAbsolutePath, final Integer PAGE_SIZE, - Integer remainder) { - Integer total = filesWithAbsolutePath.size() / PAGE_SIZE; - - if (remainder != 0) { - total++; - } - LOGGER.debug("Paging total items: {}", total); - return total; - } + Integer pagingEnd = getPagingEnd(pagingStart, pagingRemainder, totalPages, i); - private List getFilesWithAbsolutePath(SensorContext sensorContext) { - List filesWithAbsolutePath = new ArrayList<>(); + paginated.add(getFilesPathsSplitBySpace(filesWithAbsolutePath, pagingStart, pagingEnd)); - FileSystem fileSystem = getFileSystem(sensorContext); + pagingStart += PAGE_SIZE; + } + return paginated; + } - FilePredicate mainFilePredicate = getFilesFilter(fileSystem); - - String absolutePath = fileSystem.baseDir().getAbsolutePath(); - - LOGGER.debug("Files absolute path: {}", absolutePath); - - fileSystem.inputFiles(mainFilePredicate).forEach(s -> { - LOGGER.debug("Input file path: {}", s.toString()); - - String fullPath = new StringBuilder(absolutePath).append(File.separator).append(s.toString().replace("/", File.separator)) - .toString(); - - LOGGER.debug("Current file full path: {}", fullPath); - - filesWithAbsolutePath.add(fullPath); - }); - - return filesWithAbsolutePath; - } - - private FilePredicate getFilesFilter(FileSystem fileSystem) { - FilePredicate mainFilePredicate = fileSystem.predicates().and( - fileSystem.predicates().hasType(InputFile.Type.MAIN), fileSystem.predicates().hasLanguage(Dart.KEY)); - return mainFilePredicate; - } - - private FileSystem getFileSystem(SensorContext sensorContext) { - FileSystem fileSystem = sensorContext.fileSystem(); - return fileSystem; - } - - private void recordIssues(SensorContext sensorContext, List issues) { - // Record issues - issues.forEach(i -> { - File file = new File(sensorContext.fileSystem().baseDir(), i.getFilePath()); - LOGGER.debug("Inside issue forEach, file absolute path: {}", file.getAbsolutePath()); - - FilePredicate fp = sensorContext.fileSystem().predicates().hasAbsolutePath(file.getAbsolutePath()); - if (!sensorContext.fileSystem().hasFiles(fp)) { - LOGGER.warn("File not included in SonarQube {}", file.getAbsoluteFile()); - } else { - InputFile inputFile = sensorContext.fileSystem().inputFile(fp); - NewIssueLocation nil = new DefaultIssueLocation().on(inputFile) - .at(inputFile.selectLine(i.getLineNumber())).message(i.getMessage()); - sensorContext.newIssue().forRule(RuleKey.of(DartAnalyzerRulesDefinition.REPOSITORY_KEY, i.getRuleId())) - .at(nil).save(); - } - }); - } - - private void verifyIfDartAnalyzerExists() { - LOGGER.debug("Verify dart analyser..."); - new ProcBuilder(ANALYZER_COMMAND).withArg("-h").run(); - LOGGER.debug("Verify dart analyser done"); - } - - private boolean existsAnalysisOptionsFile(SensorContext sensorContext) { - return new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME).exists(); - } - - private void saveCurrentAnalysisOptionsFile(SensorContext sensorContext) { - File analysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME); - if (analysisOptionsFile.exists()) { - analysisOptionsFile - .renameTo(new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME + ".sonar")); - LOGGER.info("Backup of original analysis_options.yaml file to {}", ANALYSIS_OPTIONS_FILENAME + ".sonar"); - } - } - - private void createAnalysisOptionsFile(SensorContext sensorContext) throws IOException { - File analysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME); - URL inputUrl = getClass().getResource(ANALYSIS_OPTIONS_FILE); - Resources.asByteSource(inputUrl).copyTo(Files.asByteSink(analysisOptionsFile)); - } - - private void restoreCurrentAnalysisOptionsFile(SensorContext sensorContext) { - File analysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME); - File currentAnalysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), - ANALYSIS_OPTIONS_FILENAME + ".sonar"); - if (currentAnalysisOptionsFile.exists()) { - currentAnalysisOptionsFile.renameTo(analysisOptionsFile); - LOGGER.info("Restored original analysis_options.yaml file"); - } else { - analysisOptionsFile.delete(); - } - } + private Integer getPagingEnd(Integer pagingStart, Integer pagingRemainder, Integer totalPages, int i) { + Integer pagingEnd = pagingStart + PAGE_SIZE; + if (isLastPage(pagingRemainder, totalPages, i)) { + pagingEnd = pagingStart + pagingRemainder; + } + LOGGER.debug("Current paging end: {}", pagingEnd); + return pagingEnd; + } + + private String getFilesPathsSplitBySpace(List filesWithAbsolutePath, Integer pagingStart, + Integer pagingEnd) { + return filesWithAbsolutePath.subList(pagingStart, pagingEnd).stream().collect(Collectors.joining(" ")); + } + + private boolean isLastPage(Integer pagingRemainder, Integer totalPages, int i) { + return pagingRemainder != 0 && i + 1 == totalPages; + } + + private Integer getPaginationRemainder(List filesWithAbsolutePath) { + Integer remainder = filesWithAbsolutePath.size() % PAGE_SIZE; + + LOGGER.debug("Paging remainder: {}", remainder); + + return remainder; + } + + private Integer getPaginationTotalPages(List filesWithAbsolutePath, final Integer PAGE_SIZE, + Integer remainder) { + Integer total = filesWithAbsolutePath.size() / PAGE_SIZE; + + if (remainder != 0) { + total++; + } + LOGGER.debug("Paging total items: {}", total); + return total; + } + + private List getFilesWithAbsolutePath(SensorContext sensorContext) { + List filesWithAbsolutePath = new ArrayList<>(); + + FileSystem fileSystem = getFileSystem(sensorContext); + + FilePredicate mainFilePredicate = getFilesFilter(fileSystem); + + String absolutePath = fileSystem.baseDir().getAbsolutePath(); + + LOGGER.debug("Files absolute path: {}", absolutePath); + + fileSystem.inputFiles(mainFilePredicate).forEach(s -> { + LOGGER.debug("Input file path: {}", s.toString()); + + String fullPath = new StringBuilder(absolutePath).append(File.separator).append(s.toString().replace("/", File.separator)) + .toString(); + + LOGGER.debug("Current file full path: {}", fullPath); + + filesWithAbsolutePath.add(fullPath); + }); + + return filesWithAbsolutePath; + } + + private FilePredicate getFilesFilter(FileSystem fileSystem) { + FilePredicate mainFilePredicate = fileSystem.predicates().and( + fileSystem.predicates().hasType(InputFile.Type.MAIN), fileSystem.predicates().hasLanguage(Dart.KEY)); + return mainFilePredicate; + } + + private FileSystem getFileSystem(SensorContext sensorContext) { + FileSystem fileSystem = sensorContext.fileSystem(); + return fileSystem; + } + + private void recordIssues(SensorContext sensorContext, List issues) { + // Record issues + issues.forEach(i -> { + File file = new File(sensorContext.fileSystem().baseDir(), i.getFilePath()); + LOGGER.debug("Inside issue forEach, file absolute path: {}", file.getAbsolutePath()); + + FilePredicate fp = sensorContext.fileSystem().predicates().hasAbsolutePath(file.getAbsolutePath()); + if (!sensorContext.fileSystem().hasFiles(fp)) { + LOGGER.warn("File not included in SonarQube {}", file.getAbsoluteFile()); + } else { + InputFile inputFile = sensorContext.fileSystem().inputFile(fp); + NewIssueLocation nil = new DefaultIssueLocation().on(inputFile) + .at(inputFile.selectLine(i.getLineNumber())).message(i.getMessage()); + sensorContext.newIssue().forRule(RuleKey.of(DartAnalyzerRulesDefinition.REPOSITORY_KEY, i.getRuleId())) + .at(nil).save(); + } + }); + } + + private void verifyIfDartAnalyzerExists() { + LOGGER.debug("Verify dart analyser..."); + new ProcBuilder(LEGACY_COMMAND).withArg("-h").run(); + LOGGER.debug("Verify dart analyser done"); + } + + private boolean existsAnalysisOptionsFile(SensorContext sensorContext) { + return new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME).exists(); + } + + private void saveCurrentAnalysisOptionsFile(SensorContext sensorContext) { + File analysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME); + if (analysisOptionsFile.exists()) { + analysisOptionsFile + .renameTo(new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME + ".sonar")); + LOGGER.info("Backup of original analysis_options.yaml file to {}", ANALYSIS_OPTIONS_FILENAME + ".sonar"); + } + } + + private void createAnalysisOptionsFile(SensorContext sensorContext) throws IOException { + File analysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME); + URL inputUrl = DartAnalyzerSensor.class.getResource(ANALYSIS_OPTIONS_FILE); + Resources.asByteSource(inputUrl).copyTo(Files.asByteSink(analysisOptionsFile)); + } + + private void restoreCurrentAnalysisOptionsFile(SensorContext sensorContext) { + File analysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), ANALYSIS_OPTIONS_FILENAME); + File currentAnalysisOptionsFile = new File(sensorContext.fileSystem().baseDir(), + ANALYSIS_OPTIONS_FILENAME + ".sonar"); + if (currentAnalysisOptionsFile.exists()) { + currentAnalysisOptionsFile.renameTo(analysisOptionsFile); + LOGGER.info("Restored original analysis_options.yaml file"); + } else { + analysisOptionsFile.delete(); + } + } } diff --git a/dart-lang/src/main/resources/dart.keywords b/dart-lang/src/main/resources/dart.keywords index 2a608b1..d4cadf9 100644 --- a/dart-lang/src/main/resources/dart.keywords +++ b/dart-lang/src/main/resources/dart.keywords @@ -2,6 +2,7 @@ abstract as assert async +async* await break case @@ -14,8 +15,9 @@ do$dynamic else enum export -external extends +extension +external factory false final @@ -28,10 +30,13 @@ import in is library +mixin new null +on operator part +required rethrow return set @@ -39,6 +44,7 @@ static super switch sync +sync* this throw true @@ -48,4 +54,4 @@ void while with yield - +yield* diff --git a/dart-lang/src/main/resources/fr/insideapp/sonarqube/dart/dartanalyzer/profile-pedantic.1.9.0.xml b/dart-lang/src/main/resources/fr/insideapp/sonarqube/dart/dartanalyzer/profile-pedantic.1.9.0.xml new file mode 100644 index 0000000..c410349 --- /dev/null +++ b/dart-lang/src/main/resources/fr/insideapp/sonarqube/dart/dartanalyzer/profile-pedantic.1.9.0.xml @@ -0,0 +1,341 @@ + + + pedantic 1.9.0 + dart + + + dartanalyzer + always_declare_return_types + + + dartanalyzer + always_require_non_null_named_parameters + + + dartanalyzer + annotate_overrides + + + dartanalyzer + avoid_empty_else + + + dartanalyzer + avoid_init_to_null + + + dartanalyzer + avoid_null_checks_in_equality_operators + + + dartanalyzer + avoid_relative_lib_imports + + + dartanalyzer + avoid_return_types_on_setters + + + dartanalyzer + avoid_shadowing_type_parameters + + + dartanalyzer + avoid_types_as_parameter_names + + + dartanalyzer + camel_case_extensions + + + dartanalyzer + curly_braces_in_flow_control_structures + + + dartanalyzer + empty_catches + + + dartanalyzer + empty_constructor_bodies + + + dartanalyzer + library_names + + + dartanalyzer + library_prefixes + + + dartanalyzer + no_duplicate_case_values + + + dartanalyzer + null_closures + + + dartanalyzer + omit_local_variable_types + + + dartanalyzer + prefer_adjacent_string_concatenation + + + dartanalyzer + prefer_collection_literals + + + dartanalyzer + prefer_conditional_assignment + + + dartanalyzer + prefer_contains + + + dartanalyzer + prefer_equal_for_default_values + + + dartanalyzer + prefer_final_fields + + + dartanalyzer + prefer_for_elements_to_map_fromIterable + + + dartanalyzer + prefer_generic_function_type_aliases + + + dartanalyzer + prefer_if_null_operators + + + dartanalyzer + prefer_is_empty + + + dartanalyzer + prefer_is_not_empty + + + dartanalyzer + prefer_iterable_whereType + + + dartanalyzer + prefer_single_quotes + + + dartanalyzer + prefer_spread_collections + + + dartanalyzer + recursive_getters + + + dartanalyzer + slash_for_doc_comments + + + dartanalyzer + type_init_formals + + + dartanalyzer + unawaited_futures + + + dartanalyzer + unnecessary_const + + + dartanalyzer + unnecessary_new + + + dartanalyzer + unnecessary_null_in_if_null_operators + + + dartanalyzer + unnecessary_this + + + dartanalyzer + unrelated_type_equality_checks + + + dartanalyzer + use_function_type_syntax_for_parameters + + + dartanalyzer + use_rethrow_when_possible + + + dartanalyzer + valid_regexps + + + + dartanalyzer + avoid_returning_null_for_future + + + dartanalyzer + avoid_web_libraries_in_flutter (experimental) + + + dartanalyzer + control_flow_in_finally + + + dartanalyzer + hash_and_equals + + + dartanalyzer + invariant_booleans (experimental) + + + dartanalyzer + iterable_contains_unrelated_type + + + dartanalyzer + list_remove_unrelated_type + + + dartanalyzer + literal_only_boolean_expressions + + + dartanalyzer + no_adjacent_strings_in_list + + + dartanalyzer + no_duplicate_case_values + + + dartanalyzer + test_types_in_equals + + + dartanalyzer + throw_in_finally + + + dartanalyzer + unrelated_type_equality_checks + + + dartanalyzer + use_key_in_widget_constructors + + + dartanalyzer + valid_regexps + + + dartanalyzer + always_require_non_null_named_parameters + + + dartanalyzer + avoid_equals_and_hash_code_on_mutable_classes + + + dartanalyzer + avoid_implementing_value_types + + + dartanalyzer + avoid_js_rounded_ints + + + dartanalyzer + avoid_returning_null + + + dartanalyzer + avoid_returning_null_for_void + + + dartanalyzer + avoid_returning_this + + + dartanalyzer + avoid_void_async + + + dartanalyzer + await_only_futures + + + dartanalyzer + parameter_assignments + + + dartanalyzer + prefer_const_constructors + + + dartanalyzer + prefer_const_constructors_in_immutables + + + dartanalyzer + prefer_const_declarations + + + dartanalyzer + prefer_final_fields + + + dartanalyzer + prefer_final_in_for_each + + + dartanalyzer + prefer_final_locals + + + dartanalyzer + prefer_mixin + + + dartanalyzer + prefer_null_aware_operators + + + dartanalyzer + recursive_getters + + + dartanalyzer + unawaited_futures + + + dartanalyzer + use_full_hex_values_for_flutter_colors + + + dartanalyzer + void_checks + + + + dartanalyzer + unsafe_html + + + \ No newline at end of file diff --git a/dart-lang/src/test/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParserTest.java b/dart-lang/src/test/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParserTest.java index 9fdf87b..4c8eb09 100644 --- a/dart-lang/src/test/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParserTest.java +++ b/dart-lang/src/test/java/fr/insideapp/sonarqube/dart/lang/issues/dartanalyzer/DartAnalyzerReportParserTest.java @@ -19,11 +19,11 @@ */ package fr.insideapp.sonarqube.dart.lang.issues.dartanalyzer; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; import java.util.List; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; public class DartAnalyzerReportParserTest { @@ -89,7 +89,24 @@ public void parseWithTraces() { assertRuleId(issues.get(10), RULE_ID_UNUSED_LOCAL_VARIABLE); assertMessage(issues.get(10), "The value of the local variable 'j' isn't used."); } - + + @Test + public void parseFlutterReport() { + String input = " info • The value of the local variable 'chocolate' isn't used • lib/api/icecream/icecream_api.dart:110:29 • unused_local_variable\n" + + " info • 'nothing' is deprecated and shouldn't be used. Do not use this anymore, please use [Nothing] instead. • lib/src/path/to/very_cool_widget.dart:32:59 • deprecated_member_use_from_same_package\n" + + " info • The parameter 'foo' is required • lib/src/path/to/this_file.dart:9:5 • missing_required_param\n" + + " info • Unnecessary await keyword in return • lib/path/to/other/file.dart:37:27 • unnecessary_await_in_return"; + + List issues = parser.parse(input); + assertThat(issues).hasSize(4); + assertThat(issues.stream().map(DartAnalyzerReportIssue::getRuleId)).containsExactly( + "unused_local_variable", + "deprecated_member_use_from_same_package", + "missing_required_param", + "unnecessary_await_in_return" + ); + } + private void assertFilePath(DartAnalyzerReportIssue issue, String expectedPath) { assertThat(issue.getFilePath()).isEqualTo(expectedPath); } diff --git a/pom.xml b/pom.xml index e7c3558..1899b0a 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ fr.insideapp.sonarqube sonar-flutter - 0.3.1 + 0.3.2 pom diff --git a/sonar-flutter-plugin/pom.xml b/sonar-flutter-plugin/pom.xml index 431b867..36a912a 100644 --- a/sonar-flutter-plugin/pom.xml +++ b/sonar-flutter-plugin/pom.xml @@ -4,7 +4,7 @@ sonar-flutter fr.insideapp.sonarqube - 0.3.1 + 0.3.2 4.0.0 @@ -17,7 +17,7 @@ fr.insideapp.sonarqube dart-lang - 0.3.1 + 0.3.2 diff --git a/sonar-flutter-plugin/src/main/java/fr/insideapp/sonarqube/flutter/FlutterPlugin.java b/sonar-flutter-plugin/src/main/java/fr/insideapp/sonarqube/flutter/FlutterPlugin.java index 36801cd..e9bf9e5 100644 --- a/sonar-flutter-plugin/src/main/java/fr/insideapp/sonarqube/flutter/FlutterPlugin.java +++ b/sonar-flutter-plugin/src/main/java/fr/insideapp/sonarqube/flutter/FlutterPlugin.java @@ -19,23 +19,27 @@ */ package fr.insideapp.sonarqube.flutter; +import fr.insideapp.sonarqube.dart.lang.Dart; import fr.insideapp.sonarqube.dart.lang.DartSensor; import fr.insideapp.sonarqube.dart.lang.issues.DartProfile; +import fr.insideapp.sonarqube.dart.lang.issues.DartProfilePedantic190; +import fr.insideapp.sonarqube.dart.lang.issues.dartanalyzer.AnalyzerMode; import fr.insideapp.sonarqube.dart.lang.issues.dartanalyzer.DartAnalyzerRulesDefinition; import fr.insideapp.sonarqube.dart.lang.issues.dartanalyzer.DartAnalyzerSensor; import fr.insideapp.sonarqube.flutter.coverage.FlutterCoverageSensor; import fr.insideapp.sonarqube.flutter.tests.FlutterTestSensor; import org.sonar.api.Plugin; -import fr.insideapp.sonarqube.dart.lang.Dart; +import org.sonar.api.PropertyType; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.resources.Qualifiers; +import java.util.stream.Collectors; + public class FlutterPlugin implements Plugin { public static final String DART_CATEGORY = "Dart"; public static final String FLUTTER_CATEGORY = "Flutter"; public static final String ANALYSIS_SUBCATEGORY = "Analysis"; - public static final String GENERAL_SUBCATEGORY = "General"; public static final String TESTS_SUBCATEGORY = "Tests"; public static final String FLUTTER_TESTS_REPORT_PATH_KEY = "sonar.flutter.tests.reportPath"; @@ -44,7 +48,7 @@ public class FlutterPlugin implements Plugin { public void define(Context context) { // Language support - context.addExtensions(Dart.class, DartSensor.class, DartProfile.class); + context.addExtensions(Dart.class, DartSensor.class, DartProfile.class, DartProfilePedantic190.class); // dartanalyzer Sensor context.addExtensions(DartAnalyzerSensor.class, DartAnalyzerRulesDefinition.class); @@ -77,6 +81,27 @@ public void define(Context context) { .defaultValue("false") .build()); + context.addExtension( + PropertyDefinition.builder(DartAnalyzerSensor.FLUTTER_ANALYZER_MODE) + .name("Analyzer") + .description("Which analyzer to use") + .onQualifiers(Qualifiers.MODULE, Qualifiers.PROJECT) + .category(DART_CATEGORY) + .subCategory(ANALYSIS_SUBCATEGORY) + .options(DartAnalyzerSensor.FLUTTER_ANALYZER_MODE_OPTIONS.stream().map(Enum::name).collect(Collectors.toList())) + .defaultValue(AnalyzerMode.defaultMode.name()) + .type(PropertyType.SINGLE_SELECT_LIST) + .build()); + + context.addExtension( + PropertyDefinition.builder(DartSensor.DART_ANALYSIS_USE_EXISTING_REPORT_PATH_KEY) + .name("Use dartanalyzer report file for analysis") + .description("Path to Dartanalyzer report file. If null, dartanalyzer will be executed. The path may be either absolute or relative to the project base directory. Run dartanalyzer with '--write PATH' to create the report.") + .onQualifiers(Qualifiers.MODULE, Qualifiers.PROJECT) + .category(DART_CATEGORY) + .subCategory(ANALYSIS_SUBCATEGORY) + .build()); + // Tests context.addExtension(FlutterTestSensor.class);