Skip to main content

trex/
custom.rs

1//! The declarations a lex and a parse share: token shapes the lexer runs
2//! alongside its built-in recognizers, token kinds declared from patterns
3//! over the stream, and named sub-patterns a `\{name}` inlines.
4
5use crate::ast::Pattern;
6use crate::bytepat::{BytePat, parse as parse_bytepat};
7use crate::token::TokenKind;
8
9/// The ids below this belong to declared shapes and kinds; the ids from it
10/// up belong to the shipped library, so a pattern naming both never sees
11/// two kinds under one id.
12pub const LIBRARY_ID_BASE: u8 = 128;
13
14/// When a shape is tried relative to the built-in recognizers.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Precedence {
17    /// Tried before every built-in, so the shape wins an overlap.
18    Before,
19    /// Tried where no built-in matches.
20    After,
21}
22
23/// One declared token shape.
24#[derive(Clone, Debug)]
25pub struct TokenShape {
26    /// The name the `\{name}` atom uses.
27    pub name: String,
28    /// The whole-anchored byte-pattern, bounded by construction.
29    pub pat: BytePat,
30    /// Longest byte length this shape can match, from [`BytePat::max_len`].
31    pub window: usize,
32    /// When it is tried against the built-in cascade.
33    pub precedence: Precedence,
34    /// The payload of the [`crate::token::TokenKind::Custom`] the lexer
35    /// emits for it.
36    pub id: u8,
37    /// A check on the bytes the pattern accepted, where the shape's standard
38    /// defines one: a token the check refuses is not this shape.
39    pub guard: Option<fn(&[u8]) -> bool>,
40}
41
42/// A check on the span a kind's pattern matched, where the kind's standard
43/// defines one: the span is a number then one of these unit symbols,
44/// attached or one separator apart, so a number and a symbol a line apart
45/// are not fused.
46#[derive(Clone, Copy, Debug)]
47pub struct UnitGuard {
48    /// The symbols the span may end in.
49    pub symbols: &'static [&'static str],
50}
51
52impl UnitGuard {
53    /// Whether the span's bytes are a number then one of the symbols.
54    #[must_use]
55    pub fn accepts(self, text: &[u8]) -> bool {
56        crate::quantity::context_span(text, self.symbols)
57    }
58}
59
60/// A token kind declared from a pattern over the stream: after the lex, the
61/// tokens each match covers fuse into one token of this kind.
62#[derive(Clone, Debug)]
63pub struct PatternKind {
64    /// The name the `\{name}` atom uses.
65    pub name: String,
66    /// The payload of the [`crate::token::TokenKind::Custom`] the fused
67    /// token carries.
68    pub id: u8,
69    /// The pattern whose matches become tokens.
70    pub pattern: Pattern,
71    /// A check on the matched span, where the kind's standard defines one:
72    /// a span the check refuses is not fused.
73    pub guard: Option<UnitGuard>,
74}
75
76/// A rejected shape declaration.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ShapeError {
79    /// The offending declaration text.
80    pub decl: String,
81    /// The reason it is not a valid shape.
82    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/// How serious a rule's finding is: SARIF's own levels, which a GitHub
94/// annotation carries as `error`, `warning` and `notice`.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum Severity {
97    Error,
98    Warning,
99    Note,
100}
101
102impl Severity {
103    /// The severity `error`, `warning` or `note` names.
104    #[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    /// The name, as a rule writes it and as SARIF's `level` carries it.
115    #[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    /// The level a GitHub workflow annotation carries: `notice` for a note.
125    #[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/// One `rule` of a pattern file: a named pattern with what a finding of it
136/// says, how serious it is, the fix that replaces the match, the inputs it
137/// reads, the record it holds and the metadata a pipeline filters on. The
138/// rule is also a sub-pattern under its name, so `\{name}` reads it and a
139/// `test` line checks it.
140#[derive(Clone, Debug)]
141pub struct Rule {
142    /// The pattern file the rule is in, where the set was declared from one.
143    pub file: Option<std::path::PathBuf>,
144    /// The line the rule opens on, from one.
145    pub line: usize,
146    /// The rule's name, the id a finding is reported under.
147    pub name: String,
148    /// What the rule matches.
149    pub pattern: Pattern,
150    /// The pattern as written.
151    pub source: String,
152    /// What a finding says: a report template rendered at each match, with
153    /// the match's registers and their typed slices, its position, and
154    /// `${rule}` and `${severity}`.
155    pub message: String,
156    /// How serious a finding is; a warning where the rule says nothing.
157    pub severity: Severity,
158    /// The rewrite template a fix renders in the match's place, where the
159    /// rule has one.
160    pub fix: Option<String>,
161    /// The globs the inputs the rule reads are kept by (`*.py`) or dropped
162    /// by (`!test_*`), as `-g` takes them; every input where there are none.
163    pub files: Vec<String>,
164    /// The patterns the record must not hold for the rule to fire.
165    pub unless: Vec<Pattern>,
166    /// What a record is, for a rule that fires on a record holding the
167    /// pattern and none of `unless`: a line where a rule names `unless` and
168    /// no record; nothing for a rule that fires on each match.
169    pub record: Option<crate::records::RecordUnit>,
170    /// The `meta.KEY = VALUE` lines, in order.
171    pub meta: Vec<(String, String)>,
172}
173
174impl Rule {
175    /// Whether the rule fires on a record rather than on each match: it
176    /// names a record or a pattern the record must not hold.
177    #[must_use]
178    pub fn on_records(&self) -> bool {
179        self.record.is_some() || !self.unless.is_empty()
180    }
181
182    /// What a record is for this rule: what it names, or a line where it
183    /// names only what the record must not hold.
184    #[must_use]
185    pub fn record_unit(&self) -> crate::records::RecordUnit {
186        self.record.clone().unwrap_or(crate::records::RecordUnit::Line)
187    }
188
189    /// The values `meta.tags` lists, comma-separated.
190    #[must_use]
191    pub fn tags(&self) -> Vec<String> {
192        tags_of(&self.meta)
193    }
194}
195
196/// The values the `tags` entries of a rule's metadata list, comma-separated.
197#[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/// One `test` line of a pattern file: the texts a name matches as a whole
206/// and the texts it matches nowhere in.
207#[derive(Clone, Debug, PartialEq, Eq)]
208pub struct LibTest {
209    /// The pattern file the line is in, where the set was declared from one.
210    pub file: Option<std::path::PathBuf>,
211    /// The line's number in its text, from one.
212    pub line: usize,
213    /// The name under test: a declared shape, kind or sub-pattern, or a
214    /// library entry.
215    pub name: String,
216    /// The texts the name matches as a whole, from the first significant
217    /// token to the last.
218    pub accepts: Vec<String>,
219    /// The spans the name reads out of a larger text, as `(span, text)`.
220    ///
221    /// What `accepts` cannot say. A kind read from the tokens around it takes
222    /// part of a line and leaves the rest, so the whole extent is the wrong
223    /// expectation for it: the question is which bytes it took, and out of
224    /// what.
225    pub reads: Vec<(String, String)>,
226    /// The texts the name matches nowhere in.
227    pub rejects: Vec<String>,
228}
229
230/// One expectation of a `test` line that the name does not meet.
231#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct TestFailure {
233    /// The pattern file the line is in, where the set was declared from one.
234    pub file: Option<std::path::PathBuf>,
235    /// The line's number in its text, from one.
236    pub line: usize,
237    /// The name under test.
238    pub name: String,
239    /// The expectation and what happened instead.
240    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
252/// The extent of the significant tokens of `input`, lexed under `shapes`: the
253/// start of the first and the end of the last, or none where every token is
254/// whitespace.
255fn 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
263/// The double-quoted text opening at byte `open` of a `test` line, with
264/// `\"`, `\\`, `\n` and `\t` read, and the byte after its closing quote.
265fn 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/// The declarations a lex and a parse share.
291#[derive(Clone, Debug, Default)]
292pub struct ShapeSet {
293    shapes: Vec<TokenShape>,
294    kinds: Vec<PatternKind>,
295    /// Named sub-patterns, in declaration order; a later one shadows an
296    /// earlier one of the same name.
297    lets: Vec<(String, Pattern)>,
298    /// The `test` lines declared, in declaration order.
299    tests: Vec<LibTest>,
300    /// The `rule` declarations, in declaration order.
301    rules: Vec<Rule>,
302    /// The fields a `fields` line gives a named sub-pattern, by its name:
303    /// what the marks a pattern was built from said of each field beyond
304    /// the pattern, in the order the fields are written.
305    fields: Vec<(String, Vec<crate::infer::marks::FieldMark>)>,
306    /// The ids handed to declared shapes and kinds so far; the shipped
307    /// library's start at [`LIBRARY_ID_BASE`] and are placed by their entry.
308    next_id: u8,
309    /// Whether a parse against this set falls back to the shipped library
310    /// for a name it does not declare. Off only for the set the library is
311    /// itself parsed into.
312    library_off: bool,
313    /// The directory a relative `@file` set in a declaration is read from:
314    /// the pattern file's own while one is being declared, else the one
315    /// [`Self::set_base_dir`] gave, or none, which reads from the current
316    /// directory.
317    base: Option<std::path::PathBuf>,
318}
319
320impl ShapeSet {
321    /// An empty set.
322    #[must_use]
323    pub fn new() -> Self {
324        ShapeSet::default()
325    }
326
327    /// An empty set whose parses never consult the shipped library: the
328    /// one the library is built into.
329    pub(crate) fn without_library() -> Self {
330        ShapeSet { library_off: true, ..ShapeSet::default() }
331    }
332
333    /// Whether a parse against this set resolves a name it does not declare
334    /// from the shipped library.
335    #[must_use]
336    pub fn consults_library(&self) -> bool {
337        !self.library_off
338    }
339
340    /// The directory a relative `@file` set is read from during a parse
341    /// against this set, or none for the current directory.
342    #[must_use]
343    pub fn base_dir(&self) -> Option<&std::path::Path> {
344        self.base.as_deref()
345    }
346
347    /// Read a relative `@file` in a pattern parsed against this set, or in a
348    /// line declared into it, from `dir`, or from the current directory where
349    /// it is `None`. A pattern file declared into the set reads its own from
350    /// beside the file whatever this says.
351    pub fn set_base_dir(&mut self, dir: Option<std::path::PathBuf>) {
352        self.base = dir;
353    }
354
355    /// Declare everything the pattern file at `path` says, as
356    /// [`Self::declare_text`] does, with a relative `@file` set in it read
357    /// from beside the file. A file whose byte-order mark declares UTF-8,
358    /// UTF-16 or UTF-32 is read as its text, as an input to a scan is.
359    ///
360    /// # Errors
361    ///
362    /// The file cannot be read, or a line of it is refused.
363    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    /// The pattern file at `path` declared as [`Self::declare_file`]
392    /// declares it, and the named patterns a set built from it holds, in
393    /// order: each `let` and `rule` under its name, and each line that is no
394    /// declaration under its line number.
395    ///
396    /// # Errors
397    ///
398    /// The file cannot be read, or a line of it is refused.
399    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    /// Whether nothing here changes a lex: no shape and no kind from a
432    /// pattern. Named sub-patterns bear on the parse alone.
433    #[must_use]
434    pub fn is_empty(&self) -> bool {
435        self.shapes.is_empty() && self.kinds.is_empty()
436    }
437
438    /// The declared shapes, in declaration order.
439    #[must_use]
440    pub fn shapes(&self) -> &[TokenShape] {
441        &self.shapes
442    }
443
444    /// The kinds declared from patterns, in declaration order.
445    #[must_use]
446    pub fn pattern_kinds(&self) -> &[PatternKind] {
447        &self.kinds
448    }
449
450    /// The named sub-patterns, in declaration order.
451    #[must_use]
452    pub fn lets(&self) -> &[(String, Pattern)] {
453        &self.lets
454    }
455
456    /// The `test` lines declared, in declaration order.
457    #[must_use]
458    pub fn tests(&self) -> &[LibTest] {
459        &self.tests
460    }
461
462    /// The rules declared, in declaration order.
463    #[must_use]
464    pub fn rules(&self) -> &[Rule] {
465        &self.rules
466    }
467
468    /// The rule declared under `name`.
469    #[must_use]
470    pub fn rule_of(&self, name: &str) -> Option<&Rule> {
471        self.rules.iter().find(|r| r.name == name)
472    }
473
474    /// The id of the shape or kind called `name`: the payload of its
475    /// [`crate::token::TokenKind::Custom`].
476    #[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    /// The name of the shape or kind `id`, here or in the shipped library.
486    #[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    /// The name `kind` is written under: a declared shape or kind, here or in
497    /// the shipped library, by the name its declaration gave it, and any
498    /// other kind by its own.
499    #[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    /// The sub-pattern declared under `name`, the last so declared.
511    #[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    /// The fields a `fields` line gives the sub-pattern `name`, in order.
517    #[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    /// Record a `fields` line, `NAME {mark}...`: the fields of the
523    /// sub-pattern declared as `NAME` before it, in order, each a mark with
524    /// the example text left out, as [`crate::infer::marks::field_marks`]
525    /// reads them.
526    ///
527    /// # Errors
528    ///
529    /// A name no sub-pattern is declared under, a second `fields` line for
530    /// one, a line naming no field, a mark that does not read, a field named
531    /// twice, and a field the sub-pattern binds no register under or reads
532    /// through an accessor that is not one.
533    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    /// Whether a declaration here can take `name` without shadowing any
562    /// other: alphanumerics and underscores, not a built-in atom's, not a
563    /// shape, kind or sub-pattern declared here, and not an entry of the
564    /// shipped library.
565    #[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    /// `name` checked as a declaration name: alphanumerics and underscores,
577    /// not a built-in atom's, and not already a shape or kind here.
578    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    /// The next id for a declared shape or kind.
593    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    /// Declare a shape from `name = ` followed by a backtick byte-pattern.
606    ///
607    /// # Errors
608    ///
609    /// Rejects a malformed declaration, an unparseable byte-pattern, an
610    /// unbounded one, a duplicate name, a name held by a built-in atom, or
611    /// more shapes and kinds than the ids below [`LIBRARY_ID_BASE`] hold.
612    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    /// Add a shape the shipped library defines, under the id its entry
635    /// fixes and with its guard.
636    pub(crate) fn push_library_shape(&mut self, shape: TokenShape) {
637        self.shapes.push(shape);
638    }
639
640    /// Declare a kind from `name = ` followed by a pattern over the stream,
641    /// parsed against the declarations so far.
642    ///
643    /// # Errors
644    ///
645    /// Rejects a malformed declaration, a pattern that does not parse, a
646    /// duplicate name, a name held by a built-in atom, or more shapes and
647    /// kinds than the ids below [`LIBRARY_ID_BASE`] hold.
648    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    /// Add a kind the shipped library defines from a pattern, under the id
661    /// its entry fixes and with its guard.
662    pub(crate) fn push_library_kind(&mut self, kind: PatternKind) {
663        self.kinds.push(kind);
664    }
665
666    /// Declare a named sub-pattern from `name = ` followed by a pattern,
667    /// parsed against the declarations so far; a later declaration of the
668    /// same name shadows this one.
669    ///
670    /// # Errors
671    ///
672    /// Rejects a malformed declaration, a pattern that does not parse, or a
673    /// name held by a built-in atom or by a shape or kind here.
674    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    /// Record a `test` line from `NAME accepts "text"... reads "span" in
686    /// "text"... rejects "text"...`: the name, then any number of clauses,
687    /// `accepts` and `rejects` each followed by the double-quoted texts it
688    /// covers and `reads` by one span and the one text it is read out of, at
689    /// least one text in all. A text reads `\"`, `\\`, `\n` and `\t`.
690    ///
691    /// # Errors
692    ///
693    /// Rejects a malformed line: a name that is not alphanumerics and
694    /// underscores, a word that is no keyword, a text before any keyword, a
695    /// `reads` whose span has no `in` or no text after it,
696    /// an unterminated text or an unknown escape, or no text at all.
697    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        /// Which clause the texts that follow belong to.
704        enum Side {
705            Accepts,
706            Rejects,
707            /// `reads`, waiting on the span it names.
708            Reads,
709            /// `reads "span"`, waiting on the `in` that introduces the text.
710            ReadsSpan(String),
711            /// `reads "span" in`, waiting on the text the span is read from.
712            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                    // The span, waiting on the `in "text"` that says where it
735                    // is read from.
736                    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                        // A second `reads` repeats the keyword, so that
745                        // `reads "a" in "b" "c" in "d"` is a mistake rather
746                        // than a second pair read by position.
747                        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                // `in` goes between a span and its text and takes no texts of
756                // its own, so it leaves the side as it is.
757                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    /// Run every `test` line declared here against what the set finally
796    /// declares, so a test may be anywhere in its file. A name accepts a
797    /// text when its match in the text is the whole of it, from the first
798    /// significant token to the last, and rejects a text when it matches
799    /// nowhere in it. The expectations not met, in line order; a failing
800    /// `accepts` carries the lex of its text on a second line of its message.
801    #[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                // The lex of the text is shown under the failure, because a text
824                // that will not be taken whole usually lexed into something
825                // other than what the writer of the line had in mind, and the
826                // kinds say so where the bytes do not.
827                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            // A span read out of a larger text: the match must be exactly
849            // those bytes, at the place the text puts them. A kind read from
850            // the tokens around it takes part of a line and leaves the rest,
851            // so `accepts` - which asks for the whole extent - is the wrong
852            // question and this is the right one.
853            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    /// Declare everything a pattern file says: `let NAME = PATTERN`, `kind
887    /// NAME = PATTERN`, `shape NAME = `BYTES``, `shape-after NAME = `BYTES``
888    /// and `test NAME accepts "text"... rejects "text"...` one a line, and a
889    /// rule as a block, `rule NAME` over indented `field = value` lines, or
890    /// as one line, `rule NAME [severity] "message" = PATTERN`, its other
891    /// fields as `fix NAME = TEMPLATE`, `meta NAME KEY = VALUE`, `files NAME
892    /// = GLOBS`, `unless NAME = PATTERN`, `record NAME = UNIT`, `record-start
893    /// NAME = PATTERN` and `record-span NAME = PATTERN` lines below it. Blank
894    /// lines and lines opening with `#` are skipped. A `fields` line gives a
895    /// sub-pattern's fields, as [`Self::declare_fields`] reads it.
896    ///
897    /// # Errors
898    ///
899    /// The first line that is not a declaration, or whose declaration is
900    /// refused, with its line number in the message.
901    pub fn declare_text(&mut self, text: &str) -> Result<(), ShapeError> {
902        self.declare_lines(text, None)
903    }
904
905    /// [`Self::declare_text`], collecting into `members` the named patterns
906    /// a set built from the file holds, in order: each `let` and `rule`
907    /// under its name, and each line that is no declaration, parsed as a
908    /// pattern against the declarations so far, under its line number. A
909    /// line that is no declaration is refused where no members are
910    /// collected.
911    ///
912    /// # Errors
913    ///
914    /// As [`Self::declare_text`].
915    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                        // The block's field lines: every indented line that
961                        // follows, blank lines and comments among them skipped,
962                        // up to the next line that opens at the margin.
963                        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    /// A rule from its block: the name on the `rule NAME` line and the
1016    /// `field = value` lines under it, each with its line number. The
1017    /// pattern is read first, whichever line holds it, since the other
1018    /// fields are checked against its registers.
1019    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    /// A rule from one line: `NAME [severity] "message" = PATTERN`.
1081    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    /// A rule's pattern, parsed against the declarations so far.
1121    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    /// A field of a rule beyond its pattern, message and severity, from a
1129    /// block line or a `FIELD NAME = VALUE` line: `fix`, `files`, `unless`,
1130    /// `record`, `record-start`, `record-span` or `meta.KEY`.
1131    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    /// A `FIELD NAME ... = VALUE` line below a one-line rule: the field set
1163    /// on the rule declared under NAME above it; `meta NAME KEY = VALUE`
1164    /// carries the key before the `=`.
1165    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    /// Register `rule`: its name checked as a declaration's and against the
1198    /// rules so far, its message and fix checked as templates over its
1199    /// registers, and the rule made a sub-pattern under its name.
1200    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    /// This set with the library shapes and kinds `ids` name added, for a
1219    /// lex that has to produce the tokens a pattern naming them reads.
1220    #[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    /// The longest shape matching at `i`, as `(id, end)`, among those with the
1237    /// given precedence whose guard accepts the bytes. A longer span wins;
1238    /// equal spans go to the earlier declaration.
1239    #[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            // The window is the shape's own maximum match length, bounding the
1247            // slice each position reads. A guarded shape takes the longest
1248            // end its guard accepts: the longest the pattern reaches may run
1249            // into what follows, and a shorter end can still be the token.
1250            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
1275/// Names held by built-in `\{name}` atoms.
1276const 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        // A `reads` names one span and the one text it is read out of, and
1429        // both the keyword between them and the text after it are required:
1430        // a pair read by position would take `reads "a" "b" "c" "d"` for two
1431        // pairs, which is the reading the writer of such a line least meant.
1432        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}