use crate::{prelude::*, LintContext};
use camino::Utf8Path;
use x_core::XCoreContext;
#[derive(Clone, Debug)]
pub struct LintEngineConfig<'cfg> {
core: &'cfg XCoreContext,
project_linters: &'cfg [&'cfg dyn ProjectLinter],
package_linters: &'cfg [&'cfg dyn PackageLinter],
file_path_linters: &'cfg [&'cfg dyn FilePathLinter],
content_linters: &'cfg [&'cfg dyn ContentLinter],
fail_fast: bool,
}
impl<'cfg> LintEngineConfig<'cfg> {
pub fn new(core: &'cfg XCoreContext) -> Self {
Self {
core,
project_linters: &[],
package_linters: &[],
file_path_linters: &[],
content_linters: &[],
fail_fast: false,
}
}
pub fn with_project_linters(
&mut self,
project_linters: &'cfg [&'cfg dyn ProjectLinter],
) -> &mut Self {
self.project_linters = project_linters;
self
}
pub fn with_package_linters(
&mut self,
package_linters: &'cfg [&'cfg dyn PackageLinter],
) -> &mut Self {
self.package_linters = package_linters;
self
}
pub fn with_file_path_linters(
&mut self,
file_path_linters: &'cfg [&'cfg dyn FilePathLinter],
) -> &mut Self {
self.file_path_linters = file_path_linters;
self
}
pub fn with_content_linters(
&mut self,
content_linters: &'cfg [&'cfg dyn ContentLinter],
) -> &mut Self {
self.content_linters = content_linters;
self
}
pub fn fail_fast(&mut self, fail_fast: bool) -> &mut Self {
self.fail_fast = fail_fast;
self
}
pub fn build(&self) -> LintEngine<'cfg> {
LintEngine::new(self.clone())
}
}
#[derive(Debug)]
pub struct LintEngine<'cfg> {
config: LintEngineConfig<'cfg>,
project_ctx: ProjectContext<'cfg>,
}
impl<'cfg> LintEngine<'cfg> {
pub fn new(config: LintEngineConfig<'cfg>) -> Self {
let project_ctx = ProjectContext::new(config.core);
Self {
config,
project_ctx,
}
}
pub fn run(&self) -> Result<LintResults> {
let mut skipped = vec![];
let mut messages = vec![];
if !self.config.project_linters.is_empty() {
for linter in self.config.project_linters {
let source = self.project_ctx.source(linter.name());
let mut formatter = LintFormatter::new(source, &mut messages);
match linter.run(&self.project_ctx, &mut formatter)? {
RunStatus::Executed => {
}
RunStatus::Skipped(reason) => {
skipped.push((source, reason));
}
}
if self.config.fail_fast && !messages.is_empty() {
return Ok(LintResults { skipped, messages });
}
}
}
if !self.config.package_linters.is_empty() {
let package_graph = self.project_ctx.package_graph()?;
for (workspace_path, metadata) in package_graph.workspace().iter_by_path() {
let package_ctx = PackageContext::new(
&self.project_ctx,
package_graph,
workspace_path,
metadata,
)?;
for linter in self.config.package_linters {
let source = package_ctx.source(linter.name());
let mut formatter = LintFormatter::new(source, &mut messages);
match linter.run(&package_ctx, &mut formatter)? {
RunStatus::Executed => {
}
RunStatus::Skipped(reason) => {
skipped.push((source, reason));
}
}
if self.config.fail_fast && !messages.is_empty() {
return Ok(LintResults { skipped, messages });
}
}
}
}
if !self.config.file_path_linters.is_empty() {
let file_list = self.file_list()?;
let file_ctxs = file_list.map(|path| FilePathContext::new(&self.project_ctx, path));
for file_ctx in file_ctxs {
for linter in self.config.file_path_linters {
let source = file_ctx.source(linter.name());
let mut formatter = LintFormatter::new(source, &mut messages);
match linter.run(&file_ctx, &mut formatter)? {
RunStatus::Executed => {
}
RunStatus::Skipped(reason) => {
skipped.push((source, reason));
}
}
if self.config.fail_fast && !messages.is_empty() {
return Ok(LintResults { skipped, messages });
}
}
}
}
if !self.config.content_linters.is_empty() {
let file_list = self.file_list()?;
let file_ctxs = file_list.map(|path| FilePathContext::new(&self.project_ctx, path));
for file_ctx in file_ctxs {
let linters_to_run = self
.config
.content_linters
.iter()
.copied()
.filter_map(|linter| match linter.pre_run(&file_ctx) {
Ok(RunStatus::Executed) => Some(Ok(linter)),
Ok(RunStatus::Skipped(reason)) => {
let source = file_ctx.source(linter.name());
skipped.push((source, reason));
None
}
Err(err) => Some(Err(err)),
})
.collect::<Result<Vec<_>>>()?;
if linters_to_run.is_empty() {
continue;
}
let content_ctx = match file_ctx.load()? {
Some(content_ctx) => content_ctx,
None => {
continue;
}
};
for linter in linters_to_run {
let source = content_ctx.source(linter.name());
let mut formatter = LintFormatter::new(source, &mut messages);
match linter.run(&content_ctx, &mut formatter)? {
RunStatus::Executed => {
}
RunStatus::Skipped(reason) => {
skipped.push((source, reason));
}
}
if self.config.fail_fast && !messages.is_empty() {
return Ok(LintResults { skipped, messages });
}
}
}
}
Ok(LintResults { skipped, messages })
}
fn file_list(&self) -> Result<impl Iterator<Item = &'cfg Utf8Path> + 'cfg> {
let tracked_files = self.config.core.git_cli().tracked_files()?;
Ok(tracked_files
.iter()
.filter(|f| !f.starts_with("testsuite/diem-fuzzer/artifacts/")))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LintResults<'l> {
pub skipped: Vec<(LintSource<'l>, SkipReason<'l>)>,
pub messages: Vec<(LintSource<'l>, LintMessage)>,
}