diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 224a82981..e558d4b20 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -40,6 +40,8 @@ path-absolutize = { version = "3.1.1", optional = false, features = [ "use_unix_paths_on_wasm", ] } getrandom = { version = "0.2.11", optional = true } +lazy_static = {version = "1.4.0", optional = true } +walkdir = { version = "2.3.3", optional = true } [dev-dependencies] similar = "2.2.1" @@ -86,3 +88,4 @@ language-parsers = ["marzano-language/builtin-parser"] grit-parser = ["marzano-language/grit-parser"] absolute_filename = [] non_wasm = ["absolute_filename"] +test_utils = [] diff --git a/crates/core/src/ast_node.rs b/crates/core/src/ast_node.rs index 3ffee61aa..40e8724f2 100644 --- a/crates/core/src/ast_node.rs +++ b/crates/core/src/ast_node.rs @@ -27,6 +27,8 @@ impl ASTNode { } impl AstNodePattern for ASTNode { + const INCLUDES_TRIVIA: bool = false; + fn children(&self) -> Vec> { self.args .iter() @@ -144,7 +146,11 @@ impl AstLeafNode { } } -impl AstLeafNodePattern for AstLeafNode {} +impl AstLeafNodePattern for AstLeafNode { + fn text(&self) -> Option<&str> { + Some(&self.text) + } +} impl PatternName for AstLeafNode { fn name(&self) -> &'static str { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 195b0e0c9..e11c47dba 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -38,3 +38,5 @@ use getrandom as _; mod test; #[cfg(test)] mod test_files; +#[cfg(any(test, feature = "test_utils"))] +pub mod test_utils; diff --git a/crates/core/src/test.rs b/crates/core/src/test.rs index f8d5ed04f..28d318e8d 100644 --- a/crates/core/src/test.rs +++ b/crates/core/src/test.rs @@ -143,9 +143,9 @@ fn match_pattern_libs( Ok(execution_result) } -struct TestArg { - pattern: String, - source: String, +pub struct TestArg { + pub pattern: String, + pub source: String, } struct TestArgExpected { @@ -392,7 +392,7 @@ fn run_test_expected_with_new_file(arg: TestArgExpectedWithNewFile) -> Result<() Ok(()) } -fn run_test_match(arg: TestArg) -> Result<()> { +pub fn run_test_match(arg: TestArg) -> Result<()> { let pattern = arg.pattern; let js_lang: TargetLanguage = PatternLanguage::Tsx.try_into().unwrap(); let source = arg.source; diff --git a/crates/core/src/test_files.rs b/crates/core/src/test_files.rs index 58619740d..b2593ab6a 100644 --- a/crates/core/src/test_files.rs +++ b/crates/core/src/test_files.rs @@ -1,66 +1,14 @@ use marzano_language::target_language::TargetLanguage; -use marzano_util::{ - cache::NullCache, - rich_path::{FileName, RichFile, TryIntoInputFile}, - runtime::ExecutionContext, -}; -use serde::{Deserialize, Serialize}; -use crate::api::{MatchResult, Rewrite}; +use crate::{ + api::{MatchResult, Rewrite}, + test_utils::{run_on_test_files, SyntheticFile}, +}; -use self::{pattern_compiler::src_to_problem_libs, problem::Problem}; -use anyhow::Result; +use self::pattern_compiler::src_to_problem_libs; use super::*; -use std::{borrow::Cow, collections::BTreeMap, sync::mpsc}; - -/// SyntheticFile is used for ensuring we don't read files until their file names match -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -struct SyntheticFile { - pub path: String, - pub content: String, - pub can_read: bool, -} - -impl SyntheticFile { - pub fn new(path: String, content: String, can_read: bool) -> Self { - Self { - path, - content, - can_read, - } - } -} - -impl TryIntoInputFile for SyntheticFile { - fn try_into_cow(&self) -> Result> { - if !self.can_read { - println!("Tried to read file that should not be read: {}", self.path); - } - - Ok(Cow::Owned(RichFile::new( - self.path.clone(), - self.content.clone(), - ))) - } -} - -impl FileName for SyntheticFile { - fn name(&self) -> String { - self.path.to_owned() - } -} - -fn run_on_test_files(problem: &Problem, test_files: &[SyntheticFile]) -> Vec { - let mut results = vec![]; - let context = ExecutionContext::default(); - let (tx, rx) = mpsc::channel::>(); - problem.execute_shared(test_files.to_vec(), &context, tx, &NullCache::new()); - for r in rx.iter() { - results.extend(r) - } - results -} +use std::collections::BTreeMap; #[test] fn test_lazy_file_parsing() { diff --git a/crates/core/src/test_utils.rs b/crates/core/src/test_utils.rs new file mode 100644 index 000000000..21be197ad --- /dev/null +++ b/crates/core/src/test_utils.rs @@ -0,0 +1,115 @@ +use std::{borrow::Cow, collections::BTreeMap, sync::mpsc}; + +use anyhow::Result; +use marzano_language::target_language::TargetLanguage; +use marzano_util::{ + cache::NullCache, + rich_path::{FileName, RichFile, TryIntoInputFile}, + runtime::ExecutionContext, +}; +use serde::{Deserialize, Serialize}; + +use crate::{api::MatchResult, pattern_compiler::src_to_problem_libs, problem::Problem}; + +/// SyntheticFile is used for ensuring we don't read files until their file names match +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub(crate) struct SyntheticFile { + pub path: String, + pub content: String, + pub can_read: bool, +} + +impl SyntheticFile { + pub fn new(path: String, content: String, can_read: bool) -> Self { + Self { + path, + content, + can_read, + } + } +} + +impl TryIntoInputFile for SyntheticFile { + fn try_into_cow(&self) -> Result> { + if !self.can_read { + println!("Tried to read file that should not be read: {}", self.path); + } + + Ok(Cow::Owned(RichFile::new( + self.path.clone(), + self.content.clone(), + ))) + } +} + +impl FileName for SyntheticFile { + fn name(&self) -> String { + self.path.to_owned() + } +} + +enum TestCaseExpectation { + Match, +} + +pub struct TestCase { + files: Vec, + pattern: String, + expectation: TestCaseExpectation, +} + +impl TestCase { + pub fn new_match(file_contents: &str, pattern: &str) -> Self { + Self { + files: vec![SyntheticFile::new( + "target.js".to_string(), + file_contents.to_string(), + true, + )], + pattern: pattern.to_string(), + expectation: TestCaseExpectation::Match, + } + } +} + +pub(crate) fn run_on_test_files( + problem: &Problem, + test_files: &[SyntheticFile], +) -> Vec { + let mut results = vec![]; + let context = ExecutionContext::default(); + let (tx, rx) = mpsc::channel::>(); + problem.execute_shared(test_files.to_vec(), &context, tx, &NullCache::new()); + for r in rx.iter() { + results.extend(r) + } + results +} + +pub fn run_test(case: TestCase) -> Vec { + let pattern_src = case.pattern; + let libs = BTreeMap::new(); + + let pattern = src_to_problem_libs( + pattern_src.to_string(), + &libs, + TargetLanguage::default(), + None, + None, + None, + None, + ) + .unwrap() + .problem; + + let results = run_on_test_files(&pattern, &case.files); + + match case.expectation { + TestCaseExpectation::Match => { + let match_case = results.iter().any(|r| r.is_match()); + assert!(match_case, "Expected a match, but got none"); + } + } + + results +} diff --git a/crates/grit-pattern-matcher/src/errors.rs b/crates/grit-pattern-matcher/src/errors.rs index 7c020de2a..e71b5dd35 100644 --- a/crates/grit-pattern-matcher/src/errors.rs +++ b/crates/grit-pattern-matcher/src/errors.rs @@ -28,3 +28,27 @@ pub fn debug<'a, Q: QueryContext>( } Ok(()) } + +pub fn warning<'a, Q: QueryContext>( + analysis_logs: &mut AnalysisLogs, + state: &State<'a, Q>, + lang: &Q::Language<'a>, + message: &str, +) -> Result<()> { + let mut builder = AnalysisLogBuilder::default(); + builder.level(301_u16); + builder.message(message); + + if let Ok(file) = get_file_name(state, lang) { + builder.file(file); + } + + let log = builder.build(); + match log { + Ok(log) => analysis_logs.push(log), + Err(err) => { + bail!(err); + } + } + Ok(()) +} diff --git a/crates/grit-pattern-matcher/src/pattern/ast_node_pattern.rs b/crates/grit-pattern-matcher/src/pattern/ast_node_pattern.rs index ca4063132..58cca50d5 100644 --- a/crates/grit-pattern-matcher/src/pattern/ast_node_pattern.rs +++ b/crates/grit-pattern-matcher/src/pattern/ast_node_pattern.rs @@ -8,6 +8,10 @@ use crate::context::QueryContext; pub trait AstNodePattern: Clone + std::fmt::Debug + Matcher + PatternName + Sized { + /// Does this AST include trivia? + /// Trivia is useful for being able to re-print an AST, but not all parsers support collecting it. + const INCLUDES_TRIVIA: bool; + fn children(&self) -> Vec>; fn matches_kind_of(&self, node: &Q::Node<'_>) -> bool; @@ -17,4 +21,9 @@ pub trait AstNodePattern: pub trait AstLeafNodePattern: Clone + std::fmt::Debug + Matcher + PatternName + Sized { + /// Provides a *possible* text value for the leaf node. + /// This is not mandatory, but enables some advanced functionality. + fn text(&self) -> Option<&str> { + None + } } diff --git a/crates/grit-pattern-matcher/src/pattern/patterns.rs b/crates/grit-pattern-matcher/src/pattern/patterns.rs index fa3fcf295..bfeb2dcb3 100644 --- a/crates/grit-pattern-matcher/src/pattern/patterns.rs +++ b/crates/grit-pattern-matcher/src/pattern/patterns.rs @@ -308,6 +308,7 @@ impl Matcher for Pattern { } pub trait CodeSnippet: Clone + Debug + Matcher + PatternName { + /// Return the different patterns which could *all* possibly match the code snippet. fn patterns(&self) -> impl Iterator>; fn dynamic_snippet(&self) -> Option<&DynamicPattern>;