/*
* SonarQube Java
* Copyright (C) 2012-2016 SonarSource SA
* mailto:contact 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.ast;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.sonar.sslr.api.RecognitionException;
import com.sonar.sslr.api.typed.ActionParser;
import com.sonar.sslr.api.typed.GrammarBuilder;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.batch.fs.internal.DefaultFileSystem;
import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.sensor.internal.SensorContextTester;
import org.sonar.api.issue.NoSonarFilter;
import org.sonar.java.Measurer;
import org.sonar.java.SonarComponents;
import org.sonar.java.ast.parser.JavaNodeBuilder;
import org.sonar.java.model.InternalSyntaxToken;
import org.sonar.java.model.JavaTree;
import org.sonar.java.model.VisitorsBridge;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.SyntaxToken;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TreeVisitor;
import org.sonar.squidbridge.AstScannerExceptionHandler;
import org.sonar.squidbridge.api.AnalysisException;
import org.sonar.sslr.grammar.GrammarRuleKey;
import org.sonar.sslr.grammar.LexerlessGrammarBuilder;
import java.io.File;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
public class JavaAstScannerTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
SensorContextTester context;
private DefaultFileSystem fs;
@Before
public void setUp() throws Exception {
context = SensorContextTester.create(new File(""));
fs = context.fileSystem();
}
@Test
public void comments() {
File file = new File("src/test/files/metrics/Comments.java");
DefaultInputFile resource = new DefaultInputFile("", "src/test/files/metrics/Comments.java");
fs.add(resource);
NoSonarFilter noSonarFilter = mock(NoSonarFilter.class);
JavaAstScanner.scanSingleFileForTests(file, new VisitorsBridge(new Measurer(fs, context, noSonarFilter)));
verify(noSonarFilter).noSonarInFile(resource, ImmutableSet.of(15));
}
@Test
public void noSonarLines() throws Exception {
File file = new File("src/test/files/metrics/NoSonar.java");
DefaultInputFile resource = new DefaultInputFile("", "src/test/files/metrics/NoSonar.java");
fs.add(resource);
NoSonarFilter noSonarFilter = mock(NoSonarFilter.class);
JavaAstScanner.scanSingleFileForTests(file, new VisitorsBridge(new Measurer(fs, context, noSonarFilter)));
verify(noSonarFilter).noSonarInFile(resource, ImmutableSet.of(8));
//No Sonar on tests files
NoSonarFilter noSonarFilterForTest = mock(NoSonarFilter.class);
JavaAstScanner.scanSingleFileForTests(file, new VisitorsBridge(new Measurer(fs, context, noSonarFilterForTest).new TestFileMeasurer()));
verify(noSonarFilterForTest).noSonarInFile(resource, ImmutableSet.of(8));
}
@Test
public void scan_single_file_with_dumb_file_should_fail() throws Exception {
thrown.expect(IllegalArgumentException.class);
String filename = "!!dummy";
thrown.expectMessage(filename);
JavaAstScanner.scanSingleFileForTests(new File(filename), new VisitorsBridge(null));
}
@Test
public void should_not_fail_whole_analysis_upon_parse_error_and_notify_audit_listeners() {
FakeAuditListener listener = spy(new FakeAuditListener());
JavaAstScanner scanner = defaultJavaAstScanner();
scanner.setVisitorBridge(new VisitorsBridge(listener));
scanner.scan(ImmutableList.of(new File("src/test/resources/AstScannerParseError.txt")));
verify(listener).processRecognitionException(any(RecognitionException.class));
}
@Test
public void should_interrupt_analysis_when_InterrptedException_is_thrown() throws Exception {
File file = new File("src/test/files/metrics/NoSonar.java");
thrown.expectMessage("Analysis cancelled");
thrown.expect(new AnalysisExceptionBaseMatcher(RecognitionException.class, "instanceof AnalysisException with RecognitionException cause"));
JavaAstScanner.scanSingleFileForTests(file, new VisitorsBridge(new CheckThrowingException(new RecognitionException(42, "interrupted", new InterruptedException()))));
}
@Test
public void should_interrupt_analysis_when_InterrptedIOException_is_thrown() throws Exception {
File file = new File("src/test/files/metrics/NoSonar.java");
thrown.expectMessage("Analysis cancelled");
thrown.expect(new AnalysisExceptionBaseMatcher(RecognitionException.class, "instanceof AnalysisException with RecognitionException cause"));
JavaAstScanner.scanSingleFileForTests(file, new VisitorsBridge(new CheckThrowingException(new RecognitionException(42, "interrupted", new InterruptedIOException()))));
}
@Test
public void should_propagate_visitor_exception_when_there_also_is_a_parse_error() {
JavaAstScanner scanner = defaultJavaAstScanner();
scanner.setVisitorBridge(new VisitorsBridge(new CheckThrowingException(new NullPointerException("foo"))));
thrown.expectMessage("SonarQube is unable to analyze file");
thrown.expect(new AnalysisExceptionBaseMatcher(NullPointerException.class, "instanceof AnalysisException with NullPointerException cause"));
scanner.scan(ImmutableList.of(new File("src/test/resources/AstScannerParseError.txt")));
}
@Test
public void should_propagate_visitor_exception_when_no_parse_error() {
JavaAstScanner scanner = defaultJavaAstScanner();
scanner.setVisitorBridge(new VisitorsBridge(new CheckThrowingException(new NullPointerException("foo"))));
thrown.expectMessage("SonarQube is unable to analyze file");
thrown.expect(new AnalysisExceptionBaseMatcher(NullPointerException.class, "instanceof AnalysisException with NullPointerException cause"));
scanner.scan(ImmutableList.of(new File("src/test/resources/AstScannerNoParseError.txt")));
}
@Test
public void should_propagate_SOError() {
thrown.expect(StackOverflowError.class);
JavaAstScanner scanner = defaultJavaAstScanner();
scanner.setVisitorBridge(new VisitorsBridge(new CheckThrowingSOError()));
scanner.scan(ImmutableList.of(new File("src/test/resources/AstScannerNoParseError.txt")));
}
@Test
public void should_report_analysis_error_in_sonarLint_context_withSQ_6_0() throws Exception {
JavaAstScanner scanner = defaultJavaAstScanner();
FakeAuditListener listener = spy(new FakeAuditListener());
SonarComponents sonarComponents = mock(SonarComponents.class);
when(sonarComponents.reportAnalysisError(any(RecognitionException.class), any(File.class))).thenReturn(true);
scanner.setVisitorBridge(new VisitorsBridge(Lists.newArrayList(listener), Lists.newArrayList(), sonarComponents, false));
scanner.scan(ImmutableList.of(new File("src/test/resources/AstScannerParseError.txt")));
verify(sonarComponents).reportAnalysisError(any(RecognitionException.class), any(File.class));
verifyZeroInteractions(listener);
}
private static JavaAstScanner defaultJavaAstScanner() {
return new JavaAstScanner(new ActionParser<>(StandardCharsets.UTF_8, FakeLexer.builder(), FakeGrammar.class, new FakeTreeFactory(), new JavaNodeBuilder(), FakeLexer.ROOT));
}
private static class CheckThrowingSOError implements JavaFileScanner {
@Override
public void scanFile(JavaFileScannerContext context) {
throw new StackOverflowError();
}
}
private static class CheckThrowingException implements JavaFileScanner {
private final RuntimeException exception;
public CheckThrowingException(RuntimeException e) {
this.exception = e;
}
@Override
public void scanFile(JavaFileScannerContext context) {
throw exception;
}
}
private static class AnalysisExceptionBaseMatcher extends BaseMatcher {
private final Class<? extends Exception> expectedCause;
private final String description;
public AnalysisExceptionBaseMatcher(Class<? extends Exception> expectedCause, String description) {
this.expectedCause = expectedCause;
this.description = description;
}
@Override
public boolean matches(Object item) {
return item instanceof AnalysisException
&& expectedCause.equals(((AnalysisException) item).getCause().getClass());
}
@Override
public void describeTo(Description description) {
description.appendText(this.description);
}
}
private static class FakeAuditListener implements JavaFileScanner, AstScannerExceptionHandler {
@Override
public void processRecognitionException(RecognitionException e) {
}
@Override
public void processException(Exception e) {
}
@Override
public void scanFile(JavaFileScannerContext context) {
}
}
public static class FakeTreeFactory {
public FakeTreeFactory(){}
public Tree root(JavaTree javaTree) {
return new Tree() {
@Override
public boolean is(Kind... kind) {
return false;
}
@Override
public void accept(TreeVisitor visitor) {
}
@Override
public Kind kind() {
return null;
}
@Override
public Tree parent() {
return null;
}
@Override
public SyntaxToken firstToken() {
return null;
}
@Override
public SyntaxToken lastToken() {
return null;
}
};
}
}
public static class FakeGrammar {
final GrammarBuilder<InternalSyntaxToken> b;
final FakeTreeFactory f;
public FakeGrammar(GrammarBuilder<InternalSyntaxToken> b, FakeTreeFactory f) {
this.b = b;
this.f = f;
}
public Tree ROOT() {
return b.<Tree>nonterminal(FakeLexer.ROOT).is(f.root(b.token(FakeLexer.TOKEN)));
}
}
public enum FakeLexer implements GrammarRuleKey {
ROOT, TOKEN;
public static LexerlessGrammarBuilder builder() {
LexerlessGrammarBuilder b = LexerlessGrammarBuilder.create();
b.rule(TOKEN).is("foo");
b.setRootRule(ROOT);
return b;
}
}
}