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