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