org.sonar.plugins.php.PHPSensor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonar-php-plugin Show documentation
Show all versions of sonar-php-plugin Show documentation
Enables analysis of PHP projects.
/*
* SonarQube PHP Plugin
* Copyright (C) 2010-2022 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.plugins.php;
import com.google.common.base.Throwables;
import com.sonar.sslr.api.RecognitionException;
import java.io.File;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.DurationStatistics;
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.fs.InputFile.Type;
import org.sonar.api.batch.fs.TextRange;
import org.sonar.api.batch.rule.CheckFactory;
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.batch.sensor.cpd.NewCpdTokens;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.issue.NoSonarFilter;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.FileLinesContextFactory;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.php.PHPAnalyzer;
import org.sonar.php.checks.CheckList;
import org.sonar.php.checks.ParsingErrorCheck;
import org.sonar.php.checks.UncatchableExceptionCheck;
import org.sonar.php.checks.utils.PhpUnitCheck;
import org.sonar.php.compat.PhpFileImpl;
import org.sonar.php.metrics.CpdVisitor.CpdToken;
import org.sonar.php.metrics.FileMeasures;
import org.sonar.php.symbols.ProjectSymbolData;
import org.sonar.php.tree.visitors.LegacyIssue;
import org.sonar.plugins.php.api.Php;
import org.sonar.plugins.php.api.visitors.FileIssue;
import org.sonar.plugins.php.api.visitors.IssueLocation;
import org.sonar.plugins.php.api.visitors.LineIssue;
import org.sonar.plugins.php.api.visitors.PHPCheck;
import org.sonar.plugins.php.api.visitors.PHPCustomRuleRepository;
import org.sonar.plugins.php.api.visitors.PhpIssue;
import org.sonar.plugins.php.api.visitors.PreciseIssue;
import org.sonar.plugins.php.phpunit.CoverageResultImporter;
import org.sonar.plugins.php.phpunit.TestResultImporter;
import org.sonar.plugins.php.warning.AnalysisWarningsWrapper;
import static org.sonar.plugins.php.warning.DefaultAnalysisWarningsWrapper.NOOP_ANALYSIS_WARNINGS;
public class PHPSensor implements Sensor {
private static final Logger LOG = Loggers.get(PHPSensor.class);
private final FileLinesContextFactory fileLinesContextFactory;
private final PHPChecks checks;
private final NoSonarFilter noSonarFilter;
private final AnalysisWarningsWrapper analysisWarningsWrapper;
private RuleKey parsingErrorRuleKey;
public PHPSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter) {
this(fileLinesContextFactory, checkFactory, noSonarFilter, null, NOOP_ANALYSIS_WARNINGS);
}
public PHPSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter,
AnalysisWarningsWrapper analysisWarningsWrapper) {
this(fileLinesContextFactory, checkFactory, noSonarFilter, null, analysisWarningsWrapper);
}
public PHPSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter,
@Nullable PHPCustomRuleRepository[] customRuleRepositories) {
this(fileLinesContextFactory, checkFactory, noSonarFilter, customRuleRepositories , NOOP_ANALYSIS_WARNINGS);
}
public PHPSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter,
@Nullable PHPCustomRuleRepository[] customRuleRepositories, AnalysisWarningsWrapper analysisWarningsWrapper) {
this(fileLinesContextFactory,
PHPChecks.createPHPCheck(checkFactory).addChecks(CheckList.REPOSITORY_KEY, CheckList.getPhpChecks()).addCustomChecks(customRuleRepositories),
noSonarFilter,
analysisWarningsWrapper);
}
PHPSensor(FileLinesContextFactory fileLinesContextFactory, PHPChecks checks, NoSonarFilter noSonarFilter, AnalysisWarningsWrapper analysisWarningsWrapper) {
this.checks = checks;
this.fileLinesContextFactory = fileLinesContextFactory;
this.noSonarFilter = noSonarFilter;
this.analysisWarningsWrapper = analysisWarningsWrapper;
this.parsingErrorRuleKey = getParsingErrorRuleKey();
}
@Override
public void describe(SensorDescriptor descriptor) {
descriptor
.onlyOnLanguage(Php.KEY)
.name("PHP sensor")
.onlyOnFileType(Type.MAIN);
}
@Override
public void execute(SensorContext context) {
DurationStatistics statistics = new DurationStatistics(context.config());
FileSystem fileSystem = context.fileSystem();
FilePredicate phpFilePredicate = fileSystem.predicates().hasLanguage(Php.KEY);
List inputFiles = new ArrayList<>();
fileSystem.inputFiles(phpFilePredicate).forEach(inputFiles::add);
SymbolScanner symbolScanner = new SymbolScanner(context, statistics);
try {
symbolScanner.execute(inputFiles);
ProjectSymbolData projectSymbolData = symbolScanner.getProjectSymbolData();
new AnalysisScanner(context, projectSymbolData, statistics).execute(inputFiles);
if (!inSonarLint(context)) {
processTestsAndCoverage(context);
}
} catch (CancellationException e) {
LOG.info(e.getMessage());
}
statistics.log();
}
private static boolean inSonarLint(SensorContext context) {
return context.runtime().getProduct() == SonarProduct.SONARLINT;
}
private void processTestsAndCoverage(SensorContext context) {
new TestResultImporter(analysisWarningsWrapper).execute(context);
new CoverageResultImporter(analysisWarningsWrapper).execute(context);
}
private class AnalysisScanner extends Scanner {
PHPAnalyzer phpAnalyzer;
private final boolean hasTestFileChecks;
public AnalysisScanner(SensorContext context, ProjectSymbolData projectSymbolData, DurationStatistics statistics) {
super(context, statistics);
List allChecks = checks.all();
if (inSonarLint(context)) {
allChecks = allChecks.stream()
.filter(e -> !(e instanceof UncatchableExceptionCheck))
.collect(Collectors.toList());
}
List testFilesChecks = allChecks.stream().
filter(c -> c instanceof PhpUnitCheck).
collect(Collectors.toList());
hasTestFileChecks = !testFilesChecks.isEmpty();
File workingDir = context.fileSystem().workDir();
phpAnalyzer = new PHPAnalyzer(allChecks, testFilesChecks, workingDir, projectSymbolData, statistics);
}
@Override
String name() {
return "PHP rules";
}
@Override
void scanFile(InputFile inputFile) {
if (inputFile.type() == Type.TEST && !hasTestFileChecks) {
return;
}
try {
phpAnalyzer.nextFile(new PhpFileImpl(inputFile));
if (!inSonarLint(context)) {
phpAnalyzer.getSyntaxHighlighting(context, inputFile).save();
phpAnalyzer.getSymbolHighlighting(context, inputFile).save();
if (inputFile.type() == Type.MAIN) {
saveNewFileMeasures(context,
phpAnalyzer.computeMeasures(fileLinesContextFactory.createFor(inputFile)),
inputFile);
saveCpdData(phpAnalyzer.computeCpdTokens(), inputFile, context);
}
}
noSonarFilter.noSonarInFile(inputFile, phpAnalyzer.computeNoSonarLines());
saveIssues(context, inputFile.type() == Type.MAIN ? phpAnalyzer.analyze() : phpAnalyzer.analyzeTest(), inputFile);
} catch (RecognitionException e) {
checkInterrupted(e);
LOG.error("Unable to parse file [{}] at line {}", inputFile.uri(), e.getLine());
LOG.error(e.getMessage());
saveParsingIssue(context, e, inputFile);
}
}
@Override
void logException(Exception e, InputFile file) {
LOG.error("Could not analyse " + file.toString(), e);
}
@Override
void onEnd() {
phpAnalyzer.terminate();
}
}
private static void checkInterrupted(Exception e) {
Throwable cause = Throwables.getRootCause(e);
if (cause instanceof InterruptedException || cause instanceof InterruptedIOException) {
throw new AnalysisException("Analysis cancelled", e);
}
}
private static void saveNewFileMeasures(SensorContext context, FileMeasures fileMeasures, InputFile inputFile) {
context.newMeasure().on(inputFile).withValue(fileMeasures.getLinesOfCodeNumber()).forMetric(CoreMetrics.NCLOC).save();
context.newMeasure().on(inputFile).withValue(fileMeasures.getCommentLinesNumber()).forMetric(CoreMetrics.COMMENT_LINES).save();
context.newMeasure().on(inputFile).withValue(fileMeasures.getClassNumber()).forMetric(CoreMetrics.CLASSES).save();
context.newMeasure().on(inputFile).withValue(fileMeasures.getFunctionNumber()).forMetric(CoreMetrics.FUNCTIONS).save();
context.newMeasure().on(inputFile).withValue(fileMeasures.getStatementNumber()).forMetric(CoreMetrics.STATEMENTS).save();
context.newMeasure().on(inputFile).withValue(fileMeasures.getFileCognitiveComplexity()).forMetric(CoreMetrics.COGNITIVE_COMPLEXITY).save();
context.newMeasure().on(inputFile).withValue(fileMeasures.getFileComplexity()).forMetric(CoreMetrics.COMPLEXITY).save();
}
/**
* Creates and saves an issue for a parsing error.
*/
private void saveParsingIssue(SensorContext context, RecognitionException e, InputFile inputFile) {
if (parsingErrorRuleKey != null) {
NewIssue issue = context.newIssue();
NewIssueLocation location = issue.newLocation()
.message("A parsing error occurred in this file.")
.on(inputFile)
.at(inputFile.selectLine(e.getLine()));
issue
.forRule(parsingErrorRuleKey)
.at(location)
.save();
}
context.newAnalysisError()
.onFile(inputFile)
.at(inputFile.newPointer(e.getLine(), 0))
.message(e.getMessage())
.save();
}
private void saveIssues(SensorContext context, List issues, InputFile inputFile) {
for (PhpIssue issue : issues) {
RuleKey ruleKey = checks.ruleKeyFor(issue.check());
NewIssue newIssue = context.newIssue()
.forRule(ruleKey)
.gap(issue.cost());
if (issue instanceof LegacyIssue) {
// todo: this block should be removed as PHPIssue's usages will be removed
LegacyIssue legacyIssue = (LegacyIssue) issue;
NewIssueLocation location = newIssue.newLocation()
.message(legacyIssue.message())
.on(inputFile);
if (legacyIssue.line() > 0) {
location.at(inputFile.selectLine(legacyIssue.line()));
}
newIssue.at(location);
} else if (issue instanceof LineIssue) {
LineIssue lineIssue = (LineIssue) issue;
NewIssueLocation location = newIssue.newLocation()
.message(lineIssue.message())
.on(inputFile)
.at(inputFile.selectLine(lineIssue.line()));
newIssue.at(location);
} else if (issue instanceof FileIssue) {
FileIssue fileIssue = (FileIssue) issue;
NewIssueLocation location = newIssue.newLocation()
.message(fileIssue.message())
.on(inputFile);
newIssue.at(location);
} else {
PreciseIssue preciseIssue = (PreciseIssue) issue;
newIssue.at(newLocation(inputFile, newIssue, preciseIssue.primaryLocation()));
preciseIssue.secondaryLocations().forEach(secondary ->
addSecondaryLocation(context, inputFile, newIssue, secondary)
);
}
newIssue.save();
}
}
private static void addSecondaryLocation(SensorContext context, InputFile inputFile, NewIssue newIssue, IssueLocation secondary) {
InputFile file = inputFile;
String filePath = secondary.filePath();
if (filePath != null) {
file = context.fileSystem().inputFile(context.fileSystem().predicates().is(new File(filePath)));
}
if (file != null) {
newIssue.addLocation(newLocation(file, newIssue, secondary));
}
}
private static NewIssueLocation newLocation(InputFile inputFile, NewIssue issue, IssueLocation location) {
TextRange range = inputFile.newRange(location.startLine(), location.startLineOffset(), location.endLine(), location.endLineOffset());
NewIssueLocation newLocation = issue.newLocation()
.on(inputFile)
.at(range);
if (location.message() != null) {
newLocation.message(location.message());
}
return newLocation;
}
@Override
public String toString() {
return getClass().getSimpleName();
}
private RuleKey getParsingErrorRuleKey() {
List keys = checks.all().stream()
.filter(check -> check instanceof ParsingErrorCheck)
.map(checks::ruleKeyFor)
.collect(Collectors.toList());
return keys.isEmpty() ? null : keys.get(0);
}
private static void saveCpdData(List cpdTokens, InputFile inputFile, SensorContext context) {
NewCpdTokens newCpdTokens = context.newCpdTokens().onFile(inputFile);
cpdTokens.forEach(cpdToken -> newCpdTokens.addToken(
inputFile.newRange(
cpdToken.syntaxToken().line(),
cpdToken.syntaxToken().column(),
cpdToken.syntaxToken().endLine(),
cpdToken.syntaxToken().endColumn()),
cpdToken.image()));
newCpdTokens.save();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy