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