Skip to content

Commit

Permalink
feat(rs): allow linting only a single rule at once
Browse files Browse the repository at this point in the history
required for eslint interoperability
  • Loading branch information
charislam committed Oct 30, 2024
1 parent 6632b95 commit 1da86ce
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 15 deletions.
59 changes: 49 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use rules::RuleFilter;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{fs, io::Read};
Expand Down Expand Up @@ -128,7 +129,7 @@ impl Linter {
pub fn lint(&self, input: JsValue) -> Result<JsValue, JsValue> {
let js_target = JsLintTarget::from_js_value(input)?;
match js_target.to_lint_target() {
Ok(lint_target) => match self.lint_internal(lint_target) {
Ok(lint_target) => match self.lint_internal(lint_target, None) {
Ok(errors) => serde_wasm_bindgen::to_value(
&errors
.into_iter()
Expand All @@ -141,32 +142,70 @@ impl Linter {
Err(err) => Err(err),
}
}

#[wasm_bindgen]
pub fn lint_only_rule(&self, rule_id: JsValue, input: JsValue) -> Result<JsValue, JsValue> {
let js_target = JsLintTarget::from_js_value(input)?;
match (js_target.to_lint_target(), rule_id.as_string()) {
(Ok(lint_target), Some(rule_id)) => {
match self.lint_internal(lint_target, Some(&[rule_id.as_str()])) {
Ok(errors) => serde_wasm_bindgen::to_value(
&errors
.into_iter()
.map(|e| Into::<JsLintError>::into(e))
.collect::<Vec<_>>(),
)
.map_err(|e| JsValue::from_str(&e.to_string())),
Err(err) => Err(JsValue::from_str(&err.to_string())),
}
}
(Err(err), _) => Err(err),
(_, None) => Err(JsValue::from_str(
"A rule ID must be provided when linting only a single rule",
)),
}
}
}

impl Linter {
#[cfg(not(target_arch = "wasm32"))]
pub fn lint(&self, input: LintTarget) -> Result<Vec<LintError>> {
self.lint_internal(input)
self.lint_internal(input, None)
}

#[cfg(not(target_arch = "wasm32"))]
pub fn lint_only_rule(&self, rule_id: &str, input: LintTarget) -> Result<Vec<LintError>> {
self.lint_internal(input, Some(&[rule_id]))
}

pub fn lint_internal(&self, input: LintTarget) -> Result<Vec<LintError>> {
fn lint_internal(
&self,
input: LintTarget,
check_only_rules: RuleFilter,
) -> Result<Vec<LintError>> {
match input {
LintTarget::FileOrDirectory(path) => self.lint_file_or_directory(path),
LintTarget::String(string) => self.lint_string(&string),
LintTarget::FileOrDirectory(path) => {
self.lint_file_or_directory(path, check_only_rules)
}
LintTarget::String(string) => self.lint_string(&string, check_only_rules),
}
}

fn lint_file_or_directory(&self, path: PathBuf) -> Result<Vec<LintError>> {
fn lint_file_or_directory(
&self,
path: PathBuf,
check_only_rules: RuleFilter,
) -> Result<Vec<LintError>> {
if path.is_file() {
let mut file = fs::File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
self.lint_string(&contents)
self.lint_string(&contents, check_only_rules)
} else if path.is_dir() {
let collected_vec = fs::read_dir(path)?
.filter_map(Result::ok)
.flat_map(|entry| {
self.lint_file_or_directory(entry.path())
self.lint_file_or_directory(entry.path(), check_only_rules)
.unwrap_or_default()
})
.collect::<Vec<_>>();
Expand All @@ -179,9 +218,9 @@ impl Linter {
}
}

fn lint_string(&self, string: &str) -> Result<Vec<LintError>> {
fn lint_string(&self, string: &str, check_only_rules: RuleFilter) -> Result<Vec<LintError>> {
let parse_result = parse(string)?;
let rule_context = RuleContext::new(parse_result);
let rule_context = RuleContext::new(parse_result, check_only_rules);
self.config.rule_registry.run(&rule_context)
}
}
Expand Down
117 changes: 113 additions & 4 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,19 @@ impl RuleSettings {
}
}

pub struct RuleContext {
pub type RuleFilter<'filter> = Option<&'filter [&'filter str]>;

pub struct RuleContext<'ctx> {
parse_result: ParseResult,
check_only_rules: RuleFilter<'ctx>,
}

impl RuleContext {
pub fn new(parse_result: ParseResult) -> Self {
Self { parse_result }
impl<'ctx> RuleContext<'ctx> {
pub fn new(parse_result: ParseResult, check_only_rules: Option<&'ctx [&'ctx str]>) -> Self {
Self {
parse_result,
check_only_rules,
}
}

pub fn frontmatter_lines(&self) -> usize {
Expand Down Expand Up @@ -205,6 +211,11 @@ impl RuleRegistry {

fn check_node(&self, ast: &Node, context: &RuleContext, errors: &mut Vec<LintError>) {
for rule in &self.rules {
if let Some(filter) = &context.check_only_rules {
if !filter.contains(&rule.name()) {
continue;
}
}
if let Some(rule_errors) = rule.check(ast, context) {
errors.extend(rule_errors);
}
Expand All @@ -217,3 +228,101 @@ impl RuleRegistry {
}
}
}

#[cfg(test)]
mod tests {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};

use super::*;
use markdown::mdast::{Node, Text};
use supa_mdx_macros::RuleName;

#[derive(Clone, Default, RuleName)]
struct MockRule {
check_count: Arc<AtomicUsize>,
}

impl Rule for MockRule {
fn check(&self, _ast: &Node, _context: &RuleContext) -> Option<Vec<LintError>> {
self.check_count.fetch_add(1, Ordering::Relaxed);
None
}
}

#[derive(Clone, Default, RuleName)]
struct MockRule2 {
check_count: Arc<AtomicUsize>,
}

impl Rule for MockRule2 {
fn check(&self, _ast: &Node, _context: &RuleContext) -> Option<Vec<LintError>> {
self.check_count.fetch_add(1, Ordering::Relaxed);
None
}
}

#[test]
fn test_check_node_with_filter() {
let text_node = Node::Text(Text {
value: "test".into(),
position: None,
});

let mock_rule_1 = MockRule::default();
let mock_rule_2 = MockRule2::default();
let check_count_1 = mock_rule_1.check_count.clone();
let check_count_2 = mock_rule_2.check_count.clone();

let registry = RuleRegistry {
state: RuleRegistryState::Setup,
rules: vec![Box::new(mock_rule_1), Box::new(mock_rule_2)],
};

let parse_result = ParseResult {
ast: text_node.clone(),
frontmatter_lines: 0,
frontmatter: None,
};
let context = RuleContext::new(parse_result, Some(&["MockRule"]));

let mut errors = Vec::new();
registry.check_node(&text_node, &context, &mut errors);

assert_eq!(check_count_1.load(Ordering::Relaxed), 1);
assert_eq!(check_count_2.load(Ordering::Relaxed), 0);
}

#[test]
fn test_check_node_without_filter() {
let text_node = Node::Text(Text {
value: "test".into(),
position: None,
});

let mock_rule_1 = MockRule::default();
let mock_rule_2 = MockRule2::default();
let check_count_1 = mock_rule_1.check_count.clone();
let check_count_2 = mock_rule_2.check_count.clone();

let registry = RuleRegistry {
state: RuleRegistryState::Setup,
rules: vec![Box::new(mock_rule_1), Box::new(mock_rule_2)],
};

let parse_result = ParseResult {
ast: text_node.clone(),
frontmatter_lines: 0,
frontmatter: None,
};
let context = RuleContext::new(parse_result, None);

let mut errors = Vec::new();
registry.check_node(&text_node, &context, &mut errors);

assert_eq!(check_count_1.load(Ordering::Relaxed), 1);
assert_eq!(check_count_2.load(Ordering::Relaxed), 1);
}
}
3 changes: 2 additions & 1 deletion src/rules/rule001_heading_case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ mod tests {
})
}

fn create_rule_context() -> RuleContext {
fn create_rule_context<'ctx>() -> RuleContext<'ctx> {
RuleContext {
parse_result: ParseResult {
ast: Node::Root(markdown::mdast::Root {
Expand All @@ -279,6 +279,7 @@ mod tests {
frontmatter_lines: 0,
frontmatter: None,
},
check_only_rules: None,
}
}

Expand Down

0 comments on commit 1da86ce

Please sign in to comment.