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