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:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
153
src/xatu/lib.rs
153
src/xatu/lib.rs
@@ -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 {}
|
|
||||||
|
|||||||
@@ -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: "));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user