You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MultilineIssuesSensor.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.xoo.rule;
  21. import java.io.IOException;
  22. import java.util.Collection;
  23. import java.util.Collections;
  24. import java.util.HashMap;
  25. import java.util.LinkedList;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.Optional;
  29. import java.util.TreeMap;
  30. import java.util.regex.Matcher;
  31. import java.util.regex.Pattern;
  32. import javax.annotation.CheckForNull;
  33. import javax.annotation.Nullable;
  34. import org.sonar.api.batch.fs.FilePredicates;
  35. import org.sonar.api.batch.fs.FileSystem;
  36. import org.sonar.api.batch.fs.InputFile;
  37. import org.sonar.api.batch.fs.InputFile.Type;
  38. import org.sonar.api.batch.fs.TextPointer;
  39. import org.sonar.api.batch.sensor.Sensor;
  40. import org.sonar.api.batch.sensor.SensorContext;
  41. import org.sonar.api.batch.sensor.SensorDescriptor;
  42. import org.sonar.api.batch.sensor.issue.MessageFormatting;
  43. import org.sonar.api.batch.sensor.issue.NewIssue;
  44. import org.sonar.api.batch.sensor.issue.NewIssue.FlowType;
  45. import org.sonar.api.batch.sensor.issue.NewIssueLocation;
  46. import org.sonar.api.batch.sensor.issue.NewMessageFormatting;
  47. import org.sonar.api.rule.RuleKey;
  48. import org.sonar.xoo.Xoo;
  49. import static org.sonar.api.batch.sensor.issue.NewIssue.FlowType.DATA;
  50. import static org.sonar.api.batch.sensor.issue.NewIssue.FlowType.EXECUTION;
  51. import static org.sonar.api.utils.Preconditions.checkState;
  52. public class MultilineIssuesSensor implements Sensor {
  53. public static final String RULE_KEY = "MultilineIssue";
  54. private static final Pattern START_ISSUE_PATTERN = Pattern.compile("\\{xoo-start-issue:([0-9]+)\\}");
  55. private static final Pattern END_ISSUE_PATTERN = Pattern.compile("\\{xoo-end-issue:([0-9]+)\\}");
  56. private static final Pattern START_FLOW_PATTERN = Pattern.compile("\\{xoo-start-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
  57. private static final Pattern END_FLOW_PATTERN = Pattern.compile("\\{xoo-end-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
  58. private static final Pattern START_DATA_FLOW_PATTERN = Pattern.compile("\\{xoo-start-data-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
  59. private static final Pattern END_DATA_FLOW_PATTERN = Pattern.compile("\\{xoo-end-data-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
  60. private static final Pattern START_EXECUTION_FLOW_PATTERN = Pattern.compile("\\{xoo-start-execution-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
  61. private static final Pattern END_EXECUTION_FLOW_PATTERN = Pattern.compile("\\{xoo-end-execution-flow:([0-9]+):([0-9]+):([0-9]+)\\}");
  62. @Override
  63. public void describe(SensorDescriptor descriptor) {
  64. descriptor.name("Multiline Issues").onlyOnLanguages(Xoo.KEY).createIssuesForRuleRepositories(XooRulesDefinition.XOO_REPOSITORY);
  65. }
  66. @Override
  67. public void execute(SensorContext context) {
  68. FileSystem fs = context.fileSystem();
  69. FilePredicates p = fs.predicates();
  70. for (InputFile file : fs.inputFiles(p.and(p.hasLanguages(Xoo.KEY), p.hasType(Type.MAIN)))) {
  71. createIssues(file, context);
  72. }
  73. }
  74. public String getRuleKey() {
  75. return RULE_KEY;
  76. }
  77. private void createIssues(InputFile file, SensorContext context) {
  78. Collection<ParsedIssue> issues = parseIssues(file);
  79. FlowIndex flowIndex = new FlowIndex();
  80. parseFlows(flowIndex, file, START_FLOW_PATTERN, END_FLOW_PATTERN, null);
  81. parseFlows(flowIndex, file, START_DATA_FLOW_PATTERN, END_DATA_FLOW_PATTERN, DATA);
  82. parseFlows(flowIndex, file, START_EXECUTION_FLOW_PATTERN, END_EXECUTION_FLOW_PATTERN, EXECUTION);
  83. createIssues(file, context, issues, flowIndex);
  84. }
  85. private void createIssues(InputFile file, SensorContext context, Collection<ParsedIssue> parsedIssues, FlowIndex flowIndex) {
  86. RuleKey ruleKey = RuleKey.of(XooRulesDefinition.XOO_REPOSITORY, getRuleKey());
  87. for (ParsedIssue parsedIssue : parsedIssues) {
  88. NewIssue newIssue = context.newIssue().forRule(ruleKey);
  89. NewIssueLocation primaryLocation = newIssue.newLocation();
  90. String message = "Primary location of the issue in xoo code";
  91. List<NewMessageFormatting> newMessageFormattings = formatIssueMessage(message, primaryLocation.newMessageFormatting());
  92. newIssue.at(primaryLocation.on(file)
  93. .at(file.newRange(parsedIssue.start, parsedIssue.end))
  94. .message(message, newMessageFormattings));
  95. for (ParsedFlow flow : flowIndex.getFlows(parsedIssue.issueId)) {
  96. List<NewIssueLocation> flowLocations = new LinkedList<>();
  97. for (ParsedFlowLocation flowLocation : flow.getLocations()) {
  98. String locationMessage = "Xoo code, flow step #" + flowLocation.flowLocationId;
  99. NewIssueLocation newIssueLocation = newIssue.newLocation();
  100. List<NewMessageFormatting> locationMessageFormattings = formatIssueMessage(locationMessage, newIssueLocation.newMessageFormatting());
  101. newIssueLocation
  102. .on(file)
  103. .at(file.newRange(flowLocation.start, flowLocation.end))
  104. .message(locationMessage, locationMessageFormattings);
  105. flowLocations.add(newIssueLocation);
  106. }
  107. if (flow.getType() != null) {
  108. newIssue.addFlow(flowLocations, flow.getType(), "flow #" + flow.getFlowId());
  109. } else {
  110. newIssue.addFlow(flowLocations);
  111. }
  112. }
  113. newIssue.save();
  114. }
  115. }
  116. private static List<NewMessageFormatting> formatIssueMessage(String message, NewMessageFormatting newMessageFormatting) {
  117. int startIndex = message.toLowerCase().indexOf("xoo");
  118. if(startIndex == -1) {
  119. return List.of();
  120. }
  121. int endIndex = startIndex + "xoo".length();
  122. return List.of(newMessageFormatting.start(startIndex).end(endIndex).type(MessageFormatting.Type.CODE));
  123. }
  124. private static Collection<ParsedIssue> parseIssues(InputFile file) {
  125. Map<Integer, ParsedIssue> issuesById = new HashMap<>();
  126. int currentLine = 0;
  127. try {
  128. for (String lineStr : file.contents().split("\\r?\\n")) {
  129. currentLine++;
  130. Matcher m = START_ISSUE_PATTERN.matcher(lineStr);
  131. while (m.find()) {
  132. Integer issueId = Integer.parseInt(m.group(1));
  133. issuesById.computeIfAbsent(issueId, ParsedIssue::new).start = file.newPointer(currentLine, m.end());
  134. }
  135. m = END_ISSUE_PATTERN.matcher(lineStr);
  136. while (m.find()) {
  137. Integer issueId = Integer.parseInt(m.group(1));
  138. issuesById.computeIfAbsent(issueId, ParsedIssue::new).end = file.newPointer(currentLine, m.start());
  139. }
  140. }
  141. } catch (IOException e) {
  142. throw new IllegalStateException("Unable to read file", e);
  143. }
  144. return issuesById.values();
  145. }
  146. private void parseFlows(FlowIndex flowIndex, InputFile file, Pattern flowStartPattern, Pattern flowEndPattern, @Nullable FlowType flowType) {
  147. int currentLine = 0;
  148. try {
  149. for (String lineStr : file.contents().split("\\r?\\n")) {
  150. currentLine++;
  151. Matcher m = flowStartPattern.matcher(lineStr);
  152. while (m.find()) {
  153. ParsedFlowLocation flowLocation = new ParsedFlowLocation(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)));
  154. flowLocation.start = file.newPointer(currentLine, m.end());
  155. flowIndex.addLocation(flowLocation, flowType);
  156. }
  157. m = flowEndPattern.matcher(lineStr);
  158. while (m.find()) {
  159. ParsedFlowLocation flowLocation = new ParsedFlowLocation(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)));
  160. flowLocation.end = file.newPointer(currentLine, m.start());
  161. flowIndex.addLocation(flowLocation, flowType);
  162. }
  163. }
  164. } catch (IOException e) {
  165. throw new IllegalStateException("Unable to read file", e);
  166. }
  167. }
  168. private static class ParsedIssue {
  169. private final int issueId;
  170. private TextPointer start;
  171. private TextPointer end;
  172. private ParsedIssue(int issueId) {
  173. this.issueId = issueId;
  174. }
  175. }
  176. private static class FlowIndex {
  177. private final Map<Integer, IssueFlows> flowsByIssueId = new HashMap<>();
  178. public void addLocation(ParsedFlowLocation flowLocation, @Nullable FlowType type) {
  179. flowsByIssueId.computeIfAbsent(flowLocation.issueId, issueId -> new IssueFlows()).addLocation(flowLocation, type);
  180. }
  181. public Collection<ParsedFlow> getFlows(int issueId) {
  182. return Optional.ofNullable(flowsByIssueId.get(issueId)).map(IssueFlows::getFlows).orElse(Collections.emptyList());
  183. }
  184. }
  185. private static class IssueFlows {
  186. private final Map<Integer, ParsedFlow> flowById = new TreeMap<>();
  187. private void addLocation(ParsedFlowLocation flowLocation, @Nullable FlowType type) {
  188. flowById.computeIfAbsent(flowLocation.flowId, flowId -> new ParsedFlow(flowId, type)).addLocation(flowLocation);
  189. }
  190. Collection<ParsedFlow> getFlows() {
  191. return flowById.values();
  192. }
  193. }
  194. private static class ParsedFlow {
  195. private final int id;
  196. private final FlowType type;
  197. private final Map<Integer, ParsedFlowLocation> locationsById = new TreeMap<>();
  198. private String description;
  199. private ParsedFlow(int id, @Nullable FlowType type) {
  200. this.id = id;
  201. this.type = type;
  202. }
  203. private void addLocation(ParsedFlowLocation flowLocation) {
  204. if (locationsById.containsKey(flowLocation.flowLocationId)) {
  205. checkState(flowLocation.end != null, "Existing flow should be the end");
  206. locationsById.get(flowLocation.flowLocationId).end = flowLocation.end;
  207. } else {
  208. checkState(flowLocation.start != null, "New flow should be the start");
  209. locationsById.put(flowLocation.flowLocationId, flowLocation);
  210. }
  211. }
  212. public Collection<ParsedFlowLocation> getLocations() {
  213. return locationsById.values();
  214. }
  215. public void setDescription(@Nullable String description) {
  216. this.description = description;
  217. }
  218. @CheckForNull
  219. public String getDescription() {
  220. return description;
  221. }
  222. @CheckForNull
  223. FlowType getType() {
  224. return type;
  225. }
  226. int getFlowId() {
  227. return id;
  228. }
  229. }
  230. private static class ParsedFlowLocation {
  231. private final int issueId;
  232. private final int flowId;
  233. private final int flowLocationId;
  234. private TextPointer start;
  235. private TextPointer end;
  236. public ParsedFlowLocation(int issueId, int flowId, int flowLocationId) {
  237. this.issueId = issueId;
  238. this.flowId = flowId;
  239. this.flowLocationId = flowLocationId;
  240. }
  241. }
  242. }