refactor(xatu): reduce scaffold to hello world

Replace the initial parser prototype with a minimal hello-world skeleton so Xatu starts from a clean project shape before adding linter behavior.
This commit is contained in:
2026-09-17 00:19:43 -04:00
parent 5cb2de25f0
commit 3778a6f659
6 changed files with 7 additions and 224 deletions

4
Cargo.lock generated
View File

@@ -302,10 +302,6 @@ dependencies = [
[[package]] [[package]]
name = "xatu" name = "xatu"
version = "0.1.0" version = "0.1.0"
dependencies = [
"clap",
"rnix",
]
[[package]] [[package]]
name = "zmij" name = "zmij"

View File

@@ -4,17 +4,13 @@ rust_library(
name = "xatu_lib", name = "xatu_lib",
crate_name = "xatu", crate_name = "xatu",
srcs = ["lib.rs"], srcs = ["lib.rs"],
deps = ["//third_party/crates:rnix"],
) )
rust_binary( rust_binary(
name = "xatu", name = "xatu",
crate_name = "xatu_cli", crate_name = "xatu_cli",
srcs = ["main.rs"], srcs = ["main.rs"],
deps = [ deps = [":xatu_lib"],
":xatu_lib",
"//third_party/crates:clap",
],
) )
rust_test( rust_test(

View File

@@ -15,5 +15,3 @@ name = "lib_test"
path = "lib_test.rs" path = "lib_test.rs"
[dependencies] [dependencies]
clap = { version = "4", features = ["derive"] }
rnix = "0.14.0"

View File

@@ -1,152 +1,5 @@
use std::fmt; pub const GREETING: &str = "Hello from Xatu";
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use rnix::ParseError; pub fn greeting() -> &'static str {
pub type Result<T, E = Error> = std::result::Result<T, E>; GREETING
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub paths: Vec<PathBuf>,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct CheckReport {
diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub path: PathBuf,
pub line: usize,
pub column: usize,
pub rule: Rule,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rule {
ParseError,
}
#[derive(Debug)]
pub enum Error {
Read { path: PathBuf, source: io::Error },
}
impl CheckReport {
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn has_diagnostics(&self) -> bool {
!self.diagnostics.is_empty()
}
}
pub fn check(config: &Config) -> Result<CheckReport> {
let mut report = CheckReport::default();
for path in &config.paths {
check_file(path, &mut report)?;
}
Ok(report)
}
pub fn check_source(path: impl Into<PathBuf>, source: &str) -> CheckReport {
let path = path.into();
let parsed = rnix::Root::parse(source);
let diagnostics = parsed
.errors()
.iter()
.map(|error| parse_diagnostic(&path, source, error))
.collect();
CheckReport { diagnostics }
}
fn check_file(path: &Path, report: &mut CheckReport) -> Result<()> {
let source = fs::read_to_string(path).map_err(|source| Error::Read {
path: path.to_path_buf(),
source,
})?;
report
.diagnostics
.extend(check_source(path, &source).diagnostics);
Ok(())
}
fn parse_diagnostic(path: &Path, source: &str, error: &ParseError) -> Diagnostic {
let offset = parse_error_offset(error).unwrap_or(source.len());
let (line, column) = line_column(source, offset);
Diagnostic {
path: path.to_path_buf(),
line,
column,
rule: Rule::ParseError,
message: error.to_string(),
}
}
fn parse_error_offset(error: &ParseError) -> Option<usize> {
match error {
ParseError::Unexpected(range)
| ParseError::UnexpectedExtra(range)
| ParseError::UnexpectedWanted(_, range, _)
| ParseError::UnexpectedDoubleBind(range)
| ParseError::DuplicatedArgs(range, _) => Some(range.start().into()),
ParseError::UnexpectedEOF
| ParseError::UnexpectedEOFWanted(_)
| ParseError::RecursionLimitExceeded => None,
_ => None,
}
}
fn line_column(source: &str, offset: usize) -> (usize, usize) {
let bounded_offset = offset.min(source.len());
let before = &source[..bounded_offset];
let line = before.bytes().filter(|byte| *byte == b'\n').count() + 1;
let column = before
.rfind('\n')
.map_or(bounded_offset + 1, |newline| bounded_offset - newline);
(line, column)
}
impl fmt::Display for Diagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}:{}:{}: {}: {}",
self.path.display(),
self.line,
self.column,
self.rule,
self.message
)
}
}
impl fmt::Display for Rule {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Rule::ParseError => formatter.write_str("parse-error"),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Read { path, source } => {
write!(formatter, "failed to read {}: {source}", path.display())
}
}
}
}
impl std::error::Error for Error {}

View File

@@ -1,32 +1,4 @@
use std::path::PathBuf;
use xatu::{check_source, Rule};
#[test] #[test]
fn valid_source_has_no_diagnostics() { fn greeting_returns_hello_message() {
let report = check_source("valid.nix", "{ value = 1; }\n"); assert_eq!(xatu::greeting(), "Hello from Xatu");
assert!(report.diagnostics().is_empty());
}
#[test]
fn invalid_source_reports_parse_error() {
let report = check_source("invalid.nix", "{ value = ; }\n");
assert!(!report.diagnostics().is_empty());
let diagnostic = &report.diagnostics()[0];
assert_eq!(diagnostic.path, PathBuf::from("invalid.nix"));
assert_eq!(diagnostic.line, 1);
assert_eq!(diagnostic.rule, Rule::ParseError);
assert!(diagnostic.message.contains("unexpected"));
}
#[test]
fn display_uses_compiler_style_shape() {
let report = check_source("invalid.nix", "{ value = ; }\n");
let diagnostic = &report.diagnostics()[0];
assert!(diagnostic.to_string().starts_with("invalid.nix:1:"));
assert!(diagnostic.to_string().contains(": parse-error: "));
} }

View File

@@ -1,35 +1,3 @@
use std::path::PathBuf;
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(required = true)]
paths: Vec<PathBuf>,
}
impl From<Args> for xatu::Config {
fn from(args: Args) -> Self {
xatu::Config { paths: args.paths }
}
}
fn main() { fn main() {
let config = xatu::Config::from(Args::parse()); println!("{}", xatu::greeting());
match xatu::check(&config) {
Ok(report) => {
for diagnostic in report.diagnostics() {
println!("{diagnostic}");
}
if report.has_diagnostics() {
std::process::exit(1);
}
}
Err(error) => {
eprintln!("{error}");
std::process::exit(2);
}
}
} }