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