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