1use crate::ast::Pattern;
6use crate::bytepat::{BytePat, parse as parse_bytepat};
7
8pub const LIBRARY_ID_BASE: u8 = 128;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Precedence {
16 Before,
18 After,
20}
21
22#[derive(Clone, Debug)]
24pub struct TokenShape {
25 pub name: String,
27 pub pat: BytePat,
29 pub window: usize,
31 pub precedence: Precedence,
33 pub id: u8,
36 pub guard: Option<fn(&[u8]) -> bool>,
39}
40
41#[derive(Clone, Copy, Debug)]
46pub struct UnitGuard {
47 pub symbols: &'static [&'static str],
49}
50
51impl UnitGuard {
52 #[must_use]
54 pub fn accepts(self, text: &[u8]) -> bool {
55 crate::quantity::context_span(text, self.symbols)
56 }
57}
58
59#[derive(Clone, Debug)]
62pub struct PatternKind {
63 pub name: String,
65 pub id: u8,
68 pub pattern: Pattern,
70 pub guard: Option<UnitGuard>,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct ShapeError {
78 pub decl: String,
80 pub msg: String,
82}
83
84impl std::fmt::Display for ShapeError {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "token shape {:?}: {}", self.decl, self.msg)
87 }
88}
89
90impl std::error::Error for ShapeError {}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum Severity {
96 Error,
97 Warning,
98 Note,
99}
100
101impl Severity {
102 #[must_use]
104 pub fn parse(name: &str) -> Option<Severity> {
105 match name {
106 "error" => Some(Severity::Error),
107 "warning" => Some(Severity::Warning),
108 "note" => Some(Severity::Note),
109 _ => None,
110 }
111 }
112
113 #[must_use]
115 pub fn name(self) -> &'static str {
116 match self {
117 Severity::Error => "error",
118 Severity::Warning => "warning",
119 Severity::Note => "note",
120 }
121 }
122
123 #[must_use]
125 pub fn github(self) -> &'static str {
126 match self {
127 Severity::Error => "error",
128 Severity::Warning => "warning",
129 Severity::Note => "notice",
130 }
131 }
132}
133
134#[derive(Clone, Debug)]
140pub struct Rule {
141 pub file: Option<std::path::PathBuf>,
143 pub line: usize,
145 pub name: String,
147 pub pattern: Pattern,
149 pub source: String,
151 pub message: String,
155 pub severity: Severity,
157 pub fix: Option<String>,
160 pub files: Vec<String>,
163 pub unless: Vec<Pattern>,
165 pub record: Option<crate::records::RecordUnit>,
169 pub meta: Vec<(String, String)>,
171}
172
173impl Rule {
174 #[must_use]
177 pub fn on_records(&self) -> bool {
178 self.record.is_some() || !self.unless.is_empty()
179 }
180
181 #[must_use]
184 pub fn record_unit(&self) -> crate::records::RecordUnit {
185 self.record.clone().unwrap_or(crate::records::RecordUnit::Line)
186 }
187
188 #[must_use]
190 pub fn tags(&self) -> Vec<String> {
191 tags_of(&self.meta)
192 }
193}
194
195#[must_use]
197pub fn tags_of(meta: &[(String, String)]) -> Vec<String> {
198 meta.iter()
199 .filter(|(k, _)| k == "tags")
200 .flat_map(|(_, v)| v.split(',').map(str::trim).filter(|t| !t.is_empty()).map(str::to_string))
201 .collect()
202}
203
204#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct LibTest {
208 pub file: Option<std::path::PathBuf>,
210 pub line: usize,
212 pub name: String,
215 pub accepts: Vec<String>,
218 pub reads: Vec<(String, String)>,
225 pub rejects: Vec<String>,
227}
228
229#[derive(Clone, Debug, PartialEq, Eq)]
231pub struct TestFailure {
232 pub file: Option<std::path::PathBuf>,
234 pub line: usize,
236 pub name: String,
238 pub msg: String,
240}
241
242impl std::fmt::Display for TestFailure {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match &self.file {
245 Some(path) => write!(f, "{}:{}: {} {}", path.display(), self.line, self.name, self.msg),
246 None => write!(f, "line {}: {} {}", self.line, self.name, self.msg),
247 }
248 }
249}
250
251fn significant_extent(input: &[u8], shapes: &ShapeSet) -> Option<(usize, usize)> {
255 let blobs = crate::lexer::blob_runs(input);
256 let toks = crate::lexer::lex_with_shapes(input, &blobs, shapes, 0);
257 let first = toks.iter().find(|t| t.is_significant())?.start();
258 let last = toks.iter().rev().find(|t| t.is_significant())?.end();
259 Some((first, last))
260}
261
262fn read_test_text(line: &str, open: usize) -> Result<(String, usize), String> {
265 let mut out = String::new();
266 let body = open + 1;
267 let mut chars = line[body..].char_indices();
268 while let Some((k, c)) = chars.next() {
269 match c {
270 '"' => return Ok((out, body + k + 1)),
271 '\\' => match chars.next() {
272 Some((_, '"')) => out.push('"'),
273 Some((_, '\\')) => out.push('\\'),
274 Some((_, 'n')) => out.push('\n'),
275 Some((_, 't')) => out.push('\t'),
276 Some((_, other)) => {
277 return Err(format!(
278 "unknown escape \\{other} in a test text; it reads \\\", \\\\, \\n and \\t"
279 ));
280 }
281 None => return Err("a backslash ends the test text".to_string()),
282 },
283 other => out.push(other),
284 }
285 }
286 Err("unterminated test text; it closes with a double quote".to_string())
287}
288
289#[derive(Clone, Debug, Default)]
291pub struct ShapeSet {
292 shapes: Vec<TokenShape>,
293 kinds: Vec<PatternKind>,
294 lets: Vec<(String, Pattern)>,
297 tests: Vec<LibTest>,
299 rules: Vec<Rule>,
301 fields: Vec<(String, Vec<crate::infer::marks::FieldMark>)>,
305 next_id: u8,
308 library_off: bool,
312 base: Option<std::path::PathBuf>,
317}
318
319impl ShapeSet {
320 #[must_use]
322 pub fn new() -> Self {
323 ShapeSet::default()
324 }
325
326 pub(crate) fn without_library() -> Self {
329 ShapeSet { library_off: true, ..ShapeSet::default() }
330 }
331
332 #[must_use]
335 pub fn consults_library(&self) -> bool {
336 !self.library_off
337 }
338
339 #[must_use]
342 pub fn base_dir(&self) -> Option<&std::path::Path> {
343 self.base.as_deref()
344 }
345
346 pub fn set_base_dir(&mut self, dir: Option<std::path::PathBuf>) {
351 self.base = dir;
352 }
353
354 pub fn declare_file(&mut self, path: &std::path::Path) -> Result<(), ShapeError> {
363 let text = match std::fs::read(path) {
364 Ok(bytes) => String::from_utf8_lossy(&crate::encoding::decode(bytes)).into_owned(),
365 Err(e) => {
366 return Err(ShapeError {
367 decl: path.display().to_string(),
368 msg: format!("cannot read the pattern file: {e}"),
369 });
370 }
371 };
372 let before = self.base.take();
373 self.base = path.parent().map(std::path::Path::to_path_buf);
374 let tests_before = self.tests.len();
375 let rules_before = self.rules.len();
376 let declared = self.declare_text(&text);
377 self.base = before;
378 for test in &mut self.tests[tests_before..] {
379 test.file = Some(path.to_path_buf());
380 }
381 for rule in &mut self.rules[rules_before..] {
382 rule.file = Some(path.to_path_buf());
383 }
384 declared.map_err(|mut e| {
385 e.msg = format!("{}: {}", path.display(), e.msg);
386 e
387 })
388 }
389
390 pub fn declare_file_members(&mut self, path: &std::path::Path) -> Result<Vec<(String, Pattern)>, ShapeError> {
399 let text = match std::fs::read(path) {
400 Ok(bytes) => String::from_utf8_lossy(&crate::encoding::decode(bytes)).into_owned(),
401 Err(e) => {
402 return Err(ShapeError {
403 decl: path.display().to_string(),
404 msg: format!("cannot read the pattern file: {e}"),
405 });
406 }
407 };
408 let before = self.base.take();
409 self.base = path.parent().map(std::path::Path::to_path_buf);
410 let tests_before = self.tests.len();
411 let rules_before = self.rules.len();
412 let mut members = Vec::new();
413 let declared = self.declare_lines(&text, Some(&mut members));
414 self.base = before;
415 for test in &mut self.tests[tests_before..] {
416 test.file = Some(path.to_path_buf());
417 }
418 for rule in &mut self.rules[rules_before..] {
419 rule.file = Some(path.to_path_buf());
420 }
421 match declared {
422 Ok(()) => Ok(members),
423 Err(mut e) => {
424 e.msg = format!("{}: {}", path.display(), e.msg);
425 Err(e)
426 }
427 }
428 }
429
430 #[must_use]
433 pub fn is_empty(&self) -> bool {
434 self.shapes.is_empty() && self.kinds.is_empty()
435 }
436
437 #[must_use]
439 pub fn shapes(&self) -> &[TokenShape] {
440 &self.shapes
441 }
442
443 #[must_use]
445 pub fn pattern_kinds(&self) -> &[PatternKind] {
446 &self.kinds
447 }
448
449 #[must_use]
451 pub fn lets(&self) -> &[(String, Pattern)] {
452 &self.lets
453 }
454
455 #[must_use]
457 pub fn tests(&self) -> &[LibTest] {
458 &self.tests
459 }
460
461 #[must_use]
463 pub fn rules(&self) -> &[Rule] {
464 &self.rules
465 }
466
467 #[must_use]
469 pub fn rule_of(&self, name: &str) -> Option<&Rule> {
470 self.rules.iter().find(|r| r.name == name)
471 }
472
473 #[must_use]
476 pub fn id_of(&self, name: &str) -> Option<u8> {
477 self.shapes
478 .iter()
479 .find(|s| s.name == name)
480 .map(|s| s.id)
481 .or_else(|| self.kinds.iter().find(|k| k.name == name).map(|k| k.id))
482 }
483
484 #[must_use]
486 pub fn name_of(&self, id: u8) -> Option<&str> {
487 self.shapes
488 .iter()
489 .find(|s| s.id == id)
490 .map(|s| s.name.as_str())
491 .or_else(|| self.kinds.iter().find(|k| k.id == id).map(|k| k.name.as_str()))
492 .or_else(|| crate::library::name_of(id))
493 }
494
495 #[must_use]
497 pub fn let_of(&self, name: &str) -> Option<&Pattern> {
498 self.lets.iter().rev().find(|(n, _)| n == name).map(|(_, p)| p)
499 }
500
501 #[must_use]
503 pub fn fields_of(&self, name: &str) -> Option<&[crate::infer::marks::FieldMark]> {
504 self.fields.iter().find(|(n, _)| n == name).map(|(_, f)| f.as_slice())
505 }
506
507 pub fn declare_fields(&mut self, decl: &str) -> Result<(), ShapeError> {
519 let err = |msg: String| ShapeError { decl: decl.to_string(), msg };
520 let (name, rest) = decl.split_once(char::is_whitespace).unwrap_or((decl, ""));
521 let pattern = self
522 .let_of(name)
523 .ok_or_else(|| err(format!("no sub-pattern is declared as {name} above; a fields line follows its let")))?;
524 if self.fields_of(name).is_some() {
525 return Err(err(format!("{name} has a fields line already")));
526 }
527 let marks = crate::infer::marks::field_marks(rest).map_err(|e| err(e.to_string()))?;
528 if marks.is_empty() {
529 return Err(err("a fields line names at least one field".to_string()));
530 }
531 let bound = pattern.capture_names();
532 for (k, mark) in marks.iter().enumerate() {
533 if marks[..k].iter().any(|m| m.name == mark.name) {
534 return Err(err(format!("{} is named twice", mark.written())));
535 }
536 let body = match &mark.accessor {
537 Some(accessor) => format!("{}:{accessor}", mark.name),
538 None => mark.name.clone(),
539 };
540 crate::rewrite::Reference::parse(&body, &bound).map_err(|e| err(format!("{}: {}", mark.written(), e.msg)))?;
541 }
542 self.fields.push((name.to_string(), marks));
543 Ok(())
544 }
545
546 #[must_use]
551 pub fn is_free(&self, name: &str) -> bool {
552 !name.is_empty()
553 && name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
554 && !BUILTIN_ATOM_NAMES.contains(&name)
555 && self.id_of(name).is_none()
556 && self.let_of(name).is_none()
557 && crate::library::id_of(name).is_none()
558 && crate::library::let_of(name).is_none()
559 }
560
561 fn check_name(&self, decl: &str, name: &str) -> Result<(), ShapeError> {
564 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
565 if name.is_empty() || !name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
566 return Err(err("a name is alphanumerics and underscores"));
567 }
568 if BUILTIN_ATOM_NAMES.contains(&name) {
569 return Err(err("that name belongs to a built-in atom; pick another"));
570 }
571 if self.id_of(name).is_some() {
572 return Err(err("a shape or kind with that name exists"));
573 }
574 Ok(())
575 }
576
577 fn take_id(&mut self, decl: &str) -> Result<u8, ShapeError> {
579 if self.next_id >= LIBRARY_ID_BASE {
580 return Err(ShapeError {
581 decl: decl.to_string(),
582 msg: format!("at most {LIBRARY_ID_BASE} shapes and kinds may be declared"),
583 });
584 }
585 let id = self.next_id;
586 self.next_id += 1;
587 Ok(id)
588 }
589
590 pub fn declare(&mut self, decl: &str, precedence: Precedence) -> Result<u8, ShapeError> {
598 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
599 let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = `pattern`"))?;
600 let name = name.trim();
601 self.check_name(decl, name)?;
602 let body = rest.trim();
603 let inner = body
604 .strip_prefix('`')
605 .and_then(|b| b.strip_suffix('`'))
606 .ok_or_else(|| err("the pattern must be enclosed in backticks"))?;
607 let pat = parse_bytepat(inner.as_bytes()).map_err(|m| err(&m))?;
608 let window = pat.max_len().ok_or_else(|| {
609 err("the pattern must be bounded; `*`, `+` and `{m,}` have no fixed length")
610 })?;
611 if window == 0 {
612 return Err(err("the pattern matches nothing, so it would never end a token"));
613 }
614 let id = self.take_id(decl)?;
615 self.shapes.push(TokenShape { name: name.to_string(), pat, window, precedence, id, guard: None });
616 Ok(id)
617 }
618
619 pub(crate) fn push_library_shape(&mut self, shape: TokenShape) {
622 self.shapes.push(shape);
623 }
624
625 pub fn declare_kind(&mut self, decl: &str) -> Result<u8, ShapeError> {
634 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
635 let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = pattern`"))?;
636 let name = name.trim();
637 self.check_name(decl, name)?;
638 let pattern = crate::parser::parse_with_shapes(rest.trim(), self)
639 .map_err(|e| err(&format!("pattern error at byte {}: {}", e.pos, e.msg)))?;
640 let id = self.take_id(decl)?;
641 self.kinds.push(PatternKind { name: name.to_string(), id, pattern, guard: None });
642 Ok(id)
643 }
644
645 pub(crate) fn push_library_kind(&mut self, kind: PatternKind) {
648 self.kinds.push(kind);
649 }
650
651 pub fn declare_let(&mut self, decl: &str) -> Result<(), ShapeError> {
660 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
661 let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = pattern`"))?;
662 let name = name.trim();
663 self.check_name(decl, name)?;
664 let pattern = crate::parser::parse_with_shapes(rest.trim(), self)
665 .map_err(|e| err(&format!("pattern error at byte {}: {}", e.pos, e.msg)))?;
666 self.lets.push((name.to_string(), pattern));
667 Ok(())
668 }
669
670 pub fn declare_test(&mut self, decl: &str, line: usize) -> Result<(), ShapeError> {
683 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
684 let (name, rest) = decl.split_once(char::is_whitespace).unwrap_or((decl, ""));
685 if name.is_empty() || !name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
686 return Err(err("expected `test NAME accepts \"text\"... rejects \"text\"...`"));
687 }
688 enum Side {
690 Accepts,
691 Rejects,
692 Reads,
694 ReadsSpan(String),
696 ReadsIn(String),
698 }
699 let mut accepts = Vec::new();
700 let mut rejects = Vec::new();
701 let mut reads: Vec<(String, String)> = Vec::new();
702 let mut side: Option<Side> = None;
703 let mut i = 0;
704 while i < rest.len() {
705 let c = rest.as_bytes()[i];
706 if c.is_ascii_whitespace() {
707 i += 1;
708 } else if c == b'"' {
709 let (text, next) = read_test_text(rest, i).map_err(|m| err(&m))?;
710 side = match side.take() {
711 Some(Side::Accepts) => {
712 accepts.push(text);
713 Some(Side::Accepts)
714 }
715 Some(Side::Rejects) => {
716 rejects.push(text);
717 Some(Side::Rejects)
718 }
719 Some(Side::Reads) => Some(Side::ReadsSpan(text)),
722 Some(Side::ReadsSpan(span)) => {
723 return Err(err(&format!(
724 "`in` stands between the span {span:?} and the text it is read from"
725 )));
726 }
727 Some(Side::ReadsIn(span)) => {
728 reads.push((span, text));
729 None
733 }
734 None => return Err(err("a text follows accepts, reads or rejects")),
735 };
736 i = next;
737 } else {
738 let end = rest[i..].find(char::is_whitespace).map_or(rest.len(), |n| i + n);
739 let word = &rest[i..end];
740 if word == "in" {
743 let Some(Side::ReadsSpan(span)) = side.take() else {
744 return Err(err("`in` follows the span a `reads` names"));
745 };
746 side = Some(Side::ReadsIn(span));
747 i = end;
748 continue;
749 }
750 side = Some(match word {
751 "accepts" => Side::Accepts,
752 "rejects" => Side::Rejects,
753 "reads" => Side::Reads,
754 other => {
755 return Err(err(&format!(
756 "expected accepts, reads, rejects or a quoted text, found `{other}`"
757 )));
758 }
759 });
760 i = end;
761 }
762 }
763 if let Some(Side::ReadsSpan(span) | Side::ReadsIn(span)) = side {
764 return Err(err(&format!("reads {span:?} names no text to read it from")));
765 }
766 if accepts.is_empty() && rejects.is_empty() && reads.is_empty() {
767 return Err(err("a test names at least one text it accepts, reads or rejects"));
768 }
769 self.tests.push(LibTest {
770 file: None,
771 line,
772 name: name.to_string(),
773 accepts,
774 reads,
775 rejects,
776 });
777 Ok(())
778 }
779
780 #[must_use]
787 pub fn run_tests(&self) -> Vec<TestFailure> {
788 let mut failures = Vec::new();
789 for test in &self.tests {
790 let fail = |msg: String| TestFailure {
791 file: test.file.clone(),
792 line: test.line,
793 name: test.name.clone(),
794 msg,
795 };
796 let pattern = match crate::parser::parse_with_shapes(&format!("\\{{{}}}", test.name), self) {
797 Ok(pattern) => pattern,
798 Err(e) => {
799 failures.push(fail(format!("cannot be tested: {}", e.msg)));
800 continue;
801 }
802 };
803 let lexed_under = self.with_library_shapes(&pattern.library_kinds());
804 for text in &test.accepts {
805 let input = text.as_bytes();
806 let found = crate::engine::scan_with_shapes(&pattern, input, self);
807 let whole = significant_extent(input, &lexed_under);
808 let lexing = || {
813 let explainer = crate::explain::Explainer::new(&pattern, input, self);
814 let toks: Vec<String> = explainer
815 .tokens(0..input.len())
816 .iter()
817 .map(|(kind, text)| format!("{kind} {text:?}"))
818 .collect();
819 format!("\n tokens: {}", toks.join(", "))
820 };
821 match (found.first(), whole) {
822 (Some(m), Some((first, last))) if m.start() == first && m.end() == last => {}
823 (Some(m), _) => failures.push(fail(format!(
824 "accepts {text:?}: matched only {:?} at {}..{}{}",
825 String::from_utf8_lossy(&input[m.range()]),
826 m.start,
827 m.end,
828 lexing()
829 ))),
830 (None, _) => failures.push(fail(format!("accepts {text:?}: no match{}", lexing()))),
831 }
832 }
833 for (span, text) in &test.reads {
839 let input = text.as_bytes();
840 let Some(at) = text.find(span.as_str()) else {
841 failures.push(fail(format!("reads {span:?}: {text:?} does not hold it")));
842 continue;
843 };
844 let want = at..at + span.len();
845 match crate::engine::scan_with_shapes(&pattern, input, self).first() {
846 Some(m) if m.range() == want => {}
847 Some(m) => failures.push(fail(format!(
848 "reads {span:?} in {text:?}: read {:?} at {}..{}",
849 String::from_utf8_lossy(&input[m.range()]),
850 m.start,
851 m.end
852 ))),
853 None => failures.push(fail(format!("reads {span:?} in {text:?}: no match"))),
854 }
855 }
856 for text in &test.rejects {
857 let input = text.as_bytes();
858 if let Some(m) = crate::engine::scan_with_shapes(&pattern, input, self).first() {
859 failures.push(fail(format!(
860 "rejects {text:?}: matched {:?} at {}..{}",
861 String::from_utf8_lossy(&input[m.range()]),
862 m.start,
863 m.end
864 )));
865 }
866 }
867 }
868 failures
869 }
870
871 pub fn declare_text(&mut self, text: &str) -> Result<(), ShapeError> {
887 self.declare_lines(text, None)
888 }
889
890 pub fn declare_lines(
901 &mut self,
902 text: &str,
903 mut members: Option<&mut Vec<(String, Pattern)>>,
904 ) -> Result<(), ShapeError> {
905 let lines: Vec<&str> = text.lines().collect();
906 let mut i = 0;
907 while i < lines.len() {
908 let n = i + 1;
909 let line = lines[i].trim();
910 i += 1;
911 if line.is_empty() || line.starts_with('#') {
912 continue;
913 }
914 let at = |mut e: ShapeError| {
915 e.msg = format!("line {n}: {}", e.msg);
916 e
917 };
918 let (keyword, rest) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
919 let rest = rest.trim();
920 match keyword {
921 "let" => {
922 self.declare_let(rest).map_err(at)?;
923 if let Some(members) = members.as_mut() {
924 let name = rest.split_once('=').map_or(rest, |(name, _)| name).trim();
925 let pattern = self.let_of(name).cloned().ok_or_else(|| {
926 at(ShapeError { decl: line.to_string(), msg: "the let declared nothing".to_string() })
927 })?;
928 members.push((name.to_string(), pattern));
929 }
930 }
931 "kind" => {
932 self.declare_kind(rest).map_err(at)?;
933 }
934 "shape" => {
935 self.declare(rest, Precedence::Before).map_err(at)?;
936 }
937 "shape-after" => {
938 self.declare(rest, Precedence::After).map_err(at)?;
939 }
940 "test" => self.declare_test(rest, n).map_err(at)?,
941 "fields" => self.declare_fields(rest).map_err(at)?,
942 "rule" => {
943 let is_block = !rest.is_empty() && rest.chars().all(|c| c == '_' || c.is_ascii_alphanumeric());
944 let rule = if is_block {
945 let mut fields: Vec<(usize, &str)> = Vec::new();
949 while i < lines.len() {
950 let raw = lines[i];
951 let body = raw.trim();
952 if body.is_empty() || body.starts_with('#') {
953 i += 1;
954 continue;
955 }
956 if !raw.starts_with([' ', '\t']) {
957 break;
958 }
959 fields.push((i + 1, body));
960 i += 1;
961 }
962 self.rule_block(rest, n, &fields)?
963 } else {
964 self.rule_line(rest, n).map_err(at)?
965 };
966 let name = rule.name.clone();
967 let pattern = rule.pattern.clone();
968 self.add_rule(rule, line).map_err(at)?;
969 if let Some(members) = members.as_mut() {
970 members.push((name, pattern));
971 }
972 }
973 "fix" | "meta" | "files" | "unless" | "record" | "record-start" | "record-span" => {
974 self.rule_field_line(keyword, rest).map_err(at)?;
975 }
976 other => match members.as_mut() {
977 Some(members) => {
978 let pattern = crate::parser::parse_with_shapes(line, self).map_err(|e| {
979 at(ShapeError {
980 decl: line.to_string(),
981 msg: format!("pattern error at byte {}: {}", e.pos, e.msg),
982 })
983 })?;
984 members.push((n.to_string(), pattern));
985 }
986 None => {
987 return Err(at(ShapeError {
988 decl: line.to_string(),
989 msg: format!(
990 "unknown declaration `{other}`; a line opens with let, kind, shape, shape-after, test or rule"
991 ),
992 }));
993 }
994 },
995 }
996 }
997 Ok(())
998 }
999
1000 fn rule_block(&self, name: &str, line: usize, fields: &[(usize, &str)]) -> Result<Rule, ShapeError> {
1005 let mut split: Vec<(usize, &str, &str)> = Vec::with_capacity(fields.len());
1006 for &(n, field_line) in fields {
1007 let (field, value) = field_line.split_once('=').ok_or_else(|| ShapeError {
1008 decl: field_line.to_string(),
1009 msg: format!("line {n}: a rule's field line is `field = value`"),
1010 })?;
1011 split.push((n, field.trim(), value.trim()));
1012 }
1013 let Some(&(n, _, source)) = split.iter().find(|(_, field, _)| *field == "pattern") else {
1014 return Err(ShapeError {
1015 decl: format!("rule {name}"),
1016 msg: format!("line {line}: rule {name} has no `pattern = PATTERN` line"),
1017 });
1018 };
1019 let pattern = self.rule_pattern(source).map_err(|mut e| {
1020 e.msg = format!("line {n}: {}", e.msg);
1021 e
1022 })?;
1023 let mut rule = Rule {
1024 file: None,
1025 line,
1026 name: name.to_string(),
1027 pattern,
1028 source: source.to_string(),
1029 message: String::new(),
1030 severity: Severity::Warning,
1031 fix: None,
1032 files: Vec::new(),
1033 unless: Vec::new(),
1034 record: None,
1035 meta: Vec::new(),
1036 };
1037 for &(n, field, value) in &split {
1038 let at = |mut e: ShapeError| {
1039 e.msg = format!("line {n}: {}", e.msg);
1040 e
1041 };
1042 match field {
1043 "pattern" => {}
1044 "message" => rule.message = value.to_string(),
1045 "severity" => {
1046 rule.severity = Severity::parse(value).ok_or_else(|| {
1047 at(ShapeError {
1048 decl: format!("{field} = {value}"),
1049 msg: format!("{value:?} is not a severity; write error, warning or note"),
1050 })
1051 })?;
1052 }
1053 _ => self.rule_field(&mut rule, field, value).map_err(at)?,
1054 }
1055 }
1056 if rule.message.is_empty() {
1057 return Err(ShapeError {
1058 decl: format!("rule {name}"),
1059 msg: format!("line {line}: rule {name} has no `message = TEXT` line"),
1060 });
1061 }
1062 Ok(rule)
1063 }
1064
1065 fn rule_line(&self, decl: &str, line: usize) -> Result<Rule, ShapeError> {
1067 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
1068 let form = "expected `rule NAME [error|warning|note] \"message\" = PATTERN`, or `rule NAME` over indented `field = value` lines";
1069 let (name, rest) = decl.split_once(char::is_whitespace).ok_or_else(|| err(form))?;
1070 let rest = rest.trim_start();
1071 let (severity, rest) = match rest.split_once(char::is_whitespace) {
1072 Some((word, after)) if !word.starts_with('"') => {
1073 let severity = Severity::parse(word)
1074 .ok_or_else(|| err(&format!("{word:?} is not a severity; write error, warning or note")))?;
1075 (severity, after.trim_start())
1076 }
1077 _ => (Severity::Warning, rest),
1078 };
1079 if !rest.starts_with('"') {
1080 return Err(err(form));
1081 }
1082 let (message, next) = read_test_text(rest, 0).map_err(|m| err(&m))?;
1083 let rest = rest[next..].trim_start();
1084 let source = rest.strip_prefix('=').ok_or_else(|| err(form))?.trim();
1085 if source.is_empty() {
1086 return Err(err("the rule has no pattern after `=`"));
1087 }
1088 let pattern = self.rule_pattern(source)?;
1089 Ok(Rule {
1090 file: None,
1091 line,
1092 name: name.to_string(),
1093 pattern,
1094 source: source.to_string(),
1095 message,
1096 severity,
1097 fix: None,
1098 files: Vec::new(),
1099 unless: Vec::new(),
1100 record: None,
1101 meta: Vec::new(),
1102 })
1103 }
1104
1105 fn rule_pattern(&self, source: &str) -> Result<Pattern, ShapeError> {
1107 crate::parser::parse_with_shapes(source, self).map_err(|e| ShapeError {
1108 decl: source.to_string(),
1109 msg: format!("pattern error at byte {}: {}", e.pos, e.msg),
1110 })
1111 }
1112
1113 fn rule_field(&self, rule: &mut Rule, field: &str, value: &str) -> Result<(), ShapeError> {
1117 let err = |msg: String| ShapeError { decl: format!("{field} = {value}"), msg };
1118 match field {
1119 "fix" => rule.fix = Some(value.to_string()),
1120 "files" => {
1121 rule.files = value.split(',').map(str::trim).filter(|g| !g.is_empty()).map(str::to_string).collect();
1122 if rule.files.is_empty() {
1123 return Err(err("files names at least one glob, as -g takes them".to_string()));
1124 }
1125 }
1126 "unless" => rule.unless.push(self.rule_pattern(value)?),
1127 "record" => {
1128 rule.record = Some(crate::records::RecordUnit::parse(value).map_err(err)?);
1129 }
1130 "record-start" => rule.record = Some(crate::records::RecordUnit::Start(self.rule_pattern(value)?)),
1131 "record-span" => rule.record = Some(crate::records::RecordUnit::Span(self.rule_pattern(value)?)),
1132 _ => match field.strip_prefix("meta.") {
1133 Some(key) if !key.is_empty() && key.chars().all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric()) => {
1134 rule.meta.push((key.to_string(), value.to_string()));
1135 }
1136 Some(key) => return Err(err(format!("{key:?} is not a metadata key; a key is alphanumerics, underscores and hyphens"))),
1137 None => {
1138 return Err(err(format!(
1139 "{field:?} is not a rule field; a rule takes pattern, message, severity, fix, files, unless, record, record-start, record-span and meta.KEY"
1140 )));
1141 }
1142 },
1143 }
1144 Ok(())
1145 }
1146
1147 fn rule_field_line(&mut self, field: &str, rest: &str) -> Result<(), ShapeError> {
1151 let err = |msg: String| ShapeError { decl: format!("{field} {rest}"), msg };
1152 let (name, rest) = rest
1153 .split_once(char::is_whitespace)
1154 .ok_or_else(|| err(format!("expected `{field} NAME = VALUE`")))?;
1155 let rest = rest.trim_start();
1156 let (key, value) = match field {
1157 "meta" => {
1158 let (key, value) = rest.split_once('=').ok_or_else(|| err("expected `meta NAME KEY = VALUE`".to_string()))?;
1159 (format!("meta.{}", key.trim()), value.trim())
1160 }
1161 _ => {
1162 let value = rest.strip_prefix('=').ok_or_else(|| err(format!("expected `{field} NAME = VALUE`")))?;
1163 (field.to_string(), value.trim())
1164 }
1165 };
1166 let position = self
1167 .rules
1168 .iter()
1169 .position(|r| r.name == name)
1170 .ok_or_else(|| err(format!("no rule named {name} is declared above this line")))?;
1171 let mut rule = self.rules[position].clone();
1172 self.rule_field(&mut rule, &key, value)?;
1173 if let Some(fix) = &rule.fix {
1174 crate::rewrite::Template::parse(fix, &rule.pattern.capture_names()).map_err(|e| {
1175 err(format!("fix error at byte {}: {}", e.pos, e.msg))
1176 })?;
1177 }
1178 self.rules[position] = rule;
1179 Ok(())
1180 }
1181
1182 fn add_rule(&mut self, rule: Rule, decl: &str) -> Result<(), ShapeError> {
1186 let err = |msg: String| ShapeError { decl: decl.to_string(), msg };
1187 self.check_name(decl, &rule.name)?;
1188 if self.rules.iter().any(|r| r.name == rule.name) {
1189 return Err(err(format!("rule {} is declared twice", rule.name)));
1190 }
1191 let bound = rule.pattern.capture_names();
1192 crate::rewrite::Template::parse_report(&rule.message, &bound)
1193 .map_err(|e| err(format!("message error at byte {}: {}", e.pos, e.msg)))?;
1194 if let Some(fix) = &rule.fix {
1195 crate::rewrite::Template::parse(fix, &bound)
1196 .map_err(|e| err(format!("fix error at byte {}: {}", e.pos, e.msg)))?;
1197 }
1198 self.lets.push((rule.name.clone(), rule.pattern.clone()));
1199 self.rules.push(rule);
1200 Ok(())
1201 }
1202
1203 #[must_use]
1206 pub(crate) fn with_library_shapes(&self, ids: &[u8]) -> ShapeSet {
1207 let mut set = self.clone();
1208 for &id in ids {
1209 if set.shapes.iter().any(|s| s.id == id) || set.kinds.iter().any(|k| k.id == id) {
1210 continue;
1211 }
1212 if let Some(shape) = crate::library::shape_of(id) {
1213 set.shapes.push(shape.clone());
1214 } else if let Some(kind) = crate::library::kind_of(id) {
1215 set.kinds.push(kind.clone());
1216 }
1217 }
1218 set
1219 }
1220
1221 #[must_use]
1225 pub fn longest_at(&self, input: &[u8], i: usize, when: Precedence) -> Option<(u8, usize)> {
1226 let mut best: Option<(u8, usize)> = None;
1227 for s in &self.shapes {
1228 if s.precedence != when {
1229 continue;
1230 }
1231 let hi = (i + s.window).min(input.len());
1236 let len = match s.guard {
1237 None => match s.pat.longest_prefix(&input[i..hi]) {
1238 Some(len) => len,
1239 None => continue,
1240 },
1241 Some(guard) => {
1242 let ends = s.pat.prefix_ends(&input[i..hi]);
1243 match ends.iter().rev().find(|&&len| len > 0 && guard(&input[i..i + len])) {
1244 Some(&len) => len,
1245 None => continue,
1246 }
1247 }
1248 };
1249 if len == 0 {
1250 continue;
1251 }
1252 if best.is_none_or(|(_, e)| i + len > e) {
1253 best = Some((s.id, i + len));
1254 }
1255 }
1256 best
1257 }
1258}
1259
1260const BUILTIN_ATOM_NAMES: &[&str] = &[
1262 "number",
1263 "word",
1264 "quoted",
1265 "ip",
1266 "url",
1267 "email",
1268 "timestamp",
1269 "punct",
1270 "whitespace",
1271 "version",
1272 "uuid",
1273 "mac",
1274 "hexcolor",
1275 "cidr",
1276 "bytesize",
1277 "percent",
1278 "money",
1279 "hash",
1280 "hashdigest",
1281 "duration",
1282 "path",
1283 "jwt",
1284 "creditcard",
1285 "card",
1286 "base64",
1287 "b64",
1288 "geo",
1289 "coord",
1290 "phone",
1291 "tel",
1292 "quantity",
1293 "qty",
1294];
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299
1300 #[test]
1301 fn declares_and_matches_a_bounded_shape() {
1302 let mut set = ShapeSet::new();
1303 let id = set.declare("order = `[A-Z]{3}-[0-9]{4}`", Precedence::Before).expect("declares");
1304 assert_eq!(id, 0);
1305 assert_eq!(set.id_of("order"), Some(0));
1306 assert_eq!(set.name_of(0), Some("order"));
1307 let input = b"ABC-1234 rest";
1308 assert_eq!(set.longest_at(input, 0, Precedence::Before), Some((0, 8)));
1309 assert_eq!(set.longest_at(input, 9, Precedence::Before), None);
1310 }
1311
1312 #[test]
1313 fn an_unbounded_shape_is_rejected_at_declaration() {
1314 let mut set = ShapeSet::new();
1315 let e = set.declare("wild = `[A-Z]+`", Precedence::Before).unwrap_err();
1316 assert!(e.msg.contains("bounded"), "got {:?}", e.msg);
1317 assert!(set.is_empty(), "a rejected shape is not stored");
1318 }
1319
1320 #[test]
1321 fn malformed_declarations_are_rejected() {
1322 let mut set = ShapeSet::new();
1323 for bad in [
1324 "no equals sign",
1325 "name = missing backticks",
1326 "= `[A-Z]`",
1327 "bad name = `[A-Z]`",
1328 "x = ``",
1329 ] {
1330 assert!(set.declare(bad, Precedence::Before).is_err(), "should reject: {bad}");
1331 }
1332 }
1333
1334 #[test]
1335 fn a_name_may_not_shadow_a_builtin_atom_or_repeat() {
1336 let mut set = ShapeSet::new();
1337 assert!(set.declare("ip = `[0-9]{3}`", Precedence::Before).is_err());
1338 set.declare("tag = `[A-Z]{2}`", Precedence::Before).expect("declares");
1339 assert!(set.declare("tag = `[a-z]{2}`", Precedence::Before).is_err());
1340 }
1341
1342 #[test]
1343 fn the_longest_shape_wins_and_ties_go_to_declaration_order() {
1344 let mut set = ShapeSet::new();
1345 set.declare("short = `[A-Z]{2}`", Precedence::Before).expect("declares");
1346 set.declare("long = `[A-Z]{4}`", Precedence::Before).expect("declares");
1347 assert_eq!(set.longest_at(b"ABCD", 0, Precedence::Before), Some((1, 4)));
1348 assert_eq!(set.longest_at(b"AB c", 0, Precedence::Before), Some((0, 2)));
1349 }
1350
1351 #[test]
1352 fn a_pattern_file_declares_sub_patterns_kinds_and_shapes() {
1353 let mut set = ShapeSet::new();
1354 set.declare_text(
1355 "# a file\nlet kv = \\W \"=\" \\N\nkind pair = \\{kv}\nshape tag = `[A-Z]{3}`\n\nshape-after code = `[0-9]{2}`\n",
1356 )
1357 .expect("declares");
1358 assert!(set.let_of("kv").is_some());
1359 assert_eq!(set.id_of("pair"), Some(0));
1360 assert_eq!(set.id_of("tag"), Some(1));
1361 assert_eq!(set.id_of("code"), Some(2));
1362 assert_eq!(set.name_of(0), Some("pair"));
1363 assert_eq!(set.pattern_kinds().len(), 1);
1364 assert!(!set.is_empty());
1365 let e = set.declare_text("shrug x = `a`").unwrap_err();
1366 assert!(e.msg.contains("line 1"), "{e}");
1367 let e = set.declare_text("let tag = \\W").unwrap_err();
1368 assert!(e.msg.contains("exists"), "{e}");
1369 set.declare_text("let kv = \\N").expect("a later let is accepted");
1370 assert!(matches!(set.let_of("kv"), Some(Pattern::Atom(_))), "a later let shadows an earlier one");
1371 }
1372
1373 #[test]
1374 fn test_lines_state_what_a_name_matches_and_name_their_line_when_it_does_not() {
1375 let mut set = ShapeSet::new();
1376 set.declare_text(
1377 "let rhs = \\N | \\Q\ntest assign accepts \"x = 1\" \"y = \\\"bob\\\"\" rejects \"x == 1\"\nkind assign = \\W \"=\" \\{rhs}\ntest rhs accepts \"42\" rejects \"forty\"\n",
1378 )
1379 .expect("declares");
1380 assert_eq!(set.tests().len(), 2);
1381 assert_eq!(set.tests()[0].line, 2);
1382 assert_eq!(set.tests()[0].accepts, vec!["x = 1".to_string(), "y = \"bob\"".to_string()]);
1383 assert_eq!(set.tests()[0].rejects, vec!["x == 1".to_string()]);
1384 assert!(set.run_tests().is_empty(), "{:?}", set.run_tests());
1385
1386 let mut set = ShapeSet::new();
1387 set.declare_text(
1388 "let rhs = \\N\ntest rhs accepts \"42\" \"\\\"bob\\\"\" rejects \"4 2\"\ntest nosuch accepts \"x\"\ntest iban accepts \"GB82 WEST 1234 5698 7654 32\" rejects \"GB82WEST12345698765433\"\n",
1389 )
1390 .expect("declares");
1391 let failures: Vec<String> = set.run_tests().iter().map(ToString::to_string).collect();
1392 assert_eq!(
1393 failures,
1394 vec![
1395 "line 2: rhs accepts \"\\\"bob\\\"\": no match\n tokens: quoted \"\\\"bob\\\"\"".to_string(),
1396 "line 2: rhs rejects \"4 2\": matched \"4\" at 0..1".to_string(),
1397 "line 3: nosuch cannot be tested: unknown named atom \\{nosuch}; it is neither a built-in, a declared shape, kind or sub-pattern, nor a library entry".to_string(),
1398 ]
1399 );
1400
1401 let e = ShapeSet::new().declare_text("test rhs \"42\"").expect_err("a text before a keyword");
1402 assert!(e.msg.contains("follows accepts, reads or rejects"), "{e}");
1403 let e = ShapeSet::new().declare_text("test rhs accepts").expect_err("no text");
1404 assert!(e.msg.contains("at least one text"), "{e}");
1405 let e = ShapeSet::new().declare_text("test rhs accepts \"4").expect_err("unterminated");
1406 assert!(e.msg.contains("unterminated"), "{e}");
1407 let e = ShapeSet::new().declare_text("test rhs accepts \"\\q\"").expect_err("unknown escape");
1408 assert!(e.msg.contains("unknown escape \\q"), "{e}");
1409 let e = ShapeSet::new().declare_text("test rhs matches \"4\"").expect_err("unknown word");
1410 assert!(e.msg.contains("found `matches`"), "{e}");
1411
1412 let mut set = ShapeSet::new();
1417 set.declare_text("test kelvin reads \"4.2K\" in \"cooled to 4.2K overnight\"\n").expect("declares");
1418 assert_eq!(set.tests()[0].reads, vec![("4.2K".to_string(), "cooled to 4.2K overnight".to_string())]);
1419 assert!(set.run_tests().is_empty(), "{:?}", set.run_tests());
1420 let e = ShapeSet::new().declare_text("test rhs reads \"4\"").expect_err("no text to read from");
1421 assert!(e.msg.contains("names no text to read it from"), "{e}");
1422 let e = ShapeSet::new().declare_text("test rhs reads \"4\" in").expect_err("no text after in");
1423 assert!(e.msg.contains("names no text to read it from"), "{e}");
1424 let e = ShapeSet::new().declare_text("test rhs reads \"4\" \"a 4 b\"").expect_err("no in");
1425 assert!(e.msg.contains("`in` stands between the span \"4\""), "{e}");
1426 let e = ShapeSet::new().declare_text("test rhs accepts \"4\" in \"a\"").expect_err("in alone");
1427 assert!(e.msg.contains("`in` follows the span a `reads` names"), "{e}");
1428 }
1429
1430 #[test]
1431 fn a_guarded_shape_takes_the_longest_end_its_check_accepts() {
1432 let mut set = ShapeSet::new();
1433 set.declare("digits = `[0-9]{4,8}`", Precedence::Before).expect("declares");
1434 set.shapes[0].guard = Some(|text: &[u8]| text.len() % 2 == 1);
1435 assert_eq!(set.longest_at(b"12345678 x", 0, Precedence::Before), Some((0, 7)));
1436 assert_eq!(set.longest_at(b"1234 x", 0, Precedence::Before), None);
1437 }
1438
1439 #[test]
1440 fn precedence_partitions_the_set() {
1441 let mut set = ShapeSet::new();
1442 set.declare("early = `[A-Z]{2}`", Precedence::Before).expect("declares");
1443 set.declare("late = `[0-9]{2}`", Precedence::After).expect("declares");
1444 assert_eq!(set.longest_at(b"AB", 0, Precedence::Before), Some((0, 2)));
1445 assert_eq!(set.longest_at(b"AB", 0, Precedence::After), None);
1446 assert_eq!(set.longest_at(b"12", 0, Precedence::After), Some((1, 2)));
1447 assert_eq!(set.longest_at(b"12", 0, Precedence::Before), None);
1448 }
1449}