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