mod ast;
pub mod check;
pub mod cli;
pub mod explain;
mod rules;
mod settings;
use annotate_snippets::{Level, Renderer, Snippet};
use ast::{parse, FortitudeNode};
use colored::{ColoredString, Colorize};
use settings::Settings;
use std::cmp::Ordering;
use std::fmt;
use std::path::{Path, PathBuf};
#[allow(dead_code)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Category {
Error,
Style,
Typing,
Modules,
Precision,
FileSystem,
}
#[allow(dead_code)]
impl Category {
fn from(s: &str) -> anyhow::Result<Self> {
match s {
"E" => Ok(Self::Error),
"S" => Ok(Self::Style),
"T" => Ok(Self::Typing),
"M" => Ok(Self::Modules),
"P" => Ok(Self::Precision),
"F" => Ok(Self::FileSystem),
_ => {
anyhow::bail!("{} is not a rule category.", s)
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ViolationPosition {
None,
LineCol((usize, usize)),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Violation {
message: String,
position: ViolationPosition,
}
impl Violation {
pub fn new<T: AsRef<str>>(message: T, position: ViolationPosition) -> Self {
Self {
message: String::from(message.as_ref()),
position,
}
}
pub fn from_node<T: AsRef<str>>(message: T, node: &tree_sitter::Node) -> Self {
let position = node.start_position();
Violation::new(
message,
ViolationPosition::LineCol((position.row + 1, position.column + 1)),
)
}
pub fn message(&self) -> &str {
self.message.as_str()
}
pub fn position(&self) -> ViolationPosition {
self.position
}
}
#[macro_export]
macro_rules! violation {
($msg:expr) => {
$crate::Violation::new($msg, $crate::ViolationPosition::None)
};
($msg:expr, $line:expr, $col:expr) => {
$crate::Violation::new($msg, $crate::ViolationPosition::LineCol(($line, $col)))
};
}
pub trait Rule {
fn new(settings: &Settings) -> Self
where
Self: Sized;
fn explain(&self) -> &'static str;
}
pub trait PathRule: Rule {
fn check(&self, path: &Path) -> Option<Violation>;
}
pub trait TextRule: Rule {
fn check(&self, source: &str) -> Vec<Violation>;
}
pub trait ASTRule: Rule {
fn check(&self, node: &tree_sitter::Node, source: &str) -> Option<Vec<Violation>>;
fn entrypoints(&self) -> Vec<&'static str>;
fn apply(&self, source: &str) -> anyhow::Result<Vec<Violation>> {
let entrypoints = self.entrypoints();
Ok(parse(source)?
.root_node()
.named_descendants()
.filter(|x| entrypoints.contains(&x.kind()))
.filter_map(|x| self.check(&x, source))
.flatten()
.collect())
}
}
#[derive(Eq)]
pub struct Diagnostic {
path: PathBuf,
code: String,
violation: Violation,
}
impl Diagnostic {
pub fn new<P, S>(path: P, code: S, violation: &Violation) -> Self
where
P: AsRef<Path>,
S: AsRef<str>,
{
Self {
path: path.as_ref().to_path_buf(),
code: code.as_ref().to_string(),
violation: violation.clone(),
}
}
fn orderable(&self) -> (&Path, usize, usize, &str) {
match self.violation.position() {
ViolationPosition::None => (self.path.as_path(), 0, 0, self.code.as_str()),
ViolationPosition::LineCol((line, col)) => {
(self.path.as_path(), line, col, self.code.as_str())
}
}
}
}
impl Ord for Diagnostic {
fn cmp(&self, other: &Self) -> Ordering {
self.orderable().cmp(&other.orderable())
}
}
impl PartialOrd for Diagnostic {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Diagnostic {
fn eq(&self, other: &Self) -> bool {
self.orderable() == other.orderable()
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let path = self.path.to_string_lossy().bold();
let code = self.code.bold().bright_red();
let message = self.violation.message();
match self.violation.position() {
ViolationPosition::None => {
write!(f, "{}: {} {}", path, code, message)
}
ViolationPosition::LineCol((line, col)) => {
format_violation_line_col(self, f, line, col, message, &path, &code)
}
}
}
}
fn read_lines(filename: &PathBuf) -> Vec<String> {
std::fs::read_to_string(filename)
.unwrap() .lines() .map(String::from) .collect() }
fn format_violation_line_col(
diagnostic: &Diagnostic,
f: &mut fmt::Formatter,
line: usize,
col: usize,
message: &str,
path: &ColoredString,
code: &ColoredString,
) -> fmt::Result {
let lines = read_lines(&diagnostic.path);
let mut start_index = line.saturating_sub(2).max(1);
while start_index < line {
if !lines[start_index.saturating_sub(1)].trim().is_empty() {
break;
}
start_index = start_index.saturating_add(1);
}
let mut end_index = line.saturating_add(2).min(lines.len());
while end_index > line {
if !lines[end_index.saturating_sub(1)].trim().is_empty() {
break;
}
end_index = end_index.saturating_sub(1);
}
let content_slice = lines[start_index.saturating_sub(1)..end_index]
.iter()
.fold(String::default(), |acc, line| format!("{acc}{line}\n"));
let offset_up_to_line = lines[start_index.saturating_sub(1)..line.saturating_sub(1)]
.iter()
.fold(0, |acc, line| acc + line.chars().count() + 1);
let label_offset = offset_up_to_line + col.saturating_sub(1);
let message_line = format!("{}:{}:{}: {} {}", path, line, col, code, message);
let snippet = Level::Warning.title(&message_line).snippet(
Snippet::source(&content_slice)
.line_start(start_index)
.annotation(
Level::Error
.span(label_offset..label_offset.saturating_add(1))
.label(code),
),
);
let renderer = Renderer::styled();
let source_block = renderer.render(snippet);
writeln!(f, "{}", source_block)
}