Skip to main content

litcheck_filecheck/
lib.rs

1pub mod ast;
2pub mod check;
3mod context;
4mod cursor;
5mod env;
6pub mod errors;
7pub mod expr;
8mod input;
9pub mod parse;
10pub mod pattern;
11pub mod rules;
12mod test;
13
14pub use self::errors::{CheckFailedError, RelatedCheckError, RelatedError, TestFailed};
15#[cfg(test)]
16pub use self::test::TestContext;
17pub use self::test::{Test, TestResult};
18
19use clap::{ArgAction, Args, ColorChoice, ValueEnum, builder::ValueParser};
20use litcheck::diagnostics::{DefaultSourceManager, DiagResult, Report, SourceManager};
21use std::sync::Arc;
22
23#[doc(hidden)]
24pub use litcheck;
25
26extern crate self as litcheck_filecheck;
27
28pub(crate) mod common {
29    pub use std::{
30        borrow::Cow,
31        fmt,
32        ops::{ControlFlow, RangeBounds},
33        sync::Arc,
34    };
35
36    pub use either::Either::{self, Left, Right};
37    #[cfg(test)]
38    pub use litcheck::reporting;
39    pub use litcheck::{
40        Symbol,
41        diagnostics::{
42            Diag, DiagResult, Diagnostic, FileName, Label, LabeledSpan, Report, SourceFile,
43            SourceId, SourceLanguage, SourceManager, SourceManagerError, SourceManagerExt,
44            SourceSpan, Span, Spanned, miette,
45        },
46        range::{self, Range},
47        symbols,
48        text::{self, Newline},
49    };
50    pub use regex_automata::{meta::Regex, util::look::LookMatcher};
51    pub use smallvec::{SmallVec, smallvec};
52
53    pub use crate::Config;
54    pub use crate::ast::{Check, Constraint};
55    pub use crate::context::{Context, ContextExt, ContextGuard, MatchContext};
56    pub use crate::cursor::{Cursor, CursorGuard, CursorPosition};
57    pub use crate::env::{Env, LexicalScope, LexicalScopeMut, ScopeGuard};
58    pub use crate::errors::{
59        CheckFailedError, RelatedCheckError, RelatedError, RelatedLabel, TestFailed,
60    };
61    pub use crate::expr::{BinaryOp, Expr, Number, NumberFormat, Value, VariableName};
62    pub use crate::input::Input;
63    pub use crate::pattern::{
64        AnyMatcher, CaptureInfo, MatchInfo, MatchResult, MatchType, Matcher, MatcherMut, Matches,
65        Pattern, PatternIdentifier, PatternSearcher, Searcher,
66    };
67    pub use crate::rules::{DynRule, Rule};
68    #[cfg(test)]
69    pub(crate) use crate::test::TestContext;
70    pub use crate::test::TestResult;
71}
72
73use common::{Symbol, symbols};
74
75pub const DEFAULT_CHECK_PREFIXES: &[&str] = &["CHECK"];
76pub const DEFAULT_COMMENT_PREFIXES: &[&str] = &["COM", "RUN"];
77
78/// FileCheck reads two files, one from standard input, and one specified on
79/// the command line; and uses one to verify the other.
80pub struct Config {
81    pub source_manager: Arc<dyn SourceManager>,
82    pub options: Options,
83}
84
85impl Config {
86    #[inline(always)]
87    pub fn source_manager(&self) -> &dyn SourceManager {
88        &self.source_manager
89    }
90
91    /// Returns true if the user has passed -v, requesting diagnostic remarks be output for matches
92    pub const fn remarks_enabled(&self) -> bool {
93        self.options.verbose > 0
94    }
95
96    /// Returns true if the user has passed -vv, requesting verbose diagnostics be output
97    ///
98    /// NOTE: This is different than the tracing enabled via LITCHECK_TRACE which is tailored for
99    /// diagnosing litcheck internals. Instead, the tracing referred to here is end user-oriented,
100    /// and meant to provide helpful information to understand why a test is failing
101    pub const fn tracing_enabled(&self) -> bool {
102        self.options.verbose > 1
103    }
104}
105
106impl Default for Config {
107    fn default() -> Self {
108        Self {
109            source_manager: Arc::from(DefaultSourceManager::default()),
110            options: Options::default(),
111        }
112    }
113}
114
115impl core::fmt::Debug for Config {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        core::fmt::Debug::fmt(&self.options, f)
118    }
119}
120
121/// FileCheck reads two files, one from standard input, and one specified on
122/// the command line; and uses one to verify the other.
123#[derive(Debug, Args)]
124pub struct Options {
125    /// Allow checking empty input. By default, empty input is rejected.
126    #[arg(
127        long,
128        default_value_t = false,
129        action(clap::ArgAction::SetTrue),
130        help_heading = "Input"
131    )]
132    pub allow_empty: bool,
133    /// Which prefixes to treat as directives.
134    ///
135    /// For example, in the directive `CHECK-SAME`, `CHECK` is the prefix.
136    #[arg(
137        long,
138        alias = "check-prefix",
139        value_name = "PREFIX",
140        default_value = "CHECK",
141        action(clap::ArgAction::Append),
142        value_parser(prefix_value_parser()),
143        value_delimiter(','),
144        help_heading = "Syntax"
145    )]
146    pub check_prefixes: Vec<Symbol>,
147    /// Which prefixes to treat as comments.
148    ///
149    /// All content on a line following a comment directive is ignored,
150    /// up to the next newline.
151    #[arg(
152        long,
153        alias = "comment-prefix",
154        value_name = "PREFIX",
155        default_value = "COM,RUN",
156        action(clap::ArgAction::Append),
157        value_parser(prefix_value_parser()),
158        value_delimiter(','),
159        help_heading = "Syntax"
160    )]
161    pub comment_prefixes: Vec<Symbol>,
162    /// If specifying multiple check prefixes, this controls whether or not
163    /// to raise an error if one of the prefixes is missing in the test file.
164    #[arg(long, default_value_t = false, help_heading = "Syntax")]
165    pub allow_unused_prefixes: bool,
166    /// Disable default canonicalization of whitespace.
167    ///
168    /// By default, FileCheck canonicalizes horizontal whitespace (spaces and tabs)
169    /// which causes it to ignore these differences (a space will match a tab).
170    ///
171    /// This flag disables horizontal whitespace canonicalization.
172    ///
173    /// Newlines are always canonicalized to LF regardless of this setting.
174    #[arg(
175        long = "strict-whitespace",
176        default_value_t = false,
177        help_heading = "Matching"
178    )]
179    pub strict_whitespace: bool,
180    /// This flag changes the default matching behavior to require all positive
181    /// matches to cover an entire line. Leading/trailing whitespace is ignored
182    /// unless `--strict-whitespace` is also specified.
183    ///
184    /// By default, FileCheck allows matches of anywhere on a line, so by setting
185    /// this, you effectively insert `{{^.*}}` or `{{^}}` before, and `{{[*$]}}`
186    /// or `{{$}}` after every positive check pattern.
187    ///
188    /// NOTE Negative matches, i.e `CHECK-NOT` are not affected by this option.
189    #[arg(long, default_value_t = false, help_heading = "Matching")]
190    pub match_full_lines: bool,
191    /// Disable case-sensitive matching
192    #[arg(long, default_value_t = false, help_heading = "Matching")]
193    pub ignore_case: bool,
194    /// Adds implicit negative checks for the specified patterns between positive checks.
195    ///
196    /// This option allows writing stricter tests without polluting them with CHECK-NOTs.
197    ///
198    /// For example, `--implicit-check-not warning:` can be useful when testing diagnostic
199    /// messages from tools that don’t have an option similar to `clang -verify`. With this
200    /// option FileCheck will verify that input does not contain warnings not covered by any
201    /// `CHECK:` patterns.
202    #[arg(
203        long,
204        value_name = "PATTERN",
205        action(clap::ArgAction::Append),
206        help_heading = "Matching"
207    )]
208    pub implicit_check_not: Vec<Symbol>,
209    /// Dump input to stderr, adding annotations representing currently enabled diagnostics.
210    #[arg(long, value_enum, value_name = "TYPE", default_value_t = Dump::Fail, help_heading = "Output")]
211    pub dump_input: Dump,
212    /// Specify the parts of the input to dump when `--dump-input` is set.
213    ///
214    /// When specified, print only input lines of KIND, plus any context specified by `--dump-input-context`.
215    ///
216    /// Defaults to `error` when `--dump-input=fail`, and `all` when `--dump-input=always`
217    #[arg(
218        long,
219        value_enum,
220        value_name = "KIND",
221        default_value_t = DumpFilter::Error,
222        default_value_ifs([("dump-input", "fail", Some("error")), ("dump-input", "always", Some("all"))]),
223        help_heading = "Output"
224    )]
225    pub dump_input_filter: DumpFilter,
226    /// Enables scope for regex variables
227    ///
228    /// Variables with names that start with `$` are considered global, and remain set throughout the file
229    ///
230    /// All other variables get undefined after each encountered `CHECK-LABEL`
231    #[arg(long, default_value_t = false, help_heading = "Variables")]
232    pub enable_var_scope: bool,
233    /// Set a pattern variable VAR with value VALUE that can be used in `CHECK:` lines
234    ///
235    /// You must specify each one in `key=value` format
236    #[arg(
237        long = "define",
238        short = 'D',
239        value_name = "NAME=VALUE",
240        help_heading = "Variables"
241    )]
242    pub variables: Vec<expr::CliVariable>,
243    /// Set the verbosity level.
244    ///
245    /// If specified a single time, it causes filecheck to print good directive pattern matches
246    ///
247    /// If specified multiple times, filecheck will emit internal diagnostics to aid in troubleshooting.
248    ///
249    /// If `--dump-input=fail` or `--dump-input=always`, add information as input annotations instead.
250    #[arg(long, short = 'v', action = ArgAction::Count, help_heading = "Output")]
251    pub verbose: u8,
252    /// Whether, and how, to color terminal output
253    #[arg(
254        global(true),
255        value_enum,
256        long,
257        default_value_t = ColorChoice::Auto,
258        default_missing_value = "auto",
259        help_heading = "Output"
260    )]
261    pub color: ColorChoice,
262}
263
264/// This is implemented for [Options] so that we can use [clap::Parser::update_from] on it.
265impl clap::CommandFactory for Options {
266    fn command() -> clap::Command {
267        let cmd = clap::Command::new("filecheck")
268            .no_binary_name(true)
269            .disable_help_flag(true)
270            .disable_version_flag(true);
271        <Self as clap::Args>::augment_args(cmd)
272    }
273
274    fn command_for_update() -> clap::Command {
275        let cmd = clap::Command::new("filecheck")
276            .no_binary_name(true)
277            .disable_help_flag(true)
278            .disable_version_flag(true);
279        <Self as clap::Args>::augment_args_for_update(cmd)
280    }
281}
282
283/// This is implemented for [Options] in order to use [clap::Parser::update_from].
284impl clap::Parser for Options {}
285
286impl Default for Options {
287    fn default() -> Self {
288        Self {
289            allow_empty: false,
290            check_prefixes: vec![symbols::Check],
291            comment_prefixes: vec![symbols::Com, symbols::Run],
292            allow_unused_prefixes: false,
293            strict_whitespace: false,
294            match_full_lines: false,
295            ignore_case: false,
296            implicit_check_not: vec![],
297            dump_input: Default::default(),
298            dump_input_filter: Default::default(),
299            enable_var_scope: false,
300            variables: vec![],
301            verbose: 0,
302            color: Default::default(),
303        }
304    }
305}
306
307impl Options {
308    pub fn validate(&self) -> DiagResult<()> {
309        // Validate that we do not have overlapping check and comment prefixes
310        if self
311            .check_prefixes
312            .iter()
313            .any(|prefix| *prefix != symbols::Check)
314        {
315            for check_prefix in self.check_prefixes.iter() {
316                if self.comment_prefixes.contains(check_prefix) {
317                    return Err(Report::msg(format!(
318                        "supplied check prefix must be unique among check and comment prefixes: '{check_prefix}'"
319                    )));
320                }
321            }
322        } else if self
323            .comment_prefixes
324            .iter()
325            .any(|prefix| !matches!(*prefix, symbols::Com | symbols::Run))
326        {
327            for comment_prefix in self.comment_prefixes.iter() {
328                if self.check_prefixes.contains(comment_prefix) {
329                    return Err(Report::msg(format!(
330                        "supplied comment prefix must be unique among check and comment prefixes: '{comment_prefix}'"
331                    )));
332                }
333            }
334        }
335
336        Ok(())
337    }
338}
339
340#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, ValueEnum)]
341pub enum Dump {
342    /// Explain input dump and quit
343    Help,
344    /// Always dump input
345    Always,
346    /// Dump input on failure
347    #[default]
348    Fail,
349    /// Never dump input
350    Never,
351}
352
353#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, ValueEnum)]
354pub enum DumpFilter {
355    /// All input lines
356    All,
357    /// Input lines with annotations
358    AnnotationFull,
359    /// Input lines with starting points of annotations
360    Annotation,
361    /// Input lines with starting points of error annotations
362    #[default]
363    Error,
364}
365
366fn prefix_value_parser() -> ValueParser {
367    use clap::{Error, error::ErrorKind};
368
369    ValueParser::from(move |s: &str| -> Result<Symbol, clap::Error> {
370        if s.is_empty() {
371            return Err(Error::raw(
372                ErrorKind::ValueValidation,
373                "supplied prefix must not be an empty string",
374            ));
375        }
376        if !s.starts_with(|c: char| c.is_ascii_alphabetic()) {
377            return Err(Error::raw(
378                ErrorKind::ValueValidation,
379                "supplied prefix must start with an ASCII alphabetic character",
380            ));
381        }
382        if s.contains(|c: char| !c.is_alphanumeric() && c != '_' && c != '-') {
383            return Err(Error::raw(
384                ErrorKind::ValueValidation,
385                "supplied prefix may only contain ASCII alphanumerics, hyphens, or underscores",
386            ));
387        }
388        Ok(Symbol::intern(s))
389    })
390}
391
392/// Use `filecheck` in a Rust test directly against an input value that implements `Display`.
393///
394/// ## Example
395///
396/// ```rust
397/// #![expect(unstable_name_collisions)]
398/// use litcheck_filecheck::filecheck;
399/// use itertools::Itertools;
400///
401/// let original = "abbc";
402/// let modified = original.chars().intersperse('\n').collect::<String>();
403///
404/// filecheck!(modified, "
405/// ; CHECK: a
406/// ; CHECK-NEXT: b
407/// ; CHECK-NEXT: b
408/// ; CHECK-NEXT: c
409/// ");
410/// ```
411///
412/// If custom configuration is desired, you may instantiate the `filecheck` configuration (see
413/// [Config]) and pass it as an additional parameter:
414///
415/// ```rust
416/// #![expect(unstable_name_collisions)]
417/// use litcheck_filecheck::{filecheck, Config, Options};
418/// use itertools::Itertools;
419///
420/// let original = "abbc";
421/// let modified = original.chars().intersperse('\n').collect::<String>();
422/// let config = Config {
423///     options: Options {
424///         match_full_lines: true,
425///         ..Options::default()
426///     },
427///     ..Config::default()
428/// };
429///
430/// filecheck!(modified, "
431/// ; CHECK: a
432/// ; CHECK-NEXT: b
433/// ; CHECK-NEXT: b
434/// ; CHECK-NEXT: c
435/// ");
436/// ```
437///
438/// If successful, the `filecheck!` macro returns the pattern matches produced by verifying the
439/// checks, allowing you to examine them in more detail.
440#[macro_export]
441macro_rules! filecheck {
442    ($input:expr, $checks:expr) => {
443        ::litcheck_filecheck::filecheck!($input, $checks, ::litcheck_filecheck::Config::default())
444    };
445
446    ($input:expr, $checks:expr, $config:expr) => {{
447        #[allow(unused)]
448        let config = $config;
449        let input = ::litcheck_filecheck::source_file!(config, $input.to_string());
450        let checks = ::litcheck_filecheck::source_file!(config, $checks.to_string());
451        let mut test = ::litcheck_filecheck::Test::new(checks, &config);
452        match test.verify(input) {
453            Err(err) => {
454                let printer = ::litcheck_filecheck::litcheck::reporting::PrintDiagnostic::new(err);
455                panic!("{printer}");
456            }
457            Ok(matches) => matches,
458        }
459    }};
460}