SonarQube tryint to create a custom JavaScript plugin
authorGustavo Martin Morcuende <gu.martinm@gmail.com>
Sun, 7 Aug 2016 22:51:40 +0000 (00:51 +0200)
committerGustavo Martin Morcuende <gu.martinm@gmail.com>
Sun, 7 Aug 2016 22:51:40 +0000 (00:51 +0200)
(no success)

Sonar/Plugins/pom.xml
Sonar/Plugins/sonar-custom-javascript-plugin/pom.xml
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/AngularJSRootOnEventSubscriptionCheck.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/CheckList.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomPlugin.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomRulesDefinition.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomSensor.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/JavaScriptChecks.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/AverageLineLengthCalculator.java [new file with mode: 0644]
Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/MinificationAssessor.java [new file with mode: 0644]

index dd1430d..85b0ea5 100644 (file)
@@ -21,6 +21,7 @@
 
   <modules>
     <module>sonar-custom-java-plugin</module>
+    <module>sonar-custom-javascript-plugin</module>
   </modules>
 
 
index 7979169..3995d92 100644 (file)
       <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>
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/AngularJSRootOnEventSubscriptionCheck.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/AngularJSRootOnEventSubscriptionCheck.java
new file mode 100644 (file)
index 0000000..2c8cc6c
--- /dev/null
@@ -0,0 +1,9 @@
+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 {
+
+}
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/CheckList.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/CheckList.java
new file mode 100644 (file)
index 0000000..df17483
--- /dev/null
@@ -0,0 +1,27 @@
+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();
+       }
+}
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomPlugin.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomPlugin.java
new file mode 100644 (file)
index 0000000..641afa8
--- /dev/null
@@ -0,0 +1,18 @@
+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());
+       }
+
+}
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomRulesDefinition.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomRulesDefinition.java
new file mode 100644 (file)
index 0000000..4180004
--- /dev/null
@@ -0,0 +1,132 @@
+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"));
+    }
+  }
+}
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomSensor.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomSensor.java
new file mode 100644 (file)
index 0000000..e5285a5
--- /dev/null
@@ -0,0 +1,319 @@
+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
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/JavaScriptChecks.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/JavaScriptChecks.java
new file mode 100644 (file)
index 0000000..c6af4e7
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/AverageLineLengthCalculator.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/AverageLineLengthCalculator.java
new file mode 100644 (file)
index 0000000..8cdc3b2
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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
diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/MinificationAssessor.java b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/MinificationAssessor.java
new file mode 100644 (file)
index 0000000..c0cbffa
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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