use std::path::Path;
use ast_grep_core::{AstGrep, Doc};
use globset::GlobSet;
use serde::{Deserialize, Serialize};
use strum::Display;
use crate::finding::Finding;
use crate::rule::{Language, Rule};
mod manifest;
mod rust;
mod typescript;
pub use manifest::ManifestResolver;
use rust::Rust;
use typescript::TypeScript;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ResolutionKind {
UnresolvedImport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImportRef {
pub specifier: String,
pub line: usize,
pub column: usize,
pub end_line: usize,
pub end_column: usize,
pub matched_text: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileImports {
pub imports: Vec<ImportRef>,
pub local_names: Vec<String>,
}
impl FileImports {
pub fn is_empty(&self) -> bool {
self.imports.is_empty()
}
}
pub struct ResolutionFilter {
pub files: Option<GlobSet>,
pub ignores: Option<GlobSet>,
}
impl ResolutionFilter {
pub fn allows(&self, path: &Path) -> bool {
if !self.files.as_ref().is_none_or(|g| g.is_match(path)) {
return false;
}
if self.ignores.as_ref().is_some_and(|g| g.is_match(path)) {
return false;
}
true
}
}
trait LanguageSupport {
fn extract_imports<D: Doc>(&self, root: &AstGrep<D>) -> FileImports;
fn resolves(
&self,
import: &ImportRef,
local_names: &[String],
file_dir: &Path,
manifests: &mut ManifestResolver,
) -> bool;
}
fn first_line(text: &str) -> String {
text.lines().next().unwrap_or(text).trim().to_string()
}
pub fn extract_imports<D: Doc>(lang: &Language, root: &AstGrep<D>) -> FileImports {
match lang {
Language::Rust => Rust.extract_imports(root),
Language::TypeScript => TypeScript.extract_imports(root),
}
}
pub fn import_resolves(
lang: &Language,
import: &ImportRef,
local_names: &[String],
file_dir: &Path,
manifests: &mut ManifestResolver,
) -> bool {
match lang {
Language::Rust => Rust.resolves(import, local_names, file_dir, manifests),
Language::TypeScript => TypeScript.resolves(import, local_names, file_dir, manifests),
}
}
pub fn evaluate_file(
rule: &Rule,
kind: ResolutionKind,
lang: &Language,
path: &Path,
imports: &FileImports,
manifests: &mut ManifestResolver,
) -> Vec<Finding> {
let ResolutionKind::UnresolvedImport = kind;
let file_dir = path.parent().unwrap_or_else(|| Path::new("."));
imports
.imports
.iter()
.filter(|import| !import_resolves(lang, import, &imports.local_names, file_dir, manifests))
.map(|import| Finding {
rule_id: rule.id.clone(),
severity: rule.severity.clone(),
category: rule.category.clone().unwrap_or_default(),
message: rule.message.replace("$import", &import.specifier),
note: rule.note.clone(),
fix: rule.fix.clone(),
file: path.to_path_buf(),
line: import.line,
column: import.column,
end_line: import.end_line,
end_column: import.end_column,
matched_text: import.matched_text.clone(),
confidence: None,
escalated: false,
})
.collect()
}
#[cfg(test)]
mod tests;