Skip to content

Commit

Permalink
Add linter infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
tnballo committed Sep 2, 2023
1 parent c5727af commit 37a4ca1
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 10 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ check:
cd code_snippets/chp4/stack_example && cargo fmt
cd code_snippets/chp4/stack_example_iter && cargo fmt

# Progress check
cd tools/md_analyze && cargo fmt && cargo run
# Metrics and linting
cd tools/md_analyze && cargo fmt && cargo test && cargo run

# TODO: clean code_snippet binaries
clean:
Expand Down
16 changes: 16 additions & 0 deletions tools/md_analyze/src/book.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::{
chapter::Chapter,
content::Content,
lint::{Level, Linter, LinterBuilder},
rules::*,
traits::{GetChapter, GetMetrics},
BOOK_SRC_DIR, WORDS_PER_PAGE,
};
Expand Down Expand Up @@ -74,6 +76,20 @@ impl Book {
Ok(Book { chapters })
}

// TODO: add more rules/linters here
/// Get a linter for all sections
pub fn get_all_section_linter(&self) -> Linter<'_> {
let mut linter = LinterBuilder::new().add_rule(Level::Fatal, Rule(&rule_nonempty));

for chp in self.chapters.values() {
for content in chp.contents.iter() {
linter = linter.add_content(content);
}
}

linter.build()
}

// Collection book contents
// Adapted from: https://da-data.blogspot.com/2020/10/no-c-still-isnt-cutting-it.html
fn collect_contents(collect_section_data: bool, word_regex: &Regex) -> Vec<Content> {
Expand Down
4 changes: 1 addition & 3 deletions tools/md_analyze/src/chapter.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use crate::{content::Content, traits::GetMetrics, WORDS_PER_PAGE};

use std::{ffi::OsStr, fmt};

use colored::*;
use separator::Separatable;
use std::{ffi::OsStr, fmt};

/// Displayable chapter data model
pub struct Chapter {
Expand Down
3 changes: 2 additions & 1 deletion tools/md_analyze/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ use crate::traits::GetChapter;
use std::path::PathBuf;

/// Displayable content data model
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum Content {
/// An individual X.Y book section or chapter intro
Section {
/// Section path
path: PathBuf,
/// Section word count
word_count: usize,
#[allow(dead_code)] // TODO: add lint builder
/// Section data (optionally collected)
lines: Option<Vec<String>>,
},
Expand Down
138 changes: 138 additions & 0 deletions tools/md_analyze/src/lint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use crate::{rules::Rule, Content};
use std::path::PathBuf;

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum Level {
Fatal,
Warning,
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum LintError<'a> {
Failed {
path: &'a PathBuf,
line_number: usize,
line: String,
reason: String,
},
}

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum LeveledLintError<'a> {
Fatal(LintError<'a>),
Warning(LintError<'a>),
}

#[cfg(test)]
impl<'a> PartialEq for Rule<'a> {
fn eq(&self, other: &Self) -> bool {
// XXX: this is a test-only crime
format!("{:?}", self) == format!("{:?}", other)
}
}

#[derive(Default, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Linter<'a> {
rules: Vec<(Level, Rule<'a>)>,
contents: Vec<&'a Content>,
}

impl<'a> Linter<'a> {
pub fn builder() -> LinterBuilder<'a> {
LinterBuilder::default()
}

pub fn run(&self) -> Result<(), LeveledLintError> {
for content in &self.contents {
if let Content::Section { lines, path, .. } = content {
match lines {
Some(lines) => {
for (level, rule) in &self.rules {
rule.0(path, lines).map_err(|err| match level {
Level::Fatal => LeveledLintError::Fatal(err),
Level::Warning => LeveledLintError::Warning(err),
})?;
}
}
None => {
return Err(LeveledLintError::Fatal(LintError::Failed {
path,
line_number: 0,
line: "N/A".to_string(),
reason: format!("Empty content"),
}))
}
}
}
}

Ok(())
}
}

#[derive(Default)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct LinterBuilder<'a> {
rules: Vec<(Level, Rule<'a>)>,
contents: Vec<&'a Content>,
}

impl<'a> LinterBuilder<'a> {
pub fn new() -> LinterBuilder<'a> {
LinterBuilder {
rules: Vec::new(),
contents: Vec::new(),
}
}

pub fn add_rule(mut self, level: Level, rule: Rule<'a>) -> LinterBuilder<'a> {
self.rules.push((level, rule));
self
}

pub fn add_content(mut self, content: &'a Content) -> LinterBuilder<'a> {
self.contents.push(content);
self
}

pub fn build(self) -> Linter<'a> {
Linter {
rules: self.rules,
contents: self.contents,
}
}
}

#[test]
fn test_lint_builder() {
use crate::rules::rule_nonempty;
use std::path::PathBuf;

let empty_section = Content::Section {
path: PathBuf::from("/test/path/to/file.md"),
word_count: 0,
lines: None,
};

let default_svg = Content::Svg {
path: PathBuf::default(),
};

let linter = Linter {
rules: vec![(Level::Fatal, Rule(&rule_nonempty))],
contents: vec![&default_svg, &empty_section],
};

let linter_from_builder: Linter = LinterBuilder::new()
.add_rule(Level::Fatal, Rule(&rule_nonempty))
.add_content(&default_svg)
.add_content(&empty_section)
.build();

assert_eq!(linter, linter_from_builder);
assert!(matches!(linter.run(), Err(LeveledLintError::Fatal(_))));
}
9 changes: 5 additions & 4 deletions tools/md_analyze/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// TODO: clap args --word-count and --lint
// TODO: update change log with hashes and word/diagram totals
fn main() {
match md_analyze::Book::try_new(false) {
Ok(book) => println!("\n{}", book),
Err(e) => println!("Error: {:#?}", e),
};
let book = md_analyze::Book::try_new(true).unwrap();

println!("\n{}", book);

book.get_all_section_linter().run().unwrap();
}
6 changes: 6 additions & 0 deletions tools/md_analyze/src/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ pub use chapter::*;
mod content;
pub use content::*;

#[allow(missing_docs)]
mod lint;
pub use lint::*;

pub mod rules;

mod traits;

pub(crate) const BOOK_SRC_DIR: &str = "../../src";
Expand Down
27 changes: 27 additions & 0 deletions tools/md_analyze/src/rules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Rules the linter can apply to section data.

use crate::LintError;
use std::{fmt, path::PathBuf};

/// The signature for a rule, addable to linter builder
#[allow(clippy::type_complexity)]
pub struct Rule<'a>(pub &'a dyn Fn(&'a PathBuf, &[String]) -> Result<(), LintError<'a>>);

impl<'a> fmt::Debug for Rule<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Rule: {:p}", self.0)
}
}

/// Section is non-empty
pub fn rule_nonempty<'a>(path: &'a PathBuf, lines: &[String]) -> Result<(), LintError<'a>> {
match lines.is_empty() {
true => Err(LintError::Failed {
path,
line_number: 0,
line: "N/A".to_string(),
reason: "Missing data/contents".to_string(),
}),
false => Ok(()),
}
}

0 comments on commit 37a4ca1

Please sign in to comment.