org.sonar.go.plugin.SlangSensor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonar-go-plugin Show documentation
Show all versions of sonar-go-plugin Show documentation
SonarQube analyzer for Go language
/*
* SonarSource Go
* Copyright (C) 2018-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* 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 Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.go.plugin;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.SonarProduct;
import org.sonar.api.batch.fs.FilePredicate;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.rule.Checks;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.issue.NoSonarFilter;
import org.sonar.api.measures.FileLinesContextFactory;
import org.sonar.api.resources.Language;
import org.sonar.go.api.ASTConverter;
import org.sonar.go.api.BlockTree;
import org.sonar.go.api.ClassDeclarationTree;
import org.sonar.go.api.FunctionDeclarationTree;
import org.sonar.go.api.ImportDeclarationTree;
import org.sonar.go.api.PackageDeclarationTree;
import org.sonar.go.api.ParseException;
import org.sonar.go.api.TextPointer;
import org.sonar.go.api.Tree;
import org.sonar.go.api.checks.GoCheck;
import org.sonar.go.api.checks.GoVersion;
import org.sonar.go.plugin.caching.HashCacheUtils;
import org.sonar.go.plugin.converter.ASTConverterValidation;
import org.sonar.go.visitors.SymbolVisitor;
import org.sonar.go.visitors.TreeVisitor;
import org.sonarsource.analyzer.commons.ProgressReport;
public abstract class SlangSensor implements Sensor {
static final Predicate EXECUTABLE_LINE_PREDICATE = t -> !(t instanceof PackageDeclarationTree)
&& !(t instanceof ImportDeclarationTree)
&& !(t instanceof ClassDeclarationTree)
&& !(t instanceof FunctionDeclarationTree)
&& !(t instanceof BlockTree);
private static final Logger LOG = LoggerFactory.getLogger(SlangSensor.class);
private static final Pattern EMPTY_FILE_CONTENT_PATTERN = Pattern.compile("\\s*+");
private final NoSonarFilter noSonarFilter;
private final Language language;
private FileLinesContextFactory fileLinesContextFactory;
protected SlangSensor(NoSonarFilter noSonarFilter, FileLinesContextFactory fileLinesContextFactory, Language language) {
this.noSonarFilter = noSonarFilter;
this.fileLinesContextFactory = fileLinesContextFactory;
this.language = language;
}
@Override
public void describe(SensorDescriptor descriptor) {
descriptor
.onlyOnLanguage(language.getKey())
.name(language.getName() + " Sensor");
}
protected abstract ASTConverter astConverter(SensorContext sensorContext);
protected abstract Checks checks();
protected abstract String repositoryKey();
protected Predicate executableLineOfCodePredicate() {
return EXECUTABLE_LINE_PREDICATE;
}
private boolean analyseFiles(ASTConverter converter,
SensorContext sensorContext,
List inputFiles,
ProgressReport progressReport,
List> visitors,
DurationStatistics statistics) {
if (sensorContext.canSkipUnchangedFiles()) {
LOG.info("The {} analyzer is running in a context where unchanged files can be skipped.", this.language);
}
for (InputFile inputFile : inputFiles) {
if (sensorContext.isCancelled()) {
return false;
}
InputFileContext inputFileContext = new InputFileContext(sensorContext, inputFile);
try {
analyseFile(converter, inputFileContext, inputFile, visitors, statistics);
} catch (ParseException e) {
logParsingError(inputFile, e);
inputFileContext.reportAnalysisParseError(repositoryKey(), inputFile, e.getPosition());
}
progressReport.nextFile();
}
return true;
}
static void analyseFile(ASTConverter converter,
InputFileContext inputFileContext,
InputFile inputFile,
List> visitors,
DurationStatistics statistics) {
List> canBeSkipped = new ArrayList<>();
if (fileCanBeSkipped(inputFileContext)) {
String fileKey = inputFile.key();
LOG.debug("Checking that previous results can be reused for input file {}.", fileKey);
Map successfulCacheReuseByVisitor = visitors.stream()
.filter(PullRequestAwareVisitor.class::isInstance)
.map(PullRequestAwareVisitor.class::cast)
.collect(Collectors.toMap(visitor -> visitor, visitor -> reusePreviousResults(visitor, inputFileContext)));
boolean allVisitorsSuccessful = successfulCacheReuseByVisitor.values().stream().allMatch(Boolean.TRUE::equals);
if (allVisitorsSuccessful) {
LOG.debug("Skipping input file {} (status is unchanged).", fileKey);
HashCacheUtils.copyFromPrevious(inputFileContext);
return;
}
LOG.debug("Will convert input file {} for full analysis.", fileKey);
successfulCacheReuseByVisitor.entrySet().stream()
.filter(Map.Entry::getValue)
.map(Map.Entry::getKey)
.forEach(canBeSkipped::add);
}
String content;
String fileName;
try {
content = inputFile.contents();
fileName = inputFile.toString();
} catch (IOException | RuntimeException e) {
throw toParseException("read", inputFile, e);
}
if (EMPTY_FILE_CONTENT_PATTERN.matcher(content).matches()) {
return;
}
Tree tree = statistics.time("Parse", () -> {
try {
return converter.parse(content, fileName);
} catch (RuntimeException e) {
throw toParseException("parse", inputFile, e);
}
});
for (TreeVisitor visitor : visitors) {
try {
if (canBeSkipped.contains(visitor)) {
continue;
}
String visitorId = visitor.getClass().getSimpleName();
statistics.time(visitorId, () -> visitor.scan(inputFileContext, tree));
} catch (RuntimeException e) {
inputFileContext.reportAnalysisError(e.getMessage(), null);
LOG.warn("Cannot analyse '" + inputFile + "': " + e.getMessage(), e);
}
}
writeHashToCache(inputFileContext);
}
private static boolean fileCanBeSkipped(InputFileContext inputFileContext) {
SensorContext sensorContext = inputFileContext.sensorContext;
if (!sensorContext.canSkipUnchangedFiles()) {
return false;
}
return HashCacheUtils.hasSameHashCached(inputFileContext);
}
private static void writeHashToCache(InputFileContext inputFileContext) {
HashCacheUtils.writeHashForNextAnalysis(inputFileContext);
}
private static boolean reusePreviousResults(PullRequestAwareVisitor visitor, InputFileContext inputFileContext) {
boolean success = visitor.reusePreviousResults(inputFileContext);
if (success) {
return true;
}
String message = String.format(
"Visitor %s failed to reuse previous results for input file %s.",
visitor.getClass().getSimpleName(),
inputFileContext.inputFile.key());
LOG.debug(message);
return false;
}
private static ParseException toParseException(String action, InputFile inputFile, Exception cause) {
TextPointer position = cause instanceof ParseException actual ? actual.getPosition() : null;
return new ParseException("Cannot " + action + " '" + inputFile + "': " + cause.getMessage(), position, cause);
}
private static void logParsingError(InputFile inputFile, ParseException e) {
TextPointer position = e.getPosition();
String positionMessage = "";
if (position != null) {
positionMessage = String.format("Parse error at position %s:%s", position.line(), position.lineOffset());
}
LOG.warn("Unable to parse file: {}. {}", inputFile.uri(), positionMessage);
LOG.warn(e.getMessage());
}
@Override
public void execute(SensorContext sensorContext) {
DurationStatistics statistics = new DurationStatistics(sensorContext.config());
FileSystem fileSystem = sensorContext.fileSystem();
FilePredicate mainFilePredicate = fileSystem.predicates().and(
fileSystem.predicates().hasLanguage(language.getKey()),
fileSystem.predicates().hasType(InputFile.Type.MAIN));
var goVersion = new GoVersionAnalyzer(sensorContext).analyzeGoVersion();
List inputFiles = StreamSupport.stream(fileSystem.inputFiles(mainFilePredicate).spliterator(), false)
.toList();
List filenames = inputFiles.stream().map(InputFile::toString).toList();
ProgressReport progressReport = new ProgressReport("Progress of the " + language.getName() + " analysis", TimeUnit.SECONDS.toMillis(10));
progressReport.start(filenames);
boolean success = false;
ASTConverter converter = ASTConverterValidation.wrap(astConverter(sensorContext), sensorContext.config());
try {
success = analyseFiles(converter, sensorContext, inputFiles, progressReport, visitors(sensorContext, statistics, goVersion), statistics);
} finally {
if (success) {
progressReport.stop();
} else {
progressReport.cancel();
}
converter.terminate();
}
statistics.log();
}
private List> visitors(SensorContext sensorContext, DurationStatistics statistics, GoVersion goVersion) {
if (sensorContext.runtime().getProduct() == SonarProduct.SONARLINT) {
return Arrays.asList(
new IssueSuppressionVisitor(),
new SkipNoSonarLinesVisitor(noSonarFilter),
new SymbolVisitor<>(),
new ChecksVisitor(checks(), statistics, goVersion));
} else {
return Arrays.asList(
new IssueSuppressionVisitor(),
new MetricVisitor(fileLinesContextFactory, executableLineOfCodePredicate()),
new SkipNoSonarLinesVisitor(noSonarFilter),
new SymbolVisitor<>(),
new ChecksVisitor(checks(), statistics, goVersion),
new CpdVisitor(),
new SyntaxHighlighter());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy