Skip to main content

zincite_syntax/
inputs.rs

1//! Read-only input discovery shared by the formatter and linter.
2use std::collections::HashSet;
3use std::io;
4use std::path::{Path, PathBuf};
5
6/// Files in positional-input order, with sorted traversal within each directory.
7#[derive(Default)]
8pub struct InputFiles {
9    pub files: Vec<PathBuf>,
10    pub errors: Vec<(PathBuf, io::Error)>,
11}
12
13/// Expand directories to `.mzn`/`.dzn` files, retaining the first path to each
14/// canonical file. Explicit files keep their spelling and need no extension.
15/// Follow directory symlinks once, skip `.git`, and retain independent errors.
16pub fn discover_inputs(paths: &[PathBuf]) -> InputFiles {
17    let mut result = InputFiles::default();
18    let mut directories = HashSet::new();
19    let mut files = HashSet::new();
20    for path in paths {
21        visit(path, true, &mut directories, &mut files, &mut result);
22    }
23    result
24}
25
26fn visit(
27    path: &Path,
28    explicit: bool,
29    directories: &mut HashSet<PathBuf>,
30    files: &mut HashSet<PathBuf>,
31    result: &mut InputFiles,
32) {
33    let metadata = match std::fs::metadata(path) {
34        Ok(metadata) => metadata,
35        Err(error) => {
36            result.errors.push((path.to_owned(), error));
37            return;
38        }
39    };
40    if metadata.is_dir() && path.file_name().is_some_and(|name| name == ".git") {
41        return;
42    }
43    if !metadata.is_dir() && !explicit && !source_extension(path) {
44        return;
45    }
46    let canonical = match path.canonicalize() {
47        Ok(canonical) => canonical,
48        Err(error) => {
49            result.errors.push((path.to_owned(), error));
50            return;
51        }
52    };
53    if metadata.is_dir() {
54        if !directories.insert(canonical) {
55            return;
56        }
57        let entries = match std::fs::read_dir(path) {
58            Ok(entries) => entries,
59            Err(error) => {
60                result.errors.push((path.to_owned(), error));
61                return;
62            }
63        };
64        let mut children = Vec::new();
65        for entry in entries {
66            match entry {
67                Ok(entry) if entry.file_name() != ".git" => children.push(entry.path()),
68                Ok(_) => {}
69                Err(error) => result.errors.push((path.to_owned(), error)),
70            }
71        }
72        children.sort();
73        for child in children {
74            visit(&child, false, directories, files, result);
75        }
76    } else if metadata.is_file() && files.insert(canonical) {
77        result.files.push(path.to_owned());
78    } else if explicit && !metadata.is_file() {
79        result.errors.push((
80            path.to_owned(),
81            io::Error::new(io::ErrorKind::InvalidInput, "input is not a regular file"),
82        ));
83    }
84}
85
86fn source_extension(path: &Path) -> bool {
87    path.extension()
88        .and_then(|extension| extension.to_str())
89        .is_some_and(|extension| {
90            extension.eq_ignore_ascii_case("mzn") || extension.eq_ignore_ascii_case("dzn")
91        })
92}