@@ -30,6 +30,8 @@ import java.nio.charset.Charset; | |||
import java.nio.file.Files; | |||
import java.nio.file.Path; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
@@ -56,6 +58,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile | |||
private Metadata metadata; | |||
private boolean published; | |||
private boolean excludedForCoverage; | |||
private final Set<Integer> noSonarLines = new HashSet<>(); | |||
public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator) { | |||
this(indexedFile, metadataGenerator, null); | |||
@@ -82,7 +85,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile | |||
public InputStream inputStream() throws IOException { | |||
return contents != null ? new ByteArrayInputStream(contents.getBytes(charset())) | |||
: new BOMInputStream(Files.newInputStream(path()), | |||
ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE); | |||
ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE); | |||
} | |||
@Override | |||
@@ -242,7 +245,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile | |||
checkMetadata(); | |||
Preconditions.checkState(metadata.originalLineEndOffsets() != null, "InputFile is not properly initialized."); | |||
Preconditions.checkState(metadata.originalLineEndOffsets().length == metadata.lines(), | |||
"InputFile is not properly initialized. 'originalLineEndOffsets' property length should be equal to 'lines'"); | |||
"InputFile is not properly initialized. 'originalLineEndOffsets' property length should be equal to 'lines'"); | |||
return metadata.originalLineEndOffsets(); | |||
} | |||
@@ -299,7 +302,7 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile | |||
int line = findLine(globalOffset); | |||
int startLineOffset = originalLineStartOffsets()[line - 1]; | |||
// In case the global offset is between \r and \n, move the pointer to a valid location | |||
return new DefaultTextPointer(line, Math.min(globalOffset, originalLineEndOffsets()[line -1]) - startLineOffset); | |||
return new DefaultTextPointer(line, Math.min(globalOffset, originalLineEndOffsets()[line - 1]) - startLineOffset); | |||
} | |||
public DefaultInputFile setStatus(Status status) { | |||
@@ -369,4 +372,12 @@ public class DefaultInputFile extends DefaultInputComponent implements InputFile | |||
return indexedFile.uri(); | |||
} | |||
public void noSonarAt(Set<Integer> noSonarLines) { | |||
this.noSonarLines.addAll(noSonarLines); | |||
} | |||
public boolean hasNoSonarAt(int line) { | |||
return this.noSonarLines.contains(line); | |||
} | |||
} |
@@ -19,63 +19,34 @@ | |||
*/ | |||
package org.sonar.api.issue; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.api.batch.ScannerSide; | |||
import org.sonar.api.batch.fs.InputFile; | |||
import org.sonar.api.batch.fs.internal.DefaultInputFile; | |||
import org.sonar.api.scan.issue.filter.FilterableIssue; | |||
import org.sonar.api.scan.issue.filter.IssueFilter; | |||
import org.sonar.api.scan.issue.filter.IssueFilterChain; | |||
import org.sonar.api.scanner.ScannerSide; | |||
/** | |||
* Issue filter used to ignore issues created on lines commented with the tag "NOSONAR". | |||
* <br> | |||
* Plugins, via {@link ScannerSide}s, must feed this filter by registering the | |||
* Plugins, via {@link org.sonar.api.batch.sensor.Sensor}s, must feed this filter by registering the | |||
* lines that contain "NOSONAR". Note that filters are disabled for the issues reported by | |||
* end-users from UI or web services. | |||
* | |||
* @since 3.6 | |||
*/ | |||
public class NoSonarFilter implements IssueFilter { | |||
private final Map<String, Set<Integer>> noSonarLinesByResource = new HashMap<>(); | |||
/** | |||
* @deprecated since 5.0 use {@link #noSonarInFile(InputFile, Set)} | |||
*/ | |||
@Deprecated | |||
public NoSonarFilter addComponent(String componentKey, Set<Integer> noSonarLines) { | |||
noSonarLinesByResource.put(componentKey, noSonarLines); | |||
return this; | |||
} | |||
@ScannerSide | |||
public class NoSonarFilter { | |||
/** | |||
* Register lines in a file that contains the NOSONAR flag. | |||
* | |||
* @param inputFile | |||
* @param noSonarLines Line number starts at 1 in a file | |||
* @since 5.0 | |||
* @since 7.6 the method can be called multiple times by different sensors, and NOSONAR lines are merged | |||
*/ | |||
public NoSonarFilter noSonarInFile(InputFile inputFile, Set<Integer> noSonarLines) { | |||
noSonarLinesByResource.put(((DefaultInputFile) inputFile).key(), noSonarLines); | |||
((DefaultInputFile) inputFile).noSonarAt(noSonarLines); | |||
return this; | |||
} | |||
@Override | |||
public boolean accept(FilterableIssue issue, IssueFilterChain chain) { | |||
boolean accepted = true; | |||
if (issue.line() != null) { | |||
Set<Integer> noSonarLines = noSonarLinesByResource.get(issue.componentKey()); | |||
accepted = noSonarLines == null || !noSonarLines.contains(issue.line()); | |||
if (!accepted && StringUtils.containsIgnoreCase(issue.ruleKey().rule(), "nosonar")) { | |||
accepted = true; | |||
} | |||
} | |||
if (accepted) { | |||
accepted = chain.accept(issue); | |||
} | |||
return accepted; | |||
} | |||
} |
@@ -19,72 +19,23 @@ | |||
*/ | |||
package org.sonar.api.issue; | |||
import org.sonar.api.scan.issue.filter.FilterableIssue; | |||
import com.google.common.collect.ImmutableSet; | |||
import org.junit.Before; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import org.junit.Test; | |||
import org.sonar.api.scan.issue.filter.IssueFilterChain; | |||
import org.sonar.api.rule.RuleKey; | |||
import java.util.Set; | |||
import org.sonar.api.batch.fs.internal.DefaultInputFile; | |||
import org.sonar.api.batch.fs.internal.TestInputFileBuilder; | |||
import static org.mockito.Mockito.times; | |||
import static org.mockito.Mockito.verify; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.isA; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class NoSonarFilterTest { | |||
NoSonarFilter filter = new NoSonarFilter(); | |||
IssueFilterChain chain = mock(IssueFilterChain.class); | |||
@Before | |||
public void setupChain() { | |||
when(chain.accept(isA(FilterableIssue.class))).thenReturn(true); | |||
} | |||
@Test | |||
public void should_ignore_lines_commented_with_nosonar() { | |||
FilterableIssue issue = mock(FilterableIssue.class); | |||
when(issue.componentKey()).thenReturn("struts:org.apache.Action"); | |||
when(issue.ruleKey()).thenReturn(RuleKey.of("squid", "AvoidCycles")); | |||
Set<Integer> noSonarLines = ImmutableSet.of(31, 55); | |||
filter.addComponent("struts:org.apache.Action", noSonarLines); | |||
// issue on file | |||
when(issue.line()).thenReturn(null); | |||
assertThat(filter.accept(issue, chain)).isTrue(); | |||
// issue on lines | |||
when(issue.line()).thenReturn(31); | |||
assertThat(filter.accept(issue, chain)).isFalse(); | |||
when(issue.line()).thenReturn(222); | |||
assertThat(filter.accept(issue, chain)).isTrue(); | |||
verify(chain, times(2)).accept(issue); | |||
} | |||
@Test | |||
public void should_accept_issues_on_no_sonar_rules() { | |||
// The "No Sonar" rule logs violations on the lines that are flagged with "NOSONAR" !! | |||
FilterableIssue issue = mock(FilterableIssue.class); | |||
when(issue.componentKey()).thenReturn("struts:org.apache.Action"); | |||
when(issue.ruleKey()).thenReturn(RuleKey.of("squid", "NoSonarCheck")); | |||
Set<Integer> noSonarLines = ImmutableSet.of(31, 55); | |||
filter.addComponent("struts:org.apache.Action", noSonarLines); | |||
when(issue.line()).thenReturn(31); | |||
assertThat(filter.accept(issue, chain)).isTrue(); | |||
when(issue.line()).thenReturn(222); | |||
assertThat(filter.accept(issue, chain)).isTrue(); | |||
public void should_store_nosonar_lines_on_inputfile() { | |||
DefaultInputFile f = TestInputFileBuilder.create("module1", "myfile.java").build(); | |||
new NoSonarFilter().noSonarInFile(f, new HashSet<>(Arrays.asList(1,4))); | |||
verify(chain, times(2)).accept(issue); | |||
assertThat(f.hasNoSonarAt(1)).isTrue(); | |||
assertThat(f.hasNoSonarAt(2)).isFalse(); | |||
assertThat(f.hasNoSonarAt(4)).isTrue(); | |||
} | |||
} |
@@ -22,8 +22,10 @@ package org.sonar.scanner.issue; | |||
import java.util.Collection; | |||
import java.util.function.Consumer; | |||
import javax.annotation.concurrent.ThreadSafe; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.api.batch.fs.TextRange; | |||
import org.sonar.api.batch.fs.internal.DefaultInputComponent; | |||
import org.sonar.api.batch.fs.internal.DefaultInputFile; | |||
import org.sonar.api.batch.rule.ActiveRule; | |||
import org.sonar.api.batch.rule.ActiveRules; | |||
import org.sonar.api.batch.sensor.issue.ExternalIssue; | |||
@@ -56,6 +58,10 @@ public class ModuleIssues { | |||
public boolean initAndAddIssue(Issue issue) { | |||
DefaultInputComponent inputComponent = (DefaultInputComponent) issue.primaryLocation().inputComponent(); | |||
if (noSonar(inputComponent, issue)) { | |||
return false; | |||
} | |||
ActiveRule activeRule = activeRules.find(issue.ruleKey()); | |||
if (activeRule == null) { | |||
// rule does not exist or is not enabled -> ignore the issue | |||
@@ -71,6 +77,14 @@ public class ModuleIssues { | |||
return false; | |||
} | |||
private static boolean noSonar(DefaultInputComponent inputComponent, Issue issue) { | |||
TextRange textRange = issue.primaryLocation().textRange(); | |||
return inputComponent.isFile() | |||
&& textRange != null | |||
&& ((DefaultInputFile) inputComponent).hasNoSonarAt(textRange.start().line()) | |||
&& !StringUtils.containsIgnoreCase(issue.ruleKey().rule(), "nosonar"); | |||
} | |||
public void initAndAddExternalIssue(ExternalIssue issue) { | |||
DefaultInputComponent inputComponent = (DefaultInputComponent) issue.primaryLocation().inputComponent(); | |||
ScannerReport.ExternalIssue rawExternalIssue = createReportExternalIssue(issue, inputComponent.batchId()); |
@@ -23,7 +23,6 @@ import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.api.batch.fs.internal.DefaultInputModule; | |||
import org.sonar.api.batch.fs.internal.FileMetadata; | |||
import org.sonar.api.issue.NoSonarFilter; | |||
import org.sonar.api.scan.filesystem.FileExclusions; | |||
import org.sonar.core.extension.CoreExtensionsInstaller; | |||
import org.sonar.core.platform.ComponentContainer; | |||
@@ -130,7 +129,6 @@ public class ModuleScanContainer extends ComponentContainer { | |||
// issues | |||
ModuleIssues.class, | |||
NoSonarFilter.class, | |||
// issue exclusions | |||
IssueInclusionPatternInitializer.class, |
@@ -25,6 +25,7 @@ import org.sonar.api.batch.fs.internal.DefaultInputModule; | |||
import org.sonar.api.batch.fs.internal.InputModuleHierarchy; | |||
import org.sonar.api.batch.fs.internal.SensorStrategy; | |||
import org.sonar.api.batch.rule.CheckFactory; | |||
import org.sonar.api.issue.NoSonarFilter; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.api.resources.ResourceTypes; | |||
import org.sonar.api.scan.filesystem.PathResolver; | |||
@@ -175,6 +176,7 @@ public class ProjectScanContainer extends ComponentContainer { | |||
IssueCache.class, | |||
DefaultProjectIssues.class, | |||
IssueTransition.class, | |||
NoSonarFilter.class, | |||
// metrics | |||
DefaultMetricFinder.class, |
@@ -19,6 +19,8 @@ | |||
*/ | |||
package org.sonar.scanner.issue; | |||
import java.util.Collections; | |||
import java.util.HashSet; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.mockito.ArgumentCaptor; | |||
@@ -53,6 +55,7 @@ public class ModuleIssuesTest { | |||
static final RuleKey SQUID_RULE_KEY = RuleKey.of("squid", "AvoidCycle"); | |||
static final String SQUID_RULE_NAME = "Avoid Cycle"; | |||
private static final RuleKey NOSONAR_RULE_KEY = RuleKey.of("squid", "NoSonarCheck"); | |||
@Mock | |||
ModuleIssueFilters filters; | |||
@@ -179,6 +182,45 @@ public class ModuleIssuesTest { | |||
verifyZeroInteractions(reportPublisher); | |||
} | |||
@Test | |||
public void should_ignore_lines_commented_with_nosonar() { | |||
ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME); | |||
activeRulesBuilder.create(SQUID_RULE_KEY).setSeverity(Severity.INFO).activate(); | |||
initModuleIssues(); | |||
DefaultIssue issue = new DefaultIssue() | |||
.at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("")) | |||
.forRule(SQUID_RULE_KEY); | |||
file.noSonarAt(new HashSet<>(Collections.singletonList(3))); | |||
boolean added = moduleIssues.initAndAddIssue(issue); | |||
assertThat(added).isFalse(); | |||
verifyZeroInteractions(reportPublisher); | |||
} | |||
@Test | |||
public void should_accept_issues_on_no_sonar_rules() { | |||
// The "No Sonar" rule logs violations on the lines that are flagged with "NOSONAR" !! | |||
ruleBuilder.add(NOSONAR_RULE_KEY).setName("No Sonar"); | |||
activeRulesBuilder.create(NOSONAR_RULE_KEY).setSeverity(Severity.INFO).activate(); | |||
initModuleIssues(); | |||
file.noSonarAt(new HashSet<>(Collections.singletonList(3))); | |||
DefaultIssue issue = new DefaultIssue() | |||
.at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("")) | |||
.forRule(NOSONAR_RULE_KEY); | |||
when(filters.accept(anyString(), any(ScannerReport.Issue.class))).thenReturn(true); | |||
boolean added = moduleIssues.initAndAddIssue(issue); | |||
assertThat(added).isTrue(); | |||
verify(reportPublisher.getWriter()).appendComponentIssue(eq(file.batchId()), any()); | |||
} | |||
/** | |||
* Every rules and active rules has to be added in builders before creating ModuleIssues | |||
*/ |