<modules>
<module>sonar-custom-java-plugin</module>
+ <module>sonar-custom-javascript-plugin</module>
</modules>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
- <dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- <version>2.6</version>
- </dependency>
- <dependency>
- <groupId>org.sonarsource.sslr-squid-bridge</groupId>
- <artifactId>sslr-squid-bridge</artifactId>
- <version>2.6.1</version>
- <exclusions>
- <exclusion>
- <groupId>org.codehaus.sonar.sslr</groupId>
- <artifactId>sslr-core</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.codehaus.sonar</groupId>
- <artifactId>sonar-plugin-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.codehaus.sonar.sslr</groupId>
- <artifactId>sslr-xpath</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.slf4j</groupId>
- <artifactId>jcl-over-slf4j</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.sonarsource.java</groupId>
- <artifactId>javascript-frontend</artifactId>
- <version>javascript.plugin.version</version>
- </dependency>
-
<version>${sonar.apiVersion}</version>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>org.sonarsource.java</groupId>
- <artifactId>java-checks-testkit</artifactId>
- <version>${java.plugin.version}</version>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.easytesting</groupId>
<artifactId>fest-assert</artifactId>
<version>1.17</version>
<extensions>true</extensions>
<configuration>
+ <!--
<pluginClass>de.example.plugins.helloworld.HelloWorldPlugin</pluginClass>
+ -->
</configuration>
</plugin>
<plugin>
--- /dev/null
+package de.example.custom.javascript.checks;
+
+import org.sonar.check.Rule;
+import org.sonar.plugins.javascript.api.visitors.DoubleDispatchVisitorCheck;
+
+@Rule(key = "GUJS0001")
+public class AngularJSRootOnEventSubscriptionCheck extends DoubleDispatchVisitorCheck {
+
+}
--- /dev/null
+package de.example.custom.javascript.checks;
+
+import java.util.List;
+
+import org.sonar.plugins.javascript.api.JavaScriptCheck;
+
+import com.google.common.collect.ImmutableList;
+
+public final class CheckList {
+ public static final String REPOSITORY_KEY = "customjavascript";
+ public static final String REPOSITORY_NAME = "Custom JavaScript";
+
+ private CheckList() {
+ }
+
+ public static List<Class> getChecks() {
+ return ImmutableList.<Class>builder()
+ .addAll(getJavaScriptChecks())
+ .build();
+ }
+
+ public static List<Class<? extends JavaScriptCheck>> getJavaScriptChecks() {
+ return ImmutableList.<Class<? extends JavaScriptCheck>>builder()
+ .add(AngularJSRootOnEventSubscriptionCheck.class)
+ .build();
+ }
+}
--- /dev/null
+package de.example.plugins.custom.javascript;
+
+import org.sonar.api.Plugin;
+
+import com.google.common.collect.ImmutableList;
+
+public class CustomPlugin implements Plugin {
+
+ @Override
+ public void define(Context context) {
+ ImmutableList.Builder<Object> builder = ImmutableList.builder();
+ builder.add(
+ );
+
+ context.addExtensions(builder.build());
+ }
+
+}
--- /dev/null
+package de.example.plugins.custom.javascript;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.server.rule.RulesDefinitionAnnotationLoader;
+import org.sonar.api.utils.AnnotationUtils;
+import org.sonar.check.Cardinality;
+import org.sonar.plugins.javascript.JavaScriptLanguage;
+import org.sonar.squidbridge.annotations.RuleTemplate;
+import org.sonar.squidbridge.rules.ExternalDescriptionLoader;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Iterables;
+import com.google.common.io.Resources;
+import com.google.gson.Gson;
+
+import de.example.custom.javascript.checks.CheckList;
+
+public class CustomRulesDefinition implements RulesDefinition {
+ private static final String RESOURCE_BASE_PATH = "/de/example/l10n/javascript/rules/custom";
+
+ private final Gson gson = new Gson();
+
+ @Override
+ public void define(Context context) {
+ NewRepository repository = context
+ .createRepository(CheckList.REPOSITORY_KEY, JavaScriptLanguage.KEY)
+ .setName(CheckList.REPOSITORY_NAME);
+ List<Class> checks = CheckList.getChecks();
+ new RulesDefinitionAnnotationLoader().load(repository, Iterables.toArray(checks, Class.class));
+ for (Class ruleClass : checks) {
+ newRule(ruleClass, repository);
+ }
+ repository.done();
+ }
+
+ @VisibleForTesting
+ protected void newRule(Class<?> ruleClass, NewRepository repository) {
+
+ org.sonar.check.Rule ruleAnnotation = AnnotationUtils.getAnnotation(ruleClass, org.sonar.check.Rule.class);
+ if (ruleAnnotation == null) {
+ throw new IllegalArgumentException("No Rule annotation was found on " + ruleClass);
+ }
+ String ruleKey = ruleAnnotation.key();
+ if (StringUtils.isEmpty(ruleKey)) {
+ throw new IllegalArgumentException("No key is defined in Rule annotation of " + ruleClass);
+ }
+ NewRule rule = repository.rule(ruleKey);
+ if (rule == null) {
+ throw new IllegalStateException("No rule was created for " + ruleClass + " in " + repository.key());
+ }
+ rule.setTemplate(AnnotationUtils.getAnnotation(ruleClass, RuleTemplate.class) != null);
+ if (ruleAnnotation.cardinality() == Cardinality.MULTIPLE) {
+ throw new IllegalArgumentException("Cardinality is not supported, use the RuleTemplate annotation instead for " + ruleClass);
+ }
+ ruleMetadata(ruleClass, rule);
+ }
+
+ private void ruleMetadata(Class<?> ruleClass, NewRule rule) {
+ String metadataKey = rule.key();
+ addHtmlDescription(rule, metadataKey);
+ addMetadata(rule, metadataKey);
+
+ }
+
+ private void addMetadata(NewRule rule, String metadataKey) {
+ URL resource = ExternalDescriptionLoader.class.getResource(RESOURCE_BASE_PATH + "/" + metadataKey + "_java.json");
+ if (resource != null) {
+ RuleMetatada metatada = gson.fromJson(readResource(resource), RuleMetatada.class);
+ rule.setSeverity(metatada.defaultSeverity.toUpperCase());
+ rule.setName(metatada.title);
+ rule.addTags(metatada.tags);
+ rule.setStatus(RuleStatus.valueOf(metatada.status.toUpperCase()));
+ if(metatada.remediation != null) {
+ rule.setDebtRemediationFunction(metatada.remediation.remediationFunction(rule.debtRemediationFunctions()));
+ rule.setGapDescription(metatada.remediation.linearDesc);
+ }
+ }
+ }
+
+ private static void addHtmlDescription(NewRule rule, String metadataKey) {
+ URL resource = CustomRulesDefinition.class.getResource(RESOURCE_BASE_PATH + "/" + metadataKey + "_java.html");
+ if (resource != null) {
+ rule.setHtmlDescription(readResource(resource));
+ }
+ }
+
+ private static String readResource(URL resource) {
+ try {
+ return Resources.toString(resource, Charsets.UTF_8);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read: " + resource, e);
+ }
+ }
+
+ private static class RuleMetatada {
+ String title;
+ String status;
+ @Nullable
+ Remediation remediation;
+
+ String[] tags;
+ String defaultSeverity;
+ }
+
+ private static class Remediation {
+ String func;
+ String constantCost;
+ String linearDesc;
+ String linearOffset;
+ String linearFactor;
+
+ public DebtRemediationFunction remediationFunction(DebtRemediationFunctions drf) {
+ if(func.startsWith("Constant")) {
+ return drf.constantPerIssue(constantCost.replace("mn", "min"));
+ }
+ if("Linear".equals(func)) {
+ return drf.linear(linearFactor.replace("mn", "min"));
+ }
+ return drf.linearWithOffset(linearFactor.replace("mn", "min"), linearOffset.replace("mn", "min"));
+ }
+ }
+}
--- /dev/null
+package de.example.plugins.custom.javascript;
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import java.io.File;
+import java.io.InterruptedIOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.rule.CheckFactory;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
+import org.sonar.api.config.Settings;
+import org.sonar.api.issue.NoSonarFilter;
+import org.sonar.api.measures.FileLinesContextFactory;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.javascript.checks.ParsingErrorCheck;
+import org.sonar.javascript.highlighter.HighlightSymbolTableBuilder;
+import org.sonar.javascript.parser.JavaScriptParserBuilder;
+import org.sonar.javascript.tree.visitors.CharsetAwareVisitor;
+import org.sonar.javascript.visitors.JavaScriptVisitorContext;
+import org.sonar.plugins.javascript.JavaScriptLanguage;
+import org.sonar.plugins.javascript.api.CustomJavaScriptRulesDefinition;
+import org.sonar.plugins.javascript.api.JavaScriptCheck;
+import org.sonar.plugins.javascript.api.tree.ScriptTree;
+import org.sonar.plugins.javascript.api.tree.Tree;
+import org.sonar.plugins.javascript.api.visitors.FileIssue;
+import org.sonar.plugins.javascript.api.visitors.Issue;
+import org.sonar.plugins.javascript.api.visitors.IssueLocation;
+import org.sonar.plugins.javascript.api.visitors.LineIssue;
+import org.sonar.plugins.javascript.api.visitors.PreciseIssue;
+import org.sonar.plugins.javascript.api.visitors.TreeVisitor;
+import org.sonar.plugins.javascript.api.visitors.TreeVisitorContext;
+import org.sonar.squidbridge.api.AnalysisException;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Lists;
+import com.sonar.sslr.api.RecognitionException;
+import com.sonar.sslr.api.typed.ActionParser;
+
+import de.example.custom.javascript.checks.CheckList;
+import de.example.plugins.custom.javascript.minify.MinificationAssessor;
+
+public class CustomSensor implements Sensor {
+
+ private static final Logger LOG = Loggers.get(CustomSensor.class);
+
+ private final JavaScriptChecks checks;
+ private final FileSystem fileSystem;
+ private final FilePredicate mainFilePredicate;
+ private final Settings settings;
+ private final ActionParser<Tree> parser;
+ // parsingErrorRuleKey equals null if ParsingErrorCheck is not activated
+ private RuleKey parsingErrorRuleKey = null;
+
+ public CustomSensor(
+ CheckFactory checkFactory, FileLinesContextFactory fileLinesContextFactory, FileSystem fileSystem, NoSonarFilter noSonarFilter, Settings settings) {
+ this(checkFactory, fileSystem, settings, null);
+ }
+
+ public CustomSensor(
+ CheckFactory checkFactory, FileSystem fileSystem,
+ Settings settings, @Nullable CustomJavaScriptRulesDefinition[] customRulesDefinition
+ ) {
+
+ this.checks = JavaScriptChecks.createJavaScriptCheck(checkFactory)
+ .addChecks(CheckList.REPOSITORY_KEY, CheckList.getChecks())
+ .addCustomChecks(customRulesDefinition);
+ this.fileSystem = fileSystem;
+ this.mainFilePredicate = fileSystem.predicates().and(
+ fileSystem.predicates().hasType(InputFile.Type.MAIN),
+ fileSystem.predicates().hasLanguage(JavaScriptLanguage.KEY));
+ this.settings = settings;
+ this.parser = JavaScriptParserBuilder.createParser(getEncoding());
+ }
+
+ @VisibleForTesting
+ protected void analyseFiles(SensorContext context, List<TreeVisitor> treeVisitors, Iterable<InputFile> inputFiles) {
+ for (InputFile inputFile : inputFiles) {
+ if (!isExcluded(inputFile.file())) {
+ analyse(context, inputFile, treeVisitors);
+ }
+ }
+ }
+
+ private Charset getEncoding() {
+ return fileSystem.encoding();
+ }
+
+
+ private void analyse(SensorContext sensorContext, InputFile inputFile, List<TreeVisitor> visitors) {
+ ScriptTree scriptTree;
+
+ try {
+ scriptTree = (ScriptTree) parser.parse(new java.io.File(inputFile.absolutePath()));
+ scanFile(sensorContext, inputFile, visitors, scriptTree);
+
+ } catch (RecognitionException e) {
+ checkInterrupted(e);
+ LOG.error("Unable to parse file: " + inputFile.absolutePath());
+ LOG.error(e.getMessage());
+ processRecognitionException(e, sensorContext, inputFile);
+
+ } catch (Exception e) {
+ checkInterrupted(e);
+ throw new AnalysisException("Unable to analyse file: " + inputFile.absolutePath(), e);
+ }
+
+ }
+
+ private static void checkInterrupted(Exception e) {
+ Throwable cause = Throwables.getRootCause(e);
+ if (cause instanceof InterruptedException || cause instanceof InterruptedIOException) {
+ throw new AnalysisException("Analysis cancelled", e);
+ }
+ }
+
+ private void processRecognitionException(RecognitionException e, SensorContext sensorContext, InputFile inputFile) {
+ if (parsingErrorRuleKey != null) {
+ NewIssue newIssue = sensorContext.newIssue();
+
+ NewIssueLocation primaryLocation = newIssue.newLocation()
+ .message(e.getMessage())
+ .on(inputFile)
+ .at(inputFile.selectLine(e.getLine()));
+
+ newIssue
+ .forRule(parsingErrorRuleKey)
+ .at(primaryLocation)
+ .save();
+ }
+ }
+
+ private void scanFile(SensorContext sensorContext, InputFile inputFile, List<TreeVisitor> visitors, ScriptTree scriptTree) {
+ JavaScriptVisitorContext context = new JavaScriptVisitorContext(scriptTree, inputFile.file(), settings);
+
+ highlightSymbols(sensorContext.newSymbolTable().onFile(inputFile), context);
+
+ List<Issue> fileIssues = new ArrayList<>();
+
+ for (TreeVisitor visitor : visitors) {
+ if (visitor instanceof CharsetAwareVisitor) {
+ ((CharsetAwareVisitor) visitor).setCharset(fileSystem.encoding());
+ }
+
+ if (visitor instanceof JavaScriptCheck) {
+ fileIssues.addAll(((JavaScriptCheck) visitor).scanFile(context));
+
+ } else {
+ visitor.scanTree(context);
+ }
+
+ }
+
+ saveFileIssues(sensorContext, fileIssues, inputFile);
+ }
+
+
+ private static void highlightSymbols(NewSymbolTable newSymbolTable, TreeVisitorContext context) {
+ HighlightSymbolTableBuilder.build(newSymbolTable, context);
+ }
+
+ private void saveFileIssues(SensorContext sensorContext, List<Issue> fileIssues, InputFile inputFile) {
+ for (Issue issue : fileIssues) {
+ RuleKey ruleKey = ruleKey(issue.check());
+ if (issue instanceof FileIssue) {
+ saveFileIssue(sensorContext, inputFile, ruleKey, (FileIssue) issue);
+
+ } else if (issue instanceof LineIssue) {
+ saveLineIssue(sensorContext, inputFile, ruleKey, (LineIssue) issue);
+
+ } else {
+ savePreciseIssue(sensorContext, inputFile, ruleKey, (PreciseIssue)issue);
+ }
+ }
+ }
+
+ private static void savePreciseIssue(SensorContext sensorContext, InputFile inputFile, RuleKey ruleKey, PreciseIssue issue) {
+ NewIssue newIssue = sensorContext.newIssue();
+
+ newIssue
+ .forRule(ruleKey)
+ .at(newLocation(inputFile, newIssue, issue.primaryLocation()));
+
+ if (issue.cost() != null) {
+ newIssue.gap(issue.cost());
+ }
+
+ for (IssueLocation secondary : issue.secondaryLocations()) {
+ newIssue.addLocation(newLocation(inputFile, newIssue, secondary));
+ }
+ newIssue.save();
+ }
+
+
+ private static NewIssueLocation newLocation(InputFile inputFile, NewIssue issue, IssueLocation location) {
+ TextRange range = inputFile.newRange(
+ location.startLine(), location.startLineOffset(), location.endLine(), location.endLineOffset());
+
+ NewIssueLocation newLocation = issue.newLocation()
+ .on(inputFile)
+ .at(range);
+
+ if (location.message() != null) {
+ newLocation.message(location.message());
+ }
+ return newLocation;
+ }
+
+
+ private RuleKey ruleKey(JavaScriptCheck check) {
+ Preconditions.checkNotNull(check);
+ RuleKey ruleKey = checks.ruleKeyFor(check);
+ if (ruleKey == null) {
+ throw new IllegalStateException("No rule key found for a rule");
+ }
+ return ruleKey;
+ }
+
+ public boolean isExcluded(File file) {
+ boolean isMinified = new MinificationAssessor(getEncoding()).isMinified(file);
+ if (isMinified) {
+ LOG.debug("File [" + file.getAbsolutePath() + "] looks like a minified file and will not be analyzed");
+ }
+ return isMinified;
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor
+ .onlyOnLanguage(JavaScriptLanguage.KEY)
+ .name("Custom JavaScript Sensor")
+ .onlyOnFileType(Type.MAIN);
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ List<TreeVisitor> treeVisitors = Lists.newArrayList();
+ treeVisitors.addAll(checks.visitorChecks());
+
+ for (TreeVisitor check : treeVisitors) {
+ if (check instanceof ParsingErrorCheck) {
+ parsingErrorRuleKey = checks.ruleKeyFor((JavaScriptCheck) check);
+ break;
+ }
+ }
+
+ analyseFiles(context, treeVisitors, fileSystem.inputFiles(mainFilePredicate));
+
+ }
+
+
+ private static void saveLineIssue(SensorContext sensorContext, InputFile inputFile, RuleKey ruleKey, LineIssue issue) {
+ NewIssue newIssue = sensorContext.newIssue();
+
+ NewIssueLocation primaryLocation = newIssue.newLocation()
+ .message(issue.message())
+ .on(inputFile)
+ .at(inputFile.selectLine(issue.line()));
+
+ saveIssue(newIssue, primaryLocation, ruleKey, issue);
+ }
+
+ private static void saveFileIssue(SensorContext sensorContext, InputFile inputFile, RuleKey ruleKey, FileIssue issue) {
+ NewIssue newIssue = sensorContext.newIssue();
+
+ NewIssueLocation primaryLocation = newIssue.newLocation()
+ .message(issue.message())
+ .on(inputFile);
+
+ saveIssue(newIssue, primaryLocation, ruleKey, issue);
+ }
+
+ private static void saveIssue(NewIssue newIssue, NewIssueLocation primaryLocation, RuleKey ruleKey, Issue issue) {
+ newIssue
+ .forRule(ruleKey)
+ .at(primaryLocation);
+
+ if (issue.cost() != null) {
+ newIssue.gap(issue.cost());
+ }
+
+ newIssue.save();
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package de.example.plugins.custom.javascript;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.rule.CheckFactory;
+import org.sonar.api.batch.rule.Checks;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.javascript.se.SeCheck;
+import org.sonar.plugins.javascript.api.CustomJavaScriptRulesDefinition;
+import org.sonar.plugins.javascript.api.JavaScriptCheck;
+import org.sonar.plugins.javascript.api.visitors.TreeVisitor;
+
+/**
+ * Wrapper around Checks Object to ease the manipulation of the different JavaScript rule repositories.
+ */
+public class JavaScriptChecks {
+
+ private final CheckFactory checkFactory;
+ private Set<Checks<JavaScriptCheck>> checksByRepository = Sets.newHashSet();
+
+ private JavaScriptChecks(CheckFactory checkFactory) {
+ this.checkFactory = checkFactory;
+ }
+
+ public static JavaScriptChecks createJavaScriptCheck(CheckFactory checkFactory) {
+ return new JavaScriptChecks(checkFactory);
+ }
+
+ public JavaScriptChecks addChecks(String repositoryKey, Iterable<Class> checkClass) {
+ checksByRepository.add(checkFactory
+ .<JavaScriptCheck>create(repositoryKey)
+ .addAnnotatedChecks(checkClass));
+
+ return this;
+ }
+
+ public JavaScriptChecks addCustomChecks(@Nullable CustomJavaScriptRulesDefinition[] customRulesDefinitions) {
+ if (customRulesDefinitions != null) {
+
+ for (CustomJavaScriptRulesDefinition rulesDefinition : customRulesDefinitions) {
+ addChecks(rulesDefinition.repositoryKey(), Lists.newArrayList(rulesDefinition.checkClasses()));
+ }
+ }
+
+ return this;
+ }
+
+ private List<JavaScriptCheck> all() {
+ List<JavaScriptCheck> allVisitors = Lists.newArrayList();
+
+ for (Checks<JavaScriptCheck> checks : checksByRepository) {
+ allVisitors.addAll(checks.all());
+ }
+
+ return allVisitors;
+ }
+
+ public List<SeCheck> seChecks() {
+ List<SeCheck> checks = new ArrayList<>();
+ for (JavaScriptCheck check : all()) {
+ if (check instanceof SeCheck) {
+ checks.add((SeCheck) check);
+ }
+ }
+
+ return checks;
+ }
+
+ public List<TreeVisitor> visitorChecks() {
+ List<TreeVisitor> checks = new ArrayList<>();
+ for (JavaScriptCheck check : all()) {
+ if (check instanceof TreeVisitor) {
+ checks.add((TreeVisitor) check);
+ }
+ }
+
+ return checks;
+ }
+
+ @Nullable
+ public RuleKey ruleKeyFor(JavaScriptCheck check) {
+ RuleKey ruleKey;
+
+ for (Checks<JavaScriptCheck> checks : checksByRepository) {
+ ruleKey = checks.ruleKey(check);
+
+ if (ruleKey != null) {
+ return ruleKey;
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package de.example.plugins.custom.javascript.minify;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import org.sonar.squidbridge.api.AnalysisException;
+
+/**
+ * An instance of this class computes the average line length of file.
+ * Before making the computation, it discards all lines which are part
+ * of the header comment.
+ * The header comment is a comment which starts on the first line of the file.
+ * It may be either a C-like comment (i.e., it starts with <code>"/*"</code>) or a C++-like comment
+ * (i.e., it starts with <code>"//"</code>).
+ */
+class AverageLineLengthCalculator {
+
+ private File file;
+
+ private boolean isAtFirstLine = true;
+
+ private boolean isInHeaderComment = false;
+
+ private boolean isClike = false;
+
+ private Charset encoding;
+
+ public AverageLineLengthCalculator(File file, Charset encoding) {
+ this.file = file;
+ this.encoding = encoding;
+ }
+
+ public int getAverageLineLength() {
+ long nbLines = 0;
+ long nbCharacters = 0;
+
+ try (BufferedReader reader = getReader(file)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (!isLineInHeaderComment(line)) {
+ nbLines++;
+ nbCharacters += line.length();
+ }
+ }
+ } catch (IOException e) {
+ handleException(e, file);
+ }
+
+ return nbLines > 0 ? (int) (nbCharacters / nbLines) : 0;
+ }
+
+ public boolean isLineInHeaderComment(String line) {
+ String trimmedLine = line.trim();
+ if (isAtFirstLine) {
+ isAtFirstLine = false;
+ return isFirstLineInHeaderComment(trimmedLine);
+ } else if (isInHeaderComment) {
+ return isSubsequentLineInHeaderComment(trimmedLine);
+ }
+ return false;
+ }
+
+ private boolean isFirstLineInHeaderComment(String line) {
+ if (line.startsWith("/*")) {
+ isClike = true;
+ isInHeaderComment = !line.endsWith("*/");
+ return true;
+ } else if (line.startsWith("//")) {
+ isClike = false;
+ isInHeaderComment = true;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isSubsequentLineInHeaderComment(String line) {
+ if (isClike) {
+ if (line.endsWith("*/")) {
+ isInHeaderComment = false;
+ } else if (line.contains("*/")) {
+ // case of */ followed with something, possibly a long minified line
+ isInHeaderComment = false;
+ return false;
+ }
+ return true;
+ } else {
+ if (line.startsWith("//")) {
+ return true;
+ } else {
+ isInHeaderComment = false;
+ return false;
+ }
+ }
+ }
+
+ private BufferedReader getReader(File file) throws IOException {
+ return new BufferedReader(new InputStreamReader(new FileInputStream(file), encoding));
+ }
+
+ private static void handleException(IOException e, File file) {
+ throw new AnalysisException("Unable to analyse file: " + file.getAbsolutePath(), e);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package de.example.plugins.custom.javascript.minify;
+
+import java.io.File;
+import java.nio.charset.Charset;
+
+/**
+ * An object to assess if a .js file is a minified file or not.
+ * <p>
+ * An instance of this class is likely to consider as minified a .js file that,
+ * although formally not minified, has an unusually high average line length.
+ * This situation is typical of files that have been generated by some tool.
+ * Such files are of poor interest as regards a SonarQube analysis.
+ */
+public class MinificationAssessor {
+
+ private static final int DEFAULT_AVERAGE_LINE_LENGTH_THRESHOLD = 200;
+
+ private Charset encoding;
+
+ /**
+ * Value of the average line length
+ * (= number of chars in the file / number of lines in the file)
+ * below which a file is not assessed as being a minified file.
+ */
+ private int averageLineLengthThreshold;
+
+ public MinificationAssessor(Charset encoding) {
+ this(encoding, DEFAULT_AVERAGE_LINE_LENGTH_THRESHOLD);
+ }
+
+ public MinificationAssessor(Charset encoding, int averageLineLengthThreshold) {
+ this.encoding = encoding;
+ this.averageLineLengthThreshold = averageLineLengthThreshold;
+ }
+
+ public boolean isMinified(File file) {
+ return isJavaScriptFile(file) &&
+ (hasMinifiedFileName(file) || hasExcessiveAverageLineLength(file));
+ }
+
+ private static boolean hasMinifiedFileName(File file) {
+ String fileName = file.getName();
+ return fileName.endsWith("-min.js") || fileName.endsWith(".min.js");
+ }
+
+ private static boolean isJavaScriptFile(File file) {
+ return file.getName().endsWith(".js");
+ }
+
+ private boolean hasExcessiveAverageLineLength(File file) {
+ int averageLineLength = new AverageLineLengthCalculator(file, encoding).getAverageLineLength();
+ return averageLineLength > averageLineLengthThreshold;
+ }
+
+}
\ No newline at end of file