diff options
9 files changed, 448 insertions, 3 deletions
diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java index 9cb185a943e..3b626b8ed9f 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java @@ -34,6 +34,7 @@ import org.sonar.xoo.global.GlobalSensor; import org.sonar.xoo.lang.CpdTokenizerSensor; import org.sonar.xoo.lang.LineMeasureSensor; import org.sonar.xoo.lang.MeasureSensor; +import org.sonar.xoo.lang.SignificantCodeSensor; import org.sonar.xoo.lang.SymbolReferencesSensor; import org.sonar.xoo.lang.SyntaxHighlightingSensor; import org.sonar.xoo.lang.XooCpdMapping; @@ -173,7 +174,9 @@ public class XooPlugin implements Plugin { context.addExtension(GlobalSensor.class); } if (context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 2))) { - context.addExtension(OneExternalIssuePerLineSensor.class); + context.addExtensions( + OneExternalIssuePerLineSensor.class, + SignificantCodeSensor.class); } } diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SignificantCodeSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SignificantCodeSensor.java new file mode 100644 index 00000000000..108d232cb5a --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SignificantCodeSensor.java @@ -0,0 +1,94 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.xoo.lang; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.code.NewSignificantCode; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.xoo.Xoo; + +public class SignificantCodeSensor implements Sensor { + private static final Logger LOG = Loggers.get(SignificantCodeSensor.class); + private static final String FILE_EXTENSION = ".significantCode"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .name("Xoo Significant Code Ranges Sensor") + .onlyOnLanguages(Xoo.KEY); + } + + @Override + public void execute(SensorContext context) { + for (InputFile file : context.fileSystem().inputFiles(context.fileSystem().predicates().hasLanguages(Xoo.KEY))) { + processSignificantCodeFile(file, context); + } + } + + private void processSignificantCodeFile(InputFile inputFile, SensorContext context) { + Path ioFile = inputFile.path(); + Path significantCodeFile = ioFile.resolveSibling(ioFile.getFileName() + FILE_EXTENSION).toAbsolutePath(); + if (Files.exists(significantCodeFile) && Files.isRegularFile(significantCodeFile)) { + LOG.debug("Processing " + significantCodeFile.toString()); + try { + List<String> lines = Files.readAllLines(significantCodeFile, context.fileSystem().encoding()); + NewSignificantCode significantCode = context.newSignificantCode() + .onFile(inputFile); + for (String line : lines) { + if (StringUtils.isBlank(line) || line.startsWith("#")) { + continue; + } + processLine(line, inputFile, significantCode); + } + significantCode.save(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + private void processLine(String fileLine, InputFile inputFile, NewSignificantCode significantCode) { + String[] textPointer = fileLine.split(","); + if (textPointer.length != 3) { + throw new IllegalStateException("Invalid format in error file"); + } + + try { + int line = Integer.parseInt(textPointer[0]); + int startLineOffset = Integer.parseInt(textPointer[1]); + int endLineOffset = Integer.parseInt(textPointer[2]); + + significantCode.addRange(inputFile.newRange(line, startLineOffset, line, endLineOffset)); + + } catch (NumberFormatException e) { + throw new IllegalStateException("Invalid format in error file", e); + } + } + +} diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java index 9be43f0b4f3..dacd5b3f575 100644 --- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java @@ -62,6 +62,6 @@ public class XooPluginTest { SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("7.2"), SonarQubeSide.SCANNER); Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build(); new XooPlugin().define(context); - assertThat(context.getExtensions()).hasSize(53).contains(OneExternalIssuePerLineSensor.class); + assertThat(context.getExtensions()).hasSize(54).contains(OneExternalIssuePerLineSensor.class); } } diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/lang/SignificantCodeSensorTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/lang/SignificantCodeSensorTest.java new file mode 100644 index 00000000000..115c1c030a2 --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/lang/SignificantCodeSensorTest.java @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.xoo.lang; + +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.fs.internal.DefaultTextPointer; +import org.sonar.api.batch.fs.internal.DefaultTextRange; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; +import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor; +import org.sonar.api.batch.sensor.internal.SensorContextTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SignificantCodeSensorTest { + private SignificantCodeSensor sensor; + private SensorContextTester context; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private File baseDir; + + private InputFile inputFile; + + @Before + public void prepare() throws IOException { + baseDir = temp.newFolder(); + sensor = new SignificantCodeSensor(); + context = SensorContextTester.create(baseDir); + inputFile = new TestInputFileBuilder("foo", baseDir, new File(baseDir, "src/foo.xoo")) + .setLanguage("xoo") + .setContents("some lines\nof code\nyet another line") + .build(); + } + + @Test + public void testDescriptor() { + sensor.describe(new DefaultSensorDescriptor()); + } + + @Test + public void testNoExceptionIfNoFileWithOffsets() { + context.fileSystem().add(inputFile); + sensor.execute(context); + } + + @Test + public void testExecution() throws IOException { + File significantCode = new File(baseDir, "src/foo.xoo.significantCode"); + FileUtils.write(significantCode, "1,1,4\n2,2,5"); + context.fileSystem().add(inputFile); + + sensor.execute(context); + + assertThat(context.significantCodeTextRange("foo:src/foo.xoo", 1)).isEqualTo(range(1, 1, 4)); + assertThat(context.significantCodeTextRange("foo:src/foo.xoo", 2)).isEqualTo(range(2, 2, 5)); + } + + private static TextRange range(int line, int startOffset, int endOffset) { + return new DefaultTextRange(new DefaultTextPointer(line, startOffset), new DefaultTextPointer(line, endOffset)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/source/SourceLinesHashRepository.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/source/SourceLinesHashRepository.java index 60fc808f393..8c616407a4e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/source/SourceLinesHashRepository.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/source/SourceLinesHashRepository.java @@ -31,7 +31,7 @@ public interface SourceLinesHashRepository { List<String> getMatchingDB(Component component); /** - * The line processor will return line hashes taking into account significant code (if it was provided by a code analyzer). + * The line computer will compute line hashes taking into account significant code (if it was provided by a code analyzer). * It will use a cached value, if possible. If it's generated, it's not cached since it's assumed that it won't be * needed again after it is persisted. */ diff --git a/tests/projects/significantCode/sample-xoo/file.xoo b/tests/projects/significantCode/sample-xoo/file.xoo new file mode 100644 index 00000000000..5c50979f1b4 --- /dev/null +++ b/tests/projects/significantCode/sample-xoo/file.xoo @@ -0,0 +1,11 @@ +000100******************************************************************00010000 +000200 IDENTIFICATION DIVISION. 00020000 +000300******************************************************************00030000 +000400 PROGRAM-ID. TEST-1. 00040000 +000500 AUTHOR. SONARSOURCE. 00050000 +000600 DATE-COMPILED. APRIL 2018. 00060000 +000700 (this line has no significant code) 00070009 +000800 00080000 +000900******************************************************************00090000 +001000* *00100000 +001100 MOVE WS-NB1 TO WS-NB2. 00110000
\ No newline at end of file diff --git a/tests/projects/significantCode/sample-xoo/file_additional_line.xoo b/tests/projects/significantCode/sample-xoo/file_additional_line.xoo new file mode 100644 index 00000000000..5f3293bbbe7 --- /dev/null +++ b/tests/projects/significantCode/sample-xoo/file_additional_line.xoo @@ -0,0 +1,12 @@ +000100******************************************************************00010000 +000200 IDENTIFICATION DIVISION. 00020000 +000300******************************************************************00030000 +000400 PROGRAM-ID. TEST-1. 00040000 +000500 AUTHOR. SONARSOURCE. 00050000 +000600 DATE-COMPILED. APRIL 2018. 00060000 +000700 (this line has no significant code) 00070009 +000800 00080000 +000800 THIS LINE WAS ADDED 00080000 +000900******************************************************************00090000 +001000* *00100000 +001100 MOVE WS-NB1 TO WS-NB2. 00110000
\ No newline at end of file diff --git a/tests/projects/significantCode/sample-xoo/file_changed.xoo b/tests/projects/significantCode/sample-xoo/file_changed.xoo new file mode 100644 index 00000000000..aeb1f6f5e42 --- /dev/null +++ b/tests/projects/significantCode/sample-xoo/file_changed.xoo @@ -0,0 +1,11 @@ +000109******************************************************************00010000 +000290 IDENTIFICATION DIVISION. 00020000 +000900******************************************************************00030000 +009400 PROGRAM-ID. TEST-1. - line changed 00040000 +090500 AUTHOR. SONARSOURCE. 00050000 +900600 DATE-COMPILED. APRIL 2018. 00060000 +000700 (this line has no significant code) - line changed 00070009 +000800 00080090 +000900******************************************************************00090900 +001000* *00109000 +001100 MOVE WS-NB1 TO WS-NB2. 00190000
\ No newline at end of file diff --git a/tests/src/test/java/org/sonarqube/tests/source/SignificantCodeTest.java b/tests/src/test/java/org/sonarqube/tests/source/SignificantCodeTest.java new file mode 100644 index 00000000000..833e57e7910 --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/source/SignificantCodeTest.java @@ -0,0 +1,223 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarqube.tests.source; + +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarScanner; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.text.ParseException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.lang.StringUtils; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonarqube.qa.util.Tester; +import org.sonarqube.ws.Issues.Issue; +import org.sonarqube.ws.Issues.SearchWsResponse; +import org.sonarqube.ws.client.issues.SearchRequest; +import org.sonarqube.ws.client.sources.HashRequest; +import util.ItUtils; + +import static org.apache.commons.io.FileUtils.copyDirectory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonarqube.tests.source.SourceSuite.ORCHESTRATOR; +import static util.ItUtils.projectDir; + +public class SignificantCodeTest { + private final String PROJECT_DIRECTORY = "sample-xoo"; + private final String PROJECT_NAME = "sample-xoo"; + private final String FILE_BASE = "file.xoo"; + private final String FILE_CHANGED = "file_changed.xoo"; + private final String FILE_ADDITIONAL_LINE = "file_additional_line.xoo"; + private final String FILE_TO_ANALYSE = PROJECT_NAME + ":" + FILE_BASE; + + @ClassRule + public static Orchestrator orchestrator = ORCHESTRATOR; + + private SourceScmWS ws; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public Tester tester = new Tester(orchestrator); + + @Before + public void setUp() { + ORCHESTRATOR.resetData(); + ws = new SourceScmWS(tester); + } + + @Test + public void changes_in_margins_should_not_change_line_date() throws ParseException, IOException { + Path projectDirectory = disposableWorkspaceFor(PROJECT_DIRECTORY); + deployFileWithsignificantCodeRanges(projectDirectory, 7); + + // 1st run + SonarScanner scanner = SonarScanner.create(projectDirectory.toFile()) + .setProjectKey(PROJECT_NAME) + .setSourceDirs(FILE_BASE); + orchestrator.executeBuild(scanner); + Map<Integer, LineData> scmData1 = ws.getScmData(FILE_TO_ANALYSE); + + // 2nd run + deployChangedFile(projectDirectory, FILE_CHANGED); + + scanner = SonarScanner.create(projectDirectory.toFile()) + .setProjectKey(PROJECT_NAME) + .setSourceDirs(FILE_BASE); + orchestrator.executeBuild(scanner); + + // Check that only line 4 is considered as changed + Map<Integer, LineData> scmData2 = ws.getScmData(FILE_TO_ANALYSE); + + for (Map.Entry<Integer, LineData> e : scmData2.entrySet()) { + if (e.getKey() == 4) { + assertThat(e.getValue().date).isAfter(scmData1.get(1).date); + } else { + assertThat(e.getValue().date).isEqualTo(scmData1.get(1).date); + } + } + } + + @Test + public void migration_should_not_affect_unchanged_lines() throws IOException, ParseException { + Path projectDirectory = disposableWorkspaceFor(PROJECT_DIRECTORY); + + // 1st run + SonarScanner scanner = SonarScanner.create(projectDirectory.toFile()) + .setProjectKey(PROJECT_NAME) + .setSourceDirs(FILE_BASE); + orchestrator.executeBuild(scanner); + Map<Integer, LineData> scmData1 = ws.getScmData(FILE_TO_ANALYSE); + String[] lineHashes1 = getLineHashes(); + + // 2nd run + deployFileWithsignificantCodeRanges(projectDirectory, 7); + scanner = SonarScanner.create(projectDirectory.toFile()) + .setProjectKey(PROJECT_NAME) + .setSourceDirs(FILE_BASE); + orchestrator.executeBuild(scanner); + + // Check that no line was modified + Map<Integer, LineData> scmData2 = ws.getScmData(FILE_TO_ANALYSE); + + for (Map.Entry<Integer, LineData> e : scmData2.entrySet()) { + assertThat(e.getValue().date).isEqualTo(scmData1.get(1).date); + } + + // Check that line hashes changed for all lines, even though the file didn't change + String[] lineHashes2 = getLineHashes(); + + for (int i = 0; i < lineHashes2.length; i++) { + assertThat(lineHashes2[i]).isNotEqualTo(lineHashes1[i]); + } + } + + @Test + public void issue_tracking() throws Exception { + ORCHESTRATOR.getServer().provisionProject(PROJECT_NAME, PROJECT_NAME); + ItUtils.restoreProfile(ORCHESTRATOR, getClass().getResource("/one-xoo-issue-per-line.xml")); + ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_NAME, "xoo", "one-xoo-issue-per-line"); + + Path projectDirectory = disposableWorkspaceFor(PROJECT_DIRECTORY); + + // 1st run + deployFileWithsignificantCodeRanges(projectDirectory, 7); + SonarScanner scanner = SonarScanner.create(projectDirectory.toFile()) + .setProjectKey(PROJECT_NAME) + .setSourceDirs(FILE_BASE); + orchestrator.executeBuild(scanner); + + Map<Integer, Issue> issues1 = getIssues().stream().collect(Collectors.toMap(i -> i.getLine(), i -> i)); + + // 2nd run + deployChangedFile(projectDirectory, FILE_ADDITIONAL_LINE); + deployFileWithsignificantCodeRanges(projectDirectory, 7); + + scanner = SonarScanner.create(projectDirectory.toFile()) + .setProjectKey(PROJECT_NAME) + .setSourceDirs(FILE_BASE); + orchestrator.executeBuild(scanner); + + List<Issue> issues2 = getIssues(); + + // Check that all issues were tracking except the issue on the new line + assertThat(issues1.size()).isEqualTo(11); + assertThat(issues2.size()).isEqualTo(12); + + for (Issue issue : issues2) { + if (issue.getLine() < 9) { + assertThat(issue.getKey()).isEqualTo(issues1.get(issue.getLine()).getKey()); + } else if (issue.getLine() == 9) { + // this is the new issue + List<String> keys = issues1.values().stream().map(i -> i.getKey()).collect(Collectors.toList()); + assertThat(issue.getKey()).isNotIn(keys); + } else { + assertThat(issue.getKey()).isEqualTo(issues1.get(issue.getLine() - 1).getKey()); + } + } + + } + + private String[] getLineHashes() { + String hashes = tester.wsClient().sources().hash(new HashRequest().setKey(FILE_TO_ANALYSE)); + return StringUtils.split(hashes, "\n"); + } + + private List<Issue> getIssues() { + SearchWsResponse response = tester.wsClient().issues().search(new SearchRequest().setComponentKeys(Collections.singletonList(FILE_TO_ANALYSE))); + return response.getIssuesList(); + } + + private void deployFileWithsignificantCodeRanges(Path projectBaseDir, int lineWithoutSignificantCode) throws IOException { + Path file = projectBaseDir.resolve("file.xoo"); + Path significantLineRangesFile = projectBaseDir.resolve("file.xoo.significantCode"); + + int numLines = Files.readAllLines(file).size(); + List<String> lines = IntStream.rangeClosed(1, numLines) + .mapToObj(i -> i == lineWithoutSignificantCode ? "" : i + ",6,72") + .collect(Collectors.toList()); + + Files.write(significantLineRangesFile, lines); + } + + private void deployChangedFile(Path projectBaseDir, String fileName) throws IOException { + Path file = projectBaseDir.resolve(FILE_BASE); + Path file_changed = projectBaseDir.resolve(fileName); + Files.move(file_changed, file, StandardCopyOption.REPLACE_EXISTING); + } + + private Path disposableWorkspaceFor(String project) throws IOException { + File origin = projectDir("significantCode/" + project); + copyDirectory(origin.getParentFile(), temporaryFolder.getRoot()); + return temporaryFolder.getRoot().toPath().resolve(project); + } +} |