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