org.sonar.css.checks.verifier.CssCheckVerifier Maven / Gradle / Ivy
/*
* SonarQube CSS / SCSS / Less Analyzer
* Copyright (C) 2013-2017 David RACODON
* mailto: [email protected]
*
* 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.css.checks.verifier;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.sonar.sslr.api.typed.ActionParser;
import org.sonar.css.parser.css.CssParser;
import org.sonar.css.parser.embedded.EmbeddedCssParser;
import org.sonar.css.parser.less.LessParser;
import org.sonar.css.parser.scss.ScssParser;
import org.sonar.css.tree.impl.TreeImpl;
import org.sonar.css.visitors.CharsetAwareVisitor;
import org.sonar.css.visitors.CssTreeVisitorContext;
import org.sonar.plugins.css.api.CssCheck;
import org.sonar.plugins.css.api.tree.Tree;
import org.sonar.plugins.css.api.tree.css.SyntaxToken;
import org.sonar.plugins.css.api.tree.css.SyntaxTrivia;
import org.sonar.plugins.css.api.visitors.SubscriptionVisitorCheck;
import org.sonar.plugins.css.api.visitors.issue.*;
import org.sonar.squidbridge.checks.CheckMessagesVerifier;
import javax.annotation.Nullable;
import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.fail;
/**
* To unit test checks.
*/
public class CssCheckVerifier extends SubscriptionVisitorCheck {
private final List expectedIssues = new ArrayList<>();
/**
* Check issuesOnCssFile.
* File is parsed with CSS parser.
*
* @param check Check to test
* @param file File to test
*
* Example:
*
* CheckVerifier.issuesOnCssFile(new MyCheck(), myFile))
* .next().atLine(2).withMessage("This is message for line 2.")
* .next().atLine(3).withMessage("This is message for line 3.").withCost(2.)
* .next().atLine(8)
* .noMore();
*
*/
public static CheckMessagesVerifier issuesOnCssFile(CssCheck check, File file) {
return issuesOnCssFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#issuesOnCssFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
public static CheckMessagesVerifier issuesOnCssFile(CssCheck check, File file, Charset charset) {
if (check instanceof CharsetAwareVisitor) {
((CharsetAwareVisitor) check).setCharset(charset);
}
return CheckMessagesVerifier.verify(TreeCheckTest.getIssues(file.getAbsolutePath(), check, CssParser.createParser(charset)));
}
/**
* See {@link CssCheckVerifier#issuesOnCssFile(CssCheck, File)}
* File is parsed with Less parser.
*/
public static CheckMessagesVerifier issuesOnEmbeddedCssFile(CssCheck check, File file) {
return issuesOnEmbeddedCssFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#issuesOnEmbeddedCssFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
public static CheckMessagesVerifier issuesOnEmbeddedCssFile(CssCheck check, File file, Charset charset) {
if (check instanceof CharsetAwareVisitor) {
((CharsetAwareVisitor) check).setCharset(charset);
}
return CheckMessagesVerifier.verify(TreeCheckTest.getIssues(file.getAbsolutePath(), check, EmbeddedCssParser.createParser(charset)));
}
/**
* See {@link CssCheckVerifier#issuesOnCssFile(CssCheck, File)}
* File is parsed with Less parser.
*/
public static CheckMessagesVerifier issuesOnLessFile(CssCheck check, File file) {
return issuesOnLessFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#issuesOnLessFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
public static CheckMessagesVerifier issuesOnLessFile(CssCheck check, File file, Charset charset) {
if (check instanceof CharsetAwareVisitor) {
((CharsetAwareVisitor) check).setCharset(charset);
}
return CheckMessagesVerifier.verify(TreeCheckTest.getIssues(file.getAbsolutePath(), check, LessParser.createParser(charset)));
}
/**
* See {@link CssCheckVerifier#issuesOnCssFile(CssCheck, File)}
* File is parsed with Less parser.
*/
public static CheckMessagesVerifier issuesOnScssFile(CssCheck check, File file) {
return issuesOnScssFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#issuesOnScssFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
public static CheckMessagesVerifier issuesOnScssFile(CssCheck check, File file, Charset charset) {
if (check instanceof CharsetAwareVisitor) {
((CharsetAwareVisitor) check).setCharset(charset);
}
return CheckMessagesVerifier.verify(TreeCheckTest.getIssues(file.getAbsolutePath(), check, ScssParser.createParser(charset)));
}
/**
* To unit tests checks.
* File is parsed with CSS parser.
*
* Expected issuesOnCssFile should be provided as comments in the source file.
* Expected issue details should be provided on the line of the actual issue.
* For example:
*
* color: green; /* Noncompliant !{Error message for the issue on this line}!
*
* /* Noncompliant ![sc=2;ec=6;secondary=+2,+4]! !{Error message}!
* ...
*
*
* How to write these comments:
*
* - Put a comment starting with "Noncompliant" if you expect an issue on the line.
* - Optional - In ![...]! provide the precise issue location
sl, sc, ec, el
keywords respectively for start line, start column, end column and end line. sl=+1
by default.
* - Optional - In ![...]! provide secondary locations with the
secondary
keyword.
* - Optional - In ![...]! provide expected effort to fix (cost) with the
effortToFix
keyword.
* - Optional - In
!{MESSAGE}!
provide the expected message.
* - To specify the line you can use relative location by putting
+
or -
.
* - Note that the order matters: Noncompliant => Parameters => Error message
*
*
* Example of call:
*
* CheckVerifier.verifyCssFile(new MyCheck(), myFile));
*
*/
public static void verifyCssFile(CssCheck check, File file) {
verifyCssFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#verifyCssFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
public static void verifyCssFile(CssCheck check, File file, Charset charset) {
verify(check, file, charset, CssParser.createParser(charset), "css");
}
/**
* See {@link CssCheckVerifier#verifyCssFile(CssCheck, File)}
* File is parsed with CSS Embedded parser.
*/
public static void verifyEmbeddedCssFile(CssCheck check, File file) {
verifyEmbeddedCssFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#verifyEmbeddedCssFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
private static void verifyEmbeddedCssFile(CssCheck check, File file, Charset charset) {
verify(check, file, charset, EmbeddedCssParser.createParser(charset), "css");
}
/**
* See {@link CssCheckVerifier#verifyCssFile(CssCheck, File)}
* File is parsed with SCSS parser.
*/
public static void verifyScssFile(CssCheck check, File file) {
verifyScssFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#verifyScssFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
private static void verifyScssFile(CssCheck check, File file, Charset charset) {
verify(check, file, charset, ScssParser.createParser(charset), "scss");
}
/**
* See {@link CssCheckVerifier#verifyCssFile(CssCheck, File)}
* File is parsed with Less parser.
*/
public static void verifyLessFile(CssCheck check, File file) {
verifyLessFile(check, file, Charsets.UTF_8);
}
/**
* See {@link CssCheckVerifier#verifyLessFile(CssCheck, File)}
*
* @param charset Charset of the file to test.
*/
private static void verifyLessFile(CssCheck check, File file, Charset charset) {
verify(check, file, charset, LessParser.createParser(charset), "less");
}
private static void verify(CssCheck check, File file, Charset charset, ActionParser parser, String language) {
TreeImpl tree = (TreeImpl) parser.parse(file);
CssTreeVisitorContext context = new CssTreeVisitorContext(tree, file, language);
CssCheckVerifier checkVerifier = new CssCheckVerifier();
checkVerifier.scanFile(context);
List expectedIssues = checkVerifier.expectedIssues
.stream()
.sorted((i1, i2) -> Integer.compare(i1.line(), i2.line()))
.collect(Collectors.toList());
if (check instanceof CharsetAwareVisitor) {
((CharsetAwareVisitor) check).setCharset(charset);
}
Iterator actualIssues = getActualIssues(check, context);
for (TestIssue expected : expectedIssues) {
if (actualIssues.hasNext()) {
verifyIssue(expected, actualIssues.next(), file);
} else {
throw new AssertionError("Missing issue at line " + expected.line() + " in file " + file.getAbsolutePath());
}
}
if (actualIssues.hasNext()) {
Issue issue = actualIssues.next();
throw new AssertionError("Unexpected issue at line " + line(issue) + ": \"" + message(issue) + "\"" + " in file " + file.getAbsolutePath());
}
}
private static Iterator getActualIssues(CssCheck check, CssTreeVisitorContext context) {
List issues = check.scanFile(context);
List sortedIssues = Ordering.natural().onResultOf(new IssueToLine()).sortedCopy(issues);
return sortedIssues.iterator();
}
private static void verifyIssue(TestIssue expected, Issue actual, File file) {
if (line(actual) > expected.line()) {
fail("Missing issue at line " + expected.line() + " in file " + file.getAbsolutePath());
}
if (line(actual) < expected.line()) {
fail("Unexpected issue at line " + line(actual) + ": \"" + message(actual) + "\"" + " in file " + file.getAbsolutePath());
}
if (expected.message() != null) {
assertThat(message(actual)).as("Bad message at line " + expected.line()).isEqualTo(expected.message());
}
if (expected.effortToFix() != null) {
assertThat(actual.cost()).as("Bad effortToFix at line " + expected.line()).isEqualTo(expected.effortToFix());
}
if (expected.startColumn() != null) {
assertThat(((PreciseIssue) actual).primaryLocation().startLineOffset() + 1).as("Bad start column at line " + expected.line()).isEqualTo(expected.startColumn());
}
if (expected.endColumn() != null) {
assertThat(((PreciseIssue) actual).primaryLocation().endLineOffset() + 1).as("Bad end column at line " + expected.line()).isEqualTo(expected.endColumn());
}
if (expected.endLine() != null) {
assertThat(((PreciseIssue) 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());
}
}
@Override
public List nodesToVisit() {
return ImmutableList.of(Tree.Kind.TOKEN);
}
@Override
public void visitNode(Tree tree) {
SyntaxToken token = (SyntaxToken) tree;
for (SyntaxTrivia trivia : token.trivias()) {
String text = trivia.text().substring(3).trim();
String marker = "Noncompliant";
if (text.startsWith(marker)) {
TestIssue issue = issue(null, trivia.line());
String paramsAndMessage = text.substring(marker.length()).trim();
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);
}
}
}
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 ("sc".equalsIgnoreCase(name)) {
issue.startColumn(Integer.valueOf(value));
} else if ("sl".equalsIgnoreCase(name)) {
issue.startLine(lineValue(issue.line(), value));
} else if ("ec".equalsIgnoreCase(name)) {
issue.endColumn(Integer.valueOf(value));
} else if ("el".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 static TestIssue issue(@Nullable String message, int lineNumber) {
return TestIssue.create(message, lineNumber);
}
private static class IssueToLine implements Function {
@Override
public Integer apply(Issue issue) {
return line(issue);
}
}
private static int line(Issue issue) {
if (issue instanceof PreciseIssue) {
return ((PreciseIssue) issue).primaryLocation().startLine();
} else if (issue instanceof FileIssue) {
return 0;
} else if (issue instanceof LineIssue) {
return ((LineIssue) issue).line();
} else {
throw new IllegalStateException("Unknown type of issue.");
}
}
private static String message(Issue issue) {
if (issue instanceof PreciseIssue) {
return ((PreciseIssue) issue).primaryLocation().message();
} else if (issue instanceof FileIssue) {
return ((FileIssue) issue).message();
} else if (issue instanceof LineIssue) {
return ((LineIssue) issue).message();
} else {
throw new IllegalStateException("Unknown type of issue.");
}
}
private static List secondary(Issue issue) {
List result = new ArrayList<>();
if (issue instanceof PreciseIssue) {
result.addAll(((PreciseIssue) issue).secondaryLocations().stream()
.map(IssueLocation::startLine)
.collect(Collectors.toList()));
} else if (issue instanceof FileIssue) {
result.addAll(((FileIssue) issue).secondaryLocations().stream()
.map(IssueLocation::startLine)
.collect(Collectors.toList()));
}
return Ordering.natural().sortedCopy(result);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy