diff --git a/benches/asting.rs b/benches/asting.rs index 3e8d755..74e404f 100644 --- a/benches/asting.rs +++ b/benches/asting.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use criterion::BenchmarkId; use criterion::Criterion; use criterion::criterion_group; diff --git a/benches/parsing.rs b/benches/parsing.rs index 20548e6..455a964 100644 --- a/benches/parsing.rs +++ b/benches/parsing.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use criterion::BenchmarkId; use criterion::Criterion; use criterion::criterion_group; diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 7f8cddd..59a7791 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -63,7 +63,7 @@ pub enum Instruction { slot: VariableSlot, }, PushScope { - #[expect(unused)] + #[allow(unused)] inherit_parent: bool, }, Abort, diff --git a/src/errors.rs b/src/errors.rs index 427a343..9d843ad 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,11 +5,13 @@ use annotate_snippets::Level; use annotate_snippets::Renderer; use annotate_snippets::Snippet; use thiserror::Error; +use winnow::stream::Offset; use crate::input::NomoInput; use crate::lexer::ParseError; use crate::parser::AstError; +/// An error occurred while producing an Ast #[derive(Debug, Error)] pub struct AstFailure { errors: Vec, @@ -26,6 +28,7 @@ impl AstFailure { AstFailure { errors } } + /// Create a CLI printable report pub fn to_report(&self, source: &str) -> String { let reports = self .errors @@ -38,12 +41,14 @@ impl AstFailure { .as_deref() .unwrap_or("An error occurred while producing an Ast"), ) - .element( - annotate_snippets::Snippet::source(source).annotation( - annotate_snippets::AnnotationKind::Primary - .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), + .element(annotate_snippets::Snippet::source(source).annotation( + annotate_snippets::AnnotationKind::Primary.span( + constrain_without_whitespace( + source, + error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0), + ), ), - ) + )) .elements( error .help @@ -59,6 +64,18 @@ impl AstFailure { } } +fn constrain_without_whitespace( + input: &str, + range: std::ops::Range, +) -> std::ops::Range { + let trimmed = input[range].trim(); + let start = trimmed.offset_from(&input); + let end = start + trimmed.len(); + + start..end +} + +/// An error occurred during lexing #[derive(Debug, Error)] pub struct ParseFailure { input: Arc, @@ -79,6 +96,7 @@ impl ParseFailure { } } + /// Produce a CLi printable report pub fn to_report(&self) -> String { let reports = self .errors diff --git a/src/functions.rs b/src/functions.rs index 8eb33d2..1a2883d 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -34,6 +34,7 @@ pub trait NomoFunction: 'static + Send + Sync { #[cfg(feature = "unstable-pub")] #[derive(Default)] +#[expect(missing_docs)] pub struct FunctionMap { funcs: HashMap, } @@ -45,6 +46,7 @@ pub(crate) struct FunctionMap { } impl FunctionMap { + #[expect(missing_docs)] pub fn register, T>(&mut self, name: impl Into, func: NF) { self.funcs .insert(name.into(), ErasedNomoFunction::erase(func)); diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 65872b7..94daf26 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -1,10 +1,3 @@ -use std::sync::Arc; - -use annotate_snippets::AnnotationKind; -use annotate_snippets::Level; -use annotate_snippets::Renderer; -use annotate_snippets::Snippet; -use thiserror::Error; use winnow::LocatingSlice; use winnow::Parser; use winnow::RecoverableParser; diff --git a/src/lib.rs b/src/lib.rs index 9ed46f7..b4b047e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ macro_rules! unstable_pub { ($(#[$m:meta])* mod $name:ident) => { $(#[$m])* #[cfg(feature = "unstable-pub")] + #[allow(missing_docs)] pub mod $name; #[cfg(not(feature = "unstable-pub"))] mod $name; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 73cc0b2..5763f45 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -104,13 +104,14 @@ impl FromRecoverableError, AstError> for AstError { .filter(|t| t.kind() != TokenKind::Whitespace); let last = tokens.next(); let first = tokens.last(); + match (last, first) { (None, None) => None, (None, Some(single)) | (Some(single), None) => Some(SourceSpan { range: single.source().get_range(), }), (Some(last), Some(first)) => { - let start = first.source().get_range().start; + let start = first.source().get_range().end; let end = last.source().get_range().end; Some(SourceSpan { range: start..end }) @@ -212,7 +213,7 @@ pub enum TemplateAstExpr<'input> { ElseConditional { expression: Option>>, }, - #[expect(unused)] + #[allow(unused)] Invalid(&'input [TemplateToken]), MathOperation { op: TokenOperator, @@ -311,7 +312,7 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result`")), + ), ws, parse_expression.map(Box::new), ) @@ -601,11 +604,34 @@ where fn parse_operand<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "operand", - alt((parse_function, parse_variable_access, parse_literal)), + alt(( + parse_keywords_fail, + parse_function, + parse_variable_access, + parse_literal, + )), ) .parse_next(input) } +fn parse_keywords_fail<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + let value = alt(( + TokenKind::ConditionalIf, + TokenKind::For, + TokenKind::End, + TokenKind::In, + )) + .take() + .map(TemplateAstExpr::Invalid) + .parse_next(input)?; + + cut_err(fail::<_, (), _>.context(AstError::ctx().msg("Found literal, expected expression"))) + .value(value) + .parse_next(input) +} + fn parse_literal<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "literal", @@ -895,7 +921,7 @@ mod tests { help: None, span: Some( SourceSpan { - range: 0..6, + range: 2..6, }, ), is_fatal: false, diff --git a/tests/checks.rs b/tests/checks.rs index 200ac20..35e0976 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + #[test] fn check_files() { let files = std::fs::read_dir("tests/checks/").unwrap(); diff --git a/tests/errors/invalid_controls.1-parsed.snap b/tests/errors/invalid_controls.1-parsed.snap new file mode 100644 index 0000000..13fc397 --- /dev/null +++ b/tests/errors/invalid_controls.1-parsed.snap @@ -0,0 +1,106 @@ +--- +source: tests/file_tests.rs +expression: parsed +info: + input: "{{ if }} {{ end }}\n{{ if if }} {{ end }}\n{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}\n{{ for in bar }} {{ end }}\n{{ for blah bar }} {{ end }}\n{{ else }}" + context: {} +--- +ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [Whitespace]" " (2..3), + [ConditionalIf]"if" (3..5), + [Whitespace]" " (5..6), + [RightDelim]"}}" (6..8), + [Whitespace]" " (8..9), + [LeftDelim]"{{" (9..11), + [Whitespace]" " (11..12), + [End]"end" (12..15), + [Whitespace]" " (15..16), + [RightDelim]"}}" (16..18), + [Whitespace]"\n" (18..19), + [LeftDelim]"{{" (19..21), + [Whitespace]" " (21..22), + [ConditionalIf]"if" (22..24), + [Whitespace]" " (24..25), + [ConditionalIf]"if" (25..27), + [Whitespace]" " (27..28), + [RightDelim]"}}" (28..30), + [Whitespace]" " (30..31), + [LeftDelim]"{{" (31..33), + [Whitespace]" " (33..34), + [End]"end" (34..37), + [Whitespace]" " (37..38), + [RightDelim]"}}" (38..40), + [Whitespace]"\n" (40..41), + [LeftDelim]"{{" (41..43), + [Whitespace]" " (43..44), + [ConditionalIf]"if" (44..46), + [Whitespace]" " (46..47), + [ConditionalIf]"if" (47..49), + [Whitespace]" " (49..50), + [RightDelim]"}}" (50..52), + [Whitespace]" " (52..53), + [LeftDelim]"{{" (53..55), + [Whitespace]" " (55..56), + [For]"for" (56..59), + [Whitespace]" " (59..60), + [Ident]"foo" (60..63), + [Whitespace]" " (63..64), + [In]"in" (64..66), + [Whitespace]" " (66..67), + [Ident]"bar" (67..70), + [Whitespace]" " (70..71), + [RightDelim]"}}" (71..73), + [Whitespace]" " (73..74), + [LeftDelim]"{{" (74..76), + [Whitespace]" " (76..77), + [End]"end" (77..80), + [Whitespace]" " (80..81), + [RightDelim]"}}" (81..83), + [Whitespace]" " (83..84), + [LeftDelim]"{{" (84..86), + [Whitespace]" " (86..87), + [End]"end" (87..90), + [Whitespace]" " (90..91), + [RightDelim]"}}" (91..93), + [Whitespace]"\n" (93..94), + [LeftDelim]"{{" (94..96), + [Whitespace]" " (96..97), + [For]"for" (97..100), + [Whitespace]" " (100..101), + [In]"in" (101..103), + [Whitespace]" " (103..104), + [Ident]"bar" (104..107), + [Whitespace]" " (107..108), + [RightDelim]"}}" (108..110), + [Whitespace]" " (110..111), + [LeftDelim]"{{" (111..113), + [Whitespace]" " (113..114), + [End]"end" (114..117), + [Whitespace]" " (117..118), + [RightDelim]"}}" (118..120), + [Whitespace]"\n" (120..121), + [LeftDelim]"{{" (121..123), + [Whitespace]" " (123..124), + [For]"for" (124..127), + [Whitespace]" " (127..128), + [Ident]"blah" (128..132), + [Whitespace]" " (132..133), + [Ident]"bar" (133..136), + [Whitespace]" " (136..137), + [RightDelim]"}}" (137..139), + [Whitespace]" " (139..140), + [LeftDelim]"{{" (140..142), + [Whitespace]" " (142..143), + [End]"end" (143..146), + [Whitespace]" " (146..147), + [RightDelim]"}}" (147..149), + [Whitespace]"\n" (149..150), + [LeftDelim]"{{" (150..152), + [Whitespace]" " (152..153), + [ConditionalElse]"else" (153..157), + [Whitespace]" " (157..158), + [RightDelim]"}}" (158..160), + ], +} diff --git a/tests/errors/invalid_controls.2-ast.snap b/tests/errors/invalid_controls.2-ast.snap new file mode 100644 index 0000000..265172d --- /dev/null +++ b/tests/errors/invalid_controls.2-ast.snap @@ -0,0 +1,32 @@ +--- +source: tests/file_tests.rs +expression: ast +info: + input: "{{ if }} {{ end }}\n{{ if if }} {{ end }}\n{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}\n{{ for in bar }} {{ end }}\n{{ for blah bar }} {{ end }}\n{{ else }}" + context: {} +--- +error: Expected an expression after 'if' +  ╭▸  +1 │ {{ if }} {{ end }} + │ ━━ + ╰╴ +error: Expected an expression after 'if' +  ╭▸  +2 │ {{ if if }} {{ end }} + ╰╴ ━━ +error: Expected an expression after 'if' +  ╭▸  +3 │ {{ if if }} {{ for foo in bar }} {{ end }} {{ end }} + ╰╴ ━━ +error: Expected identifier here +  ╭▸  +4 │ {{ for in bar }} {{ end }} + ╰╴ ━━━━━━ +error: Missing `in` in `for _ in ` +  ╭▸  +5 │ {{ for blah bar }} {{ end }} + ╰╴ ━━━━━━━━ +error: An error occurred while producing an Ast +  ╭▸  +6 │ {{ else }} + ╰╴ ━━━━━━━ diff --git a/tests/errors/invalid_controls.nomo b/tests/errors/invalid_controls.nomo new file mode 100644 index 0000000..f64a1b2 --- /dev/null +++ b/tests/errors/invalid_controls.nomo @@ -0,0 +1,8 @@ +{} +--- +{{ if }} {{ end }} +{{ if if }} {{ end }} +{{ if if }} {{ for foo in bar }} {{ end }} {{ end }} +{{ for in bar }} {{ end }} +{{ for blah bar }} {{ end }} +{{ else }} \ No newline at end of file diff --git a/tests/errors/invalid_if.1-parsed.snap b/tests/errors/invalid_if.1-parsed.snap new file mode 100644 index 0000000..7c8ec1a --- /dev/null +++ b/tests/errors/invalid_if.1-parsed.snap @@ -0,0 +1,24 @@ +--- +source: tests/file_tests.rs +expression: parsed +info: + input: "{{ if if }} {{ end }}" + context: {} +--- +ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [Whitespace]" " (2..3), + [ConditionalIf]"if" (3..5), + [Whitespace]" " (5..6), + [ConditionalIf]"if" (6..8), + [Whitespace]" " (8..9), + [RightDelim]"}}" (9..11), + [Whitespace]" " (11..12), + [LeftDelim]"{{" (12..14), + [Whitespace]" " (14..15), + [End]"end" (15..18), + [Whitespace]" " (18..19), + [RightDelim]"}}" (19..21), + ], +} diff --git a/tests/errors/invalid_if.2-ast.snap b/tests/errors/invalid_if.2-ast.snap new file mode 100644 index 0000000..b1781a4 --- /dev/null +++ b/tests/errors/invalid_if.2-ast.snap @@ -0,0 +1,11 @@ +--- +source: tests/file_tests.rs +expression: ast +info: + input: "{{ if if }} {{ end }}" + context: {} +--- +error: Expected an expression after 'if' +  ╭▸  +1 │ {{ if if }} {{ end }} + ╰╴ ━━ diff --git a/tests/errors/invalid_if.nomo b/tests/errors/invalid_if.nomo new file mode 100644 index 0000000..7a492ca --- /dev/null +++ b/tests/errors/invalid_if.nomo @@ -0,0 +1,3 @@ +{} +--- +{{ if if }} {{ end }} \ No newline at end of file diff --git a/tests/file_tests.rs b/tests/file_tests.rs index b26c356..5f5b325 100644 --- a/tests/file_tests.rs +++ b/tests/file_tests.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::collections::HashMap; use std::path::Path; @@ -6,6 +8,8 @@ use nomo::functions::FunctionMap; test_each_file::test_each_path! { for ["nomo"] in "./tests/cases/" as cases => check_for_input } +test_each_file::test_each_path! { for ["nomo"] in "./tests/errors/" as error_cases => check_errors } + #[derive(serde::Serialize)] struct Info { input: String, @@ -63,3 +67,67 @@ fn check_for_input([path]: [&Path; 1]) { insta::assert_debug_snapshot!(format!("{basename}.4-output"), output); } + +fn check_errors([path]: [&Path; 1]) { + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_path("errors"); + settings.set_prepend_module_to_snapshot(false); + + let basename = path.file_stem().unwrap().to_string_lossy(); + let input = std::fs::read_to_string(path).unwrap(); + + let (context, input) = input.split_once("\n---\n").unwrap_or_else(|| ("", &input)); + + let map = if !context.is_empty() { + serde_json::from_str::>(context).unwrap() + } else { + HashMap::new() + }; + + settings.set_info(&Info { + input: input.to_string(), + context: map.clone(), + }); + + let mut context = Context::new(); + + for (k, v) in map { + context.try_insert(k, v).unwrap(); + } + + let _guard = settings.bind_to_scope(); + + let parsed = nomo::lexer::parse(input.into()).map_err(|err| err.to_report()); + + match &parsed { + Ok(parsed) => insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed), + Err(parsed) => insta::assert_snapshot!(format!("{basename}.1-parsed"), parsed), + } + + let Ok(parsed) = parsed else { + return; + }; + + let ast = nomo::parser::parse(parsed.tokens()).map_err(|err| err.to_report(input)); + + match &ast { + Ok(ast) => insta::assert_debug_snapshot!(format!("{basename}.2-ast"), ast), + Err(ast) => insta::assert_snapshot!(format!("{basename}.2-ast"), ast), + } + + let Ok(ast) = ast else { + return; + }; + + let emit = nomo::compiler::emit_machine(ast); + + insta::assert_debug_snapshot!(format!("{basename}.3-instructions"), emit); + + let output = nomo::eval::execute(&FunctionMap::default(), &emit, &context) + .map_err(|err| err.to_string()); + + match &output { + Ok(output) => insta::assert_debug_snapshot!(format!("{basename}.4-output"), output), + Err(output) => insta::assert_snapshot!(format!("{basename}.4-output"), output), + } +}