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.

RulesDefinitionXmlLoader.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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.api.server.rule;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.io.InputStreamReader;
  24. import java.io.Reader;
  25. import java.nio.charset.Charset;
  26. import java.util.ArrayList;
  27. import java.util.List;
  28. import javax.annotation.Nullable;
  29. import javax.xml.namespace.QName;
  30. import javax.xml.stream.XMLEventReader;
  31. import javax.xml.stream.XMLInputFactory;
  32. import javax.xml.stream.XMLStreamException;
  33. import javax.xml.stream.events.Attribute;
  34. import javax.xml.stream.events.StartElement;
  35. import javax.xml.stream.events.XMLEvent;
  36. import org.apache.commons.io.ByteOrderMark;
  37. import org.apache.commons.io.input.BOMInputStream;
  38. import org.apache.commons.lang.StringUtils;
  39. import org.sonar.api.ce.ComputeEngineSide;
  40. import org.sonar.api.rule.RuleStatus;
  41. import org.sonar.api.rule.Severity;
  42. import org.sonar.api.rules.RuleType;
  43. import org.sonar.api.server.ServerSide;
  44. import org.sonar.api.server.debt.DebtRemediationFunction;
  45. import org.sonar.check.Cardinality;
  46. import org.sonarsource.api.sonarlint.SonarLintSide;
  47. import static java.lang.String.format;
  48. import static org.apache.commons.lang.StringUtils.isNotBlank;
  49. import static org.apache.commons.lang.StringUtils.trim;
  50. /**
  51. * Loads definitions of rules from a XML file.
  52. *
  53. * <h3>Usage</h3>
  54. * <pre>
  55. * public class MyJsRulesDefinition implements RulesDefinition {
  56. *
  57. * private static final String PATH = "my-js-rules.xml";
  58. * private final RulesDefinitionXmlLoader xmlLoader;
  59. *
  60. * public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) {
  61. * this.xmlLoader = xmlLoader;
  62. * }
  63. *
  64. * {@literal @}Override
  65. * public void define(Context context) {
  66. * try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(PATH), StandardCharsets.UTF_8)) {
  67. * NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
  68. * xmlLoader.load(repository, reader);
  69. * repository.done();
  70. * } catch (IOException e) {
  71. * throw new IllegalStateException(String.format("Fail to read file %s", PATH), e);
  72. * }
  73. * }
  74. * }
  75. * </pre>
  76. *
  77. * <h3>XML Format</h3>
  78. * <pre>
  79. * &lt;rules&gt;
  80. * &lt;rule&gt;
  81. * &lt;!-- Required key. Max length is 200 characters. --&gt;
  82. * &lt;key&gt;the-rule-key&lt;/key&gt;
  83. *
  84. * &lt;!-- Required name. Max length is 200 characters. --&gt;
  85. * &lt;name&gt;The purpose of the rule&lt;/name&gt;
  86. *
  87. * &lt;!-- Required description. No max length. --&gt;
  88. * &lt;description&gt;
  89. * &lt;![CDATA[The description]]&gt;
  90. * &lt;/description&gt;
  91. * &lt;!-- Optional format of description. Supported values are HTML (default) and MARKDOWN. --&gt;
  92. * &lt;descriptionFormat&gt;HTML&lt;/descriptionFormat&gt;
  93. *
  94. * &lt;!-- Optional key for configuration of some rule engines --&gt;
  95. * &lt;internalKey&gt;Checker/TreeWalker/LocalVariableName&lt;/internalKey&gt;
  96. *
  97. * &lt;!-- Default severity when enabling the rule in a Quality profile. --&gt;
  98. * &lt;!-- Possible values are INFO, MINOR, MAJOR (default), CRITICAL, BLOCKER. --&gt;
  99. * &lt;severity&gt;BLOCKER&lt;/severity&gt;
  100. *
  101. * &lt;!-- Possible values are SINGLE (default) and MULTIPLE for template rules --&gt;
  102. * &lt;cardinality&gt;SINGLE&lt;/cardinality&gt;
  103. *
  104. * &lt;!-- Status displayed in rules console. Possible values are BETA, READY (default), DEPRECATED. --&gt;
  105. * &lt;status&gt;BETA&lt;/status&gt;
  106. *
  107. * &lt;!-- Type as defined by the SonarQube Quality Model. Possible values are CODE_SMELL (default), BUG and VULNERABILITY.--&gt;
  108. * &lt;type&gt;BUG&lt;/type&gt;
  109. *
  110. * &lt;!-- Optional tags. See org.sonar.api.server.rule.RuleTagFormat. The maximal length of all tags is 4000 characters. --&gt;
  111. * &lt;tag&gt;misra&lt;/tag&gt;
  112. * &lt;tag&gt;multi-threading&lt;/tag&gt;
  113. *
  114. * &lt;!-- Optional parameters --&gt;
  115. * &lt;param&gt;
  116. * &lt;!-- Required key. Max length is 128 characters. --&gt;
  117. * &lt;key&gt;the-param-key&lt;/key&gt;
  118. * &lt;description&gt;
  119. * &lt;![CDATA[the optional description, in HTML format. Max length is 4000 characters.]]&gt;
  120. * &lt;/description&gt;
  121. * &lt;!-- Optional default value, used when enabling the rule in a Quality profile. Max length is 4000 characters. --&gt;
  122. * &lt;defaultValue&gt;42&lt;/defaultValue&gt;
  123. * &lt;/param&gt;
  124. * &lt;param&gt;
  125. * &lt;key&gt;another-param&lt;/key&gt;
  126. * &lt;/param&gt;
  127. *
  128. * &lt;!-- Quality Model - type of debt remediation function --&gt;
  129. * &lt;!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values --&gt;
  130. * &lt;!-- It was previously named 'debtRemediationFunction'. --&gt;
  131. * &lt;!-- Since 5.5 --&gt;
  132. * &lt;remediationFunction&gt;LINEAR_OFFSET&lt;/remediationFunction&gt;
  133. *
  134. * &lt;!-- Quality Model - raw description of the "gap", used for some types of remediation functions. --&gt;
  135. * &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setGapDescription(String)} --&gt;
  136. * &lt;!-- It was previously named 'effortToFixDescription'. --&gt;
  137. * &lt;!-- Since 5.5 --&gt;
  138. * &lt;gapDescription&gt;Effort to test one uncovered condition&lt;/gapFixDescription&gt;
  139. *
  140. * &lt;!-- Quality Model - gap multiplier of debt remediation function. Must be defined only for some function types. --&gt;
  141. * &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
  142. * &lt;!-- It was previously named 'debtRemediationFunctionCoefficient'. --&gt;
  143. * &lt;!-- Since 5.5 --&gt;
  144. * &lt;remediationFunctionGapMultiplier&gt;10min&lt;/remediationFunctionGapMultiplier&gt;
  145. *
  146. * &lt;!-- Quality Model - base effort of debt remediation function. Must be defined only for some function types. --&gt;
  147. * &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
  148. * &lt;!-- It was previously named 'debtRemediationFunctionOffset'. --&gt;
  149. * &lt;!-- Since 5.5 --&gt;
  150. * &lt;remediationFunctionBaseEffort&gt;2min&lt;/remediationFunctionBaseEffort&gt;
  151. *
  152. * &lt;!-- Deprecated field, replaced by "internalKey" --&gt;
  153. * &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
  154. *
  155. * &lt;!-- Deprecated field, replaced by "severity" --&gt;
  156. * &lt;priority&gt;BLOCKER&lt;/priority&gt;
  157. *
  158. * &lt;/rule&gt;
  159. * &lt;/rules&gt;
  160. * </pre>
  161. *
  162. * <h3>XML Example</h3>
  163. * <pre>
  164. * &lt;rules&gt;
  165. * &lt;rule&gt;
  166. * &lt;key&gt;S1442&lt;/key&gt;
  167. * &lt;name&gt;"alert(...)" should not be used&lt;/name&gt;
  168. * &lt;description&gt;alert(...) can be useful for debugging during development, but ...&lt;/description&gt;
  169. * &lt;tag&gt;cwe&lt;/tag&gt;
  170. * &lt;tag&gt;security&lt;/tag&gt;
  171. * &lt;tag&gt;user-experience&lt;/tag&gt;
  172. * &lt;debtRemediationFunction&gt;CONSTANT_ISSUE&lt;/debtRemediationFunction&gt;
  173. * &lt;debtRemediationFunctionBaseOffset&gt;10min&lt;/debtRemediationFunctionBaseOffset&gt;
  174. * &lt;/rule&gt;
  175. *
  176. * &lt;!-- another rules... --&gt;
  177. * &lt;/rules&gt;
  178. * </pre>
  179. *
  180. * @see org.sonar.api.server.rule.RulesDefinition
  181. * @since 4.3
  182. * @deprecated since 9.0. Use the sonar-check-api to annotate rule classes instead of loading the metadata from XML files. See {@link org.sonar.check.Rule}.
  183. */
  184. @ServerSide
  185. @ComputeEngineSide
  186. @SonarLintSide
  187. @Deprecated
  188. public class RulesDefinitionXmlLoader {
  189. private static final String ELEMENT_RULES = "rules";
  190. private static final String ELEMENT_RULE = "rule";
  191. private static final String ELEMENT_PARAM = "param";
  192. private enum DescriptionFormat {
  193. HTML, MARKDOWN
  194. }
  195. /**
  196. * Loads rules by reading the XML input stream. The input stream is not always closed by the method, so it
  197. * should be handled by the caller.
  198. *
  199. * @since 4.3
  200. */
  201. public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) {
  202. load(repo, input, Charset.forName(encoding));
  203. }
  204. /**
  205. * @since 5.1
  206. */
  207. public void load(RulesDefinition.NewRepository repo, InputStream input, Charset charset) {
  208. try (Reader reader = new InputStreamReader(new BOMInputStream(input,
  209. ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,
  210. ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE), charset)) {
  211. load(repo, reader);
  212. } catch (IOException e) {
  213. throw new IllegalStateException("Error while reading XML rules definition for repository " + repo.key(), e);
  214. }
  215. }
  216. /**
  217. * Loads rules by reading the XML input stream. The reader is not closed by the method, so it
  218. * should be handled by the caller.
  219. *
  220. * @since 4.3
  221. */
  222. public void load(RulesDefinition.NewRepository repo, Reader inputReader) {
  223. XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
  224. xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
  225. xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
  226. // just so it won't try to load DTD in if there's DOCTYPE
  227. xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
  228. xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
  229. try {
  230. final XMLEventReader reader = xmlFactory.createXMLEventReader(inputReader);
  231. while (reader.hasNext()) {
  232. final XMLEvent event = reader.nextEvent();
  233. if (event.isStartElement() && event.asStartElement().getName()
  234. .getLocalPart().equals(ELEMENT_RULES)) {
  235. parseRules(repo, reader);
  236. }
  237. }
  238. } catch (XMLStreamException e) {
  239. throw new IllegalStateException("XML is not valid", e);
  240. }
  241. }
  242. private static void parseRules(RulesDefinition.NewRepository repo, XMLEventReader reader) throws XMLStreamException {
  243. while (reader.hasNext()) {
  244. final XMLEvent event = reader.nextEvent();
  245. if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(ELEMENT_RULES)) {
  246. return;
  247. }
  248. if (event.isStartElement()) {
  249. final StartElement element = event.asStartElement();
  250. final String elementName = element.getName().getLocalPart();
  251. if (ELEMENT_RULE.equals(elementName)) {
  252. processRule(repo, element, reader);
  253. }
  254. }
  255. }
  256. }
  257. private static void processRule(RulesDefinition.NewRepository repo, StartElement ruleElement, XMLEventReader reader) throws XMLStreamException {
  258. String key = null;
  259. String name = null;
  260. String description = null;
  261. // enum is not used as variable type as we want to raise an exception with the rule key when format is not supported
  262. String descriptionFormat = DescriptionFormat.HTML.name();
  263. String internalKey = null;
  264. String severity = Severity.defaultSeverity();
  265. String type = null;
  266. RuleStatus status = RuleStatus.defaultStatus();
  267. boolean template = false;
  268. String gapDescription = null;
  269. String debtRemediationFunction = null;
  270. String debtRemediationFunctionGapMultiplier = null;
  271. String debtRemediationFunctionBaseEffort = null;
  272. List<ParamStruct> params = new ArrayList<>();
  273. List<String> tags = new ArrayList<>();
  274. /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
  275. Attribute keyAttribute = ruleElement.getAttributeByName(new QName("key"));
  276. if (keyAttribute != null && StringUtils.isNotBlank(keyAttribute.getValue())) {
  277. key = trim(keyAttribute.getValue());
  278. }
  279. Attribute priorityAttribute = ruleElement.getAttributeByName(new QName("priority"));
  280. if (priorityAttribute != null && StringUtils.isNotBlank(priorityAttribute.getValue())) {
  281. severity = trim(priorityAttribute.getValue());
  282. }
  283. while (reader.hasNext()) {
  284. final XMLEvent event = reader.nextEvent();
  285. if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(ELEMENT_RULE)) {
  286. buildRule(repo, key, name, description, descriptionFormat, internalKey, severity, type, status, template,
  287. gapDescription, debtRemediationFunction, debtRemediationFunctionGapMultiplier, debtRemediationFunctionBaseEffort, params, tags);
  288. return;
  289. }
  290. if (event.isStartElement()) {
  291. final StartElement element = event.asStartElement();
  292. final String elementName = element.getName().getLocalPart();
  293. if ("name".equalsIgnoreCase(elementName)) {
  294. name = StringUtils.trim(reader.getElementText());
  295. } else if ("type".equalsIgnoreCase(elementName)) {
  296. type = StringUtils.trim(reader.getElementText());
  297. } else if ("description".equalsIgnoreCase(elementName)) {
  298. description = StringUtils.trim(reader.getElementText());
  299. } else if ("descriptionFormat".equalsIgnoreCase(elementName)) {
  300. descriptionFormat = StringUtils.trim(reader.getElementText());
  301. } else if ("key".equalsIgnoreCase(elementName)) {
  302. key = StringUtils.trim(reader.getElementText());
  303. } else if ("configKey".equalsIgnoreCase(elementName)) {
  304. // deprecated field, replaced by internalKey
  305. internalKey = StringUtils.trim(reader.getElementText());
  306. } else if ("internalKey".equalsIgnoreCase(elementName)) {
  307. internalKey = StringUtils.trim(reader.getElementText());
  308. } else if ("priority".equalsIgnoreCase(elementName) || "severity".equalsIgnoreCase(elementName)) {
  309. // "priority" is deprecated field and has been replaced by "severity"
  310. severity = StringUtils.trim(reader.getElementText());
  311. } else if ("cardinality".equalsIgnoreCase(elementName)) {
  312. template = Cardinality.MULTIPLE == Cardinality.valueOf(StringUtils.trim(reader.getElementText()));
  313. } else if ("gapDescription".equalsIgnoreCase(elementName) || "effortToFixDescription".equalsIgnoreCase(elementName)) {
  314. gapDescription = StringUtils.trim(reader.getElementText());
  315. } else if ("remediationFunction".equalsIgnoreCase(elementName) || "debtRemediationFunction".equalsIgnoreCase(elementName)) {
  316. debtRemediationFunction = StringUtils.trim(reader.getElementText());
  317. } else if ("remediationFunctionBaseEffort".equalsIgnoreCase(elementName) || "debtRemediationFunctionOffset".equalsIgnoreCase(elementName)) {
  318. debtRemediationFunctionGapMultiplier = StringUtils.trim(reader.getElementText());
  319. } else if ("remediationFunctionGapMultiplier".equalsIgnoreCase(elementName) || "debtRemediationFunctionCoefficient".equalsIgnoreCase(elementName)) {
  320. debtRemediationFunctionBaseEffort = StringUtils.trim(reader.getElementText());
  321. } else if ("status".equalsIgnoreCase(elementName)) {
  322. String s = StringUtils.trim(reader.getElementText());
  323. if (s != null) {
  324. status = RuleStatus.valueOf(s);
  325. }
  326. } else if (ELEMENT_PARAM.equalsIgnoreCase(elementName)) {
  327. params.add(processParameter(element, reader));
  328. } else if ("tag".equalsIgnoreCase(elementName)) {
  329. tags.add(StringUtils.trim(reader.getElementText()));
  330. }
  331. }
  332. }
  333. }
  334. private static void buildRule(RulesDefinition.NewRepository repo, String key, String name, @Nullable String description,
  335. String descriptionFormat, @Nullable String internalKey, String severity, @Nullable String type, RuleStatus status,
  336. boolean template, @Nullable String gapDescription, @Nullable String debtRemediationFunction, @Nullable String debtRemediationFunctionGapMultiplier,
  337. @Nullable String debtRemediationFunctionBaseEffort, List<ParamStruct> params, List<String> tags) {
  338. try {
  339. RulesDefinition.NewRule rule = repo.createRule(key)
  340. .setSeverity(severity)
  341. .setName(name)
  342. .setInternalKey(internalKey)
  343. .setTags(tags.toArray(new String[tags.size()]))
  344. .setTemplate(template)
  345. .setStatus(status)
  346. .setGapDescription(gapDescription);
  347. if (type != null) {
  348. rule.setType(RuleType.valueOf(type));
  349. }
  350. fillDescription(rule, descriptionFormat, description);
  351. fillRemediationFunction(rule, debtRemediationFunction, debtRemediationFunctionGapMultiplier, debtRemediationFunctionBaseEffort);
  352. fillParams(rule, params);
  353. } catch (Exception e) {
  354. throw new IllegalStateException(format("Fail to load the rule with key [%s:%s]", repo.key(), key), e);
  355. }
  356. }
  357. private static void fillDescription(RulesDefinition.NewRule rule, String descriptionFormat, @Nullable String description) {
  358. if (isNotBlank(description)) {
  359. switch (DescriptionFormat.valueOf(descriptionFormat)) {
  360. case HTML:
  361. rule.setHtmlDescription(description);
  362. break;
  363. case MARKDOWN:
  364. rule.setMarkdownDescription(description);
  365. break;
  366. default:
  367. throw new IllegalArgumentException("Value of descriptionFormat is not supported: " + descriptionFormat);
  368. }
  369. }
  370. }
  371. private static void fillRemediationFunction(RulesDefinition.NewRule rule, @Nullable String debtRemediationFunction,
  372. @Nullable String functionOffset, @Nullable String functionCoeff) {
  373. if (isNotBlank(debtRemediationFunction)) {
  374. DebtRemediationFunction.Type functionType = DebtRemediationFunction.Type.valueOf(debtRemediationFunction);
  375. rule.setDebtRemediationFunction(rule.debtRemediationFunctions().create(functionType, functionCoeff, functionOffset));
  376. }
  377. }
  378. private static void fillParams(RulesDefinition.NewRule rule, List<ParamStruct> params) {
  379. for (ParamStruct param : params) {
  380. rule.createParam(param.key)
  381. .setDefaultValue(param.defaultValue)
  382. .setType(param.type)
  383. .setDescription(param.description);
  384. }
  385. }
  386. private static class ParamStruct {
  387. String key;
  388. String description;
  389. String defaultValue;
  390. RuleParamType type = RuleParamType.STRING;
  391. }
  392. private static ParamStruct processParameter(StartElement paramElement, XMLEventReader reader) throws XMLStreamException {
  393. ParamStruct param = new ParamStruct();
  394. // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
  395. Attribute keyAttribute = paramElement.getAttributeByName(new QName("key"));
  396. if (keyAttribute != null && StringUtils.isNotBlank(keyAttribute.getValue())) {
  397. param.key = StringUtils.trim(keyAttribute.getValue());
  398. }
  399. // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
  400. Attribute typeAttribute = paramElement.getAttributeByName(new QName("type"));
  401. if (typeAttribute != null && StringUtils.isNotBlank(typeAttribute.getValue())) {
  402. param.type = RuleParamType.parse(StringUtils.trim(typeAttribute.getValue()));
  403. }
  404. while (reader.hasNext()) {
  405. final XMLEvent event = reader.nextEvent();
  406. if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(ELEMENT_PARAM)) {
  407. return param;
  408. }
  409. if (event.isStartElement()) {
  410. final StartElement element = event.asStartElement();
  411. final String elementName = element.getName().getLocalPart();
  412. if ("key".equalsIgnoreCase(elementName)) {
  413. param.key = StringUtils.trim(reader.getElementText());
  414. } else if ("description".equalsIgnoreCase(elementName)) {
  415. param.description = StringUtils.trim(reader.getElementText());
  416. } else if ("type".equalsIgnoreCase(elementName)) {
  417. param.type = RuleParamType.parse(StringUtils.trim(reader.getElementText()));
  418. } else if ("defaultValue".equalsIgnoreCase(elementName)) {
  419. param.defaultValue = StringUtils.trim(reader.getElementText());
  420. }
  421. }
  422. }
  423. return param;
  424. }
  425. }