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
78pub 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 pub const fn remarks_enabled(&self) -> bool {
93 self.options.verbose > 0
94 }
95
96 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#[derive(Debug, Args)]
124pub struct Options {
125 #[arg(
127 long,
128 default_value_t = false,
129 action(clap::ArgAction::SetTrue),
130 help_heading = "Input"
131 )]
132 pub allow_empty: bool,
133 #[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 #[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 #[arg(long, default_value_t = false, help_heading = "Syntax")]
165 pub allow_unused_prefixes: bool,
166 #[arg(
175 long = "strict-whitespace",
176 default_value_t = false,
177 help_heading = "Matching"
178 )]
179 pub strict_whitespace: bool,
180 #[arg(long, default_value_t = false, help_heading = "Matching")]
190 pub match_full_lines: bool,
191 #[arg(long, default_value_t = false, help_heading = "Matching")]
193 pub ignore_case: bool,
194 #[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 #[arg(long, value_enum, value_name = "TYPE", default_value_t = Dump::Fail, help_heading = "Output")]
211 pub dump_input: Dump,
212 #[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 #[arg(long, default_value_t = false, help_heading = "Variables")]
232 pub enable_var_scope: bool,
233 #[arg(
237 long = "define",
238 short = 'D',
239 value_name = "NAME=VALUE",
240 help_heading = "Variables"
241 )]
242 pub variables: Vec<expr::CliVariable>,
243 #[arg(long, short = 'v', action = ArgAction::Count, help_heading = "Output")]
251 pub verbose: u8,
252 #[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
264impl 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
283impl 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 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 Help,
344 Always,
346 #[default]
348 Fail,
349 Never,
351}
352
353#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, ValueEnum)]
354pub enum DumpFilter {
355 All,
357 AnnotationFull,
359 Annotation,
361 #[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#[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}