package com.pablissimo.sonar; import com.pablissimo.sonar.model.TsLintIssue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.rule.ActiveRule; 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.fs.InputFile; import org.sonar.api.config.Settings; import org.sonar.api.rule.RuleKey; import java.io.File; import java.io.IOException; import java.util.*; public class TsLintSensor implements Sensor { private static final Logger LOG = LoggerFactory.getLogger(TsLintExecutorImpl.class); private Settings settings; private PathResolver resolver; private TsLintExecutor executor; private TsLintParser parser; public TsLintSensor(Settings settings, PathResolver resolver, TsLintExecutor executor, TsLintParser parser) { this.settings = settings; this.resolver = resolver; this.executor = executor; this.parser = parser; } @Override public void describe(SensorDescriptor desc) { desc .name("Linting sensor for TypeScript files") .onlyOnLanguage(TypeScriptLanguage.LANGUAGE_KEY); } @Override public void execute(SensorContext ctx) { if (!this.settings.getBoolean(TypeScriptPlugin.SETTING_TS_LINT_ENABLED)) { LOG.debug("Skipping tslint execution - {} - set to false", TypeScriptPlugin.SETTING_TS_LINT_ENABLED); return; } TsLintExecutorConfig config = TsLintExecutorConfig.fromSettings(this.settings, ctx, this.resolver); if (!config.useExistingTsLintOutput()) { if (config.getPathToTsLint() == null) { LOG.warn("Path to tslint not defined or not found. Skipping tslint analysis."); return; } else { if (config.getConfigFile() == null && config.getPathToTsConfig() == null) { LOG.warn("Path to tslint.json and tsconfig.json configuration files either not defined or not found - at least one is required. Skipping tslint analysis."); return; } } } boolean skipTypeDefFiles = settings.getBoolean(TypeScriptPlugin.SETTING_EXCLUDE_TYPE_DEFINITION_FILES); Collection<ActiveRule> allRules = ctx.activeRules().findByRepository(TsRulesDefinition.REPOSITORY_NAME); HashSet<String> ruleNames = new HashSet<>(); for (ActiveRule rule : allRules) { ruleNames.add(rule.ruleKey().rule()); } FileSystem fs = ctx.fileSystem(); List<String> paths = new ArrayList<>(); for (InputFile file : fs.inputFiles(fs.predicates().hasLanguage(TypeScriptLanguage.LANGUAGE_KEY))) { if (shouldSkipFile(file.file(), skipTypeDefFiles)) { continue; } String pathAdjusted = file.absolutePath(); paths.add(pathAdjusted); } List<String> jsonResults = this.executor.execute(config, paths); Map<String, List<TsLintIssue>> issues = this.parser.parse(jsonResults); if (issues == null) { LOG.warn("TsLint returned no result at all"); return; } File baseDir = fs.baseDir(); String baseDirPath = baseDir.getPath(); String baseDirCanonicalPath = null; try { baseDirCanonicalPath = baseDir.getCanonicalPath(); } catch (IOException e) { LOG.error("Failed to canonicalize " + baseDirPath, e); } // Each issue bucket will contain info about a single file for (Map.Entry<String, List<TsLintIssue>> kvp : issues.entrySet()) { String filePath = kvp.getKey(); List<TsLintIssue> batchIssues = kvp.getValue(); if (batchIssues == null || batchIssues.isEmpty()) { continue; } if (baseDirCanonicalPath != null) { filePath = filePath.replace(baseDirCanonicalPath, baseDirPath); } File matchingFile = fs.resolvePath(filePath); InputFile inputFile = null; if (shouldSkipFile(matchingFile, skipTypeDefFiles)) { continue; } if (matchingFile != null) { try { inputFile = fs.inputFile(fs.predicates().is(matchingFile)); } catch (IllegalArgumentException e) { LOG.error("Failed to resolve " + filePath + " to a single path", e); continue; } } if (inputFile == null) { LOG.warn("TsLint reported issues against a file that isn't in the analysis set - will be ignored: {}", filePath); continue; } else { LOG.debug("Handling TsLint output for '{}' reporting against '{}'", filePath, inputFile.absolutePath()); } for (TsLintIssue issue : batchIssues) { // Make sure the rule we're violating is one we recognise - if not, we'll // fall back to the generic 'tslint-issue' rule String ruleName = issue.getRuleName(); if (!ruleNames.contains(ruleName)) { ruleName = TsRulesDefinition.TSLINT_UNKNOWN_RULE.key; } NewIssue newIssue = ctx .newIssue() .forRule(RuleKey.of(TsRulesDefinition.REPOSITORY_NAME, ruleName)); NewIssueLocation newIssueLocation = newIssue .newLocation() .on(inputFile) .message(issue.getFailure()) .at(inputFile.selectLine(issue.getStartPosition().getLine() + 1)); newIssue.at(newIssueLocation); newIssue.save(); } } } private boolean shouldSkipFile(File f, boolean skipTypeDefFiles) { return skipTypeDefFiles && f.getName().toLowerCase().endsWith("." + TypeScriptLanguage.LANGUAGE_DEFINITION_EXTENSION); } }