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
76pub 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 pub const fn remarks_enabled(&self) -> bool {
91 self.options.verbose > 0
92 }
93
94 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#[derive(Debug, Args)]
122pub struct Options {
123 #[arg(
125 long,
126 default_value_t = false,
127 action(clap::ArgAction::SetTrue),
128 help_heading = "Input"
129 )]
130 pub allow_empty: bool,
131 #[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 #[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 #[arg(long, default_value_t = false, help_heading = "Syntax")]
163 pub allow_unused_prefixes: bool,
164 #[arg(
173 long = "strict-whitespace",
174 default_value_t = false,
175 help_heading = "Matching"
176 )]
177 pub strict_whitespace: bool,
178 #[arg(long, default_value_t = false, help_heading = "Matching")]
188 pub match_full_lines: bool,
189 #[arg(long, default_value_t = false, help_heading = "Matching")]
191 pub ignore_case: bool,
192 #[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 #[arg(long, value_enum, value_name = "TYPE", default_value_t = Dump::Fail, help_heading = "Output")]
209 pub dump_input: Dump,
210 #[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 #[arg(long, default_value_t = false, help_heading = "Variables")]
230 pub enable_var_scope: bool,
231 #[arg(
235 long = "define",
236 short = 'D',
237 value_name = "NAME=VALUE",
238 help_heading = "Variables"
239 )]
240 pub variables: Vec<expr::CliVariable>,
241 #[arg(long, short = 'v', action = ArgAction::Count, help_heading = "Output")]
249 pub verbose: u8,
250 #[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
262impl 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
281impl 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 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 Help,
342 Always,
344 #[default]
346 Fail,
347 Never,
349}
350
351#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, ValueEnum)]
352pub enum DumpFilter {
353 All,
355 AnnotationFull,
357 Annotation,
359 #[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#[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}