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