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::{builder::ValueParser, ArgAction, Args, ColorChoice, ValueEnum};
20use std::sync::Arc;
21
22#[doc(hidden)]
23pub use litcheck;
24
25pub(crate) mod common {
26    pub use std::{
27        borrow::Cow,
28        fmt,
29        ops::{ControlFlow, RangeBounds},
30        sync::Arc,
31    };
32
33    pub use either::Either::{self, Left, Right};
34    pub use litcheck::{
35        diagnostics::{
36            ArcSource, Diag, DiagResult, Diagnostic, Label, NamedSourceFile, Report, Source,
37            SourceFile, SourceSpan, Span, Spanned,
38        },
39        range::{self, Range},
40        text::{self, Newline},
41        StringInterner, Symbol,
42    };
43    pub use regex_automata::{meta::Regex, util::look::LookMatcher};
44    pub use smallvec::{smallvec, SmallVec};
45
46    pub use crate::ast::{Check, Constraint};
47    pub use crate::context::{Context, ContextExt, ContextGuard, MatchContext};
48    pub use crate::cursor::{Cursor, CursorGuard, CursorPosition};
49    pub use crate::env::{Env, LexicalScope, LexicalScopeExtend, LexicalScopeMut, ScopeGuard};
50    pub use crate::errors::{
51        CheckFailedError, RelatedCheckError, RelatedError, RelatedLabel, TestFailed,
52    };
53    pub use crate::expr::{BinaryOp, Expr, Number, NumberFormat, Value, VariableName};
54    pub use crate::input::Input;
55    pub use crate::pattern::{
56        AnyMatcher, CaptureInfo, MatchInfo, MatchResult, MatchType, Matcher, MatcherMut, Matches,
57        Pattern, PatternIdentifier, PatternSearcher, Searcher,
58    };
59    pub use crate::rules::{DynRule, Rule};
60    #[cfg(test)]
61    pub(crate) use crate::test::TestContext;
62    pub use crate::test::TestResult;
63    pub use crate::Config;
64}
65
66pub const DEFAULT_CHECK_PREFIXES: &[&str] = &["CHECK"];
67pub const DEFAULT_COMMENT_PREFIXES: &[&str] = &["COM", "RUN"];
68
69/// FileCheck reads two files, one from standard input, and one specified on
70/// the command line; and uses one to verify the other.
71#[derive(Debug, Args)]
72pub struct Config {
73    /// Allow checking empty input. By default, empty input is rejected.
74    #[arg(
75        default_value_t = false,
76        action(clap::ArgAction::SetTrue),
77        help_heading = "Input"
78    )]
79    pub allow_empty: bool,
80    /// Which prefixes to treat as directives.
81    ///
82    /// For example, in the directive `CHECK-SAME`, `CHECK` is the prefix.
83    #[arg(
84        long = "check-prefix",
85        value_name = "PREFIX",
86        default_value = "CHECK",
87        action(clap::ArgAction::Append),
88        value_parser(re_value_parser("^[A-Za-z][A-Za-z0-9_]*")),
89        help_heading = "Syntax"
90    )]
91    pub check_prefixes: Vec<Arc<str>>,
92    /// Which prefixes to treat as comments.
93    ///
94    /// All content on a line following a comment directive is ignored,
95    /// up to the next newline.
96    #[arg(
97        long = "comment-prefix",
98        value_name = "PREFIX",
99        default_value = "COM,RUN",
100        action(clap::ArgAction::Append),
101        value_parser(re_value_parser("^[A-Za-z][A-Za-z0-9_]*")),
102        help_heading = "Syntax"
103    )]
104    pub comment_prefixes: Vec<Arc<str>>,
105    /// If specifying multiple check prefixes, this controls whether or not
106    /// to raise an error if one of the prefixes is missing in the test file.
107    #[arg(long, default_value_t = false, help_heading = "Syntax")]
108    pub allow_unused_prefixes: bool,
109    /// Disable default canonicalization of whitespace.
110    ///
111    /// By default, FileCheck canonicalizes horizontal whitespace (spaces and tabs)
112    /// which causes it to ignore these differences (a space will match a tab).
113    ///
114    /// This flag disables horizontal whitespace canonicalization.
115    ///
116    /// Newlines are always canonicalized to LF regardless of this setting.
117    #[arg(
118        long = "strict-whitespace",
119        default_value_t = false,
120        help_heading = "Matching"
121    )]
122    pub strict_whitespace: bool,
123    /// This flag changes the default matching behavior to require all positive
124    /// matches to cover an entire line. Leading/trailing whitespace is ignored
125    /// unless `--strict-whitespace` is also specified.
126    ///
127    /// By default, FileCheck allows matches of anywhere on a line, so by setting
128    /// this, you effectively insert `{{^.*}}` or `{{^}}` before, and `{{[*$]}}`
129    /// or `{{$}}` after every positive check pattern.
130    ///
131    /// NOTE Negative matches, i.e `CHECK-NOT` are not affected by this option.
132    #[arg(long, default_value_t = false, help_heading = "Matching")]
133    pub match_full_lines: bool,
134    /// Disable case-sensitive matching
135    #[arg(long, default_value_t = false, help_heading = "Matching")]
136    pub ignore_case: bool,
137    /// Adds implicit negative checks for the specified patterns between positive checks.
138    ///
139    /// This option allows writing stricter tests without polluting them with CHECK-NOTs.
140    ///
141    /// For example, `--implicit-check-not warning:` can be useful when testing diagnostic
142    /// messages from tools that don’t have an option similar to `clang -verify`. With this
143    /// option FileCheck will verify that input does not contain warnings not covered by any
144    /// `CHECK:` patterns.
145    #[arg(long, value_name = "CHECK", help_heading = "Matching")]
146    pub implicit_check_not: Vec<String>,
147    /// Dump input to stderr, adding annotations representing currently enabled diagnostics.
148    #[arg(long, value_enum, value_name = "TYPE", default_value_t = Dump::Fail, help_heading = "Output")]
149    pub dump_input: Dump,
150    /// Specify the parts of the input to dump when `--dump-input` is set.
151    ///
152    /// When specified, print only input lines of KIND, plus any context specified by `--dump-input-context`.
153    ///
154    /// Defaults to `error` when `--dump-input=fail`, and `all` when `--dump-input=always`
155    #[arg(
156        long,
157        value_enum,
158        value_name = "KIND",
159        default_value_t = DumpFilter::Error,
160        default_value_ifs([("dump-input", "fail", Some("error")), ("dump-input", "always", Some("all"))]),
161        help_heading = "Output"
162    )]
163    pub dump_input_filter: DumpFilter,
164    /// Enables scope for regex variables
165    ///
166    /// Variables with names that start with `$` are considered global, and remain set throughout the file
167    ///
168    /// All other variables get undefined after each encountered `CHECK-LABEL`
169    #[arg(long, default_value_t = false, help_heading = "Variables")]
170    pub enable_var_scope: bool,
171    /// Set a pattern variable VAR with value VALUE that can be used in `CHECK:` lines
172    ///
173    /// You must specify each one in `key=value` format
174    #[arg(
175        long = "define",
176        short = 'D',
177        value_name = "NAME=VALUE",
178        help_heading = "Variables"
179    )]
180    pub variables: Vec<expr::CliVariable>,
181    /// Set the verbosity level.
182    ///
183    /// If specified a single time, it causes filecheck to print good directive pattern matches
184    ///
185    /// If specified multiple times, filecheck will emit internal diagnostics to aid in troubleshooting.
186    ///
187    /// If `--dump-input=fail` or `--dump-input=always`, add information as input annotations instead.
188    #[arg(long, short = 'v', action = ArgAction::Count, help_heading = "Output")]
189    pub verbose: u8,
190    /// Whether, and how, to color terminal output
191    #[arg(
192        global(true),
193        value_enum,
194        long,
195        default_value_t = ColorChoice::Auto,
196        default_missing_value = "auto",
197        help_heading = "Output"
198    )]
199    pub color: ColorChoice,
200}
201impl Default for Config {
202    fn default() -> Self {
203        Self {
204            allow_empty: false,
205            check_prefixes: vec![Arc::from("CHECK".to_string().into_boxed_str())],
206            comment_prefixes: vec![
207                Arc::from("COM".to_string().into_boxed_str()),
208                Arc::from("RUN".to_string().into_boxed_str()),
209            ],
210            allow_unused_prefixes: false,
211            strict_whitespace: false,
212            match_full_lines: false,
213            ignore_case: false,
214            implicit_check_not: vec![],
215            dump_input: Default::default(),
216            dump_input_filter: Default::default(),
217            enable_var_scope: false,
218            variables: vec![],
219            verbose: 0,
220            color: Default::default(),
221        }
222    }
223}
224
225#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, ValueEnum)]
226pub enum Dump {
227    /// Explain input dump and quit
228    Help,
229    /// Always dump input
230    Always,
231    /// Dump input on failure
232    #[default]
233    Fail,
234    /// Never dump input
235    Never,
236}
237
238#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, ValueEnum)]
239pub enum DumpFilter {
240    /// All input lines
241    All,
242    /// Input lines with annotations
243    AnnotationFull,
244    /// Input lines with starting points of annotations
245    Annotation,
246    /// Input lines with starting points of error annotations
247    #[default]
248    Error,
249}
250
251fn re_value_parser(r: &'static str) -> ValueParser {
252    use clap::{error::ErrorKind, Error};
253
254    ValueParser::from(move |s: &str| -> Result<Arc<str>, clap::Error> {
255        let re = regex::Regex::new(r).unwrap();
256        if re.is_match(s) {
257            Ok(Arc::from(s.to_owned().into_boxed_str()))
258        } else {
259            Err(Error::raw(
260                ErrorKind::ValueValidation,
261                "'{s}' does not match expected pattern `{r}`",
262            ))
263        }
264    })
265}
266
267/// Use `filecheck` in a Rust test directly against an input value that implements `Display`.
268///
269/// ## Example
270///
271/// ```rust
272/// #![expect(unstable_name_collisions)]
273/// use litcheck_filecheck::filecheck;
274/// use itertools::Itertools;
275///
276/// let original = "abbc";
277/// let modified = original.chars().intersperse('\n').collect::<String>();
278///
279/// filecheck!(modified, "
280/// ; CHECK: a
281/// ; CHECK-NEXT: b
282/// ; CHECK-NEXT: b
283/// ; CHECK-NEXT: c
284/// ");
285/// ```
286///
287/// If custom configuration is desired, you may instantiate the `filecheck` configuration (see
288/// [Config]) and pass it as an additional parameter:
289///
290/// ```rust
291/// #![expect(unstable_name_collisions)]
292/// use litcheck_filecheck::{filecheck, Config};
293/// use itertools::Itertools;
294///
295/// let original = "abbc";
296/// let modified = original.chars().intersperse('\n').collect::<String>();
297/// let config = Config {
298///     match_full_lines: true,
299///     ..Config::default()
300/// };
301///
302/// filecheck!(modified, "
303/// ; CHECK: a
304/// ; CHECK-NEXT: b
305/// ; CHECK-NEXT: b
306/// ; CHECK-NEXT: c
307/// ");
308/// ```
309///
310/// If successful, the `filecheck!` macro returns the pattern matches produced by verifying the
311/// checks, allowing you to examine them in more detail.
312#[macro_export]
313macro_rules! filecheck {
314    ($input:expr, $checks:expr) => {
315        $crate::filecheck!($input, $checks, $crate::Config::default())
316    };
317
318    ($input:expr, $checks:expr, $config:expr) => {{
319        let config = $config;
320        let input = $input.to_string();
321        let checks = $checks.to_string();
322        let mut test = $crate::Test::new(checks, &config);
323        match test.verify(input) {
324            Err(err) => {
325                let printer = $crate::litcheck::diagnostics::reporting::PrintDiagnostic::new(err);
326                panic!("{printer}");
327            }
328            Ok(matches) => matches,
329        }
330    }};
331}