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};
7
8/// The ids below this belong to declared shapes and kinds; the ids from it
9/// up belong to the shipped library, so a pattern naming both never sees
10/// two kinds under one id.
11pub const LIBRARY_ID_BASE: u8 = 128;
12
13/// Where a shape sits relative to the built-in recognizers.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Precedence {
16    /// Tried before every built-in, so the shape wins an overlap.
17    Before,
18    /// Tried where no built-in matches.
19    After,
20}
21
22/// One declared token shape.
23#[derive(Clone, Debug)]
24pub struct TokenShape {
25    /// The name the `\{name}` atom uses.
26    pub name: String,
27    /// The whole-anchored byte-pattern, bounded by construction.
28    pub pat: BytePat,
29    /// Longest byte length this shape can match, from [`BytePat::max_len`].
30    pub window: usize,
31    /// Where it sits against the built-in cascade.
32    pub precedence: Precedence,
33    /// The payload of the [`crate::token::TokenKind::Custom`] the lexer
34    /// emits for it.
35    pub id: u8,
36    /// A check on the bytes the pattern accepted, where the shape's standard
37    /// defines one: a token the check refuses is not this shape.
38    pub guard: Option<fn(&[u8]) -> bool>,
39}
40
41/// A check on the span a kind's pattern matched, where the kind's standard
42/// defines one: the span is a number then one of these unit symbols,
43/// attached or one separator apart, so a number and a symbol a line apart
44/// are not fused.
45#[derive(Clone, Copy, Debug)]
46pub struct UnitGuard {
47    /// The symbols the span may end in.
48    pub symbols: &'static [&'static str],
49}
50
51impl UnitGuard {
52    /// Whether the span's bytes are a number then one of the symbols.
53    #[must_use]
54    pub fn accepts(self, text: &[u8]) -> bool {
55        crate::quantity::context_span(text, self.symbols)
56    }
57}
58
59/// A token kind declared from a pattern over the stream: after the lex, the
60/// tokens each match covers fuse into one token of this kind.
61#[derive(Clone, Debug)]
62pub struct PatternKind {
63    /// The name the `\{name}` atom uses.
64    pub name: String,
65    /// The payload of the [`crate::token::TokenKind::Custom`] the fused
66    /// token carries.
67    pub id: u8,
68    /// The pattern whose matches become tokens.
69    pub pattern: Pattern,
70    /// A check on the matched span, where the kind's standard defines one:
71    /// a span the check refuses is not fused.
72    pub guard: Option<UnitGuard>,
73}
74
75/// A rejected shape declaration.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct ShapeError {
78    /// The offending declaration text.
79    pub decl: String,
80    /// The reason it is not a valid shape.
81    pub msg: String,
82}
83
84impl std::fmt::Display for ShapeError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "token shape {:?}: {}", self.decl, self.msg)
87    }
88}
89
90impl std::error::Error for ShapeError {}
91
92/// How serious a rule's finding is: SARIF's own levels, which a GitHub
93/// annotation carries as `error`, `warning` and `notice`.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum Severity {
96    Error,
97    Warning,
98    Note,
99}
100
101impl Severity {
102    /// The severity `error`, `warning` or `note` names.
103    #[must_use]
104    pub fn parse(name: &str) -> Option<Severity> {
105        match name {
106            "error" => Some(Severity::Error),
107            "warning" => Some(Severity::Warning),
108            "note" => Some(Severity::Note),
109            _ => None,
110        }
111    }
112
113    /// The name, as a rule writes it and as SARIF's `level` carries it.
114    #[must_use]
115    pub fn name(self) -> &'static str {
116        match self {
117            Severity::Error => "error",
118            Severity::Warning => "warning",
119            Severity::Note => "note",
120        }
121    }
122
123    /// The level a GitHub workflow annotation carries: `notice` for a note.
124    #[must_use]
125    pub fn github(self) -> &'static str {
126        match self {
127            Severity::Error => "error",
128            Severity::Warning => "warning",
129            Severity::Note => "notice",
130        }
131    }
132}
133
134/// One `rule` of a pattern file: a named pattern with what a finding of it
135/// says, how serious it is, the fix that replaces the match, the inputs it
136/// reads, the record it holds and the metadata a pipeline filters on. The
137/// rule is also a sub-pattern under its name, so `\{name}` reads it and a
138/// `test` line checks it.
139#[derive(Clone, Debug)]
140pub struct Rule {
141    /// The pattern file the rule is in, where the set was declared from one.
142    pub file: Option<std::path::PathBuf>,
143    /// The line the rule opens on, from one.
144    pub line: usize,
145    /// The rule's name, the id a finding is reported under.
146    pub name: String,
147    /// What the rule matches.
148    pub pattern: Pattern,
149    /// The pattern as written.
150    pub source: String,
151    /// What a finding says: a report template rendered at each match, with
152    /// the match's registers and their typed slices, where it stands, and
153    /// `${rule}` and `${severity}`.
154    pub message: String,
155    /// How serious a finding is; a warning where the rule says nothing.
156    pub severity: Severity,
157    /// The rewrite template a fix renders in the match's place, where the
158    /// rule has one.
159    pub fix: Option<String>,
160    /// The globs the inputs the rule reads are kept by (`*.py`) or dropped
161    /// by (`!test_*`), as `-g` takes them; every input where there are none.
162    pub files: Vec<String>,
163    /// The patterns the record must not hold for the rule to fire.
164    pub unless: Vec<Pattern>,
165    /// What a record is, for a rule that fires on a record holding the
166    /// pattern and none of `unless`: a line where a rule names `unless` and
167    /// no record; nothing for a rule that fires on each match.
168    pub record: Option<crate::records::RecordUnit>,
169    /// The `meta.KEY = VALUE` lines, in order.
170    pub meta: Vec<(String, String)>,
171}
172
173impl Rule {
174    /// Whether the rule fires on a record rather than on each match: it
175    /// names a record or a pattern the record must not hold.
176    #[must_use]
177    pub fn on_records(&self) -> bool {
178        self.record.is_some() || !self.unless.is_empty()
179    }
180
181    /// What a record is for this rule: what it names, or a line where it
182    /// names only what the record must not hold.
183    #[must_use]
184    pub fn record_unit(&self) -> crate::records::RecordUnit {
185        self.record.clone().unwrap_or(crate::records::RecordUnit::Line)
186    }
187
188    /// The values `meta.tags` lists, comma-separated.
189    #[must_use]
190    pub fn tags(&self) -> Vec<String> {
191        tags_of(&self.meta)
192    }
193}
194
195/// The values the `tags` entries of a rule's metadata list, comma-separated.
196#[must_use]
197pub fn tags_of(meta: &[(String, String)]) -> Vec<String> {
198    meta.iter()
199        .filter(|(k, _)| k == "tags")
200        .flat_map(|(_, v)| v.split(',').map(str::trim).filter(|t| !t.is_empty()).map(str::to_string))
201        .collect()
202}
203
204/// One `test` line of a pattern file: the texts a name matches as a whole
205/// and the texts it matches nowhere in.
206#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct LibTest {
208    /// The pattern file the line is in, where the set was declared from one.
209    pub file: Option<std::path::PathBuf>,
210    /// The line's number in its text, from one.
211    pub line: usize,
212    /// The name under test: a declared shape, kind or sub-pattern, or a
213    /// library entry.
214    pub name: String,
215    /// The texts the name matches as a whole, from the first significant
216    /// token to the last.
217    pub accepts: Vec<String>,
218    /// The spans the name reads out of a larger text, as `(span, text)`.
219    ///
220    /// What `accepts` cannot say. A kind read from the tokens around it takes
221    /// part of a line and leaves the rest, so the whole extent is the wrong
222    /// expectation for it: the question is which bytes it took, and out of
223    /// what.
224    pub reads: Vec<(String, String)>,
225    /// The texts the name matches nowhere in.
226    pub rejects: Vec<String>,
227}
228
229/// One expectation of a `test` line that the name does not meet.
230#[derive(Clone, Debug, PartialEq, Eq)]
231pub struct TestFailure {
232    /// The pattern file the line is in, where the set was declared from one.
233    pub file: Option<std::path::PathBuf>,
234    /// The line's number in its text, from one.
235    pub line: usize,
236    /// The name under test.
237    pub name: String,
238    /// The expectation and what happened instead.
239    pub msg: String,
240}
241
242impl std::fmt::Display for TestFailure {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        match &self.file {
245            Some(path) => write!(f, "{}:{}: {} {}", path.display(), self.line, self.name, self.msg),
246            None => write!(f, "line {}: {} {}", self.line, self.name, self.msg),
247        }
248    }
249}
250
251/// Where the significant tokens of `input` lie under `shapes`: the start of
252/// the first and the end of the last, or none where every token is
253/// whitespace.
254fn significant_extent(input: &[u8], shapes: &ShapeSet) -> Option<(usize, usize)> {
255    let blobs = crate::lexer::blob_runs(input);
256    let toks = crate::lexer::lex_with_shapes(input, &blobs, shapes, 0);
257    let first = toks.iter().find(|t| t.is_significant())?.start();
258    let last = toks.iter().rev().find(|t| t.is_significant())?.end();
259    Some((first, last))
260}
261
262/// The double-quoted text opening at byte `open` of a `test` line, with
263/// `\"`, `\\`, `\n` and `\t` read, and the byte after its closing quote.
264fn read_test_text(line: &str, open: usize) -> Result<(String, usize), String> {
265    let mut out = String::new();
266    let body = open + 1;
267    let mut chars = line[body..].char_indices();
268    while let Some((k, c)) = chars.next() {
269        match c {
270            '"' => return Ok((out, body + k + 1)),
271            '\\' => match chars.next() {
272                Some((_, '"')) => out.push('"'),
273                Some((_, '\\')) => out.push('\\'),
274                Some((_, 'n')) => out.push('\n'),
275                Some((_, 't')) => out.push('\t'),
276                Some((_, other)) => {
277                    return Err(format!(
278                        "unknown escape \\{other} in a test text; it reads \\\", \\\\, \\n and \\t"
279                    ));
280                }
281                None => return Err("a backslash ends the test text".to_string()),
282            },
283            other => out.push(other),
284        }
285    }
286    Err("unterminated test text; it closes with a double quote".to_string())
287}
288
289/// The declarations a lex and a parse share.
290#[derive(Clone, Debug, Default)]
291pub struct ShapeSet {
292    shapes: Vec<TokenShape>,
293    kinds: Vec<PatternKind>,
294    /// Named sub-patterns, in declaration order; a later one shadows an
295    /// earlier one of the same name.
296    lets: Vec<(String, Pattern)>,
297    /// The `test` lines declared, in declaration order.
298    tests: Vec<LibTest>,
299    /// The `rule` declarations, in declaration order.
300    rules: Vec<Rule>,
301    /// The fields a `fields` line gives a named sub-pattern, by its name:
302    /// what the marks a pattern was built from said of each field beyond
303    /// the pattern, in the order the fields are written.
304    fields: Vec<(String, Vec<crate::infer::marks::FieldMark>)>,
305    /// The ids handed to declared shapes and kinds so far; the shipped
306    /// library's start at [`LIBRARY_ID_BASE`] and are placed by their entry.
307    next_id: u8,
308    /// Whether a parse against this set falls back to the shipped library
309    /// for a name it does not declare. Off only for the set the library is
310    /// itself parsed into.
311    library_off: bool,
312    /// The directory a relative `@file` set in a declaration is read from:
313    /// the pattern file's own while one is being declared, else the one
314    /// [`Self::set_base_dir`] gave, or none, which reads from the current
315    /// directory.
316    base: Option<std::path::PathBuf>,
317}
318
319impl ShapeSet {
320    /// An empty set.
321    #[must_use]
322    pub fn new() -> Self {
323        ShapeSet::default()
324    }
325
326    /// An empty set whose parses never consult the shipped library: the
327    /// one the library is built into.
328    pub(crate) fn without_library() -> Self {
329        ShapeSet { library_off: true, ..ShapeSet::default() }
330    }
331
332    /// Whether a parse against this set resolves a name it does not declare
333    /// from the shipped library.
334    #[must_use]
335    pub fn consults_library(&self) -> bool {
336        !self.library_off
337    }
338
339    /// The directory a relative `@file` set is read from during a parse
340    /// against this set, or none for the current directory.
341    #[must_use]
342    pub fn base_dir(&self) -> Option<&std::path::Path> {
343        self.base.as_deref()
344    }
345
346    /// Read a relative `@file` in a pattern parsed against this set, or in a
347    /// line declared into it, from `dir`, or from the current directory where
348    /// it is `None`. A pattern file declared into the set reads its own from
349    /// beside the file whatever this says.
350    pub fn set_base_dir(&mut self, dir: Option<std::path::PathBuf>) {
351        self.base = dir;
352    }
353
354    /// Declare everything the pattern file at `path` says, as
355    /// [`Self::declare_text`] does, with a relative `@file` set in it read
356    /// from beside the file. A file whose byte-order mark declares UTF-8,
357    /// UTF-16 or UTF-32 is read as its text, as an input to a scan is.
358    ///
359    /// # Errors
360    ///
361    /// The file cannot be read, or a line of it is refused.
362    pub fn declare_file(&mut self, path: &std::path::Path) -> Result<(), ShapeError> {
363        let text = match std::fs::read(path) {
364            Ok(bytes) => String::from_utf8_lossy(&crate::encoding::decode(bytes)).into_owned(),
365            Err(e) => {
366                return Err(ShapeError {
367                    decl: path.display().to_string(),
368                    msg: format!("cannot read the pattern file: {e}"),
369                });
370            }
371        };
372        let before = self.base.take();
373        self.base = path.parent().map(std::path::Path::to_path_buf);
374        let tests_before = self.tests.len();
375        let rules_before = self.rules.len();
376        let declared = self.declare_text(&text);
377        self.base = before;
378        for test in &mut self.tests[tests_before..] {
379            test.file = Some(path.to_path_buf());
380        }
381        for rule in &mut self.rules[rules_before..] {
382            rule.file = Some(path.to_path_buf());
383        }
384        declared.map_err(|mut e| {
385            e.msg = format!("{}: {}", path.display(), e.msg);
386            e
387        })
388    }
389
390    /// The pattern file at `path` declared as [`Self::declare_file`]
391    /// declares it, and the named patterns a set built from it holds, in
392    /// order: each `let` and `rule` under its name, and each line that is no
393    /// declaration under its line number.
394    ///
395    /// # Errors
396    ///
397    /// The file cannot be read, or a line of it is refused.
398    pub fn declare_file_members(&mut self, path: &std::path::Path) -> Result<Vec<(String, Pattern)>, ShapeError> {
399        let text = match std::fs::read(path) {
400            Ok(bytes) => String::from_utf8_lossy(&crate::encoding::decode(bytes)).into_owned(),
401            Err(e) => {
402                return Err(ShapeError {
403                    decl: path.display().to_string(),
404                    msg: format!("cannot read the pattern file: {e}"),
405                });
406            }
407        };
408        let before = self.base.take();
409        self.base = path.parent().map(std::path::Path::to_path_buf);
410        let tests_before = self.tests.len();
411        let rules_before = self.rules.len();
412        let mut members = Vec::new();
413        let declared = self.declare_lines(&text, Some(&mut members));
414        self.base = before;
415        for test in &mut self.tests[tests_before..] {
416            test.file = Some(path.to_path_buf());
417        }
418        for rule in &mut self.rules[rules_before..] {
419            rule.file = Some(path.to_path_buf());
420        }
421        match declared {
422            Ok(()) => Ok(members),
423            Err(mut e) => {
424                e.msg = format!("{}: {}", path.display(), e.msg);
425                Err(e)
426            }
427        }
428    }
429
430    /// Whether nothing here changes a lex: no shape and no kind from a
431    /// pattern. Named sub-patterns bear on the parse alone.
432    #[must_use]
433    pub fn is_empty(&self) -> bool {
434        self.shapes.is_empty() && self.kinds.is_empty()
435    }
436
437    /// The declared shapes, in declaration order.
438    #[must_use]
439    pub fn shapes(&self) -> &[TokenShape] {
440        &self.shapes
441    }
442
443    /// The kinds declared from patterns, in declaration order.
444    #[must_use]
445    pub fn pattern_kinds(&self) -> &[PatternKind] {
446        &self.kinds
447    }
448
449    /// The named sub-patterns, in declaration order.
450    #[must_use]
451    pub fn lets(&self) -> &[(String, Pattern)] {
452        &self.lets
453    }
454
455    /// The `test` lines declared, in declaration order.
456    #[must_use]
457    pub fn tests(&self) -> &[LibTest] {
458        &self.tests
459    }
460
461    /// The rules declared, in declaration order.
462    #[must_use]
463    pub fn rules(&self) -> &[Rule] {
464        &self.rules
465    }
466
467    /// The rule declared under `name`.
468    #[must_use]
469    pub fn rule_of(&self, name: &str) -> Option<&Rule> {
470        self.rules.iter().find(|r| r.name == name)
471    }
472
473    /// The id of the shape or kind called `name`: the payload of its
474    /// [`crate::token::TokenKind::Custom`].
475    #[must_use]
476    pub fn id_of(&self, name: &str) -> Option<u8> {
477        self.shapes
478            .iter()
479            .find(|s| s.name == name)
480            .map(|s| s.id)
481            .or_else(|| self.kinds.iter().find(|k| k.name == name).map(|k| k.id))
482    }
483
484    /// The name of the shape or kind `id`, here or in the shipped library.
485    #[must_use]
486    pub fn name_of(&self, id: u8) -> Option<&str> {
487        self.shapes
488            .iter()
489            .find(|s| s.id == id)
490            .map(|s| s.name.as_str())
491            .or_else(|| self.kinds.iter().find(|k| k.id == id).map(|k| k.name.as_str()))
492            .or_else(|| crate::library::name_of(id))
493    }
494
495    /// The sub-pattern declared under `name`, the last so declared.
496    #[must_use]
497    pub fn let_of(&self, name: &str) -> Option<&Pattern> {
498        self.lets.iter().rev().find(|(n, _)| n == name).map(|(_, p)| p)
499    }
500
501    /// The fields a `fields` line gives the sub-pattern `name`, in order.
502    #[must_use]
503    pub fn fields_of(&self, name: &str) -> Option<&[crate::infer::marks::FieldMark]> {
504        self.fields.iter().find(|(n, _)| n == name).map(|(_, f)| f.as_slice())
505    }
506
507    /// Record a `fields` line, `NAME {mark}...`: the fields of the
508    /// sub-pattern declared as `NAME` before it, in order, each a mark with
509    /// the example text left out, as [`crate::infer::marks::field_marks`]
510    /// reads them.
511    ///
512    /// # Errors
513    ///
514    /// A name no sub-pattern is declared under, a second `fields` line for
515    /// one, a line naming no field, a mark that does not read, a field named
516    /// twice, and a field the sub-pattern binds no register under or reads
517    /// through an accessor that is not one.
518    pub fn declare_fields(&mut self, decl: &str) -> Result<(), ShapeError> {
519        let err = |msg: String| ShapeError { decl: decl.to_string(), msg };
520        let (name, rest) = decl.split_once(char::is_whitespace).unwrap_or((decl, ""));
521        let pattern = self
522            .let_of(name)
523            .ok_or_else(|| err(format!("no sub-pattern is declared as {name} above; a fields line follows its let")))?;
524        if self.fields_of(name).is_some() {
525            return Err(err(format!("{name} has a fields line already")));
526        }
527        let marks = crate::infer::marks::field_marks(rest).map_err(|e| err(e.to_string()))?;
528        if marks.is_empty() {
529            return Err(err("a fields line names at least one field".to_string()));
530        }
531        let bound = pattern.capture_names();
532        for (k, mark) in marks.iter().enumerate() {
533            if marks[..k].iter().any(|m| m.name == mark.name) {
534                return Err(err(format!("{} is named twice", mark.written())));
535            }
536            let body = match &mark.accessor {
537                Some(accessor) => format!("{}:{accessor}", mark.name),
538                None => mark.name.clone(),
539            };
540            crate::rewrite::Reference::parse(&body, &bound).map_err(|e| err(format!("{}: {}", mark.written(), e.msg)))?;
541        }
542        self.fields.push((name.to_string(), marks));
543        Ok(())
544    }
545
546    /// Whether a declaration here can take `name` without shadowing any
547    /// other: alphanumerics and underscores, not a built-in atom's, not a
548    /// shape, kind or sub-pattern declared here, and not an entry of the
549    /// shipped library.
550    #[must_use]
551    pub fn is_free(&self, name: &str) -> bool {
552        !name.is_empty()
553            && name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
554            && !BUILTIN_ATOM_NAMES.contains(&name)
555            && self.id_of(name).is_none()
556            && self.let_of(name).is_none()
557            && crate::library::id_of(name).is_none()
558            && crate::library::let_of(name).is_none()
559    }
560
561    /// `name` checked as a declaration name: alphanumerics and underscores,
562    /// not a built-in atom's, and not already a shape or kind here.
563    fn check_name(&self, decl: &str, name: &str) -> Result<(), ShapeError> {
564        let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
565        if name.is_empty() || !name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
566            return Err(err("a name is alphanumerics and underscores"));
567        }
568        if BUILTIN_ATOM_NAMES.contains(&name) {
569            return Err(err("that name belongs to a built-in atom; pick another"));
570        }
571        if self.id_of(name).is_some() {
572            return Err(err("a shape or kind with that name exists"));
573        }
574        Ok(())
575    }
576
577    /// The next id for a declared shape or kind.
578    fn take_id(&mut self, decl: &str) -> Result<u8, ShapeError> {
579        if self.next_id >= LIBRARY_ID_BASE {
580            return Err(ShapeError {
581                decl: decl.to_string(),
582                msg: format!("at most {LIBRARY_ID_BASE} shapes and kinds may be declared"),
583            });
584        }
585        let id = self.next_id;
586        self.next_id += 1;
587        Ok(id)
588    }
589
590    /// Declare a shape from `name = ` followed by a backtick byte-pattern.
591    ///
592    /// # Errors
593    ///
594    /// Rejects a malformed declaration, an unparseable byte-pattern, an
595    /// unbounded one, a duplicate name, a name held by a built-in atom, or
596    /// more shapes and kinds than the ids below [`LIBRARY_ID_BASE`] hold.
597    pub fn declare(&mut self, decl: &str, precedence: Precedence) -> Result<u8, ShapeError> {
598        let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
599        let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = `pattern`"))?;
600        let name = name.trim();
601        self.check_name(decl, name)?;
602        let body = rest.trim();
603        let inner = body
604            .strip_prefix('`')
605            .and_then(|b| b.strip_suffix('`'))
606            .ok_or_else(|| err("the pattern must be enclosed in backticks"))?;
607        let pat = parse_bytepat(inner.as_bytes()).map_err(|m| err(&m))?;
608        let window = pat.max_len().ok_or_else(|| {
609            err("the pattern must be bounded; `*`, `+` and `{m,}` have no fixed length")
610        })?;
611        if window == 0 {
612            return Err(err("the pattern matches nothing, so it would never end a token"));
613        }
614        let id = self.take_id(decl)?;
615        self.shapes.push(TokenShape { name: name.to_string(), pat, window, precedence, id, guard: None });
616        Ok(id)
617    }
618
619    /// Add a shape the shipped library defines, under the id its entry
620    /// fixes and with its guard.
621    pub(crate) fn push_library_shape(&mut self, shape: TokenShape) {
622        self.shapes.push(shape);
623    }
624
625    /// Declare a kind from `name = ` followed by a pattern over the stream,
626    /// parsed against the declarations so far.
627    ///
628    /// # Errors
629    ///
630    /// Rejects a malformed declaration, a pattern that does not parse, a
631    /// duplicate name, a name held by a built-in atom, or more shapes and
632    /// kinds than the ids below [`LIBRARY_ID_BASE`] hold.
633    pub fn declare_kind(&mut self, decl: &str) -> Result<u8, ShapeError> {
634        let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
635        let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = pattern`"))?;
636        let name = name.trim();
637        self.check_name(decl, name)?;
638        let pattern = crate::parser::parse_with_shapes(rest.trim(), self)
639            .map_err(|e| err(&format!("pattern error at byte {}: {}", e.pos, e.msg)))?;
640        let id = self.take_id(decl)?;
641        self.kinds.push(PatternKind { name: name.to_string(), id, pattern, guard: None });
642        Ok(id)
643    }
644
645    /// Add a kind the shipped library defines from a pattern, under the id
646    /// its entry fixes and with its guard.
647    pub(crate) fn push_library_kind(&mut self, kind: PatternKind) {
648        self.kinds.push(kind);
649    }
650
651    /// Declare a named sub-pattern from `name = ` followed by a pattern,
652    /// parsed against the declarations so far; a later declaration of the
653    /// same name shadows this one.
654    ///
655    /// # Errors
656    ///
657    /// Rejects a malformed declaration, a pattern that does not parse, or a
658    /// name held by a built-in atom or by a shape or kind here.
659    pub fn declare_let(&mut self, decl: &str) -> Result<(), ShapeError> {
660        let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
661        let (name, rest) = decl.split_once('=').ok_or_else(|| err("expected `name = pattern`"))?;
662        let name = name.trim();
663        self.check_name(decl, name)?;
664        let pattern = crate::parser::parse_with_shapes(rest.trim(), self)
665            .map_err(|e| err(&format!("pattern error at byte {}: {}", e.pos, e.msg)))?;
666        self.lets.push((name.to_string(), pattern));
667        Ok(())
668    }
669
670    /// Record a `test` line from `NAME accepts "text"... reads "span" in
671    /// "text"... rejects "text"...`: the name, then any number of clauses,
672    /// `accepts` and `rejects` each followed by the double-quoted texts it
673    /// covers and `reads` by one span and the one text it is read out of, at
674    /// least one text in all. A text reads `\"`, `\\`, `\n` and `\t`.
675    ///
676    /// # Errors
677    ///
678    /// Rejects a malformed line: a name that is not alphanumerics and
679    /// underscores, a word that is no keyword, a text before any keyword, a
680    /// `reads` whose span stands without `in` or without the text after it,
681    /// an unterminated text or an unknown escape, or no text at all.
682    pub fn declare_test(&mut self, decl: &str, line: usize) -> Result<(), ShapeError> {
683        let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
684        let (name, rest) = decl.split_once(char::is_whitespace).unwrap_or((decl, ""));
685        if name.is_empty() || !name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
686            return Err(err("expected `test NAME accepts \"text\"... rejects \"text\"...`"));
687        }
688        /// Which clause the texts that follow belong to.
689        enum Side {
690            Accepts,
691            Rejects,
692            /// `reads`, waiting on the span it names.
693            Reads,
694            /// `reads "span"`, waiting on the `in` that introduces the text.
695            ReadsSpan(String),
696            /// `reads "span" in`, waiting on the text the span is read from.
697            ReadsIn(String),
698        }
699        let mut accepts = Vec::new();
700        let mut rejects = Vec::new();
701        let mut reads: Vec<(String, String)> = Vec::new();
702        let mut side: Option<Side> = None;
703        let mut i = 0;
704        while i < rest.len() {
705            let c = rest.as_bytes()[i];
706            if c.is_ascii_whitespace() {
707                i += 1;
708            } else if c == b'"' {
709                let (text, next) = read_test_text(rest, i).map_err(|m| err(&m))?;
710                side = match side.take() {
711                    Some(Side::Accepts) => {
712                        accepts.push(text);
713                        Some(Side::Accepts)
714                    }
715                    Some(Side::Rejects) => {
716                        rejects.push(text);
717                        Some(Side::Rejects)
718                    }
719                    // The span, waiting on the `in "text"` that says where it
720                    // is read from.
721                    Some(Side::Reads) => Some(Side::ReadsSpan(text)),
722                    Some(Side::ReadsSpan(span)) => {
723                        return Err(err(&format!(
724                            "`in` stands between the span {span:?} and the text it is read from"
725                        )));
726                    }
727                    Some(Side::ReadsIn(span)) => {
728                        reads.push((span, text));
729                        // A second `reads` repeats the keyword, so that
730                        // `reads "a" in "b" "c" in "d"` is a mistake rather
731                        // than a second pair read by position.
732                        None
733                    }
734                    None => return Err(err("a text follows accepts, reads or rejects")),
735                };
736                i = next;
737            } else {
738                let end = rest[i..].find(char::is_whitespace).map_or(rest.len(), |n| i + n);
739                let word = &rest[i..end];
740                // `in` stands between a span and its text and takes no texts
741                // of its own, so it leaves the side as it is.
742                if word == "in" {
743                    let Some(Side::ReadsSpan(span)) = side.take() else {
744                        return Err(err("`in` follows the span a `reads` names"));
745                    };
746                    side = Some(Side::ReadsIn(span));
747                    i = end;
748                    continue;
749                }
750                side = Some(match word {
751                    "accepts" => Side::Accepts,
752                    "rejects" => Side::Rejects,
753                    "reads" => Side::Reads,
754                    other => {
755                        return Err(err(&format!(
756                            "expected accepts, reads, rejects or a quoted text, found `{other}`"
757                        )));
758                    }
759                });
760                i = end;
761            }
762        }
763        if let Some(Side::ReadsSpan(span) | Side::ReadsIn(span)) = side {
764            return Err(err(&format!("reads {span:?} names no text to read it from")));
765        }
766        if accepts.is_empty() && rejects.is_empty() && reads.is_empty() {
767            return Err(err("a test names at least one text it accepts, reads or rejects"));
768        }
769        self.tests.push(LibTest {
770            file: None,
771            line,
772            name: name.to_string(),
773            accepts,
774            reads,
775            rejects,
776        });
777        Ok(())
778    }
779
780    /// Run every `test` line declared here against what the set finally
781    /// declares, so a test stands anywhere in its file. A name accepts a
782    /// text when its match in the text is the whole of it, from the first
783    /// significant token to the last, and rejects a text when it matches
784    /// nowhere in it. The expectations not met, in line order; a failing
785    /// `accepts` carries the lex of its text on a second line of its message.
786    #[must_use]
787    pub fn run_tests(&self) -> Vec<TestFailure> {
788        let mut failures = Vec::new();
789        for test in &self.tests {
790            let fail = |msg: String| TestFailure {
791                file: test.file.clone(),
792                line: test.line,
793                name: test.name.clone(),
794                msg,
795            };
796            let pattern = match crate::parser::parse_with_shapes(&format!("\\{{{}}}", test.name), self) {
797                Ok(pattern) => pattern,
798                Err(e) => {
799                    failures.push(fail(format!("cannot be tested: {}", e.msg)));
800                    continue;
801                }
802            };
803            let lexed_under = self.with_library_shapes(&pattern.library_kinds());
804            for text in &test.accepts {
805                let input = text.as_bytes();
806                let found = crate::engine::scan_with_shapes(&pattern, input, self);
807                let whole = significant_extent(input, &lexed_under);
808                // The lex of the text stands under the failure, because a text
809                // that will not be taken whole usually lexed into something
810                // other than what the writer of the line had in mind, and the
811                // kinds say so where the bytes do not.
812                let lexing = || {
813                    let explainer = crate::explain::Explainer::new(&pattern, input, self);
814                    let toks: Vec<String> = explainer
815                        .tokens(0..input.len())
816                        .iter()
817                        .map(|(kind, text)| format!("{kind} {text:?}"))
818                        .collect();
819                    format!("\n  tokens: {}", toks.join(", "))
820                };
821                match (found.first(), whole) {
822                    (Some(m), Some((first, last))) if m.start() == first && m.end() == last => {}
823                    (Some(m), _) => failures.push(fail(format!(
824                        "accepts {text:?}: matched only {:?} at {}..{}{}",
825                        String::from_utf8_lossy(&input[m.range()]),
826                        m.start,
827                        m.end,
828                        lexing()
829                    ))),
830                    (None, _) => failures.push(fail(format!("accepts {text:?}: no match{}", lexing()))),
831                }
832            }
833            // A span read out of a larger text: the match must be exactly
834            // those bytes, at the place the text puts them. A kind read from
835            // the tokens around it takes part of a line and leaves the rest,
836            // so `accepts` - which asks for the whole extent - is the wrong
837            // question and this is the right one.
838            for (span, text) in &test.reads {
839                let input = text.as_bytes();
840                let Some(at) = text.find(span.as_str()) else {
841                    failures.push(fail(format!("reads {span:?}: {text:?} does not hold it")));
842                    continue;
843                };
844                let want = at..at + span.len();
845                match crate::engine::scan_with_shapes(&pattern, input, self).first() {
846                    Some(m) if m.range() == want => {}
847                    Some(m) => failures.push(fail(format!(
848                        "reads {span:?} in {text:?}: read {:?} at {}..{}",
849                        String::from_utf8_lossy(&input[m.range()]),
850                        m.start,
851                        m.end
852                    ))),
853                    None => failures.push(fail(format!("reads {span:?} in {text:?}: no match"))),
854                }
855            }
856            for text in &test.rejects {
857                let input = text.as_bytes();
858                if let Some(m) = crate::engine::scan_with_shapes(&pattern, input, self).first() {
859                    failures.push(fail(format!(
860                        "rejects {text:?}: matched {:?} at {}..{}",
861                        String::from_utf8_lossy(&input[m.range()]),
862                        m.start,
863                        m.end
864                    )));
865                }
866            }
867        }
868        failures
869    }
870
871    /// Declare everything a pattern file says: `let NAME = PATTERN`, `kind
872    /// NAME = PATTERN`, `shape NAME = `BYTES``, `shape-after NAME = `BYTES``
873    /// and `test NAME accepts "text"... rejects "text"...` one a line, and a
874    /// rule as a block, `rule NAME` over indented `field = value` lines, or
875    /// as one line, `rule NAME [severity] "message" = PATTERN`, its other
876    /// fields as `fix NAME = TEMPLATE`, `meta NAME KEY = VALUE`, `files NAME
877    /// = GLOBS`, `unless NAME = PATTERN`, `record NAME = UNIT`, `record-start
878    /// NAME = PATTERN` and `record-span NAME = PATTERN` lines below it. Blank
879    /// lines and lines opening with `#` are skipped. A `fields` line gives a
880    /// sub-pattern's fields, as [`Self::declare_fields`] reads it.
881    ///
882    /// # Errors
883    ///
884    /// The first line that is not a declaration, or whose declaration is
885    /// refused, with its line number in the message.
886    pub fn declare_text(&mut self, text: &str) -> Result<(), ShapeError> {
887        self.declare_lines(text, None)
888    }
889
890    /// [`Self::declare_text`], collecting into `members` the named patterns
891    /// a set built from the file holds, in order: each `let` and `rule`
892    /// under its name, and each line that is no declaration, parsed as a
893    /// pattern against the declarations so far, under its line number. A
894    /// line that is no declaration is refused where no members are
895    /// collected.
896    ///
897    /// # Errors
898    ///
899    /// As [`Self::declare_text`].
900    pub fn declare_lines(
901        &mut self,
902        text: &str,
903        mut members: Option<&mut Vec<(String, Pattern)>>,
904    ) -> Result<(), ShapeError> {
905        let lines: Vec<&str> = text.lines().collect();
906        let mut i = 0;
907        while i < lines.len() {
908            let n = i + 1;
909            let line = lines[i].trim();
910            i += 1;
911            if line.is_empty() || line.starts_with('#') {
912                continue;
913            }
914            let at = |mut e: ShapeError| {
915                e.msg = format!("line {n}: {}", e.msg);
916                e
917            };
918            let (keyword, rest) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
919            let rest = rest.trim();
920            match keyword {
921                "let" => {
922                    self.declare_let(rest).map_err(at)?;
923                    if let Some(members) = members.as_mut() {
924                        let name = rest.split_once('=').map_or(rest, |(name, _)| name).trim();
925                        let pattern = self.let_of(name).cloned().ok_or_else(|| {
926                            at(ShapeError { decl: line.to_string(), msg: "the let declared nothing".to_string() })
927                        })?;
928                        members.push((name.to_string(), pattern));
929                    }
930                }
931                "kind" => {
932                    self.declare_kind(rest).map_err(at)?;
933                }
934                "shape" => {
935                    self.declare(rest, Precedence::Before).map_err(at)?;
936                }
937                "shape-after" => {
938                    self.declare(rest, Precedence::After).map_err(at)?;
939                }
940                "test" => self.declare_test(rest, n).map_err(at)?,
941                "fields" => self.declare_fields(rest).map_err(at)?,
942                "rule" => {
943                    let is_block = !rest.is_empty() && rest.chars().all(|c| c == '_' || c.is_ascii_alphanumeric());
944                    let rule = if is_block {
945                        // The block's field lines: every indented line that
946                        // follows, blank lines and comments among them skipped,
947                        // up to the next line that opens at the margin.
948                        let mut fields: Vec<(usize, &str)> = Vec::new();
949                        while i < lines.len() {
950                            let raw = lines[i];
951                            let body = raw.trim();
952                            if body.is_empty() || body.starts_with('#') {
953                                i += 1;
954                                continue;
955                            }
956                            if !raw.starts_with([' ', '\t']) {
957                                break;
958                            }
959                            fields.push((i + 1, body));
960                            i += 1;
961                        }
962                        self.rule_block(rest, n, &fields)?
963                    } else {
964                        self.rule_line(rest, n).map_err(at)?
965                    };
966                    let name = rule.name.clone();
967                    let pattern = rule.pattern.clone();
968                    self.add_rule(rule, line).map_err(at)?;
969                    if let Some(members) = members.as_mut() {
970                        members.push((name, pattern));
971                    }
972                }
973                "fix" | "meta" | "files" | "unless" | "record" | "record-start" | "record-span" => {
974                    self.rule_field_line(keyword, rest).map_err(at)?;
975                }
976                other => match members.as_mut() {
977                    Some(members) => {
978                        let pattern = crate::parser::parse_with_shapes(line, self).map_err(|e| {
979                            at(ShapeError {
980                                decl: line.to_string(),
981                                msg: format!("pattern error at byte {}: {}", e.pos, e.msg),
982                            })
983                        })?;
984                        members.push((n.to_string(), pattern));
985                    }
986                    None => {
987                        return Err(at(ShapeError {
988                            decl: line.to_string(),
989                            msg: format!(
990                                "unknown declaration `{other}`; a line opens with let, kind, shape, shape-after, test or rule"
991                            ),
992                        }));
993                    }
994                },
995            }
996        }
997        Ok(())
998    }
999
1000    /// A rule from its block: the name on the `rule NAME` line and the
1001    /// `field = value` lines under it, each with its line number. The
1002    /// pattern is read first, whichever line holds it, since the other
1003    /// fields are checked against its registers.
1004    fn rule_block(&self, name: &str, line: usize, fields: &[(usize, &str)]) -> Result<Rule, ShapeError> {
1005        let mut split: Vec<(usize, &str, &str)> = Vec::with_capacity(fields.len());
1006        for &(n, field_line) in fields {
1007            let (field, value) = field_line.split_once('=').ok_or_else(|| ShapeError {
1008                decl: field_line.to_string(),
1009                msg: format!("line {n}: a rule's field line is `field = value`"),
1010            })?;
1011            split.push((n, field.trim(), value.trim()));
1012        }
1013        let Some(&(n, _, source)) = split.iter().find(|(_, field, _)| *field == "pattern") else {
1014            return Err(ShapeError {
1015                decl: format!("rule {name}"),
1016                msg: format!("line {line}: rule {name} has no `pattern = PATTERN` line"),
1017            });
1018        };
1019        let pattern = self.rule_pattern(source).map_err(|mut e| {
1020            e.msg = format!("line {n}: {}", e.msg);
1021            e
1022        })?;
1023        let mut rule = Rule {
1024            file: None,
1025            line,
1026            name: name.to_string(),
1027            pattern,
1028            source: source.to_string(),
1029            message: String::new(),
1030            severity: Severity::Warning,
1031            fix: None,
1032            files: Vec::new(),
1033            unless: Vec::new(),
1034            record: None,
1035            meta: Vec::new(),
1036        };
1037        for &(n, field, value) in &split {
1038            let at = |mut e: ShapeError| {
1039                e.msg = format!("line {n}: {}", e.msg);
1040                e
1041            };
1042            match field {
1043                "pattern" => {}
1044                "message" => rule.message = value.to_string(),
1045                "severity" => {
1046                    rule.severity = Severity::parse(value).ok_or_else(|| {
1047                        at(ShapeError {
1048                            decl: format!("{field} = {value}"),
1049                            msg: format!("{value:?} is not a severity; write error, warning or note"),
1050                        })
1051                    })?;
1052                }
1053                _ => self.rule_field(&mut rule, field, value).map_err(at)?,
1054            }
1055        }
1056        if rule.message.is_empty() {
1057            return Err(ShapeError {
1058                decl: format!("rule {name}"),
1059                msg: format!("line {line}: rule {name} has no `message = TEXT` line"),
1060            });
1061        }
1062        Ok(rule)
1063    }
1064
1065    /// A rule from one line: `NAME [severity] "message" = PATTERN`.
1066    fn rule_line(&self, decl: &str, line: usize) -> Result<Rule, ShapeError> {
1067        let err = |msg: &str| ShapeError { decl: decl.to_string(), msg: msg.to_string() };
1068        let form = "expected `rule NAME [error|warning|note] \"message\" = PATTERN`, or `rule NAME` over indented `field = value` lines";
1069        let (name, rest) = decl.split_once(char::is_whitespace).ok_or_else(|| err(form))?;
1070        let rest = rest.trim_start();
1071        let (severity, rest) = match rest.split_once(char::is_whitespace) {
1072            Some((word, after)) if !word.starts_with('"') => {
1073                let severity = Severity::parse(word)
1074                    .ok_or_else(|| err(&format!("{word:?} is not a severity; write error, warning or note")))?;
1075                (severity, after.trim_start())
1076            }
1077            _ => (Severity::Warning, rest),
1078        };
1079        if !rest.starts_with('"') {
1080            return Err(err(form));
1081        }
1082        let (message, next) = read_test_text(rest, 0).map_err(|m| err(&m))?;
1083        let rest = rest[next..].trim_start();
1084        let source = rest.strip_prefix('=').ok_or_else(|| err(form))?.trim();
1085        if source.is_empty() {
1086            return Err(err("the rule has no pattern after `=`"));
1087        }
1088        let pattern = self.rule_pattern(source)?;
1089        Ok(Rule {
1090            file: None,
1091            line,
1092            name: name.to_string(),
1093            pattern,
1094            source: source.to_string(),
1095            message,
1096            severity,
1097            fix: None,
1098            files: Vec::new(),
1099            unless: Vec::new(),
1100            record: None,
1101            meta: Vec::new(),
1102        })
1103    }
1104
1105    /// A rule's pattern, parsed against the declarations so far.
1106    fn rule_pattern(&self, source: &str) -> Result<Pattern, ShapeError> {
1107        crate::parser::parse_with_shapes(source, self).map_err(|e| ShapeError {
1108            decl: source.to_string(),
1109            msg: format!("pattern error at byte {}: {}", e.pos, e.msg),
1110        })
1111    }
1112
1113    /// A field of a rule beyond its pattern, message and severity, from a
1114    /// block line or a `FIELD NAME = VALUE` line: `fix`, `files`, `unless`,
1115    /// `record`, `record-start`, `record-span` or `meta.KEY`.
1116    fn rule_field(&self, rule: &mut Rule, field: &str, value: &str) -> Result<(), ShapeError> {
1117        let err = |msg: String| ShapeError { decl: format!("{field} = {value}"), msg };
1118        match field {
1119            "fix" => rule.fix = Some(value.to_string()),
1120            "files" => {
1121                rule.files = value.split(',').map(str::trim).filter(|g| !g.is_empty()).map(str::to_string).collect();
1122                if rule.files.is_empty() {
1123                    return Err(err("files names at least one glob, as -g takes them".to_string()));
1124                }
1125            }
1126            "unless" => rule.unless.push(self.rule_pattern(value)?),
1127            "record" => {
1128                rule.record = Some(crate::records::RecordUnit::parse(value).map_err(err)?);
1129            }
1130            "record-start" => rule.record = Some(crate::records::RecordUnit::Start(self.rule_pattern(value)?)),
1131            "record-span" => rule.record = Some(crate::records::RecordUnit::Span(self.rule_pattern(value)?)),
1132            _ => match field.strip_prefix("meta.") {
1133                Some(key) if !key.is_empty() && key.chars().all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric()) => {
1134                    rule.meta.push((key.to_string(), value.to_string()));
1135                }
1136                Some(key) => return Err(err(format!("{key:?} is not a metadata key; a key is alphanumerics, underscores and hyphens"))),
1137                None => {
1138                    return Err(err(format!(
1139                        "{field:?} is not a rule field; a rule takes pattern, message, severity, fix, files, unless, record, record-start, record-span and meta.KEY"
1140                    )));
1141                }
1142            },
1143        }
1144        Ok(())
1145    }
1146
1147    /// A `FIELD NAME ... = VALUE` line below a one-line rule: the field set
1148    /// on the rule declared under NAME above it; `meta NAME KEY = VALUE`
1149    /// carries the key before the `=`.
1150    fn rule_field_line(&mut self, field: &str, rest: &str) -> Result<(), ShapeError> {
1151        let err = |msg: String| ShapeError { decl: format!("{field} {rest}"), msg };
1152        let (name, rest) = rest
1153            .split_once(char::is_whitespace)
1154            .ok_or_else(|| err(format!("expected `{field} NAME = VALUE`")))?;
1155        let rest = rest.trim_start();
1156        let (key, value) = match field {
1157            "meta" => {
1158                let (key, value) = rest.split_once('=').ok_or_else(|| err("expected `meta NAME KEY = VALUE`".to_string()))?;
1159                (format!("meta.{}", key.trim()), value.trim())
1160            }
1161            _ => {
1162                let value = rest.strip_prefix('=').ok_or_else(|| err(format!("expected `{field} NAME = VALUE`")))?;
1163                (field.to_string(), value.trim())
1164            }
1165        };
1166        let position = self
1167            .rules
1168            .iter()
1169            .position(|r| r.name == name)
1170            .ok_or_else(|| err(format!("no rule named {name} is declared above this line")))?;
1171        let mut rule = self.rules[position].clone();
1172        self.rule_field(&mut rule, &key, value)?;
1173        if let Some(fix) = &rule.fix {
1174            crate::rewrite::Template::parse(fix, &rule.pattern.capture_names()).map_err(|e| {
1175                err(format!("fix error at byte {}: {}", e.pos, e.msg))
1176            })?;
1177        }
1178        self.rules[position] = rule;
1179        Ok(())
1180    }
1181
1182    /// Register `rule`: its name checked as a declaration's and against the
1183    /// rules so far, its message and fix checked as templates over its
1184    /// registers, and the rule made a sub-pattern under its name.
1185    fn add_rule(&mut self, rule: Rule, decl: &str) -> Result<(), ShapeError> {
1186        let err = |msg: String| ShapeError { decl: decl.to_string(), msg };
1187        self.check_name(decl, &rule.name)?;
1188        if self.rules.iter().any(|r| r.name == rule.name) {
1189            return Err(err(format!("rule {} is declared twice", rule.name)));
1190        }
1191        let bound = rule.pattern.capture_names();
1192        crate::rewrite::Template::parse_report(&rule.message, &bound)
1193            .map_err(|e| err(format!("message error at byte {}: {}", e.pos, e.msg)))?;
1194        if let Some(fix) = &rule.fix {
1195            crate::rewrite::Template::parse(fix, &bound)
1196                .map_err(|e| err(format!("fix error at byte {}: {}", e.pos, e.msg)))?;
1197        }
1198        self.lets.push((rule.name.clone(), rule.pattern.clone()));
1199        self.rules.push(rule);
1200        Ok(())
1201    }
1202
1203    /// This set with the library shapes and kinds `ids` name added, for a
1204    /// lex that has to produce the tokens a pattern naming them reads.
1205    #[must_use]
1206    pub(crate) fn with_library_shapes(&self, ids: &[u8]) -> ShapeSet {
1207        let mut set = self.clone();
1208        for &id in ids {
1209            if set.shapes.iter().any(|s| s.id == id) || set.kinds.iter().any(|k| k.id == id) {
1210                continue;
1211            }
1212            if let Some(shape) = crate::library::shape_of(id) {
1213                set.shapes.push(shape.clone());
1214            } else if let Some(kind) = crate::library::kind_of(id) {
1215                set.kinds.push(kind.clone());
1216            }
1217        }
1218        set
1219    }
1220
1221    /// The longest shape matching at `i`, as `(id, end)`, among those with the
1222    /// given precedence whose guard accepts the bytes. A longer span wins;
1223    /// equal spans go to the earlier declaration.
1224    #[must_use]
1225    pub fn longest_at(&self, input: &[u8], i: usize, when: Precedence) -> Option<(u8, usize)> {
1226        let mut best: Option<(u8, usize)> = None;
1227        for s in &self.shapes {
1228            if s.precedence != when {
1229                continue;
1230            }
1231            // The window is the shape's own maximum match length, bounding the
1232            // slice each position reads. A guarded shape takes the longest
1233            // end its guard accepts: the longest the pattern reaches may run
1234            // into what follows, and a shorter end can still be the token.
1235            let hi = (i + s.window).min(input.len());
1236            let len = match s.guard {
1237                None => match s.pat.longest_prefix(&input[i..hi]) {
1238                    Some(len) => len,
1239                    None => continue,
1240                },
1241                Some(guard) => {
1242                    let ends = s.pat.prefix_ends(&input[i..hi]);
1243                    match ends.iter().rev().find(|&&len| len > 0 && guard(&input[i..i + len])) {
1244                        Some(&len) => len,
1245                        None => continue,
1246                    }
1247                }
1248            };
1249            if len == 0 {
1250                continue;
1251            }
1252            if best.is_none_or(|(_, e)| i + len > e) {
1253                best = Some((s.id, i + len));
1254            }
1255        }
1256        best
1257    }
1258}
1259
1260/// Names held by built-in `\{name}` atoms.
1261const BUILTIN_ATOM_NAMES: &[&str] = &[
1262    "number",
1263    "word",
1264    "quoted",
1265    "ip",
1266    "url",
1267    "email",
1268    "timestamp",
1269    "punct",
1270    "whitespace",
1271    "version",
1272    "uuid",
1273    "mac",
1274    "hexcolor",
1275    "cidr",
1276    "bytesize",
1277    "percent",
1278    "money",
1279    "hash",
1280    "hashdigest",
1281    "duration",
1282    "path",
1283    "jwt",
1284    "creditcard",
1285    "card",
1286    "base64",
1287    "b64",
1288    "geo",
1289    "coord",
1290    "phone",
1291    "tel",
1292    "quantity",
1293    "qty",
1294];
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::*;
1299
1300    #[test]
1301    fn declares_and_matches_a_bounded_shape() {
1302        let mut set = ShapeSet::new();
1303        let id = set.declare("order = `[A-Z]{3}-[0-9]{4}`", Precedence::Before).expect("declares");
1304        assert_eq!(id, 0);
1305        assert_eq!(set.id_of("order"), Some(0));
1306        assert_eq!(set.name_of(0), Some("order"));
1307        let input = b"ABC-1234 rest";
1308        assert_eq!(set.longest_at(input, 0, Precedence::Before), Some((0, 8)));
1309        assert_eq!(set.longest_at(input, 9, Precedence::Before), None);
1310    }
1311
1312    #[test]
1313    fn an_unbounded_shape_is_rejected_at_declaration() {
1314        let mut set = ShapeSet::new();
1315        let e = set.declare("wild = `[A-Z]+`", Precedence::Before).unwrap_err();
1316        assert!(e.msg.contains("bounded"), "got {:?}", e.msg);
1317        assert!(set.is_empty(), "a rejected shape is not stored");
1318    }
1319
1320    #[test]
1321    fn malformed_declarations_are_rejected() {
1322        let mut set = ShapeSet::new();
1323        for bad in [
1324            "no equals sign",
1325            "name = missing backticks",
1326            "= `[A-Z]`",
1327            "bad name = `[A-Z]`",
1328            "x = ``",
1329        ] {
1330            assert!(set.declare(bad, Precedence::Before).is_err(), "should reject: {bad}");
1331        }
1332    }
1333
1334    #[test]
1335    fn a_name_may_not_shadow_a_builtin_atom_or_repeat() {
1336        let mut set = ShapeSet::new();
1337        assert!(set.declare("ip = `[0-9]{3}`", Precedence::Before).is_err());
1338        set.declare("tag = `[A-Z]{2}`", Precedence::Before).expect("declares");
1339        assert!(set.declare("tag = `[a-z]{2}`", Precedence::Before).is_err());
1340    }
1341
1342    #[test]
1343    fn the_longest_shape_wins_and_ties_go_to_declaration_order() {
1344        let mut set = ShapeSet::new();
1345        set.declare("short = `[A-Z]{2}`", Precedence::Before).expect("declares");
1346        set.declare("long = `[A-Z]{4}`", Precedence::Before).expect("declares");
1347        assert_eq!(set.longest_at(b"ABCD", 0, Precedence::Before), Some((1, 4)));
1348        assert_eq!(set.longest_at(b"AB c", 0, Precedence::Before), Some((0, 2)));
1349    }
1350
1351    #[test]
1352    fn a_pattern_file_declares_sub_patterns_kinds_and_shapes() {
1353        let mut set = ShapeSet::new();
1354        set.declare_text(
1355            "# a file\nlet kv = \\W \"=\" \\N\nkind pair = \\{kv}\nshape tag = `[A-Z]{3}`\n\nshape-after code = `[0-9]{2}`\n",
1356        )
1357        .expect("declares");
1358        assert!(set.let_of("kv").is_some());
1359        assert_eq!(set.id_of("pair"), Some(0));
1360        assert_eq!(set.id_of("tag"), Some(1));
1361        assert_eq!(set.id_of("code"), Some(2));
1362        assert_eq!(set.name_of(0), Some("pair"));
1363        assert_eq!(set.pattern_kinds().len(), 1);
1364        assert!(!set.is_empty());
1365        let e = set.declare_text("shrug x = `a`").unwrap_err();
1366        assert!(e.msg.contains("line 1"), "{e}");
1367        let e = set.declare_text("let tag = \\W").unwrap_err();
1368        assert!(e.msg.contains("exists"), "{e}");
1369        set.declare_text("let kv = \\N").expect("a later let is accepted");
1370        assert!(matches!(set.let_of("kv"), Some(Pattern::Atom(_))), "a later let shadows an earlier one");
1371    }
1372
1373    #[test]
1374    fn test_lines_state_what_a_name_matches_and_name_their_line_when_it_does_not() {
1375        let mut set = ShapeSet::new();
1376        set.declare_text(
1377            "let rhs = \\N | \\Q\ntest assign accepts \"x = 1\" \"y = \\\"bob\\\"\" rejects \"x == 1\"\nkind assign = \\W \"=\" \\{rhs}\ntest rhs accepts \"42\" rejects \"forty\"\n",
1378        )
1379        .expect("declares");
1380        assert_eq!(set.tests().len(), 2);
1381        assert_eq!(set.tests()[0].line, 2);
1382        assert_eq!(set.tests()[0].accepts, vec!["x = 1".to_string(), "y = \"bob\"".to_string()]);
1383        assert_eq!(set.tests()[0].rejects, vec!["x == 1".to_string()]);
1384        assert!(set.run_tests().is_empty(), "{:?}", set.run_tests());
1385
1386        let mut set = ShapeSet::new();
1387        set.declare_text(
1388            "let rhs = \\N\ntest rhs accepts \"42\" \"\\\"bob\\\"\" rejects \"4 2\"\ntest nosuch accepts \"x\"\ntest iban accepts \"GB82 WEST 1234 5698 7654 32\" rejects \"GB82WEST12345698765433\"\n",
1389        )
1390        .expect("declares");
1391        let failures: Vec<String> = set.run_tests().iter().map(ToString::to_string).collect();
1392        assert_eq!(
1393            failures,
1394            vec![
1395                "line 2: rhs accepts \"\\\"bob\\\"\": no match\n  tokens: quoted \"\\\"bob\\\"\"".to_string(),
1396                "line 2: rhs rejects \"4 2\": matched \"4\" at 0..1".to_string(),
1397                "line 3: nosuch cannot be tested: unknown named atom \\{nosuch}; it is neither a built-in, a declared shape, kind or sub-pattern, nor a library entry".to_string(),
1398            ]
1399        );
1400
1401        let e = ShapeSet::new().declare_text("test rhs \"42\"").expect_err("a text before a keyword");
1402        assert!(e.msg.contains("follows accepts, reads or rejects"), "{e}");
1403        let e = ShapeSet::new().declare_text("test rhs accepts").expect_err("no text");
1404        assert!(e.msg.contains("at least one text"), "{e}");
1405        let e = ShapeSet::new().declare_text("test rhs accepts \"4").expect_err("unterminated");
1406        assert!(e.msg.contains("unterminated"), "{e}");
1407        let e = ShapeSet::new().declare_text("test rhs accepts \"\\q\"").expect_err("unknown escape");
1408        assert!(e.msg.contains("unknown escape \\q"), "{e}");
1409        let e = ShapeSet::new().declare_text("test rhs matches \"4\"").expect_err("unknown word");
1410        assert!(e.msg.contains("found `matches`"), "{e}");
1411
1412        // A `reads` names one span and the one text it is read out of, and
1413        // both the keyword between them and the text after it are required:
1414        // a pair read by position would take `reads "a" "b" "c" "d"` for two
1415        // pairs, which is the reading the writer of such a line least meant.
1416        let mut set = ShapeSet::new();
1417        set.declare_text("test kelvin reads \"4.2K\" in \"cooled to 4.2K overnight\"\n").expect("declares");
1418        assert_eq!(set.tests()[0].reads, vec![("4.2K".to_string(), "cooled to 4.2K overnight".to_string())]);
1419        assert!(set.run_tests().is_empty(), "{:?}", set.run_tests());
1420        let e = ShapeSet::new().declare_text("test rhs reads \"4\"").expect_err("no text to read from");
1421        assert!(e.msg.contains("names no text to read it from"), "{e}");
1422        let e = ShapeSet::new().declare_text("test rhs reads \"4\" in").expect_err("no text after in");
1423        assert!(e.msg.contains("names no text to read it from"), "{e}");
1424        let e = ShapeSet::new().declare_text("test rhs reads \"4\" \"a 4 b\"").expect_err("no in");
1425        assert!(e.msg.contains("`in` stands between the span \"4\""), "{e}");
1426        let e = ShapeSet::new().declare_text("test rhs accepts \"4\" in \"a\"").expect_err("in alone");
1427        assert!(e.msg.contains("`in` follows the span a `reads` names"), "{e}");
1428    }
1429
1430    #[test]
1431    fn a_guarded_shape_takes_the_longest_end_its_check_accepts() {
1432        let mut set = ShapeSet::new();
1433        set.declare("digits = `[0-9]{4,8}`", Precedence::Before).expect("declares");
1434        set.shapes[0].guard = Some(|text: &[u8]| text.len() % 2 == 1);
1435        assert_eq!(set.longest_at(b"12345678 x", 0, Precedence::Before), Some((0, 7)));
1436        assert_eq!(set.longest_at(b"1234 x", 0, Precedence::Before), None);
1437    }
1438
1439    #[test]
1440    fn precedence_partitions_the_set() {
1441        let mut set = ShapeSet::new();
1442        set.declare("early = `[A-Z]{2}`", Precedence::Before).expect("declares");
1443        set.declare("late = `[0-9]{2}`", Precedence::After).expect("declares");
1444        assert_eq!(set.longest_at(b"AB", 0, Precedence::Before), Some((0, 2)));
1445        assert_eq!(set.longest_at(b"AB", 0, Precedence::After), None);
1446        assert_eq!(set.longest_at(b"12", 0, Precedence::After), Some((1, 2)));
1447        assert_eq!(set.longest_at(b"12", 0, Precedence::Before), None);
1448    }
1449}