org.sonar.xoo.rule.MultilineIssuesSensor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonar-xoo-plugin Show documentation
Show all versions of sonar-xoo-plugin Show documentation
Open source platform for continuous inspection of code quality
The newest version!
/*
* SonarQube
* Copyright (C) 2009-2024 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.xoo.rule;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.FilePredicates;
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.TextPointer;
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.issue.MessageFormatting;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssue.FlowType;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.batch.sensor.issue.NewMessageFormatting;
import org.sonar.api.rule.RuleKey;
import org.sonar.xoo.Xoo;
import static org.sonar.api.batch.sensor.issue.NewIssue.FlowType.DATA;
import static org.sonar.api.batch.sensor.issue.NewIssue.FlowType.EXECUTION;
import static org.sonar.api.utils.Preconditions.checkState;
public class MultilineIssuesSensor implements Sensor {
public static final String RULE_KEY = "MultilineIssue";
private static final Pattern START_ISSUE_PATTERN = Pattern.compile("\\{xoo-start-issue:([0-9]+)\\}");
private static final Pattern END_ISSUE_PATTERN = Pattern.compile("\\{xoo-end-issue:([0-9]+)\\}");
private static final Pattern START_FLOW_PATTERN = Pattern.compile("\\{xoo-start-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
private static final Pattern END_FLOW_PATTERN = Pattern.compile("\\{xoo-end-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
private static final Pattern START_DATA_FLOW_PATTERN = Pattern.compile("\\{xoo-start-data-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
private static final Pattern END_DATA_FLOW_PATTERN = Pattern.compile("\\{xoo-end-data-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
private static final Pattern START_EXECUTION_FLOW_PATTERN = Pattern.compile("\\{xoo-start-execution-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
private static final Pattern END_EXECUTION_FLOW_PATTERN = Pattern.compile("\\{xoo-end-execution-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
@Override
public void describe(SensorDescriptor descriptor) {
descriptor.name("Multiline Issues").onlyOnLanguages(Xoo.KEY).createIssuesForRuleRepositories(XooRulesDefinition.XOO_REPOSITORY);
}
@Override
public void execute(SensorContext context) {
FileSystem fs = context.fileSystem();
FilePredicates p = fs.predicates();
for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(Xoo.KEY), p.hasType(Type.MAIN)))) {
createIssues(file, context);
}
}
public String getRuleKey() {
return RULE_KEY;
}
private void createIssues(InputFile file, SensorContext context) {
Collection issues = parseIssues(file);
FlowIndex flowIndex = new FlowIndex();
parseFlows(flowIndex, file, START_FLOW_PATTERN, END_FLOW_PATTERN, null);
parseFlows(flowIndex, file, START_DATA_FLOW_PATTERN, END_DATA_FLOW_PATTERN, DATA);
parseFlows(flowIndex, file, START_EXECUTION_FLOW_PATTERN, END_EXECUTION_FLOW_PATTERN, EXECUTION);
createIssues(file, context, issues, flowIndex);
}
private void createIssues(InputFile file, SensorContext context, Collection parsedIssues, FlowIndex flowIndex) {
RuleKey ruleKey = RuleKey.of(XooRulesDefinition.XOO_REPOSITORY, getRuleKey());
for (ParsedIssue parsedIssue : parsedIssues) {
NewIssue newIssue = context.newIssue().forRule(ruleKey);
NewIssueLocation primaryLocation = newIssue.newLocation();
String message = "Primary location of the issue in xoo code";
List newMessageFormattings = formatIssueMessage(message, primaryLocation.newMessageFormatting());
newIssue.at(primaryLocation.on(file)
.at(file.newRange(parsedIssue.start, parsedIssue.end))
.message(message, newMessageFormattings));
for (ParsedFlow flow : flowIndex.getFlows(parsedIssue.issueId)) {
List flowLocations = new LinkedList<>();
for (ParsedFlowLocation flowLocation : flow.getLocations()) {
String locationMessage = "Xoo code, flow step #" + flowLocation.flowLocationId;
NewIssueLocation newIssueLocation = newIssue.newLocation();
List locationMessageFormattings = formatIssueMessage(locationMessage, newIssueLocation.newMessageFormatting());
newIssueLocation
.on(file)
.at(file.newRange(flowLocation.start, flowLocation.end))
.message(locationMessage, locationMessageFormattings);
flowLocations.add(newIssueLocation);
}
if (flow.getType() != null) {
newIssue.addFlow(flowLocations, flow.getType(), "flow #" + flow.getFlowId());
} else {
newIssue.addFlow(flowLocations);
}
}
newIssue.save();
}
}
private static List formatIssueMessage(String message, NewMessageFormatting newMessageFormatting) {
int startIndex = message.toLowerCase().indexOf("xoo");
if(startIndex == -1) {
return List.of();
}
int endIndex = startIndex + "xoo".length();
return List.of(newMessageFormatting.start(startIndex).end(endIndex).type(MessageFormatting.Type.CODE));
}
private static Collection parseIssues(InputFile file) {
Map issuesById = new HashMap<>();
int currentLine = 0;
try {
for (String lineStr : file.contents().split("\\r?\\n")) {
currentLine++;
Matcher m = START_ISSUE_PATTERN.matcher(lineStr);
while (m.find()) {
Integer issueId = Integer.parseInt(m.group(1));
issuesById.computeIfAbsent(issueId, ParsedIssue::new).start = file.newPointer(currentLine, m.end());
}
m = END_ISSUE_PATTERN.matcher(lineStr);
while (m.find()) {
Integer issueId = Integer.parseInt(m.group(1));
issuesById.computeIfAbsent(issueId, ParsedIssue::new).end = file.newPointer(currentLine, m.start());
}
}
} catch (IOException e) {
throw new IllegalStateException("Unable to read file", e);
}
return issuesById.values();
}
private void parseFlows(FlowIndex flowIndex, InputFile file, Pattern flowStartPattern, Pattern flowEndPattern, @Nullable FlowType flowType) {
int currentLine = 0;
try {
for (String lineStr : file.contents().split("\\r?\\n")) {
currentLine++;
Matcher m = flowStartPattern.matcher(lineStr);
while (m.find()) {
ParsedFlowLocation flowLocation = new ParsedFlowLocation(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)));
flowLocation.start = file.newPointer(currentLine, m.end());
flowIndex.addLocation(flowLocation, flowType);
}
m = flowEndPattern.matcher(lineStr);
while (m.find()) {
ParsedFlowLocation flowLocation = new ParsedFlowLocation(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)));
flowLocation.end = file.newPointer(currentLine, m.start());
flowIndex.addLocation(flowLocation, flowType);
}
}
} catch (IOException e) {
throw new IllegalStateException("Unable to read file", e);
}
}
private static class ParsedIssue {
private final int issueId;
private TextPointer start;
private TextPointer end;
private ParsedIssue(int issueId) {
this.issueId = issueId;
}
}
private static class FlowIndex {
private final Map flowsByIssueId = new HashMap<>();
public void addLocation(ParsedFlowLocation flowLocation, @Nullable FlowType type) {
flowsByIssueId.computeIfAbsent(flowLocation.issueId, issueId -> new IssueFlows()).addLocation(flowLocation, type);
}
public Collection getFlows(int issueId) {
return Optional.ofNullable(flowsByIssueId.get(issueId)).map(IssueFlows::getFlows).orElse(Collections.emptyList());
}
}
private static class IssueFlows {
private final Map flowById = new TreeMap<>();
private void addLocation(ParsedFlowLocation flowLocation, @Nullable FlowType type) {
flowById.computeIfAbsent(flowLocation.flowId, flowId -> new ParsedFlow(flowId, type)).addLocation(flowLocation);
}
Collection getFlows() {
return flowById.values();
}
}
private static class ParsedFlow {
private final int id;
private final FlowType type;
private final Map locationsById = new TreeMap<>();
private String description;
private ParsedFlow(int id, @Nullable FlowType type) {
this.id = id;
this.type = type;
}
private void addLocation(ParsedFlowLocation flowLocation) {
if (locationsById.containsKey(flowLocation.flowLocationId)) {
checkState(flowLocation.end != null, "Existing flow should be the end");
locationsById.get(flowLocation.flowLocationId).end = flowLocation.end;
} else {
checkState(flowLocation.start != null, "New flow should be the start");
locationsById.put(flowLocation.flowLocationId, flowLocation);
}
}
public Collection getLocations() {
return locationsById.values();
}
public void setDescription(@Nullable String description) {
this.description = description;
}
@CheckForNull
public String getDescription() {
return description;
}
@CheckForNull
FlowType getType() {
return type;
}
int getFlowId() {
return id;
}
}
private static class ParsedFlowLocation {
private final int issueId;
private final int flowId;
private final int flowLocationId;
private TextPointer start;
private TextPointer end;
public ParsedFlowLocation(int issueId, int flowId, int flowLocationId) {
this.issueId = issueId;
this.flowId = flowId;
this.flowLocationId = flowLocationId;
}
}
}