1use crate::ast::Pattern;
6use crate::bytepat::{BytePat, parse as parse_bytepat};
7use crate::token::TokenKind;
8
9pub const LIBRARY_ID_BASE: u8 = 128;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Precedence {
17 Before,
19 After,
21}
22
23#[derive(Clone, Debug)]
25pub struct TokenShape {
26 pub name: String,
28 pub pat: BytePat,
30 pub window: usize,
32 pub precedence: Precedence,
34 pub id: u8,
37 pub guard: Option<fn(&[u8]) -> bool>,
40}
41
42#[derive(Clone, Copy, Debug)]
47pub struct UnitGuard {
48 pub symbols: &'static [&'static str],
50}
51
52impl UnitGuard {
53 #[must_use]
55 pub fn accepts(self, text: &[u8]) -> bool {
56 crate::quantity::context_span(text, self.symbols)
57 }
58}
59
60#[derive(Clone, Debug)]
63pub struct PatternKind {
64 pub name: String,
66 pub id: u8,
69 pub pattern: Pattern,
71 pub guard: Option<UnitGuard>,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ShapeError {
79 pub decl: String,
81 pub msg: String,
83}
84
85impl std::fmt::Display for ShapeError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(f, "token shape {:?}: {}", self.decl, self.msg)
88 }
89}
90
91impl std::error::Error for ShapeError {}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum Severity {
97 Error,
98 Warning,
99 Note,
100}
101
102impl Severity {
103 #[must_use]
105 pub fn parse(name: &str) -> Option<Severity> {
106 match name {
107 "error" => Some(Severity::Error),
108 "warning" => Some(Severity::Warning),
109 "note" => Some(Severity::Note),
110 _ => None,
111 }
112 }
113
114 #[must_use]
116 pub fn name(self) -> &'static str {
117 match self {
118 Severity::Error => "error",
119 Severity::Warning => "warning",
120 Severity::Note => "note",
121 }
122 }
123
124 #[must_use]
126 pub fn github(self) -> &'static str {
127 match self {
128 Severity::Error => "error",
129 Severity::Warning => "warning",
130 Severity::Note => "notice",
131 }
132 }
133}
134
135#[derive(Clone, Debug)]
141pub struct Rule {
142 pub file: Option<std::path::PathBuf>,
144 pub line: usize,
146 pub name: String,
148 pub pattern: Pattern,
150 pub source: String,
152 pub message: String,
156 pub severity: Severity,
158 pub fix: Option<String>,
161 pub files: Vec<String>,
164 pub unless: Vec<Pattern>,
166 pub record: Option<crate::records::RecordUnit>,
170 pub meta: Vec<(String, String)>,
172}
173
174impl Rule {
175 #[must_use]
178 pub fn on_records(&self) -> bool {
179 self.record.is_some() || !self.unless.is_empty()
180 }
181
182 #[must_use]
185 pub fn record_unit(&self) -> crate::records::RecordUnit {
186 self.record.clone().unwrap_or(crate::records::RecordUnit::Line)
187 }
188
189 #[must_use]
191 pub fn tags(&self) -> Vec<String> {
192 tags_of(&self.meta)
193 }
194}
195
196#[must_use]
198pub fn tags_of(meta: &[(String, String)]) -> Vec<String> {
199 meta.iter()
200 .filter(|(k, _)| k == "tags")
201 .flat_map(|(_, v)| v.split(',').map(str::trim).filter(|t| !t.is_empty()).map(str::to_string))
202 .collect()
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
208pub struct LibTest {
209 pub file: Option<std::path::PathBuf>,
211 pub line: usize,
213 pub name: String,
216 pub accepts: Vec<String>,
219 pub reads: Vec<(String, String)>,
226 pub rejects: Vec<String>,
228}
229
230#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct TestFailure {
233 pub file: Option<std::path::PathBuf>,
235 pub line: usize,
237 pub name: String,
239 pub msg: String,
241}
242
243impl std::fmt::Display for TestFailure {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match &self.file {
246 Some(path) => write!(f, "{}:{}: {} {}", path.display(), self.line, self.name, self.msg),
247 None => write!(f, "line {}: {} {}", self.line, self.name, self.msg),
248 }
249 }
250}
251
252fn significant_extent(input: &[u8], shapes: &ShapeSet) -> Option<(usize, usize)> {
256 let blobs = crate::lexer::blob_runs(input);
257 let toks = crate::lexer::lex_with_shapes(input, &blobs, shapes, 0);
258 let first = toks.iter().find(|t| t.is_significant())?.start();
259 let last = toks.iter().rev().find(|t| t.is_significant())?.end();
260 Some((first, last))
261}
262
263fn read_test_text(line: &str, open: usize) -> Result<(String, usize), String> {
266 let mut out = String::new();
267 let body = open + 1;
268 let mut chars = line[body..].char_indices();
269 while let Some((k, c)) = chars.next() {
270 match c {
271 '"' => return Ok((out, body + k + 1)),
272 '\\' => match chars.next() {
273 Some((_, '"')) => out.push('"'),
274 Some((_, '\\')) => out.push('\\'),
275 Some((_, 'n')) => out.push('\n'),
276 Some((_, 't')) => out.push('\t'),
277 Some((_, other)) => {
278 return Err(format!(
279 "unknown escape \\{other} in a test text; it reads \\\", \\\\, \\n and \\t"
280 ));
281 }
282 None => return Err("a backslash ends the test text".to_string()),
283 },
284 other => out.push(other),
285 }
286 }
287 Err("unterminated test text; it closes with a double quote".to_string())
288}
289
290#[derive(Clone, Debug, Default)]
292pub struct ShapeSet {
293 shapes: Vec<TokenShape>,
294 kinds: Vec<PatternKind>,
295 lets: Vec<(String, Pattern)>,
298 tests: Vec<LibTest>,
300 rules: Vec<Rule>,
302 fields: Vec<(String, Vec<crate::infer::marks::FieldMark>)>,
306 next_id: u8,
309 library_off: bool,
313 base: Option<std::path::PathBuf>,
318}
319
320impl ShapeSet {
321 #[must_use]
323 pub fn new() -> Self {
324 ShapeSet::default()
325 }
326
327 pub(crate) fn without_library() -> Self {
330 ShapeSet { library_off: true, ..ShapeSet::default() }
331 }
332
333 #[must_use]
336 pub fn consults_library(&self) -> bool {
337 !self.library_off
338 }
339
340 #[must_use]
343 pub fn base_dir(&self) -> Option<&std::path::Path> {
344 self.base.as_deref()
345 }
346
347 pub fn set_base_dir(&mut self, dir: Option<std::path::PathBuf>) {
352 self.base = dir;
353 }
354
355 pub fn declare_file(&mut self, path: &std::path::Path) -> Result<(), ShapeError> {
364 let text = match std::fs::read(path) {
365 Ok(bytes) => String::from_utf8_lossy(&crate::encoding::decode(bytes)).into_owned(),
366 Err(e) => {
367 return Err(ShapeError {
368 decl: path.display().to_string(),
369 msg: format!("cannot read the pattern file: {e}"),
370 });
371 }
372 };
373 let before = self.base.take();
374 self.base = path.parent().map(std::path::Path::to_path_buf);
375 let tests_before = self.tests.len();
376 let rules_before = self.rules.len();
377 let declared = self.declare_text(&text);
378 self.base = before;
379 for test in &mut self.tests[tests_before..] {
380 test.file = Some(path.to_path_buf());
381 }
382 for rule in &mut self.rules[rules_before..] {
383 rule.file = Some(path.to_path_buf());
384 }
385 declared.map_err(|mut e| {
386 e.msg = format!("{}: {}", path.display(), e.msg);
387 e
388 })
389 }
390
391 pub fn declare_file_members(&mut self, path: &std::path::Path) -> Result<Vec<(String, Pattern)>, ShapeError> {
400 let text = match std::fs::read(path) {
401 Ok(bytes) => String::from_utf8_lossy(&crate::encoding::decode(bytes)).into_owned(),
402 Err(e) => {
403 return Err(ShapeError {
404 decl: path.display().to_string(),
405 msg: format!("cannot read the pattern file: {e}"),
406 });
407 }
408 };
409 let before = self.base.take();
410 self.base = path.parent().map(std::path::Path::to_path_buf);
411 let tests_before = self.tests.len();
412 let rules_before = self.rules.len();
413 let mut members = Vec::new();
414 let declared = self.declare_lines(&text, Some(&mut members));
415 self.base = before;
416 for test in &mut self.tests[tests_before..] {
417 test.file = Some(path.to_path_buf());
418 }
419 for rule in &mut self.rules[rules_before..] {
420 rule.file = Some(path.to_path_buf());
421 }
422 match declared {
423 Ok(()) => Ok(members),
424 Err(mut e) => {
425 e.msg = format!("{}: {}", path.display(), e.msg);
426 Err(e)
427 }
428 }
429 }
430
431 #[must_use]
434 pub fn is_empty(&self) -> bool {
435 self.shapes.is_empty() && self.kinds.is_empty()
436 }
437
438 #[must_use]
440 pub fn shapes(&self) -> &[TokenShape] {
441 &self.shapes
442 }
443
444 #[must_use]
446 pub fn pattern_kinds(&self) -> &[PatternKind] {
447 &self.kinds
448 }
449
450 #[must_use]
452 pub fn lets(&self) -> &[(String, Pattern)] {
453 &self.lets
454 }
455
456 #[must_use]
458 pub fn tests(&self) -> &[LibTest] {
459 &self.tests
460 }
461
462 #[must_use]
464 pub fn rules(&self) -> &[Rule] {
465 &self.rules
466 }
467
468 #[must_use]
470 pub fn rule_of(&self, name: &str) -> Option<&Rule> {
471 self.rules.iter().find(|r| r.name == name)
472 }
473
474 #[must_use]
477 pub fn id_of(&self, name: &str) -> Option<u8> {
478 self.shapes
479 .iter()
480 .find(|s| s.name == name)
481 .map(|s| s.id)
482 .or_else(|| self.kinds.iter().find(|k| k.name == name).map(|k| k.id))
483 }
484
485 #[must_use]
487 pub fn name_of(&self, id: u8) -> Option<&str> {
488 self.shapes
489 .iter()
490 .find(|s| s.id == id)
491 .map(|s| s.name.as_str())
492 .or_else(|| self.kinds.iter().find(|k| k.id == id).map(|k| k.name.as_str()))
493 .or_else(|| crate::library::name_of(id))
494 }
495
496 #[must_use]
500 pub fn kind_name(&self, kind: TokenKind) -> String {
501 match kind {
502 TokenKind::Custom(id) => match self.name_of(id) {
503 Some(name) => name.to_string(),
504 None => kind.name().to_string(),
505 },
506 other => other.name().to_string(),
507 }
508 }
509
510 #[must_use]
512 pub fn let_of(&self, name: &str) -> Option<&Pattern> {
513 self.lets.iter().rev().find(|(n, _)| n == name).map(|(_, p)| p)
514 }
515
516 #[must_use]
518 pub fn fields_of(&self, name: &str) -> Option<&[crate::infer::marks::FieldMark]> {
519 self.fields.iter().find(|(n, _)| n == name).map(|(_, f)| f.as_slice())
520 }
521
522 pub fn declare_fields(&mut self, decl: &str) -> Result<(), ShapeError> {
534 let err = |msg: String| ShapeError { decl: decl.to_string(), msg };
535 let (name, rest) = decl.split_once(char::is_whitespace).unwrap_or((decl, ""));
536 let pattern = self
537 .let_of(name)
538 .ok_or_else(|| err(format!("no sub-pattern is declared as {name} above; a fields line follows its let")))?;
539 if self.fields_of(name).is_some() {
540 return Err(err(format!("{name} has a fields line already")));
541 }
542 let marks = crate::infer::marks::field_marks(rest).map_err(|e| err(e.to_string()))?;
543 if marks.is_empty() {
544 return Err(err("a fields line names at least one field".to_string()));
545 }
546 let bound = pattern.capture_names();
547 for (k, mark) in marks.iter().enumerate() {
548 if marks[..k].iter().any(|m| m.name == mark.name) {
549 return Err(err(format!("{} is named twice", mark.written())));
550 }
551 let body = match &mark.accessor {
552 Some(accessor) => format!("{}:{accessor}", mark.name),
553 None => mark.name.clone(),
554 };
555 crate::rewrite::Reference::parse(&body, &bound).map_err(|e| err(format!("{}: {}", mark.written(), e.msg)))?;
556 }
557 self.fields.push((name.to_string(), marks));
558 Ok(())
559 }
560
561 #[must_use]
566 pub fn is_free(&self, name: &str) -> bool {
567 !name.is_empty()
568 && name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
569 && !BUILTIN_ATOM_NAMES.contains(&name)
570 && self.id_of(name).is_none()
571 && self.let_of(name).is_none()
572 && crate::library::id_of(name).is_none()
573 && crate::library::let_of(name).is_none()
574 }
575
576 fn check_name(&self, decl: &str, name: &str) -> Result<(), ShapeError> {
579 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
580 if name.is_empty() || !name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
581 return Err(err("a name is alphanumerics and underscores"));
582 }
583 if BUILTIN_ATOM_NAMES.contains(&name) {
584 return Err(err("that name belongs to a built-in atom; pick another"));
585 }
586 if self.id_of(name).is_some() {
587 return Err(err("a shape or kind with that name exists"));
588 }
589 Ok(())
590 }
591
592 fn take_id(&mut self, decl: &str) -> Result<u8, ShapeError> {
594 if self.next_id >= LIBRARY_ID_BASE {
595 return Err(ShapeError {
596 decl: decl.to_string(),
597 msg: format!("at most {LIBRARY_ID_BASE} shapes and kinds may be declared"),
598 });
599 }
600 let id = self.next_id;
601 self.next_id += 1;
602 Ok(id)
603 }
604
605 pub fn declare(&mut self, decl: &str, precedence: Precedence) -> Result<u8, ShapeError> {
613 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
614 let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = `pattern`"))?;
615 let name = name.trim();
616 self.check_name(decl, name)?;
617 let body = rest.trim();
618 let inner = body
619 .strip_prefix('`')
620 .and_then(|b| b.strip_suffix('`'))
621 .ok_or_else(|| err("the pattern must be enclosed in backticks"))?;
622 let pat = parse_bytepat(inner.as_bytes()).map_err(|m| err(&m))?;
623 let window = pat.max_len().ok_or_else(|| {
624 err("the pattern must be bounded; `*`, `+` and `{m,}` have no fixed length")
625 })?;
626 if window == 0 {
627 return Err(err("the pattern matches nothing, so it would never end a token"));
628 }
629 let id = self.take_id(decl)?;
630 self.shapes.push(TokenShape { name: name.to_string(), pat, window, precedence, id, guard: None });
631 Ok(id)
632 }
633
634 pub(crate) fn push_library_shape(&mut self, shape: TokenShape) {
637 self.shapes.push(shape);
638 }
639
640 pub fn declare_kind(&mut self, decl: &str) -> Result<u8, ShapeError> {
649 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
650 let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = pattern`"))?;
651 let name = name.trim();
652 self.check_name(decl, name)?;
653 let pattern = crate::parser::parse_with_shapes(rest.trim(), self)
654 .map_err(|e| err(&format!("pattern error at byte {}: {}", e.pos, e.msg)))?;
655 let id = self.take_id(decl)?;
656 self.kinds.push(PatternKind { name: name.to_string(), id, pattern, guard: None });
657 Ok(id)
658 }
659
660 pub(crate) fn push_library_kind(&mut self, kind: PatternKind) {
663 self.kinds.push(kind);
664 }
665
666 pub fn declare_let(&mut self, decl: &str) -> Result<(), ShapeError> {
675 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
676 let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = pattern`"))?;
677 let name = name.trim();
678 self.check_name(decl, name)?;
679 let pattern = crate::parser::parse_with_shapes(rest.trim(), self)
680 .map_err(|e| err(&format!("pattern error at byte {}: {}", e.pos, e.msg)))?;
681 self.lets.push((name.to_string(), pattern));
682 Ok(())
683 }
684
685 pub fn declare_test(&mut self, decl: &str, line: usize) -> Result<(), ShapeError> {
698 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
699 let (name, rest) = decl.split_once(char::is_whitespace).unwrap_or((decl, ""));
700 if name.is_empty() || !name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
701 return Err(err("expected `test NAME accepts \"text\"... rejects \"text\"...`"));
702 }
703 enum Side {
705 Accepts,
706 Rejects,
707 Reads,
709 ReadsSpan(String),
711 ReadsIn(String),
713 }
714 let mut accepts = Vec::new();
715 let mut rejects = Vec::new();
716 let mut reads: Vec<(String, String)> = Vec::new();
717 let mut side: Option<Side> = None;
718 let mut i = 0;
719 while i < rest.len() {
720 let c = rest.as_bytes()[i];
721 if c.is_ascii_whitespace() {
722 i += 1;
723 } else if c == b'"' {
724 let (text, next) = read_test_text(rest, i).map_err(|m| err(&m))?;
725 side = match side.take() {
726 Some(Side::Accepts) => {
727 accepts.push(text);
728 Some(Side::Accepts)
729 }
730 Some(Side::Rejects) => {
731 rejects.push(text);
732 Some(Side::Rejects)
733 }
734 Some(Side::Reads) => Some(Side::ReadsSpan(text)),
737 Some(Side::ReadsSpan(span)) => {
738 return Err(err(&format!(
739 "`in` goes between the span {span:?} and the text it is read from"
740 )));
741 }
742 Some(Side::ReadsIn(span)) => {
743 reads.push((span, text));
744 None
748 }
749 None => return Err(err("a text follows accepts, reads or rejects")),
750 };
751 i = next;
752 } else {
753 let end = rest[i..].find(char::is_whitespace).map_or(rest.len(), |n| i + n);
754 let word = &rest[i..end];
755 if word == "in" {
758 let Some(Side::ReadsSpan(span)) = side.take() else {
759 return Err(err("`in` follows the span a `reads` names"));
760 };
761 side = Some(Side::ReadsIn(span));
762 i = end;
763 continue;
764 }
765 side = Some(match word {
766 "accepts" => Side::Accepts,
767 "rejects" => Side::Rejects,
768 "reads" => Side::Reads,
769 other => {
770 return Err(err(&format!(
771 "expected accepts, reads, rejects or a quoted text, found `{other}`"
772 )));
773 }
774 });
775 i = end;
776 }
777 }
778 if let Some(Side::ReadsSpan(span) | Side::ReadsIn(span)) = side {
779 return Err(err(&format!("reads {span:?} names no text to read it from")));
780 }
781 if accepts.is_empty() && rejects.is_empty() && reads.is_empty() {
782 return Err(err("a test names at least one text it accepts, reads or rejects"));
783 }
784 self.tests.push(LibTest {
785 file: None,
786 line,
787 name: name.to_string(),
788 accepts,
789 reads,
790 rejects,
791 });
792 Ok(())
793 }
794
795 #[must_use]
802 pub fn run_tests(&self) -> Vec<TestFailure> {
803 let mut failures = Vec::new();
804 for test in &self.tests {
805 let fail = |msg: String| TestFailure {
806 file: test.file.clone(),
807 line: test.line,
808 name: test.name.clone(),
809 msg,
810 };
811 let pattern = match crate::parser::parse_with_shapes(&format!("\\{{{}}}", test.name), self) {
812 Ok(pattern) => pattern,
813 Err(e) => {
814 failures.push(fail(format!("cannot be tested: {}", e.msg)));
815 continue;
816 }
817 };
818 let lexed_under = self.with_library_shapes(&pattern.library_kinds());
819 for text in &test.accepts {
820 let input = text.as_bytes();
821 let found = crate::engine::scan_with_shapes(&pattern, input, self);
822 let whole = significant_extent(input, &lexed_under);
823 let lexing = || {
828 let explainer = crate::explain::Explainer::new(&pattern, input, self);
829 let toks: Vec<String> = explainer
830 .tokens(0..input.len())
831 .iter()
832 .map(|(kind, text)| format!("{kind} {text:?}"))
833 .collect();
834 format!("\n tokens: {}", toks.join(", "))
835 };
836 match (found.first(), whole) {
837 (Some(m), Some((first, last))) if m.start() == first && m.end() == last => {}
838 (Some(m), _) => failures.push(fail(format!(
839 "accepts {text:?}: matched only {:?} at {}..{}{}",
840 String::from_utf8_lossy(&input[m.range()]),
841 m.start,
842 m.end,
843 lexing()
844 ))),
845 (None, _) => failures.push(fail(format!("accepts {text:?}: no match{}", lexing()))),
846 }
847 }
848 for (span, text) in &test.reads {
854 let input = text.as_bytes();
855 let Some(at) = text.find(span.as_str()) else {
856 failures.push(fail(format!("reads {span:?}: {text:?} does not hold it")));
857 continue;
858 };
859 let want = at..at + span.len();
860 match crate::engine::scan_with_shapes(&pattern, input, self).first() {
861 Some(m) if m.range() == want => {}
862 Some(m) => failures.push(fail(format!(
863 "reads {span:?} in {text:?}: read {:?} at {}..{}",
864 String::from_utf8_lossy(&input[m.range()]),
865 m.start,
866 m.end
867 ))),
868 None => failures.push(fail(format!("reads {span:?} in {text:?}: no match"))),
869 }
870 }
871 for text in &test.rejects {
872 let input = text.as_bytes();
873 if let Some(m) = crate::engine::scan_with_shapes(&pattern, input, self).first() {
874 failures.push(fail(format!(
875 "rejects {text:?}: matched {:?} at {}..{}",
876 String::from_utf8_lossy(&input[m.range()]),
877 m.start,
878 m.end
879 )));
880 }
881 }
882 }
883 failures
884 }
885
886 pub fn declare_text(&mut self, text: &str) -> Result<(), ShapeError> {
902 self.declare_lines(text, None)
903 }
904
905 pub fn declare_lines(
916 &mut self,
917 text: &str,
918 mut members: Option<&mut Vec<(String, Pattern)>>,
919 ) -> Result<(), ShapeError> {
920 let lines: Vec<&str> = text.lines().collect();
921 let mut i = 0;
922 while i < lines.len() {
923 let n = i + 1;
924 let line = lines[i].trim();
925 i += 1;
926 if line.is_empty() || line.starts_with('#') {
927 continue;
928 }
929 let at = |mut e: ShapeError| {
930 e.msg = format!("line {n}: {}", e.msg);
931 e
932 };
933 let (keyword, rest) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
934 let rest = rest.trim();
935 match keyword {
936 "let" => {
937 self.declare_let(rest).map_err(at)?;
938 if let Some(members) = members.as_mut() {
939 let name = rest.split_once('=').map_or(rest, |(name, _)| name).trim();
940 let pattern = self.let_of(name).cloned().ok_or_else(|| {
941 at(ShapeError { decl: line.to_string(), msg: "the let declared nothing".to_string() })
942 })?;
943 members.push((name.to_string(), pattern));
944 }
945 }
946 "kind" => {
947 self.declare_kind(rest).map_err(at)?;
948 }
949 "shape" => {
950 self.declare(rest, Precedence::Before).map_err(at)?;
951 }
952 "shape-after" => {
953 self.declare(rest, Precedence::After).map_err(at)?;
954 }
955 "test" => self.declare_test(rest, n).map_err(at)?,
956 "fields" => self.declare_fields(rest).map_err(at)?,
957 "rule" => {
958 let is_block = !rest.is_empty() && rest.chars().all(|c| c == '_' || c.is_ascii_alphanumeric());
959 let rule = if is_block {
960 let mut fields: Vec<(usize, &str)> = Vec::new();
964 while i < lines.len() {
965 let raw = lines[i];
966 let body = raw.trim();
967 if body.is_empty() || body.starts_with('#') {
968 i += 1;
969 continue;
970 }
971 if !raw.starts_with([' ', '\t']) {
972 break;
973 }
974 fields.push((i + 1, body));
975 i += 1;
976 }
977 self.rule_block(rest, n, &fields)?
978 } else {
979 self.rule_line(rest, n).map_err(at)?
980 };
981 let name = rule.name.clone();
982 let pattern = rule.pattern.clone();
983 self.add_rule(rule, line).map_err(at)?;
984 if let Some(members) = members.as_mut() {
985 members.push((name, pattern));
986 }
987 }
988 "fix" | "meta" | "files" | "unless" | "record" | "record-start" | "record-span" => {
989 self.rule_field_line(keyword, rest).map_err(at)?;
990 }
991 other => match members.as_mut() {
992 Some(members) => {
993 let pattern = crate::parser::parse_with_shapes(line, self).map_err(|e| {
994 at(ShapeError {
995 decl: line.to_string(),
996 msg: format!("pattern error at byte {}: {}", e.pos, e.msg),
997 })
998 })?;
999 members.push((n.to_string(), pattern));
1000 }
1001 None => {
1002 return Err(at(ShapeError {
1003 decl: line.to_string(),
1004 msg: format!(
1005 "unknown declaration `{other}`; a line opens with let, kind, shape, shape-after, test, fields or rule, or one of a rule's fix, meta, files, unless, record, record-start or record-span"
1006 ),
1007 }));
1008 }
1009 },
1010 }
1011 }
1012 Ok(())
1013 }
1014
1015 fn rule_block(&self, name: &str, line: usize, fields: &[(usize, &str)]) -> Result<Rule, ShapeError> {
1020 let mut split: Vec<(usize, &str, &str)> = Vec::with_capacity(fields.len());
1021 for &(n, field_line) in fields {
1022 let (field, value) = field_line.split_once('=').ok_or_else(|| ShapeError {
1023 decl: field_line.to_string(),
1024 msg: format!("line {n}: a rule's field line is `field = value`"),
1025 })?;
1026 split.push((n, field.trim(), value.trim()));
1027 }
1028 let Some(&(n, _, source)) = split.iter().find(|(_, field, _)| *field == "pattern") else {
1029 return Err(ShapeError {
1030 decl: format!("rule {name}"),
1031 msg: format!("line {line}: rule {name} has no `pattern = PATTERN` line"),
1032 });
1033 };
1034 let pattern = self.rule_pattern(source).map_err(|mut e| {
1035 e.msg = format!("line {n}: {}", e.msg);
1036 e
1037 })?;
1038 let mut rule = Rule {
1039 file: None,
1040 line,
1041 name: name.to_string(),
1042 pattern,
1043 source: source.to_string(),
1044 message: String::new(),
1045 severity: Severity::Warning,
1046 fix: None,
1047 files: Vec::new(),
1048 unless: Vec::new(),
1049 record: None,
1050 meta: Vec::new(),
1051 };
1052 for &(n, field, value) in &split {
1053 let at = |mut e: ShapeError| {
1054 e.msg = format!("line {n}: {}", e.msg);
1055 e
1056 };
1057 match field {
1058 "pattern" => {}
1059 "message" => rule.message = value.to_string(),
1060 "severity" => {
1061 rule.severity = Severity::parse(value).ok_or_else(|| {
1062 at(ShapeError {
1063 decl: format!("{field} = {value}"),
1064 msg: format!("{value:?} is not a severity; write error, warning or note"),
1065 })
1066 })?;
1067 }
1068 _ => self.rule_field(&mut rule, field, value).map_err(at)?,
1069 }
1070 }
1071 if rule.message.is_empty() {
1072 return Err(ShapeError {
1073 decl: format!("rule {name}"),
1074 msg: format!("line {line}: rule {name} has no `message = TEXT` line"),
1075 });
1076 }
1077 Ok(rule)
1078 }
1079
1080 fn rule_line(&self, decl: &str, line: usize) -> Result<Rule, ShapeError> {
1082 let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
1083 let form = "expected `rule NAME [error|warning|note] \"message\" = PATTERN`, or `rule NAME` over indented `field = value` lines";
1084 let (name, rest) = decl.split_once(char::is_whitespace).ok_or_else(|| err(form))?;
1085 let rest = rest.trim_start();
1086 let (severity, rest) = match rest.split_once(char::is_whitespace) {
1087 Some((word, after)) if !word.starts_with('"') => {
1088 let severity = Severity::parse(word)
1089 .ok_or_else(|| err(&format!("{word:?} is not a severity; write error, warning or note")))?;
1090 (severity, after.trim_start())
1091 }
1092 _ => (Severity::Warning, rest),
1093 };
1094 if !rest.starts_with('"') {
1095 return Err(err(form));
1096 }
1097 let (message, next) = read_test_text(rest, 0).map_err(|m| err(&m))?;
1098 let rest = rest[next..].trim_start();
1099 let source = rest.strip_prefix('=').ok_or_else(|| err(form))?.trim();
1100 if source.is_empty() {
1101 return Err(err("the rule has no pattern after `=`"));
1102 }
1103 let pattern = self.rule_pattern(source)?;
1104 Ok(Rule {
1105 file: None,
1106 line,
1107 name: name.to_string(),
1108 pattern,
1109 source: source.to_string(),
1110 message,
1111 severity,
1112 fix: None,
1113 files: Vec::new(),
1114 unless: Vec::new(),
1115 record: None,
1116 meta: Vec::new(),
1117 })
1118 }
1119
1120 fn rule_pattern(&self, source: &str) -> Result<Pattern, ShapeError> {
1122 crate::parser::parse_with_shapes(source, self).map_err(|e| ShapeError {
1123 decl: source.to_string(),
1124 msg: format!("pattern error at byte {}: {}", e.pos, e.msg),
1125 })
1126 }
1127
1128 fn rule_field(&self, rule: &mut Rule, field: &str, value: &str) -> Result<(), ShapeError> {
1132 let err = |msg: String| ShapeError { decl: format!("{field} = {value}"), msg };
1133 match field {
1134 "fix" => rule.fix = Some(value.to_string()),
1135 "files" => {
1136 rule.files = value.split(',').map(str::trim).filter(|g| !g.is_empty()).map(str::to_string).collect();
1137 if rule.files.is_empty() {
1138 return Err(err("files names at least one glob, as -g takes them".to_string()));
1139 }
1140 }
1141 "unless" => rule.unless.push(self.rule_pattern(value)?),
1142 "record" => {
1143 rule.record = Some(crate::records::RecordUnit::parse(value).map_err(err)?);
1144 }
1145 "record-start" => rule.record = Some(crate::records::RecordUnit::Start(self.rule_pattern(value)?)),
1146 "record-span" => rule.record = Some(crate::records::RecordUnit::Span(self.rule_pattern(value)?)),
1147 _ => match field.strip_prefix("meta.") {
1148 Some(key) if !key.is_empty() && key.chars().all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric()) => {
1149 rule.meta.push((key.to_string(), value.to_string()));
1150 }
1151 Some(key) => return Err(err(format!("{key:?} is not a metadata key; a key is alphanumerics, underscores and hyphens"))),
1152 None => {
1153 return Err(err(format!(
1154 "{field:?} is not a rule field; a rule takes pattern, message, severity, fix, files, unless, record, record-start, record-span and meta.KEY"
1155 )));
1156 }
1157 },
1158 }
1159 Ok(())
1160 }
1161
1162 fn rule_field_line(&mut self, field: &str, rest: &str) -> Result<(), ShapeError> {
1166 let err = |msg: String| ShapeError { decl: format!("{field} {rest}"), msg };
1167 let (name, rest) = rest
1168 .split_once(char::is_whitespace)
1169 .ok_or_else(|| err(format!("expected `{field} NAME = VALUE`")))?;
1170 let rest = rest.trim_start();
1171 let (key, value) = match field {
1172 "meta" => {
1173 let (key, value) = rest.split_once('=').ok_or_else(|| err("expected `meta NAME KEY = VALUE`".to_string()))?;
1174 (format!("meta.{}", key.trim()), value.trim())
1175 }
1176 _ => {
1177 let value = rest.strip_prefix('=').ok_or_else(|| err(format!("expected `{field} NAME = VALUE`")))?;
1178 (field.to_string(), value.trim())
1179 }
1180 };
1181 let position = self
1182 .rules
1183 .iter()
1184 .position(|r| r.name == name)
1185 .ok_or_else(|| err(format!("no rule named {name} is declared above this line")))?;
1186 let mut rule = self.rules[position].clone();
1187 self.rule_field(&mut rule, &key, value)?;
1188 if let Some(fix) = &rule.fix {
1189 crate::rewrite::Template::parse(fix, &rule.pattern.capture_names()).map_err(|e| {
1190 err(format!("fix error at byte {}: {}", e.pos, e.msg))
1191 })?;
1192 }
1193 self.rules[position] = rule;
1194 Ok(())
1195 }
1196
1197 fn add_rule(&mut self, rule: Rule, decl: &str) -> Result<(), ShapeError> {
1201 let err = |msg: String| ShapeError { decl: decl.to_string(), msg };
1202 self.check_name(decl, &rule.name)?;
1203 if self.rules.iter().any(|r| r.name == rule.name) {
1204 return Err(err(format!("rule {} is declared twice", rule.name)));
1205 }
1206 let bound = rule.pattern.capture_names();
1207 crate::rewrite::Template::parse_report(&rule.message, &bound)
1208 .map_err(|e| err(format!("message error at byte {}: {}", e.pos, e.msg)))?;
1209 if let Some(fix) = &rule.fix {
1210 crate::rewrite::Template::parse(fix, &bound)
1211 .map_err(|e| err(format!("fix error at byte {}: {}", e.pos, e.msg)))?;
1212 }
1213 self.lets.push((rule.name.clone(), rule.pattern.clone()));
1214 self.rules.push(rule);
1215 Ok(())
1216 }
1217
1218 #[must_use]
1221 pub(crate) fn with_library_shapes(&self, ids: &[u8]) -> ShapeSet {
1222 let mut set = self.clone();
1223 for &id in ids {
1224 if set.shapes.iter().any(|s| s.id == id) || set.kinds.iter().any(|k| k.id == id) {
1225 continue;
1226 }
1227 if let Some(shape) = crate::library::shape_of(id) {
1228 set.shapes.push(shape.clone());
1229 } else if let Some(kind) = crate::library::kind_of(id) {
1230 set.kinds.push(kind.clone());
1231 }
1232 }
1233 set
1234 }
1235
1236 #[must_use]
1240 pub fn longest_at(&self, input: &[u8], i: usize, when: Precedence) -> Option<(u8, usize)> {
1241 let mut best: Option<(u8, usize)> = None;
1242 for s in &self.shapes {
1243 if s.precedence != when {
1244 continue;
1245 }
1246 let hi = (i + s.window).min(input.len());
1251 let len = match s.guard {
1252 None => match s.pat.longest_prefix(&input[i..hi]) {
1253 Some(len) => len,
1254 None => continue,
1255 },
1256 Some(guard) => {
1257 let ends = s.pat.prefix_ends(&input[i..hi]);
1258 match ends.iter().rev().find(|&&len| len > 0 && guard(&input[i..i + len])) {
1259 Some(&len) => len,
1260 None => continue,
1261 }
1262 }
1263 };
1264 if len == 0 {
1265 continue;
1266 }
1267 if best.is_none_or(|(_, e)| i + len > e) {
1268 best = Some((s.id, i + len));
1269 }
1270 }
1271 best
1272 }
1273}
1274
1275const BUILTIN_ATOM_NAMES: &[&str] = &[
1277 "number",
1278 "word",
1279 "quoted",
1280 "ip",
1281 "url",
1282 "email",
1283 "timestamp",
1284 "punct",
1285 "whitespace",
1286 "version",
1287 "uuid",
1288 "mac",
1289 "hexcolor",
1290 "cidr",
1291 "bytesize",
1292 "percent",
1293 "money",
1294 "hash",
1295 "hashdigest",
1296 "duration",
1297 "path",
1298 "jwt",
1299 "creditcard",
1300 "card",
1301 "base64",
1302 "b64",
1303 "hex",
1304 "geo",
1305 "coord",
1306 "phone",
1307 "tel",
1308 "quantity",
1309 "qty",
1310];
1311
1312#[cfg(test)]
1313mod tests {
1314 use super::*;
1315
1316 #[test]
1317 fn declares_and_matches_a_bounded_shape() {
1318 let mut set = ShapeSet::new();
1319 let id = set.declare("order = `[A-Z]{3}-[0-9]{4}`", Precedence::Before).expect("declares");
1320 assert_eq!(id, 0);
1321 assert_eq!(set.id_of("order"), Some(0));
1322 assert_eq!(set.name_of(0), Some("order"));
1323 let input = b"ABC-1234 rest";
1324 assert_eq!(set.longest_at(input, 0, Precedence::Before), Some((0, 8)));
1325 assert_eq!(set.longest_at(input, 9, Precedence::Before), None);
1326 }
1327
1328 #[test]
1329 fn an_unbounded_shape_is_rejected_at_declaration() {
1330 let mut set = ShapeSet::new();
1331 let e = set.declare("wild = `[A-Z]+`", Precedence::Before).unwrap_err();
1332 assert!(e.msg.contains("bounded"), "got {:?}", e.msg);
1333 assert!(set.is_empty(), "a rejected shape is not stored");
1334 }
1335
1336 #[test]
1337 fn malformed_declarations_are_rejected() {
1338 let mut set = ShapeSet::new();
1339 for bad in [
1340 "no equals sign",
1341 "name = missing backticks",
1342 "= `[A-Z]`",
1343 "bad name = `[A-Z]`",
1344 "x = ``",
1345 ] {
1346 assert!(set.declare(bad, Precedence::Before).is_err(), "should reject: {bad}");
1347 }
1348 }
1349
1350 #[test]
1351 fn a_name_may_not_shadow_a_builtin_atom_or_repeat() {
1352 let mut set = ShapeSet::new();
1353 assert!(set.declare("ip = `[0-9]{3}`", Precedence::Before).is_err());
1354 set.declare("tag = `[A-Z]{2}`", Precedence::Before).expect("declares");
1355 assert!(set.declare("tag = `[a-z]{2}`", Precedence::Before).is_err());
1356 }
1357
1358 #[test]
1359 fn the_longest_shape_wins_and_ties_go_to_declaration_order() {
1360 let mut set = ShapeSet::new();
1361 set.declare("short = `[A-Z]{2}`", Precedence::Before).expect("declares");
1362 set.declare("long = `[A-Z]{4}`", Precedence::Before).expect("declares");
1363 assert_eq!(set.longest_at(b"ABCD", 0, Precedence::Before), Some((1, 4)));
1364 assert_eq!(set.longest_at(b"AB c", 0, Precedence::Before), Some((0, 2)));
1365 }
1366
1367 #[test]
1368 fn a_pattern_file_declares_sub_patterns_kinds_and_shapes() {
1369 let mut set = ShapeSet::new();
1370 set.declare_text(
1371 "# a file\nlet kv = \\W \"=\" \\N\nkind pair = \\{kv}\nshape tag = `[A-Z]{3}`\n\nshape-after code = `[0-9]{2}`\n",
1372 )
1373 .expect("declares");
1374 assert!(set.let_of("kv").is_some());
1375 assert_eq!(set.id_of("pair"), Some(0));
1376 assert_eq!(set.id_of("tag"), Some(1));
1377 assert_eq!(set.id_of("code"), Some(2));
1378 assert_eq!(set.name_of(0), Some("pair"));
1379 assert_eq!(set.pattern_kinds().len(), 1);
1380 assert!(!set.is_empty());
1381 let e = set.declare_text("shrug x = `a`").unwrap_err();
1382 assert!(e.msg.contains("line 1"), "{e}");
1383 let e = set.declare_text("let tag = \\W").unwrap_err();
1384 assert!(e.msg.contains("exists"), "{e}");
1385 set.declare_text("let kv = \\N").expect("a later let is accepted");
1386 assert!(matches!(set.let_of("kv"), Some(Pattern::Atom(_))), "a later let shadows an earlier one");
1387 }
1388
1389 #[test]
1390 fn test_lines_state_what_a_name_matches_and_name_their_line_when_it_does_not() {
1391 let mut set = ShapeSet::new();
1392 set.declare_text(
1393 "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",
1394 )
1395 .expect("declares");
1396 assert_eq!(set.tests().len(), 2);
1397 assert_eq!(set.tests()[0].line, 2);
1398 assert_eq!(set.tests()[0].accepts, vec!["x = 1".to_string(), "y = \"bob\"".to_string()]);
1399 assert_eq!(set.tests()[0].rejects, vec!["x == 1".to_string()]);
1400 assert!(set.run_tests().is_empty(), "{:?}", set.run_tests());
1401
1402 let mut set = ShapeSet::new();
1403 set.declare_text(
1404 "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",
1405 )
1406 .expect("declares");
1407 let failures: Vec<String> = set.run_tests().iter().map(ToString::to_string).collect();
1408 assert_eq!(
1409 failures,
1410 vec![
1411 "line 2: rhs accepts \"\\\"bob\\\"\": no match\n tokens: quoted \"\\\"bob\\\"\"".to_string(),
1412 "line 2: rhs rejects \"4 2\": matched \"4\" at 0..1".to_string(),
1413 "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(),
1414 ]
1415 );
1416
1417 let e = ShapeSet::new().declare_text("test rhs \"42\"").expect_err("a text before a keyword");
1418 assert!(e.msg.contains("follows accepts, reads or rejects"), "{e}");
1419 let e = ShapeSet::new().declare_text("test rhs accepts").expect_err("no text");
1420 assert!(e.msg.contains("at least one text"), "{e}");
1421 let e = ShapeSet::new().declare_text("test rhs accepts \"4").expect_err("unterminated");
1422 assert!(e.msg.contains("unterminated"), "{e}");
1423 let e = ShapeSet::new().declare_text("test rhs accepts \"\\q\"").expect_err("unknown escape");
1424 assert!(e.msg.contains("unknown escape \\q"), "{e}");
1425 let e = ShapeSet::new().declare_text("test rhs matches \"4\"").expect_err("unknown word");
1426 assert!(e.msg.contains("found `matches`"), "{e}");
1427
1428 let mut set = ShapeSet::new();
1433 set.declare_text("test kelvin reads \"4.2K\" in \"cooled to 4.2K overnight\"\n").expect("declares");
1434 assert_eq!(set.tests()[0].reads, vec![("4.2K".to_string(), "cooled to 4.2K overnight".to_string())]);
1435 assert!(set.run_tests().is_empty(), "{:?}", set.run_tests());
1436 let e = ShapeSet::new().declare_text("test rhs reads \"4\"").expect_err("no text to read from");
1437 assert!(e.msg.contains("names no text to read it from"), "{e}");
1438 let e = ShapeSet::new().declare_text("test rhs reads \"4\" in").expect_err("no text after in");
1439 assert!(e.msg.contains("names no text to read it from"), "{e}");
1440 let e = ShapeSet::new().declare_text("test rhs reads \"4\" \"a 4 b\"").expect_err("no in");
1441 assert!(e.msg.contains("`in` goes between the span \"4\""), "{e}");
1442 let e = ShapeSet::new().declare_text("test rhs accepts \"4\" in \"a\"").expect_err("in alone");
1443 assert!(e.msg.contains("`in` follows the span a `reads` names"), "{e}");
1444 }
1445
1446 #[test]
1447 fn a_guarded_shape_takes_the_longest_end_its_check_accepts() {
1448 let mut set = ShapeSet::new();
1449 set.declare("digits = `[0-9]{4,8}`", Precedence::Before).expect("declares");
1450 set.shapes[0].guard = Some(|text: &[u8]| text.len() % 2 == 1);
1451 assert_eq!(set.longest_at(b"12345678 x", 0, Precedence::Before), Some((0, 7)));
1452 assert_eq!(set.longest_at(b"1234 x", 0, Precedence::Before), None);
1453 }
1454
1455 #[test]
1456 fn precedence_partitions_the_set() {
1457 let mut set = ShapeSet::new();
1458 set.declare("early = `[A-Z]{2}`", Precedence::Before).expect("declares");
1459 set.declare("late = `[0-9]{2}`", Precedence::After).expect("declares");
1460 assert_eq!(set.longest_at(b"AB", 0, Precedence::Before), Some((0, 2)));
1461 assert_eq!(set.longest_at(b"AB", 0, Precedence::After), None);
1462 assert_eq!(set.longest_at(b"12", 0, Precedence::After), Some((1, 2)));
1463 assert_eq!(set.longest_at(b"12", 0, Precedence::Before), None);
1464 }
1465}