001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.server.rule; 021 022import com.google.common.base.MoreObjects; 023import com.google.common.base.Strings; 024import com.google.common.collect.ImmutableList; 025import com.google.common.collect.ImmutableMap; 026import com.google.common.collect.ImmutableSortedSet; 027import com.google.common.collect.Maps; 028import com.google.common.collect.Sets; 029import java.io.IOException; 030import java.net.URL; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037import javax.annotation.CheckForNull; 038import javax.annotation.Nullable; 039import javax.annotation.concurrent.Immutable; 040import org.apache.commons.io.IOUtils; 041import org.apache.commons.lang.StringUtils; 042import org.sonar.api.ExtensionPoint; 043import org.sonar.api.ce.ComputeEngineSide; 044import org.sonar.api.rule.RuleStatus; 045import org.sonar.api.rule.Severity; 046import org.sonar.api.rules.RuleType; 047import org.sonar.api.server.ServerSide; 048import org.sonar.api.server.debt.DebtRemediationFunction; 049import org.sonar.api.utils.log.Loggers; 050import org.sonarsource.api.sonarlint.SonarLintSide; 051 052import static com.google.common.base.Preconditions.checkArgument; 053import static com.google.common.base.Preconditions.checkState; 054import static java.lang.String.format; 055import static java.nio.charset.StandardCharsets.UTF_8; 056import static org.apache.commons.lang.StringUtils.trimToNull; 057 058/** 059 * Defines some coding rules of the same repository. For example the Java Findbugs plugin provides an implementation of 060 * this extension point in order to define the rules that it supports. 061 * <br> 062 * This interface replaces the deprecated class org.sonar.api.rules.RuleRepository. 063 * <br> 064 * <h3>How to use</h3> 065 * <pre> 066 * public class MyJsRulesDefinition implements RulesDefinition { 067 * 068 * {@literal @}Override 069 * public void define(Context context) { 070 * NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer"); 071 * 072 * // define a rule programmatically. Note that rules 073 * // could be loaded from files (JSON, XML, ...) 074 * NewRule x1Rule = repository.createRule("x1") 075 * .setName("No empty line") 076 * .setHtmlDescription("Generate an issue on empty lines") 077 * 078 * // optional tags 079 * .setTags("style", "stupid") 080 * 081 * // optional status. Default value is READY. 082 * .setStatus(RuleStatus.BETA) 083 * 084 * // default severity when the rule is activated on a Quality profile. Default value is MAJOR. 085 * .setSeverity(Severity.MINOR); 086 * 087 * // optional type for SonarQube Quality Model. Default is RulesDefinition.Type.CODE_SMELL. 088 * .setType(RulesDefinition.Type.BUG) 089 * 090 * x1Rule 091 * .setDebtRemediationFunction(x1Rule.debtRemediationFunctions().linearWithOffset("1h", "30min")); 092 * 093 * x1Rule.createParam("acceptWhitespace") 094 * .setDefaultValue("false") 095 * .setType(RuleParamType.BOOLEAN) 096 * .setDescription("Accept whitespaces on the line"); 097 * 098 * // don't forget to call done() to finalize the definition 099 * repository.done(); 100 * } 101 * } 102 * </pre> 103 * <br> 104 * If rules are declared in a XML file with the standard SonarQube format (see 105 * {@link org.sonar.api.server.rule.RulesDefinitionXmlLoader}), then it can be loaded by using : 106 * <br> 107 * <pre> 108 * public class MyJsRulesDefinition implements RulesDefinition { 109 * 110 * private final RulesDefinitionXmlLoader xmlLoader; 111 * 112 * public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) { 113 * this.xmlLoader = xmlLoader; 114 * } 115 * 116 * {@literal @}Override 117 * public void define(Context context) { 118 * NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer"); 119 * // see javadoc of RulesDefinitionXmlLoader for the format 120 * xmlLoader.load(repository, getClass().getResourceAsStream("/path/to/rules.xml")); 121 * repository.done(); 122 * } 123 * } 124 * </pre> 125 * <br> 126 * In the above example, XML file must contain name and description of each rule. If it's not the case, then the 127 * (deprecated) English bundles can be used : 128 * <br> 129 * <pre> 130 * public class MyJsRulesDefinition implements RulesDefinition { 131 * 132 * private final RulesDefinitionXmlLoader xmlLoader; 133 * private final RulesDefinitionI18nLoader i18nLoader; 134 * 135 * public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader, RulesDefinitionI18nLoader i18nLoader) { 136 * this.xmlLoader = xmlLoader; 137 * this.i18nLoader = i18nLoader; 138 * } 139 * 140 * {@literal @}Override 141 * public void define(Context context) { 142 * NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer"); 143 * xmlLoader.load(repository, getClass().getResourceAsStream("/path/to/rules.xml"), "UTF-8"); 144 * i18nLoader.load(repository); 145 * repository.done(); 146 * } 147 * } 148 * </pre> 149 * 150 * @since 4.3 151 */ 152@ServerSide 153@ComputeEngineSide 154@SonarLintSide 155@ExtensionPoint 156public interface RulesDefinition { 157 158 /** 159 * Default sub-characteristics of technical debt model. See http://www.sqale.org 160 * @deprecated in 5.5. SQALE Quality Model is replaced by SonarQube Quality Model. 161 * See https://jira.sonarsource.com/browse/MMF-184 162 */ 163 @Deprecated 164 final class SubCharacteristics { 165 /** 166 * Related to characteristic REUSABILITY 167 */ 168 public static final String MODULARITY = "MODULARITY"; 169 170 /** 171 * Related to characteristic REUSABILITY 172 */ 173 public static final String TRANSPORTABILITY = "TRANSPORTABILITY"; 174 175 /** 176 * Related to characteristic PORTABILITY 177 */ 178 public static final String COMPILER_RELATED_PORTABILITY = "COMPILER_RELATED_PORTABILITY"; 179 180 /** 181 * Related to characteristic PORTABILITY 182 */ 183 public static final String HARDWARE_RELATED_PORTABILITY = "HARDWARE_RELATED_PORTABILITY"; 184 185 /** 186 * Related to characteristic PORTABILITY 187 */ 188 public static final String LANGUAGE_RELATED_PORTABILITY = "LANGUAGE_RELATED_PORTABILITY"; 189 190 /** 191 * Related to characteristic PORTABILITY 192 */ 193 public static final String OS_RELATED_PORTABILITY = "OS_RELATED_PORTABILITY"; 194 195 /** 196 * Related to characteristic PORTABILITY 197 */ 198 public static final String SOFTWARE_RELATED_PORTABILITY = "SOFTWARE_RELATED_PORTABILITY"; 199 200 /** 201 * Related to characteristic PORTABILITY 202 */ 203 public static final String TIME_ZONE_RELATED_PORTABILITY = "TIME_ZONE_RELATED_PORTABILITY"; 204 205 /** 206 * Related to characteristic MAINTAINABILITY 207 */ 208 public static final String READABILITY = "READABILITY"; 209 210 /** 211 * Related to characteristic MAINTAINABILITY 212 */ 213 public static final String UNDERSTANDABILITY = "UNDERSTANDABILITY"; 214 215 /** 216 * Related to characteristic SECURITY 217 */ 218 public static final String API_ABUSE = "API_ABUSE"; 219 220 /** 221 * Related to characteristic SECURITY 222 */ 223 public static final String ERRORS = "ERRORS"; 224 225 /** 226 * Related to characteristic SECURITY 227 */ 228 public static final String INPUT_VALIDATION_AND_REPRESENTATION = "INPUT_VALIDATION_AND_REPRESENTATION"; 229 230 /** 231 * Related to characteristic SECURITY 232 */ 233 public static final String SECURITY_FEATURES = "SECURITY_FEATURES"; 234 235 /** 236 * Related to characteristic EFFICIENCY 237 */ 238 public static final String CPU_EFFICIENCY = "CPU_EFFICIENCY"; 239 240 /** 241 * Related to characteristic EFFICIENCY 242 */ 243 public static final String MEMORY_EFFICIENCY = "MEMORY_EFFICIENCY"; 244 245 /** 246 * Related to characteristic EFFICIENCY 247 */ 248 public static final String NETWORK_USE = "NETWORK_USE"; 249 250 /** 251 * Related to characteristic CHANGEABILITY 252 */ 253 public static final String ARCHITECTURE_CHANGEABILITY = "ARCHITECTURE_CHANGEABILITY"; 254 255 /** 256 * Related to characteristic CHANGEABILITY 257 */ 258 public static final String DATA_CHANGEABILITY = "DATA_CHANGEABILITY"; 259 260 /** 261 * Related to characteristic CHANGEABILITY 262 */ 263 public static final String LOGIC_CHANGEABILITY = "LOGIC_CHANGEABILITY"; 264 265 /** 266 * Related to characteristic RELIABILITY 267 */ 268 public static final String ARCHITECTURE_RELIABILITY = "ARCHITECTURE_RELIABILITY"; 269 270 /** 271 * Related to characteristic RELIABILITY 272 */ 273 public static final String DATA_RELIABILITY = "DATA_RELIABILITY"; 274 275 /** 276 * Related to characteristic RELIABILITY 277 */ 278 public static final String EXCEPTION_HANDLING = "EXCEPTION_HANDLING"; 279 280 /** 281 * Related to characteristic RELIABILITY 282 */ 283 public static final String FAULT_TOLERANCE = "FAULT_TOLERANCE"; 284 285 /** 286 * Related to characteristic RELIABILITY 287 */ 288 public static final String INSTRUCTION_RELIABILITY = "INSTRUCTION_RELIABILITY"; 289 290 /** 291 * Related to characteristic RELIABILITY 292 */ 293 public static final String LOGIC_RELIABILITY = "LOGIC_RELIABILITY"; 294 295 /** 296 * Related to characteristic RELIABILITY 297 */ 298 public static final String RESOURCE_RELIABILITY = "RESOURCE_RELIABILITY"; 299 300 /** 301 * Related to characteristic RELIABILITY 302 */ 303 public static final String SYNCHRONIZATION_RELIABILITY = "SYNCHRONIZATION_RELIABILITY"; 304 305 /** 306 * Related to characteristic RELIABILITY 307 */ 308 public static final String UNIT_TESTS = "UNIT_TESTS"; 309 310 /** 311 * Related to characteristic TESTABILITY 312 */ 313 public static final String INTEGRATION_TESTABILITY = "INTEGRATION_TESTABILITY"; 314 315 /** 316 * Related to characteristic TESTABILITY 317 */ 318 public static final String UNIT_TESTABILITY = "UNIT_TESTABILITY"; 319 320 /** 321 * Related to characteristic ACCESSIBILITY 322 */ 323 public static final String USABILITY_ACCESSIBILITY = "USABILITY_ACCESSIBILITY"; 324 325 /** 326 * Related to characteristic ACCESSIBILITY 327 */ 328 public static final String USABILITY_COMPLIANCE = "USABILITY_COMPLIANCE"; 329 330 /** 331 * Related to characteristic ACCESSIBILITY 332 */ 333 public static final String USABILITY_EASE_OF_USE = "USABILITY_EASE_OF_USE"; 334 335 /** 336 * Related to characteristic REUSABILITY 337 */ 338 public static final String REUSABILITY_COMPLIANCE = "REUSABILITY_COMPLIANCE"; 339 340 /** 341 * Related to characteristic PORTABILITY 342 */ 343 public static final String PORTABILITY_COMPLIANCE = "PORTABILITY_COMPLIANCE"; 344 345 /** 346 * Related to characteristic MAINTAINABILITY 347 */ 348 public static final String MAINTAINABILITY_COMPLIANCE = "MAINTAINABILITY_COMPLIANCE"; 349 350 /** 351 * Related to characteristic SECURITY 352 */ 353 public static final String SECURITY_COMPLIANCE = "SECURITY_COMPLIANCE"; 354 355 /** 356 * Related to characteristic EFFICIENCY 357 */ 358 public static final String EFFICIENCY_COMPLIANCE = "EFFICIENCY_COMPLIANCE"; 359 360 /** 361 * Related to characteristic CHANGEABILITY 362 */ 363 public static final String CHANGEABILITY_COMPLIANCE = "CHANGEABILITY_COMPLIANCE"; 364 365 /** 366 * Related to characteristic RELIABILITY 367 */ 368 public static final String RELIABILITY_COMPLIANCE = "RELIABILITY_COMPLIANCE"; 369 370 /** 371 * Related to characteristic TESTABILITY 372 */ 373 public static final String TESTABILITY_COMPLIANCE = "TESTABILITY_COMPLIANCE"; 374 375 private SubCharacteristics() { 376 // only constants 377 } 378 } 379 380 /** 381 * Instantiated by core but not by plugins, except for their tests. 382 */ 383 class Context { 384 private final Map<String, Repository> repositoriesByKey = Maps.newHashMap(); 385 386 /** 387 * New builder for {@link org.sonar.api.server.rule.RulesDefinition.Repository}. 388 * <br> 389 * A plugin can add rules to a repository that is defined then executed by another plugin. For instance 390 * the FbContrib plugin contributes to the Findbugs plugin rules. In this case no need 391 * to execute {@link org.sonar.api.server.rule.RulesDefinition.NewRepository#setName(String)} 392 */ 393 public NewRepository createRepository(String key, String language) { 394 return new NewRepositoryImpl(this, key, language); 395 } 396 397 /** 398 * @deprecated since 5.2. Simply use {@link #createRepository(String, String)} 399 */ 400 @Deprecated 401 public NewRepository extendRepository(String key, String language) { 402 return createRepository(key, language); 403 } 404 405 @CheckForNull 406 public Repository repository(String key) { 407 return repositoriesByKey.get(key); 408 } 409 410 public List<Repository> repositories() { 411 return ImmutableList.copyOf(repositoriesByKey.values()); 412 } 413 414 /** 415 * @deprecated returns empty list since 5.2. Concept of "extended repository" was misleading and not valuable. Simply declare 416 * repositories and use {@link #repositories()}. See http://jira.sonarsource.com/browse/SONAR-6709 417 */ 418 @Deprecated 419 public List<ExtendedRepository> extendedRepositories(String repositoryKey) { 420 return Collections.emptyList(); 421 } 422 423 /** 424 * @deprecated returns empty list since 5.2. Concept of "extended repository" was misleading and not valuable. Simply declare 425 * repositories and use {@link #repositories()}. See http://jira.sonarsource.com/browse/SONAR-6709 426 */ 427 @Deprecated 428 public List<ExtendedRepository> extendedRepositories() { 429 return Collections.emptyList(); 430 } 431 432 private void registerRepository(NewRepositoryImpl newRepository) { 433 Repository existing = repositoriesByKey.get(newRepository.key()); 434 if (existing != null) { 435 String existingLanguage = existing.language(); 436 checkState(existingLanguage.equals(newRepository.language), 437 "The rule repository '%s' must not be defined for two different languages: %s and %s", 438 newRepository.key, existingLanguage, newRepository.language); 439 } 440 repositoriesByKey.put(newRepository.key, new RepositoryImpl(newRepository, existing)); 441 } 442 } 443 444 interface NewExtendedRepository { 445 /** 446 * Create a rule with specified key. Max length of key is 200 characters. Key must be unique 447 * among the repository 448 * @throws IllegalArgumentException is key is not unique. Note a warning was logged up to version 5.4 (included) 449 */ 450 NewRule createRule(String ruleKey); 451 452 @CheckForNull 453 NewRule rule(String ruleKey); 454 455 Collection<NewRule> rules(); 456 457 String key(); 458 459 void done(); 460 } 461 462 interface NewRepository extends NewExtendedRepository { 463 NewRepository setName(String s); 464 } 465 466 class NewRepositoryImpl implements NewRepository { 467 private final Context context; 468 private final String key; 469 private String language; 470 private String name; 471 private final Map<String, NewRule> newRules = Maps.newHashMap(); 472 473 private NewRepositoryImpl(Context context, String key, String language) { 474 this.context = context; 475 this.key = this.name = key; 476 this.language = language; 477 } 478 479 @Override 480 public String key() { 481 return key; 482 } 483 484 @Override 485 public NewRepositoryImpl setName(@Nullable String s) { 486 if (StringUtils.isNotEmpty(s)) { 487 this.name = s; 488 } 489 return this; 490 } 491 492 @Override 493 public NewRule createRule(String ruleKey) { 494 checkArgument(!newRules.containsKey(ruleKey), "The rule '%s' of repository '%s' is declared several times", ruleKey, key); 495 NewRule newRule = new NewRule(key, ruleKey); 496 newRules.put(ruleKey, newRule); 497 return newRule; 498 } 499 500 @CheckForNull 501 @Override 502 public NewRule rule(String ruleKey) { 503 return newRules.get(ruleKey); 504 } 505 506 @Override 507 public Collection<NewRule> rules() { 508 return newRules.values(); 509 } 510 511 @Override 512 public void done() { 513 // note that some validations can be done here, for example for 514 // verifying that at least one rule is declared 515 516 context.registerRepository(this); 517 } 518 519 @Override 520 public String toString() { 521 return MoreObjects.toStringHelper(this) 522 .add("key", key) 523 .add("language", language) 524 .toString(); 525 } 526 } 527 528 interface ExtendedRepository { 529 String key(); 530 531 String language(); 532 533 @CheckForNull 534 Rule rule(String ruleKey); 535 536 List<Rule> rules(); 537 } 538 539 interface Repository extends ExtendedRepository { 540 String name(); 541 } 542 543 @Immutable 544 class RepositoryImpl implements Repository { 545 private final String key; 546 private final String language; 547 private final String name; 548 private final Map<String, Rule> rulesByKey; 549 550 private RepositoryImpl(NewRepositoryImpl newRepository, @Nullable Repository mergeInto) { 551 this.key = newRepository.key; 552 this.language = newRepository.language; 553 554 Map<String, Rule> ruleBuilder = new HashMap<>(); 555 if (mergeInto != null) { 556 if (!StringUtils.equals(newRepository.language, mergeInto.language()) || !StringUtils.equals(newRepository.key, mergeInto.key())) { 557 throw new IllegalArgumentException(format("Bug - language and key of the repositories to be merged should be the sames: %s and %s", newRepository, mergeInto)); 558 } 559 this.name = StringUtils.defaultIfBlank(mergeInto.name(), newRepository.name); 560 for (Rule rule : mergeInto.rules()) { 561 if (!newRepository.key().startsWith("common-") && ruleBuilder.containsKey(rule.key())) { 562 Loggers.get(getClass()).warn("The rule '{}' of repository '{}' is declared several times", rule.key(), mergeInto.key()); 563 } 564 ruleBuilder.put(rule.key(), rule); 565 } 566 } else { 567 this.name = newRepository.name; 568 } 569 for (NewRule newRule : newRepository.newRules.values()) { 570 newRule.validate(); 571 ruleBuilder.put(newRule.key, new Rule(this, newRule)); 572 } 573 this.rulesByKey = ImmutableMap.copyOf(ruleBuilder); 574 } 575 576 @Override 577 public String key() { 578 return key; 579 } 580 581 @Override 582 public String language() { 583 return language; 584 } 585 586 @Override 587 public String name() { 588 return name; 589 } 590 591 @Override 592 @CheckForNull 593 public Rule rule(String ruleKey) { 594 return rulesByKey.get(ruleKey); 595 } 596 597 @Override 598 public List<Rule> rules() { 599 return ImmutableList.copyOf(rulesByKey.values()); 600 } 601 602 @Override 603 public boolean equals(Object o) { 604 if (this == o) { 605 return true; 606 } 607 if (o == null || getClass() != o.getClass()) { 608 return false; 609 } 610 RepositoryImpl that = (RepositoryImpl) o; 611 return key.equals(that.key); 612 } 613 614 @Override 615 public int hashCode() { 616 return key.hashCode(); 617 } 618 619 @Override 620 public String toString() { 621 return MoreObjects.toStringHelper(this) 622 .add("language", language) 623 .add("key", key) 624 .toString(); 625 } 626 } 627 628 /** 629 * Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction}. 630 */ 631 interface DebtRemediationFunctions { 632 633 /** 634 * Shortcut for {@code create(Type.LINEAR, gap multiplier, null)}. 635 * @param gapMultiplier the duration to fix one issue. See {@link DebtRemediationFunction} for details about format. 636 * @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR 637 */ 638 DebtRemediationFunction linear(String gapMultiplier); 639 640 /** 641 * Shortcut for {@code create(Type.LINEAR_OFFSET, gap multiplier, base effort)}. 642 * @param gapMultiplier duration to fix one point of complexity. See {@link DebtRemediationFunction} for details and format. 643 * @param baseEffort duration to make basic analysis. See {@link DebtRemediationFunction} for details and format. 644 * @see org.sonar.api.server.debt.DebtRemediationFunction.Type#LINEAR_OFFSET 645 */ 646 DebtRemediationFunction linearWithOffset(String gapMultiplier, String baseEffort); 647 648 /** 649 * Shortcut for {@code create(Type.CONSTANT_ISSUE, null, base effort)}. 650 * @param baseEffort cost per issue. See {@link DebtRemediationFunction} for details and format. 651 * @see org.sonar.api.server.debt.DebtRemediationFunction.Type#CONSTANT_ISSUE 652 */ 653 DebtRemediationFunction constantPerIssue(String baseEffort); 654 655 /** 656 * Flexible way to create a {@link DebtRemediationFunction}. An unchecked exception is thrown if 657 * coefficient and/or offset are not valid according to the given @{code type}. 658 * @since 5.3 659 */ 660 DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String gapMultiplier, @Nullable String baseEffort); 661 } 662 663 class NewRule { 664 private final String repoKey; 665 private final String key; 666 private RuleType type; 667 private String name; 668 private String htmlDescription; 669 private String markdownDescription; 670 private String internalKey; 671 private String severity = Severity.MAJOR; 672 private boolean template; 673 private RuleStatus status = RuleStatus.defaultStatus(); 674 private DebtRemediationFunction debtRemediationFunction; 675 private String gapDescription; 676 private final Set<String> tags = Sets.newTreeSet(); 677 private final Map<String, NewParam> paramsByKey = Maps.newHashMap(); 678 private final DebtRemediationFunctions functions; 679 private boolean activatedByDefault; 680 681 private NewRule(String repoKey, String key) { 682 this.repoKey = repoKey; 683 this.key = key; 684 this.functions = new DefaultDebtRemediationFunctions(repoKey, key); 685 } 686 687 public String key() { 688 return this.key; 689 } 690 691 /** 692 * Required rule name 693 */ 694 public NewRule setName(String s) { 695 this.name = trimToNull(s); 696 return this; 697 } 698 699 public NewRule setTemplate(boolean template) { 700 this.template = template; 701 return this; 702 } 703 704 /** 705 * Should this rule be enabled by default. For example in SonarLint standalone. 706 * @since 6.0 707 */ 708 public NewRule setActivatedByDefault(boolean activatedByDefault) { 709 this.activatedByDefault = activatedByDefault; 710 return this; 711 } 712 713 public NewRule setSeverity(String s) { 714 checkArgument(Severity.ALL.contains(s), "Severity of rule %s is not correct: %s", this, s); 715 this.severity = s; 716 return this; 717 } 718 719 /** 720 * The type as defined by the SonarQube Quality Model. 721 * <br> 722 * When a plugin does not define rule type, then it is deduced from 723 * tags: 724 * <ul> 725 * <li>if the rule has the "bug" tag then type is {@link RuleType#BUG}</li> 726 * <li>if the rule has the "security" tag then type is {@link RuleType#VULNERABILITY}</li> 727 * <li>if the rule has both tags "bug" and "security", then type is {@link RuleType#BUG}</li> 728 * <li>default type is {@link RuleType#CODE_SMELL}</li> 729 * </ul> 730 * Finally the "bug" and "security" tags are considered as reserved. They 731 * are silently removed from the final state of definition. 732 * @since 5.5 733 */ 734 public NewRule setType(RuleType t) { 735 this.type = t; 736 return this; 737 } 738 739 /** 740 * The optional description, in HTML format, has no max length. It's exclusive with markdown description 741 * (see {@link #setMarkdownDescription(String)}) 742 */ 743 public NewRule setHtmlDescription(@Nullable String s) { 744 checkState(markdownDescription == null, "Rule '%s' already has a Markdown description", this); 745 this.htmlDescription = trimToNull(s); 746 return this; 747 } 748 749 /** 750 * Load description from a file available in classpath. Example : <code>setHtmlDescription(getClass().getResource("/myrepo/Rule1234.html")</code> 751 */ 752 public NewRule setHtmlDescription(@Nullable URL classpathUrl) { 753 if (classpathUrl != null) { 754 try { 755 setHtmlDescription(IOUtils.toString(classpathUrl, UTF_8)); 756 } catch (IOException e) { 757 throw new IllegalStateException("Fail to read: " + classpathUrl, e); 758 } 759 } else { 760 this.htmlDescription = null; 761 } 762 return this; 763 } 764 765 /** 766 * The optional description, in a restricted Markdown format, has no max length. It's exclusive with HTML description 767 * (see {@link #setHtmlDescription(String)}) 768 */ 769 public NewRule setMarkdownDescription(@Nullable String s) { 770 checkState(htmlDescription == null, "Rule '%s' already has an HTML description", this); 771 this.markdownDescription = trimToNull(s); 772 return this; 773 } 774 775 /** 776 * Load description from a file available in classpath. Example : {@code setMarkdownDescription(getClass().getResource("/myrepo/Rule1234.md")} 777 */ 778 public NewRule setMarkdownDescription(@Nullable URL classpathUrl) { 779 if (classpathUrl != null) { 780 try { 781 setMarkdownDescription(IOUtils.toString(classpathUrl, UTF_8)); 782 } catch (IOException e) { 783 throw new IllegalStateException("Fail to read: " + classpathUrl, e); 784 } 785 } else { 786 this.markdownDescription = null; 787 } 788 return this; 789 } 790 791 /** 792 * Default value is {@link org.sonar.api.rule.RuleStatus#READY}. The value 793 * {@link org.sonar.api.rule.RuleStatus#REMOVED} is not accepted and raises an 794 * {@link java.lang.IllegalArgumentException}. 795 */ 796 public NewRule setStatus(RuleStatus status) { 797 checkArgument(RuleStatus.REMOVED != status, "Status 'REMOVED' is not accepted on rule '%s'", this); 798 this.status = status; 799 return this; 800 } 801 802 /** 803 * SQALE sub-characteristic. See http://www.sqale.org 804 * 805 * @see org.sonar.api.server.rule.RulesDefinition.SubCharacteristics for constant values 806 * @deprecated in 5.5. SQALE Quality Model is replaced by SonarQube Quality Model. This method does nothing. 807 * See https://jira.sonarsource.com/browse/MMF-184 808 * @see #setType(RuleType) 809 */ 810 public NewRule setDebtSubCharacteristic(@Nullable String s) { 811 return this; 812 } 813 814 /** 815 * Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction} 816 */ 817 public DebtRemediationFunctions debtRemediationFunctions() { 818 return functions; 819 } 820 821 /** 822 * @see #debtRemediationFunctions() 823 */ 824 public NewRule setDebtRemediationFunction(@Nullable DebtRemediationFunction fn) { 825 this.debtRemediationFunction = fn; 826 return this; 827 } 828 829 /** 830 * @deprecated since 5.5, replaced by {@link #setGapDescription(String)} 831 */ 832 @Deprecated 833 public NewRule setEffortToFixDescription(@Nullable String s) { 834 return setGapDescription(s); 835 } 836 837 /** 838 * For rules that use LINEAR or LINEAR_OFFSET remediation functions, the meaning 839 * of the function parameter (= "gap") must be set. This description 840 * explains what 1 point of "gap" represents for the rule. 841 * <br> 842 * Example: for the "Insufficient condition coverage", this description for the 843 * remediation function gap multiplier/base effort would be something like 844 * "Effort to test one uncovered condition". 845 */ 846 public NewRule setGapDescription(@Nullable String s) { 847 this.gapDescription = s; 848 return this; 849 } 850 851 /** 852 * Create a parameter with given unique key. Max length of key is 128 characters. 853 */ 854 public NewParam createParam(String paramKey) { 855 checkArgument(!paramsByKey.containsKey(paramKey), "The parameter '%s' is declared several times on the rule %s", paramKey, this); 856 NewParam param = new NewParam(paramKey); 857 paramsByKey.put(paramKey, param); 858 return param; 859 } 860 861 @CheckForNull 862 public NewParam param(String paramKey) { 863 return paramsByKey.get(paramKey); 864 } 865 866 public Collection<NewParam> params() { 867 return paramsByKey.values(); 868 } 869 870 /** 871 * @see RuleTagFormat 872 */ 873 public NewRule addTags(String... list) { 874 for (String tag : list) { 875 RuleTagFormat.validate(tag); 876 tags.add(tag); 877 } 878 return this; 879 } 880 881 /** 882 * @see RuleTagFormat 883 */ 884 public NewRule setTags(String... list) { 885 tags.clear(); 886 addTags(list); 887 return this; 888 } 889 890 /** 891 * Optional key that can be used by the rule engine. Not displayed 892 * in webapp. For example the Java Checkstyle plugin feeds this field 893 * with the internal path ("Checker/TreeWalker/AnnotationUseStyle"). 894 */ 895 public NewRule setInternalKey(@Nullable String s) { 896 this.internalKey = s; 897 return this; 898 } 899 900 private void validate() { 901 if (Strings.isNullOrEmpty(name)) { 902 throw new IllegalStateException(format("Name of rule %s is empty", this)); 903 } 904 if (Strings.isNullOrEmpty(htmlDescription) && Strings.isNullOrEmpty(markdownDescription)) { 905 throw new IllegalStateException(format("One of HTML description or Markdown description must be defined for rule %s", this)); 906 } 907 } 908 909 @Override 910 public String toString() { 911 return format("[repository=%s, key=%s]", repoKey, key); 912 } 913 } 914 915 @Immutable 916 class Rule { 917 private final Repository repository; 918 private final String repoKey; 919 private final String key; 920 private final String name; 921 private final RuleType type; 922 private final String htmlDescription; 923 private final String markdownDescription; 924 private final String internalKey; 925 private final String severity; 926 private final boolean template; 927 private final DebtRemediationFunction debtRemediationFunction; 928 private final String gapDescription; 929 private final Set<String> tags; 930 private final Map<String, Param> params; 931 private final RuleStatus status; 932 private final boolean activatedByDefault; 933 934 private Rule(Repository repository, NewRule newRule) { 935 this.repository = repository; 936 this.repoKey = newRule.repoKey; 937 this.key = newRule.key; 938 this.name = newRule.name; 939 this.htmlDescription = newRule.htmlDescription; 940 this.markdownDescription = newRule.markdownDescription; 941 this.internalKey = newRule.internalKey; 942 this.severity = newRule.severity; 943 this.template = newRule.template; 944 this.status = newRule.status; 945 this.debtRemediationFunction = newRule.debtRemediationFunction; 946 this.gapDescription = newRule.gapDescription; 947 this.type = newRule.type == null ? RuleTagsToTypeConverter.convert(newRule.tags) : newRule.type; 948 this.tags = ImmutableSortedSet.copyOf(Sets.difference(newRule.tags, RuleTagsToTypeConverter.RESERVED_TAGS)); 949 ImmutableMap.Builder<String, Param> paramsBuilder = ImmutableMap.builder(); 950 for (NewParam newParam : newRule.paramsByKey.values()) { 951 paramsBuilder.put(newParam.key, new Param(newParam)); 952 } 953 this.params = paramsBuilder.build(); 954 this.activatedByDefault = newRule.activatedByDefault; 955 } 956 957 public Repository repository() { 958 return repository; 959 } 960 961 public String key() { 962 return key; 963 } 964 965 public String name() { 966 return name; 967 } 968 969 /** 970 * @since 5.5 971 * @see NewRule#setType(RuleType) 972 */ 973 public RuleType type() { 974 return type; 975 } 976 977 public String severity() { 978 return severity; 979 } 980 981 @CheckForNull 982 public String htmlDescription() { 983 return htmlDescription; 984 } 985 986 @CheckForNull 987 public String markdownDescription() { 988 return markdownDescription; 989 } 990 991 public boolean template() { 992 return template; 993 } 994 995 /** 996 * Should this rule be enabled by default. For example in SonarLint standalone. 997 * @since 6.0 998 */ 999 public boolean activatedByDefault() { 1000 return activatedByDefault; 1001 } 1002 1003 public RuleStatus status() { 1004 return status; 1005 } 1006 1007 /** 1008 * @deprecated in 5.5. SQALE Quality Model is replaced by SonarQube Quality Model. {@code null} is 1009 * always returned. See https://jira.sonarsource.com/browse/MMF-184 1010 * @see #type() 1011 */ 1012 @CheckForNull 1013 @Deprecated 1014 public String debtSubCharacteristic() { 1015 return null; 1016 } 1017 1018 @CheckForNull 1019 public DebtRemediationFunction debtRemediationFunction() { 1020 return debtRemediationFunction; 1021 } 1022 1023 /** 1024 * @deprecated since 5.5, replaced by {@link #gapDescription()} 1025 */ 1026 @Deprecated 1027 @CheckForNull 1028 public String effortToFixDescription() { 1029 return gapDescription(); 1030 } 1031 1032 @CheckForNull 1033 public String gapDescription() { 1034 return gapDescription; 1035 } 1036 1037 @CheckForNull 1038 public Param param(String key) { 1039 return params.get(key); 1040 } 1041 1042 public List<Param> params() { 1043 return ImmutableList.copyOf(params.values()); 1044 } 1045 1046 public Set<String> tags() { 1047 return tags; 1048 } 1049 1050 /** 1051 * @see RulesDefinition.NewRule#setInternalKey(String) 1052 */ 1053 @CheckForNull 1054 public String internalKey() { 1055 return internalKey; 1056 } 1057 1058 @Override 1059 public boolean equals(Object o) { 1060 if (this == o) { 1061 return true; 1062 } 1063 if (o == null || getClass() != o.getClass()) { 1064 return false; 1065 } 1066 Rule other = (Rule) o; 1067 return key.equals(other.key) && repoKey.equals(other.repoKey); 1068 } 1069 1070 @Override 1071 public int hashCode() { 1072 int result = repoKey.hashCode(); 1073 result = 31 * result + key.hashCode(); 1074 return result; 1075 } 1076 1077 @Override 1078 public String toString() { 1079 return format("[repository=%s, key=%s]", repoKey, key); 1080 } 1081 } 1082 1083 class NewParam { 1084 private final String key; 1085 private String name; 1086 private String description; 1087 private String defaultValue; 1088 private RuleParamType type = RuleParamType.STRING; 1089 1090 private NewParam(String key) { 1091 this.key = this.name = key; 1092 } 1093 1094 public String key() { 1095 return key; 1096 } 1097 1098 public NewParam setName(@Nullable String s) { 1099 // name must never be null. 1100 this.name = StringUtils.defaultIfBlank(s, key); 1101 return this; 1102 } 1103 1104 public NewParam setType(RuleParamType t) { 1105 this.type = t; 1106 return this; 1107 } 1108 1109 /** 1110 * Plain-text description. Can be null. Max length is 4000 characters. 1111 */ 1112 public NewParam setDescription(@Nullable String s) { 1113 this.description = StringUtils.defaultIfBlank(s, null); 1114 return this; 1115 } 1116 1117 /** 1118 * Empty default value will be converted to null. Max length is 4000 characters. 1119 */ 1120 public NewParam setDefaultValue(@Nullable String s) { 1121 this.defaultValue = Strings.emptyToNull(s); 1122 return this; 1123 } 1124 } 1125 1126 @Immutable 1127 class Param { 1128 private final String key; 1129 private final String name; 1130 private final String description; 1131 private final String defaultValue; 1132 private final RuleParamType type; 1133 1134 private Param(NewParam newParam) { 1135 this.key = newParam.key; 1136 this.name = newParam.name; 1137 this.description = newParam.description; 1138 this.defaultValue = newParam.defaultValue; 1139 this.type = newParam.type; 1140 } 1141 1142 public String key() { 1143 return key; 1144 } 1145 1146 public String name() { 1147 return name; 1148 } 1149 1150 @Nullable 1151 public String description() { 1152 return description; 1153 } 1154 1155 @Nullable 1156 public String defaultValue() { 1157 return defaultValue; 1158 } 1159 1160 public RuleParamType type() { 1161 return type; 1162 } 1163 1164 @Override 1165 public boolean equals(Object o) { 1166 if (this == o) { 1167 return true; 1168 } 1169 if (o == null || getClass() != o.getClass()) { 1170 return false; 1171 } 1172 Param that = (Param) o; 1173 return key.equals(that.key); 1174 } 1175 1176 @Override 1177 public int hashCode() { 1178 return key.hashCode(); 1179 } 1180 } 1181 1182 /** 1183 * This method is executed when server is started. 1184 */ 1185 void define(Context context); 1186 1187}