use crate::checkers::Checker;
use crate::utils::types::{LintIssue, Severity};
use crate::{Language, Result};
use std::path::Path;
use std::process::Command;
pub struct TypeScriptChecker;
impl TypeScriptChecker {
pub fn new() -> Self {
Self
}
fn find_eslint_config(path: &Path) -> Option<std::path::PathBuf> {
let mut current = if path.is_file() {
path.parent()?.to_path_buf()
} else {
path.to_path_buf()
};
let config_names = [
".linthis/configs/javascript/.eslintrc.js", ".linthis/configs/javascript/.eslintrc.json",
".linthis/configs/typescript/.eslintrc.js",
".linthis/configs/typescript/.eslintrc.json",
".eslintrc.js",
".eslintrc.json",
".eslintrc.yml",
".eslintrc.yaml",
".eslintrc",
];
loop {
for config_name in &config_names {
let config_path = current.join(config_name);
if config_path.exists() {
return Some(config_path);
}
}
if !current.pop() {
break;
}
}
None
}
fn parse_eslint_output(&self, output: &str, file_path: &Path) -> Vec<LintIssue> {
let mut issues = Vec::new();
if let Ok(json) = serde_json::from_str::<serde_json::Value>(output) {
if let Some(files) = json.as_array() {
for file_result in files {
let messages = file_result.get("messages").and_then(|m| m.as_array());
let file = file_result
.get("filePath")
.and_then(|f| f.as_str())
.map(std::path::PathBuf::from)
.unwrap_or_else(|| file_path.to_path_buf());
if let Some(msgs) = messages {
for msg in msgs {
if let Some(issue) = self.parse_eslint_message(msg, &file) {
issues.push(issue);
}
}
}
}
}
}
issues
}
fn parse_eslint_message(&self, msg: &serde_json::Value, file_path: &Path) -> Option<LintIssue> {
let line = msg.get("line").and_then(|l| l.as_u64()).unwrap_or(1) as usize;
let column = msg
.get("column")
.and_then(|c| c.as_u64())
.map(|c| c as usize);
let message = msg.get("message").and_then(|m| m.as_str()).unwrap_or("");
let rule_id = msg.get("ruleId").and_then(|r| r.as_str()).unwrap_or("");
let severity_num = msg.get("severity").and_then(|s| s.as_u64()).unwrap_or(1);
if message.contains("File ignored because no matching configuration was supplied")
|| message.contains("File ignored because of a matching ignore pattern")
{
return None;
}
let severity = match severity_num {
2 => Severity::Error,
1 => Severity::Warning,
_ => Severity::Info,
};
let mut issue =
LintIssue::new(file_path.to_path_buf(), line, message.to_string(), severity)
.with_source("eslint".to_string());
if !rule_id.is_empty() {
issue = issue.with_code(rule_id.to_string());
}
if let Some(c) = column {
issue = issue.with_column(c);
}
Some(issue)
}
}
impl Default for TypeScriptChecker {
fn default() -> Self {
Self::new()
}
}
impl Checker for TypeScriptChecker {
fn name(&self) -> &str {
"eslint"
}
fn supported_languages(&self) -> &[Language] {
&[Language::TypeScript, Language::JavaScript]
}
fn check(&self, path: &Path) -> Result<Vec<LintIssue>> {
let mut cmd = Command::new("eslint");
cmd.args(["--format", "json", "--no-error-on-unmatched-pattern"]);
if let Some(config_path) = Self::find_eslint_config(path) {
cmd.arg("-c").arg(config_path);
}
let output = cmd.arg(path).output().map_err(|e| {
crate::LintisError::checker("eslint", path, format!("Failed to run: {}", e))
})?;
let stdout = String::from_utf8_lossy(&output.stdout);
let issues = self.parse_eslint_output(&stdout, path);
Ok(issues)
}
fn is_available(&self) -> bool {
Command::new("eslint")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eslint_file_ignored_filtered() {
let checker = TypeScriptChecker::new();
let msg = serde_json::json!({
"line": 0,
"column": 0,
"message": "File ignored because no matching configuration was supplied.",
"severity": 1,
"ruleId": null
});
let result = checker.parse_eslint_message(&msg, Path::new("test.js"));
assert!(result.is_none());
}
#[test]
fn test_eslint_file_ignored_pattern_filtered() {
let checker = TypeScriptChecker::new();
let msg = serde_json::json!({
"line": 0,
"column": 0,
"message": "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.",
"severity": 1,
"ruleId": null
});
let result = checker.parse_eslint_message(&msg, Path::new("test.js"));
assert!(result.is_none());
}
#[test]
fn test_eslint_real_issue_not_filtered() {
let checker = TypeScriptChecker::new();
let msg = serde_json::json!({
"line": 10,
"column": 5,
"message": "Unexpected console statement.",
"severity": 1,
"ruleId": "no-console"
});
let result = checker.parse_eslint_message(&msg, Path::new("test.js"));
assert!(result.is_some());
let issue = result.unwrap();
assert_eq!(issue.line, 10);
assert_eq!(issue.code, Some("no-console".to_string()));
}
}