
org.sonar.java.checks.verifier.internal.InternalCheckVerifier Maven / Gradle / Ivy
/*
* SonarQube Java
* Copyright (C) 2012-2024 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.java.checks.verifier.internal;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.cache.ReadCache;
import org.sonar.api.batch.sensor.cache.WriteCache;
import org.sonar.java.SonarComponents;
import org.sonar.java.annotations.Beta;
import org.sonar.java.annotations.VisibleForTesting;
import org.sonar.java.ast.JavaAstScanner;
import org.sonar.java.caching.DummyCache;
import org.sonar.java.caching.JavaReadCacheImpl;
import org.sonar.java.caching.JavaWriteCacheImpl;
import org.sonar.java.checks.verifier.CheckVerifier;
import org.sonar.java.checks.verifier.FilesUtils;
import org.sonar.java.model.JavaVersionImpl;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.reporting.AnalyzerMessage.TextSpan;
import org.sonar.java.reporting.JavaQuickFix;
import org.sonar.java.reporting.JavaTextEdit;
import org.sonar.java.test.classpath.TestClasspathUtils;
import org.sonar.java.testing.JavaFileScannerContextForTests;
import org.sonar.java.testing.VisitorsBridgeForTests;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaVersion;
import org.sonar.plugins.java.api.caching.CacheContext;
import static org.sonar.java.checks.verifier.internal.CheckVerifierUtils.CHECK_OR_CHECKS;
import static org.sonar.java.checks.verifier.internal.CheckVerifierUtils.FILE_OR_FILES;
import static org.sonar.java.checks.verifier.internal.CheckVerifierUtils.requiresNonEmpty;
import static org.sonar.java.checks.verifier.internal.CheckVerifierUtils.requiresNonNull;
import static org.sonar.java.checks.verifier.internal.CheckVerifierUtils.requiresNull;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.EFFORT_TO_FIX;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.END_COLUMN;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.END_LINE;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.FLOWS;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.MESSAGE;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.SECONDARY_LOCATIONS;
import static org.sonar.java.checks.verifier.internal.Expectations.IssueAttribute.START_COLUMN;
public class InternalCheckVerifier implements CheckVerifier {
private static final JavaVersion DEFAULT_JAVA_VERSION = new JavaVersionImpl();
private static final List DEFAULT_CLASSPATH;
static {
Path path = Paths.get(FilesUtils.DEFAULT_TEST_CLASSPATH_FILE.replace('/', File.separatorChar));
// Because of 'java-custom-rules-example' module, we silently use an empty classpath if the file does not exist
DEFAULT_CLASSPATH = Files.exists(path) ? TestClasspathUtils.loadFromFile(path.toString()) : new ArrayList<>();
Optional.of(new File(FilesUtils.DEFAULT_TEST_CLASSES_DIRECTORY)).filter(File::exists).ifPresent(DEFAULT_CLASSPATH::add);
}
private boolean withoutSemantic = false;
// should be set by user
private List checks = null;
private List files = null;
private JavaVersion javaVersion = null;
private boolean inAndroidContext = false;
private boolean isCacheEnabled = false;
private List classpath = null;
private Consumer> customIssueVerifier = null;
private boolean collectQuickFixes = false;
private Expectations expectations = new Expectations();
@VisibleForTesting
CacheContext cacheContext = null;
private ReadCache readCache;
private WriteCache writeCache;
private InternalCheckVerifier() {
}
public static InternalCheckVerifier newInstance() {
return new InternalCheckVerifier();
}
@Override
public InternalCheckVerifier withCheck(JavaFileScanner check) {
requiresNull(checks, CHECK_OR_CHECKS);
checks = Collections.singletonList(check);
return this;
}
@Override
public InternalCheckVerifier withChecks(JavaFileScanner... checks) {
requiresNull(this.checks, CHECK_OR_CHECKS);
requiresNonEmpty(Arrays.asList(checks), "check");
this.checks = Arrays.asList(checks);
return this;
}
@Override
public InternalCheckVerifier withClassPath(Collection classpath) {
requiresNull(this.classpath, "classpath");
// it is completely OK to provide nothing (empty list) as classpath
this.classpath = new ArrayList<>(classpath);
return this;
}
@Beta
public InternalCheckVerifier withCustomIssueVerifier(Consumer> customIssueVerifier) {
requiresNull(this.customIssueVerifier, "custom issue verifier");
this.customIssueVerifier = customIssueVerifier;
return this;
}
@Beta
public InternalCheckVerifier withQuickFixes() {
this.collectQuickFixes = true;
this.expectations.setCollectQuickFixes();
return this;
}
@Override
public InternalCheckVerifier withJavaVersion(int javaVersionAsInt) {
requiresNull(javaVersion, "java version");
return withJavaVersion(javaVersionAsInt, false);
}
@Override
public InternalCheckVerifier withJavaVersion(int javaVersionAsInt, boolean enablePreviewFeatures) {
requiresNull(javaVersion, "java version");
if (enablePreviewFeatures && javaVersionAsInt != JavaVersionImpl.MAX_SUPPORTED) {
var message = String.format(
"Preview features can only be enabled when the version == latest supported Java version (%d != %d)",
javaVersionAsInt,
JavaVersionImpl.MAX_SUPPORTED
);
throw new IllegalArgumentException(message);
}
javaVersion = new JavaVersionImpl(javaVersionAsInt, enablePreviewFeatures);
return this;
}
@Override
public CheckVerifier withinAndroidContext(boolean inAndroidContext) {
this.inAndroidContext = inAndroidContext;
return this;
}
@Override
public InternalCheckVerifier onFile(String filename) {
requiresNull(files, FILE_OR_FILES);
return onFiles(Collections.singletonList(filename));
}
@Override
public InternalCheckVerifier onFiles(String... filenames) {
List asList = Arrays.asList(filenames);
requiresNonEmpty(asList, "file");
return onFiles(Arrays.asList(filenames));
}
@Override
public InternalCheckVerifier onFiles(Collection filenames) {
requiresNull(files, FILE_OR_FILES);
requiresNonEmpty(filenames, "file");
files = new ArrayList<>();
return addFiles(InputFile.Status.SAME, filenames);
}
@Override
public InternalCheckVerifier addFiles(InputFile.Status status, String... modifiedFileNames) {
return addFiles(status, Arrays.asList(modifiedFileNames));
}
@Override
public InternalCheckVerifier addFiles(InputFile.Status status, Collection modifiedFileNames) {
requiresNonEmpty(modifiedFileNames, "file");
if (files == null) {
files = new ArrayList<>(modifiedFileNames.size());
}
var filesToAdd = modifiedFileNames.stream()
.map(name -> InternalInputFile.inputFile("", new File(name), status))
.toList();
var filesToAddStrings = filesToAdd.stream().map(Object::toString).toList();
files.forEach(inputFile -> {
if (filesToAddStrings.contains(inputFile.toString())) {
throw new IllegalArgumentException(String.format("File %s was already added.", inputFile));
}
});
files.addAll(filesToAdd);
return this;
}
@Override
public InternalCheckVerifier withoutSemantic() {
// can be called any number of time
withoutSemantic = true;
return this;
}
@Override
public CheckVerifier withCache(@Nullable ReadCache readCache, @Nullable WriteCache writeCache) {
this.isCacheEnabled = true;
this.readCache = readCache;
this.writeCache = writeCache;
this.cacheContext = new InternalCacheContext(
true,
readCache == null ? new DummyCache() : new JavaReadCacheImpl(readCache),
writeCache == null ? new DummyCache() : new JavaWriteCacheImpl(writeCache)
);
return this;
}
@Override
public void verifyIssues() {
requiresNonNull(checks, CHECK_OR_CHECKS);
requiresNonNull(files, FILE_OR_FILES);
verifyAll();
}
@Override
public void verifyIssueOnFile(String expectedIssueMessage) {
requiresNonNull(checks, CHECK_OR_CHECKS);
requiresNonNull(files, FILE_OR_FILES);
expectations.setExpectedFileIssue(expectedIssueMessage);
verifyAll();
}
@Override
public void verifyIssueOnProject(String expectedIssueMessage) {
requiresNonNull(checks, CHECK_OR_CHECKS);
requiresNonNull(files, FILE_OR_FILES);
expectations.setExpectedProjectIssue(expectedIssueMessage);
verifyAll();
}
@Override
public void verifyNoIssues() {
requiresNonNull(checks, CHECK_OR_CHECKS);
requiresNonNull(files, FILE_OR_FILES);
expectations.setExpectNoIssues();
verifyAll();
}
private void verifyAll() {
List visitors = new ArrayList<>(checks);
if (withoutSemantic && expectations.expectNoIssues()) {
visitors.add(expectations.noEffectParser());
} else {
visitors.add(expectations.parser());
}
SonarComponents sonarComponents = CheckVerifierUtils.sonarComponents(isCacheEnabled, readCache, writeCache);
VisitorsBridgeForTests visitorsBridge;
JavaVersion actualVersion = javaVersion == null ? DEFAULT_JAVA_VERSION : javaVersion;
if (withoutSemantic) {
visitorsBridge = new VisitorsBridgeForTests(visitors, sonarComponents, actualVersion);
} else {
List actualClasspath = classpath == null ? DEFAULT_CLASSPATH : classpath;
visitorsBridge = new VisitorsBridgeForTests(visitors, actualClasspath, sonarComponents, actualVersion);
}
JavaAstScanner astScanner = new JavaAstScanner(sonarComponents);
visitorsBridge.setInAndroidContext(inAndroidContext);
astScanner.setVisitorBridge(visitorsBridge);
List filesToParse = files;
if (isCacheEnabled) {
visitorsBridge.setCacheContext(cacheContext);
filesToParse = astScanner.scanWithoutParsing(files).get(false);
}
astScanner.scan(filesToParse);
JavaFileScannerContextForTests testJavaFileScannerContext = visitorsBridge.lastCreatedTestContext();
JavaFileScannerContextForTests testModuleScannerContext = visitorsBridge.lastCreatedModuleContext();
if (testJavaFileScannerContext != null) {
var issues = new LinkedHashSet<>(testJavaFileScannerContext.getIssues());
issues.addAll(testModuleScannerContext.getIssues());
var quickFixes = new HashMap<>(testJavaFileScannerContext.getQuickFixes());
quickFixes.putAll(testModuleScannerContext.getQuickFixes());
checkIssues(issues, quickFixes);
} else {
checkIssues(Collections.emptySet(), Collections.emptyMap());
}
}
private void checkIssues(Set issues, Map> quickFixes) {
if (expectations.expectNoIssues()) {
assertNoIssues(issues);
} else if (expectations.expectIssueAtFileLevel() || expectations.expectIssueAtProjectLevel()) {
assertComponentIssue(issues);
} else {
assertMultipleIssues(issues, quickFixes);
}
if (customIssueVerifier != null) {
customIssueVerifier.accept(issues);
}
}
private static void assertNoIssues(Set issues) {
if (issues.isEmpty()) {
return;
}
String issuesAsString = issues.stream()
.sorted(issueLineSorter())
.map(issue -> String.format("'%s' in %s%s", issue.getMessage(), issue.getInputComponent(), (issue.getLine() == null ? "" : (":" + issue.getLine()))))
.collect(Collectors.joining("\n--> ", "\n--> ", ""));
throw new AssertionError(String.format("No issues expected but got %d issue(s):%s", issues.size(), issuesAsString));
}
private static Comparator super AnalyzerMessage> issueLineSorter() {
return (i1, i2) -> {
if (i1.getLine() == null) {
return 1;
}
if (i2.getLine() == null) {
return -1;
}
return Integer.compare(i1.getLine(), i2.getLine());
};
}
private void assertComponentIssue(Set issues) {
String expectedMessage = expectations.expectedFileIssue();
String component = "file";
String otherComponent = "project";
if (expectations.expectIssueAtProjectLevel()) {
expectedMessage = expectations.expectedProjectIssue();
component = "project";
otherComponent = "file";
}
if (issues.size() != 1) {
String issueNumberMessage = issues.isEmpty() ? "none has been raised" : String.format("%d issues have been raised", issues.size());
throw new AssertionError(String.format("A single issue is expected on the %s, but %s", component, issueNumberMessage));
}
AnalyzerMessage issue = issues.iterator().next();
if (issue.getLine() != null) {
throw new AssertionError(String.format("Expected an issue directly on %s but was raised on line %d", component, issue.getLine()));
}
if ((expectations.expectIssueAtProjectLevel() && issue.getInputComponent().isFile())
|| (expectations.expectIssueAtFileLevel() && !issue.getInputComponent().isFile())) {
throw new AssertionError(String.format("Expected the issue to be raised at %s level, not at %s level", component, otherComponent));
}
if (!expectedMessage.equals(issue.getMessage())) {
throw new AssertionError(String.format("Expected the issue message to be:%n\t\"%s\"%nbut was:%n\t\"%s\"", expectedMessage, issue.getMessage()));
}
}
private void assertMultipleIssues(Set issues, Map> quickFixes) throws AssertionError {
if (issues.isEmpty()) {
throw new AssertionError("No issue raised. At least one issue expected");
}
List unexpectedLines = new LinkedList<>();
Expectations.RemediationFunction remediationFunction = Expectations.remediationFunction(issues.iterator().next());
Map> expected = expectations.issues;
for (AnalyzerMessage issue : issues) {
validateIssue(expected, unexpectedLines, issue, remediationFunction);
}
if (!expected.isEmpty() || !unexpectedLines.isEmpty()) {
Collections.sort(unexpectedLines);
List expectedLines = expected.keySet().stream().sorted().toList();
throw new AssertionError(new StringBuilder()
.append(expectedLines.isEmpty() ? "" : String.format("Expected at %s", expectedLines))
.append(expectedLines.isEmpty() || unexpectedLines.isEmpty() ? "" : ", ")
.append(unexpectedLines.isEmpty() ? "" : String.format("Unexpected at %s", unexpectedLines))
.toString());
}
assertSuperfluousFlows();
if (collectQuickFixes) {
new QuickFixesVerifier(expectations.quickFixes(), quickFixes).accept(issues);
}
}
private void validateIssue(
Map> expected,
List unexpectedLines,
AnalyzerMessage issue,
@Nullable Expectations.RemediationFunction remediationFunction) {
int line = issue.getLine();
if (expected.containsKey(line)) {
Expectations.Issue attrs = expected.get(line).get(0);
validateRemediationFunction(attrs, issue, remediationFunction);
validateAnalyzerMessageAttributes(attrs, issue);
expected.computeIfPresent(line, (l, issues) -> {
issues.remove(attrs);
// remove the key if nothing remaining
return issues.isEmpty() ? null : issues;
});
} else {
unexpectedLines.add(line);
}
}
private static void validateRemediationFunction(Expectations.Issue attributes, AnalyzerMessage issue, @Nullable Expectations.RemediationFunction remediationFunction) {
if (remediationFunction == null) {
return;
}
Double effortToFix = issue.getCost();
if (effortToFix != null) {
if (remediationFunction == Expectations.RemediationFunction.CONST) {
throw new AssertionError("Rule with constant remediation function shall not provide cost");
}
assertAttributeMatch(issue, effortToFix, attributes, EFFORT_TO_FIX);
} else if (remediationFunction == Expectations.RemediationFunction.LINEAR) {
throw new AssertionError("A cost should be provided for a rule with linear remediation function");
}
}
private void assertSuperfluousFlows() {
Set unseenFlowIds = expectations.unseenFlowIds();
Map unseenFlowWithLines = unseenFlowIds.stream()
.collect(Collectors.toMap(Function.identity(), expectations::flowToLines));
if (!unseenFlowWithLines.isEmpty()) {
throw new AssertionError(String.format("Following flow comments were observed, but not referenced by any issue: %s", unseenFlowWithLines));
}
}
private static void assertAttributeMatch(AnalyzerMessage issue, Object value, Map attributes, Expectations.IssueAttribute attribute) {
if (attributes.containsKey(attribute) && !value.equals(attribute.get(attributes))) {
throw new AssertionError(
String.format("line %d attribute mismatch for '%s'. Expected: '%s', but was: '%s'",
issue.getLine(),
attribute,
attribute.get(attributes),
value));
}
}
private void validateAnalyzerMessageAttributes(Expectations.Issue attrs, AnalyzerMessage analyzerMessage) {
assertAttributeMatch(analyzerMessage, analyzerMessage.getMessage(), attrs, MESSAGE);
validateLocation(analyzerMessage, attrs);
if (attrs.containsKey(SECONDARY_LOCATIONS)) {
List actual = analyzerMessage.flows.stream().map(l -> l.isEmpty() ? null : l.get(0)).filter(Objects::nonNull).toList();
List expected = (List) attrs.get(SECONDARY_LOCATIONS);
validateSecondaryLocations(analyzerMessage, actual, expected);
}
if (attrs.containsKey(FLOWS)) {
validateFlows(analyzerMessage.flows, (List) attrs.get(FLOWS));
}
}
private static void validateSecondaryLocations(AnalyzerMessage parentIssue, List actual, List expected) {
List actualLines = actual.stream().map(AnalyzerMessage::getLine).toList();
List unexpected = new ArrayList<>();
for (Integer actualLine : actualLines) {
if (expected.contains(actualLine)) {
expected.remove(actualLine);
} else {
unexpected.add(actualLine);
}
}
if (!expected.isEmpty() || !unexpected.isEmpty()) {
throw new AssertionError(
String.format("Secondary locations: expected: %s unexpected: %s. In %s:%d",
expected,
unexpected,
((InputFile) parentIssue.getInputComponent()).filename(),
parentIssue.getLine()));
}
}
private static void validateLocation(AnalyzerMessage analyzerMessage, Map attrs) {
AnalyzerMessage.TextSpan textSpan = analyzerMessage.primaryLocation();
Objects.requireNonNull(textSpan);
assertAttributeMatch(analyzerMessage, normalizeColumn(textSpan.startCharacter), attrs, START_COLUMN);
assertAttributeMatch(analyzerMessage, textSpan.endLine, attrs, END_LINE);
assertAttributeMatch(analyzerMessage, normalizeColumn(textSpan.endCharacter), attrs, END_COLUMN);
}
private static int normalizeColumn(int startCharacter) {
return startCharacter + 1;
}
private void validateFlows(List> actual, List expectedFlowIds) {
Map> foundFlows = new HashMap<>();
List> unexpectedFlows = new ArrayList<>();
actual.forEach(f -> validateFlow(f, foundFlows, unexpectedFlows));
expectedFlowIds.removeAll(foundFlows.keySet());
assertExpectedAndMissingFlows(expectedFlowIds, unexpectedFlows);
validateFoundFlows(foundFlows);
}
private void assertExpectedAndMissingFlows(List expectedFlowIds, List> unexpectedFlows) {
if (expectedFlowIds.size() == 1 && expectedFlowIds.size() == unexpectedFlows.size()) {
assertSoleFlowDiscrepancy(expectedFlowIds.get(0), unexpectedFlows.get(0));
}
String unexpectedMsg = unexpectedFlows.stream()
.map(InternalCheckVerifier::flowToString)
.collect(Collectors.joining("\n"));
String missingMsg = expectedFlowIds.stream().map(fid -> String.format("%s [%s]", fid, expectations.flowToLines(fid))).collect(Collectors.joining(","));
if (!unexpectedMsg.isEmpty() || !missingMsg.isEmpty()) {
unexpectedMsg = unexpectedMsg.isEmpty() ? "" : String.format("Unexpected flows: %s. ", unexpectedMsg);
missingMsg = missingMsg.isEmpty() ? "" : String.format("Missing flows: %s.", missingMsg);
throw new AssertionError(unexpectedMsg + missingMsg);
}
}
private void assertSoleFlowDiscrepancy(String expectedId, List actualFlow) {
Set expected = expectations.flows.get(expectedId);
List expectedLines = expected.stream().map(flow -> flow.line).toList();
List actualLines = actualFlow.stream().map(AnalyzerMessage::getLine).toList();
if (!actualLines.equals(expectedLines)) {
throw new AssertionError(String.format("Flow %s has line differences. Expected: %s but was: %s", expectedId, expectedLines, actualLines));
}
}
private void validateFlow(List flow, Map> foundFlows, List> unexpectedFlows) {
Optional flowId = expectations.containFlow(flow);
if (flowId.isPresent()) {
foundFlows.put(flowId.get(), flow);
} else {
unexpectedFlows.add(flow);
}
}
private void validateFoundFlows(Map> foundFlows) {
foundFlows.forEach((flowId, flow) -> validateFlowAttributes(flow, flowId));
}
private void validateFlowAttributes(List actual, String flowId) {
SortedSet expected = expectations.flows.get(flowId);
validateFlowMessages(actual, flowId, expected);
Iterator actualIterator = actual.iterator();
Iterator expectedIterator = expected.iterator();
while (actualIterator.hasNext() && expectedIterator.hasNext()) {
AnalyzerMessage actualFlow = actualIterator.next();
if (actualFlow.primaryLocation() == null) {
throw new AssertionError(String.format("Flow without location: %s", actualFlow));
}
validateLocation(actualFlow, expectedIterator.next().attributes);
}
}
private void validateFlowMessages(List actual, String flowId, SortedSet expected) {
List actualMessages = actual.stream()
.map(AnalyzerMessage::getMessage)
.map(InternalCheckVerifier::addQuotes)
.toList();
List expectedMessages = expected.stream()
.map(Expectations.FlowComment::message)
.map(InternalCheckVerifier::addQuotes)
.toList();
expectedMessages = replaceExpectedNullWithActual(actualMessages, expectedMessages);
if (!actualMessages.equals(expectedMessages)) {
throw new AssertionError(
String.format("Wrong messages in flow %s [%s]. Expected: %s but was: %s",
flowId,
expectations.flowToLines(flowId),
expectedMessages,
actualMessages));
}
}
private static String addQuotes(@Nullable String s) {
return s != null ? String.format("\"%s\"", s) : s;
}
private static List replaceExpectedNullWithActual(List actualMessages, List expectedMessages) {
List newExceptedMessages = new ArrayList<>(expectedMessages);
if (actualMessages.size() == newExceptedMessages.size()) {
for (int i = 0; i < actualMessages.size(); i++) {
if (newExceptedMessages.get(i) == null) {
newExceptedMessages.set(i, actualMessages.get(i));
}
}
}
return newExceptedMessages;
}
private static String flowToString(List flow) {
return flow.stream().map(m -> String.valueOf(m.getLine())).collect(Collectors.joining(",", "[", "]"));
}
private static class QuickFixesVerifier implements Consumer> {
private final Map> expectedQuickFixes;
private final Map> actualQuickFixes;
public QuickFixesVerifier(Map> expectedQuickFixes, Map> actualQuickFixes) {
this.expectedQuickFixes = expectedQuickFixes;
this.actualQuickFixes = actualQuickFixes;
}
@Override
public void accept(Set issues) {
for (AnalyzerMessage issue : issues) {
AnalyzerMessage.TextSpan primaryLocation = issue.primaryLocation();
List expected = expectedQuickFixes.get(primaryLocation);
if (expected == null) {
// We don't have to always test quick fixes, we do nothing if there is no expected quick fix.
continue;
}
List actual = actualQuickFixes.get(primaryLocation);
if (expected.isEmpty()) {
if (actual != null && !actual.isEmpty()) {
throw new AssertionError(String.format("[Quick Fix] Issue on line %d contains quick fixes while none where expected", primaryLocation.startLine));
}
// Else: no issue in both expected and actual, nothing to do
} else {
validateIfSameSize(expected, actual, issue);
}
}
}
private static void validateIfSameSize(List expected, @Nullable List actual, AnalyzerMessage issue) {
AnalyzerMessage.TextSpan primaryLocation = issue.primaryLocation();
if (actual == null || actual.isEmpty()) {
// At this point, we know that expected is not empty
throw new AssertionError(String.format("[Quick Fix] Missing quick fix for issue on line %d", primaryLocation.startLine));
}
int actualSize = actual.size();
int expectedSize = expected.size();
if (actualSize != expectedSize) {
throw new AssertionError(
String.format("[Quick Fix] Number of quickfixes expected is not equal to the number of expected on line %d: expected: %d , actual: %d",
primaryLocation.startLine,
expectedSize,
actualSize));
}
for (int i = 0; i < actualSize; i++) {
validate(issue, actual.get(i), expected.get(i));
}
}
private static void validate(AnalyzerMessage actualIssue, JavaQuickFix actual, JavaQuickFix expected) {
String actualDescription = actual.getDescription();
String expectedDescription = expected.getDescription();
if (!actualDescription.equals(expectedDescription)) {
throw new AssertionError(String.format("[Quick Fix] Wrong description for issue on line %d.%nExpected: {{%s}}%nbut was: {{%s}}",
actualIssue.getLine(),
expectedDescription,
actualDescription));
}
List actualTextEdits = actual.getTextEdits();
List expectedTextEdits = expected.getTextEdits();
if (actualTextEdits.size() != expectedTextEdits.size()) {
throw new AssertionError(String.format("[Quick Fix] Wrong number of edits for issue on line %d.%nExpected: {{%d}}%nbut was: {{%d}}",
actualIssue.getLine(),
expectedTextEdits.size(),
actualTextEdits.size()));
}
for (int i = 0; i < actualTextEdits.size(); i++) {
JavaTextEdit actualTextEdit = actualTextEdits.get(i);
JavaTextEdit expectedTextEdit = expectedTextEdits.get(i);
String expectedReplacement = expectedTextEdit.getReplacement();
String actualReplacement = actualTextEdit.getReplacement();
if (expectedReplacement.contains("\\n")) {
// new lines are expected
expectedReplacement = expectedReplacement.replace("\\n", "\n");
}
if (!actualReplacement.equals(expectedReplacement)) {
throw new AssertionError(String.format("[Quick Fix] Wrong text replacement of edit %d for issue on line %d.%nExpected: {{%s}}%nbut was: {{%s}}",
(i + 1),
actualIssue.getLine(),
expectedReplacement,
actualReplacement));
}
AnalyzerMessage.TextSpan actualNormalizedTextSpan = actualTextEdit.getTextSpan();
if (!actualNormalizedTextSpan.equals(expectedTextEdit.getTextSpan())) {
throw new AssertionError(String.format("[Quick Fix] Wrong change location of edit %d for issue on line %d.%nExpected: {{%s}}%nbut was: {{%s}}",
(i + 1),
actualIssue.getLine(),
editorTextSpan(expectedTextEdit.getTextSpan()),
editorTextSpan(actualNormalizedTextSpan)));
}
}
}
private static TextSpan editorTextSpan(TextSpan textSpan) {
return new TextSpan(textSpan.startLine, textSpan.startCharacter + 1, textSpan.endLine, textSpan.endCharacter + 1);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy