/*
* Sonar JavaScript Plugin
* Copyright (C) 2011 Eriks Nukis
* dev@sonar.codehaus.org
*
* 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 02
*/
package org.sonar.plugins.javascript.jslint;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.List;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.Sensor;
import org.sonar.api.batch.SensorContext;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.ProjectFileSystem;
import org.sonar.api.resources.Resource;
import org.sonar.api.rules.ActiveRule;
import org.sonar.api.rules.ActiveRuleParam;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
import org.sonar.api.rules.RuleQuery;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.javascript.JavaScript;
import org.sonar.plugins.javascript.JavaScriptFile;
import org.sonar.plugins.javascript.JavaScriptPlugin;
import com.googlecode.jslint4java.Issue;
import com.googlecode.jslint4java.JSFunction;
import com.googlecode.jslint4java.JSIdentifier;
import com.googlecode.jslint4java.JSLint;
import com.googlecode.jslint4java.JSLintBuilder;
import com.googlecode.jslint4java.JSLintResult;
import com.googlecode.jslint4java.Option;
public class JavaScriptJSLintSensor implements Sensor {
private static final Logger LOG = LoggerFactory.getLogger(JavaScriptJSLintSensor.class);
private Configuration configuration;
private RulesProfile rulesProfile;
private RuleFinder ruleFinder;
private JavaScript javascript;
private JSLint jsLint;
private JsLintRuleManager jsLintRuleManager;
public JavaScriptJSLintSensor(RuleFinder ruleFinder, JavaScript javascript, RulesProfile rulesProfile,
JsLintRuleManager jsLintRuleManager, Configuration configuration) {
this.configuration = configuration;
this.ruleFinder = ruleFinder;
this.javascript = javascript;
this.rulesProfile = rulesProfile;
this.jsLintRuleManager = jsLintRuleManager;
this.jsLint = new JSLintBuilder().fromDefault();
LOG.debug("Using JSLint version: {}", this.jsLint.getEdition());
initializeJsLint();
}
private boolean isActivated(String ruleKey, List<ActiveRule> rules) {
for (ActiveRule rule : rules) {
if (ruleKey.equals(rule.getRuleKey())) {
return true;
}
}
return false;
}
public void analyse(Project project, SensorContext sensorContext) {
for (File javaScriptFile : project.getFileSystem().getSourceFiles(javascript)) {
try {
analyzeFile(javaScriptFile, project.getFileSystem(), sensorContext);
} catch (IOException e) {
LOG.error("Can not analyze the file {}", javaScriptFile.getAbsolutePath());
}
}
}
protected void analyzeFile(File file, ProjectFileSystem projectFileSystem, SensorContext sensorContext) throws IOException {
Resource resource = JavaScriptFile.fromIOFile(file, projectFileSystem.getSourceDirs());
Reader reader = null;
try {
reader = new StringReader(FileUtils.readFileToString(file, projectFileSystem.getSourceCharset().name()));
JSLintResult result = jsLint.lint(file.getPath(), reader);
// capture function count in file
List<JSFunction> functions = result.getFunctions();
sensorContext.saveMeasure(resource, CoreMetrics.FUNCTIONS, (double) functions.size());
// process issues found by JSLint
List<Issue> issues = result.getIssues();
for (Issue issue : issues) {
LOG.debug("JSLint warning message {}", issue.getRaw());
Rule rule = ruleFinder.findByKey(JavaScriptRuleRepository.REPOSITORY_KEY, jsLintRuleManager.getRuleIdByMessage(issue.getRaw()));
Violation violation = Violation.create(rule, resource);
violation.setLineId(issue.getLine());
violation.setMessage(issue.getReason());
sensorContext.saveViolation(violation);
}
// add special violation for unused names
List<JSIdentifier> unused = result.getUnused();
for (JSIdentifier unusedName : unused) {
Violation violation = Violation.create(
ruleFinder.findByKey(JavaScriptRuleRepository.REPOSITORY_KEY, JsLintRuleManager.UNUSED_NAMES_KEY), resource);
violation.setLineId(unusedName.getLine());
violation.setMessage("'" + unusedName.getName() + "' is unused");
sensorContext.saveViolation(violation);
}
} finally {
IOUtils.closeQuietly(reader);
}
}
public boolean shouldExecuteOnProject(Project project) {
return project.getLanguage().equals(javascript);
}
private void initializeJsLint() {
RuleQuery query = RuleQuery.create();
query.withRepositoryKey(JavaScriptRuleRepository.REPOSITORY_KEY);
List<ActiveRule> activeRules = this.rulesProfile.getActiveRules();
LOG.debug("Adding JSLint options. Activated rules: {}", activeRules.size());
// set JSLint options for activated rules
for (Option option : Option.values()) {
// not inverse rule and activated
if ( !jsLintRuleManager.isRuleInverse(option.name()) && isActivated(option.name(), activeRules)) {
LOG.debug("Adding JSLint option from rule: {}", option.name());
this.jsLint.addOption(option);
// inverse rule and not activated
} else if (jsLintRuleManager.isRuleInverse(option.name()) && !isActivated(option.name(), activeRules)) {
LOG.debug("Adding JSLint option from inverse rule: {}", option.name());
this.jsLint.addOption(option);
}
}
/*
* order of these two functions is important as values set from project/global settings can be overwritten by rule parameters
*/
setOptionsSpecifiedAsProjectSettings();
setOptionsSpecifiedAsRuleParameters(activeRules);
}
private void setOptionsSpecifiedAsRuleParameters(List<ActiveRule> activeRules) {
LOG.debug("Adding Options Specified As Rule Parameters");
for (ActiveRule activeRule : activeRules) {
for (ActiveRuleParam activeRuleParam : activeRule.getActiveRuleParams()) {
String value = activeRuleParam.getValue();
Option option = jsLintRuleManager.getOptionByName(activeRuleParam.getKey());
LOG.debug("Rule: " + activeRule.getRuleKey() + ", ruleParam: " + activeRuleParam.getKey() + ", ruleParamValue: " + value);
/*
* predefined variables are already set from project global settings, this will concatenate value with value set on rule be defined
* on rule level and
*/
if (Option.PREDEF.equals(option)) {
String predefinedVariablesOnProjectLevel = configuration.getString(JavaScriptPlugin.PREDEFINED_KEY, "");
if ( !"".equals(predefinedVariablesOnProjectLevel.trim())) {
value = value + "," + predefinedVariablesOnProjectLevel;
}
}
if (option != null && value != null) {
LOG.debug("Adding JSLint option from rule parameter: {} with value: {}", option.name(), value);
this.jsLint.addOption(option, value);
}
}
}
}
private void setOptionsSpecifiedAsProjectSettings() {
LOG.debug("Adding Options Specified As Project Settings");
for (String fullparameterName : JavaScriptPlugin.GLOBAL_PARAMETERS) {
String parameterName = fullparameterName.substring(fullparameterName.lastIndexOf(".") + 1);
String value = configuration.getString(fullparameterName);
LOG.debug("Project/global setting name retrieved from global parameter: {} with value {}", parameterName, value);
Option option = jsLintRuleManager.getOptionByName(parameterName);
if (option != null && value != null) {
LOG.debug("Adding JSLint option from project/global settings: {} with value: {}", option, value);
this.jsLint.addOption(option, value);
}
}
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}