From: Gustavo Martin Morcuende Date: Sun, 7 Aug 2016 22:51:40 +0000 (+0200) Subject: SonarQube tryint to create a custom JavaScript plugin X-Git-Url: https://git.gumartinm.name/?a=commitdiff_plain;h=35db5d6ed49781de337e5eac887358ad4e40f388;p=JavaForFun SonarQube tryint to create a custom JavaScript plugin (no success) --- diff --git a/Sonar/Plugins/pom.xml b/Sonar/Plugins/pom.xml index dd1430d..85b0ea5 100644 --- a/Sonar/Plugins/pom.xml +++ b/Sonar/Plugins/pom.xml @@ -21,6 +21,7 @@ sonar-custom-java-plugin + sonar-custom-javascript-plugin diff --git a/Sonar/Plugins/sonar-custom-javascript-plugin/pom.xml b/Sonar/Plugins/sonar-custom-javascript-plugin/pom.xml index 7979169..3995d92 100644 --- a/Sonar/Plugins/sonar-custom-javascript-plugin/pom.xml +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/pom.xml @@ -48,40 +48,6 @@ guava 19.0 - - commons-lang - commons-lang - 2.6 - - - org.sonarsource.sslr-squid-bridge - sslr-squid-bridge - 2.6.1 - - - org.codehaus.sonar.sslr - sslr-core - - - org.codehaus.sonar - sonar-plugin-api - - - org.codehaus.sonar.sslr - sslr-xpath - - - org.slf4j - jcl-over-slf4j - - - - - org.sonarsource.java - javascript-frontend - javascript.plugin.version - - @@ -92,12 +58,6 @@ ${sonar.apiVersion} test - - org.sonarsource.java - java-checks-testkit - ${java.plugin.version} - test - org.easytesting fest-assert @@ -154,7 +114,9 @@ 1.17 true + 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 index 0000000..2c8cc6c --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/AngularJSRootOnEventSubscriptionCheck.java @@ -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 index 0000000..df17483 --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/custom/javascript/checks/CheckList.java @@ -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 getChecks() { + return ImmutableList.builder() + .addAll(getJavaScriptChecks()) + .build(); + } + + public static List> getJavaScriptChecks() { + return ImmutableList.>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 index 0000000..641afa8 --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomPlugin.java @@ -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 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 index 0000000..4180004 --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomRulesDefinition.java @@ -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 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 index 0000000..e5285a5 --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/CustomSensor.java @@ -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 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 treeVisitors, Iterable 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 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 visitors, ScriptTree scriptTree) { + JavaScriptVisitorContext context = new JavaScriptVisitorContext(scriptTree, inputFile.file(), settings); + + highlightSymbols(sensorContext.newSymbolTable().onFile(inputFile), context); + + List 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 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 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 index 0000000..c6af4e7 --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/JavaScriptChecks.java @@ -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> 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 checkClass) { + checksByRepository.add(checkFactory + .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 all() { + List allVisitors = Lists.newArrayList(); + + for (Checks checks : checksByRepository) { + allVisitors.addAll(checks.all()); + } + + return allVisitors; + } + + public List seChecks() { + List checks = new ArrayList<>(); + for (JavaScriptCheck check : all()) { + if (check instanceof SeCheck) { + checks.add((SeCheck) check); + } + } + + return checks; + } + + public List visitorChecks() { + List 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 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 index 0000000..8cdc3b2 --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/AverageLineLengthCalculator.java @@ -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 "/*") or a C++-like comment + * (i.e., it starts with "//"). + */ +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 index 0000000..c0cbffa --- /dev/null +++ b/Sonar/Plugins/sonar-custom-javascript-plugin/src/main/java/de/example/plugins/custom/javascript/minify/MinificationAssessor.java @@ -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. + *

+ * 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