
org.apiaddicts.apitools.dosonarapi.OpenApiCheckVerifier Maven / Gradle / Ivy
/*
* doSonarAPI: SonarQube OpenAPI Plugin
* Copyright (C) 2021-2022 Apiaddicts
* contacta AT apiaddicts DOT org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.apiaddicts.apitools.dosonarapi;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Ordering;
import com.sonar.sslr.api.Token;
import com.sonar.sslr.api.Trivia;
import org.apiaddicts.apitools.dosonarapi.api.IssueLocation;
import org.apiaddicts.apitools.dosonarapi.api.OpenApiCheck;
import org.apiaddicts.apitools.dosonarapi.api.OpenApiVisitor;
import org.apiaddicts.apitools.dosonarapi.api.PreciseIssue;
import org.apiaddicts.apitools.dosonarapi.api.TestOpenApiVisitorRunner;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
/**
* It is possible to specify the absolute line number on which the issue should appear by appending {@literal "@"} to "Noncompliant".
* But usually better to use line number relative to the current, this is possible to do by prefixing the number with either '+' or '-'.
* For example:
*
* paths:
* # Noncompliant@+1 {{do not use terminal '/'}}
* /pets/:
*
* Full syntax:
*
* // Noncompliant@+1 [[startColumn=1;endLine=+1;endColumn=2;effortToFix=4;secondary=3,4]] {{issue message}}
*
* Attributes between [[]] are optional:
*
* - startColumn: column where the highlight starts
* - endLine: relative endLine where the highlight ends (i.e. +1), same line if omitted
* - endColumn: column where the highlight ends
* - effortToFix: the cost to fix as integer
* - secondary: a comma separated list of integers identifying the lines of secondary locations if any
*
*/
public class OpenApiCheckVerifier {
private List expectedIssues = new ArrayList<>();
public static List scanFileForIssues(File file, OpenApiCheck check, boolean isV2) {
return check.scanFileForIssues(TestOpenApiVisitorRunner.createContext(file, isV2));
}
/**
* Verifies that the provided file will raise all the expected issues when analyzed with the given check.
*
* @param path The file to be analyzed
* @param check The check to be used for the analysis
* @param isV2 true
if OpenApiSpecification version 2 or false
if version 3
*/
public static void verify(String path, OpenApiCheck check, boolean isV2) {
OpenApiCheckVerifier verifier = new OpenApiCheckVerifier();
OpenApiVisitor collector = new ExpectedIssueCollector(verifier);
File file = new File(path);
TestOpenApiVisitorRunner.scanFileForComments(file, isV2, collector);
Iterator actualIssues = getActualIssues(file, check, isV2);
verifier.checkIssues(actualIssues);
if (actualIssues.hasNext()) {
PreciseIssue issue = actualIssues.next();
throw new AssertionError("Unexpected issue at line " + line(issue) + ": \"" + issue.primaryLocation().message() + "\"");
}
}
private static int line(PreciseIssue issue) {
return issue.primaryLocation().startLine();
}
private void checkIssues(Iterator actualIssues) {
for (TestIssue expected : expectedIssues) {
if (actualIssues.hasNext()) {
verifyIssue(expected, actualIssues.next());
} else {
throw new AssertionError("Missing issue at line " + expected.line());
}
}
}
private void verifyIssue(TestIssue expected, PreciseIssue actual) {
if (line(actual) > expected.line()) {
fail("Missing issue at line " + expected.line());
}
if (line(actual) < expected.line()) {
fail("Unexpected issue at line " + line(actual) + ": \"" + actual.primaryLocation().message() + "\"");
}
if (expected.message() != null) {
assertThat(actual.primaryLocation().message()).as("Bad message at line " + expected.line()).isEqualTo(expected.message());
}
if (expected.effortToFix() != null) {
assertThat(actual.cost().intValue()).as("Bad effortToFix at line " + expected.line()).isEqualTo(expected.effortToFix());
}
if (expected.startColumn() != null) {
assertThat(actual.primaryLocation().startLineOffset()).as("Bad start column at line " +
expected.line()).isEqualTo(expected.startColumn());
}
if (expected.endColumn() != null) {
assertThat(actual.primaryLocation().endLineOffset()).as("Bad end column at line " + expected.line()).isEqualTo(expected.endColumn());
}
if (expected.endLine() != null) {
assertThat(actual.primaryLocation().endLine()).as("Bad end line at line " + expected.line()).isEqualTo(expected.endLine());
}
if (expected.secondaryLines() != null) {
assertThat(secondary(actual)).as("Bad secondary locations at line " + expected.line()).isEqualTo(expected.secondaryLines());
}
}
private static List secondary(PreciseIssue issue) {
List result = new ArrayList<>();
for (IssueLocation issueLocation : issue.secondaryLocations()) {
result.add(issueLocation.startLine());
}
return Ordering.natural().sortedCopy(result);
}
private static Iterator getActualIssues(File file, OpenApiCheck check, boolean isV2) {
List issues = scanFileForIssues(file, check, isV2);
List sortedIssues = Ordering.natural().onResultOf(OpenApiCheckVerifier::line).sortedCopy(issues);
return sortedIssues.iterator();
}
public void collectExpectedIssue(Trivia trivia) {
String text = trivia.getToken().getValue().trim();
String marker = "Noncompliant";
if (text.startsWith(marker)) {
int issueLine = trivia.getToken().getLine();
String paramsAndMessage = text.substring(marker.length()).trim();
if (paramsAndMessage.startsWith("@")) {
String[] spaceSplit = paramsAndMessage.split("[\\s\\[{]", 2);
String lineMarker = spaceSplit[0].substring(1); // remove @
issueLine = lineValue(issueLine, lineMarker);
paramsAndMessage = spaceSplit.length > 1 ? spaceSplit[1] : "";
}
TestIssue issue = TestIssue.create(null, issueLine);
if (paramsAndMessage.startsWith("[[")) {
int endIndex = paramsAndMessage.indexOf("]]");
addParams(issue, paramsAndMessage.substring(2, endIndex));
paramsAndMessage = paramsAndMessage.substring(endIndex + 2).trim();
}
if (paramsAndMessage.startsWith("{{")) {
int endIndex = paramsAndMessage.indexOf("}}");
String message = paramsAndMessage.substring(2, endIndex);
issue.message(message);
}
expectedIssues.add(issue);
} else if (text.startsWith("^")) {
addPreciseLocation(trivia);
}
}
private static void addParams(TestIssue issue, String params) {
for (String param : Splitter.on(';').split(params)) {
int equalIndex = param.indexOf('=');
if (equalIndex == -1) {
throw new IllegalStateException("Invalid param at line 1: " + param);
}
String name = param.substring(0, equalIndex);
String value = param.substring(equalIndex + 1);
if ("effortToFix".equalsIgnoreCase(name)) {
issue.effortToFix(Integer.valueOf(value));
} else if ("startColumn".equalsIgnoreCase(name)) {
issue.startColumn(Integer.valueOf(value));
} else if ("endColumn".equalsIgnoreCase(name)) {
issue.endColumn(Integer.valueOf(value));
} else if ("endLine".equalsIgnoreCase(name)) {
issue.endLine(lineValue(issue.line(), value));
} else if ("secondary".equalsIgnoreCase(name)) {
addSecondaryLines(issue, value);
} else {
throw new IllegalStateException("Invalid param at line 1: " + name);
}
}
}
private static void addSecondaryLines(TestIssue issue, String value) {
List secondaryLines = new ArrayList<>();
if (!"".equals(value)) {
for (String secondary : Splitter.on(',').split(value)) {
secondaryLines.add(lineValue(issue.line(), secondary));
}
}
issue.secondary(secondaryLines);
}
private static int lineValue(int baseLine, String shift) {
if (shift.startsWith("+")) {
return baseLine + Integer.valueOf(shift.substring(1));
}
if (shift.startsWith("-")) {
return baseLine - Integer.valueOf(shift.substring(1));
}
return Integer.valueOf(shift);
}
private void addPreciseLocation(Trivia trivia) {
Token token = trivia.getToken();
int line = token.getLine();
String text = token.getValue();
if (token.getColumn() > 1) {
throw new IllegalStateException("Line " + line + ": comments asserting a precise location should start at column 1");
}
String missingAssertionMessage = String.format("Invalid test file: a precise location is provided at line %s but no issue is asserted at line %s", line, line - 1);
if (expectedIssues.isEmpty()) {
throw new IllegalStateException(missingAssertionMessage);
}
TestIssue issue = expectedIssues.get(expectedIssues.size() - 1);
if (issue.line() != line - 1) {
throw new IllegalStateException(missingAssertionMessage);
}
issue.endLine(issue.line());
issue.startColumn(text.indexOf('^') + 1);
issue.endColumn(text.lastIndexOf('^') + 2);
}
@VisibleForTesting
List getCollectedIssues() {
return Collections.unmodifiableList(expectedIssues);
}
private static final class ExpectedIssueCollector extends OpenApiVisitor {
private final OpenApiCheckVerifier verifier;
private ExpectedIssueCollector(OpenApiCheckVerifier verifier) {
this.verifier = verifier;
}
@Override
protected void visitToken(Token token) {
for (Trivia trivia : token.getTrivia()) {
verifier.collectExpectedIssue(trivia);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy