Compare commits
5 Commits
247712582a
...
1313a95204
| Author | SHA1 | Date | |
|---|---|---|---|
| 1313a95204 | |||
| 1d6abab548 | |||
| e2ccc03286 | |||
| 7023f38136 | |||
| d522d5b9b6 |
@@ -96,8 +96,8 @@
|
||||
- `rules_rust` `rust_test` targets cannot set both `crate` and `srcs`.
|
||||
Use one input style per test target.
|
||||
- Rust tests in this monorepo live in separate `*_test.rs` files, not inline `#[cfg(test)]` modules.
|
||||
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
||||
`src/ariadne/Cargo.toml` unless explicitly requested.
|
||||
- Rust tests in this monorepo are declared in Bazel `BUILD` files, not Cargo manifests,
|
||||
unless explicitly requested.
|
||||
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
||||
- The flake does not currently define `formatter.x86_64-linux`.
|
||||
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||
|
||||
57
Cargo.lock
generated
57
Cargo.lock
generated
@@ -107,18 +107,6 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -147,14 +135,6 @@ version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "nix-style-linter"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rnix",
|
||||
"rowan",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -179,33 +159,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rnix"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c163bd17372eecdf10d351c34584b7de7c1a33be4e92a32f3fb3f5a7fe3f579b"
|
||||
dependencies = [
|
||||
"rowan",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||
dependencies = [
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"rustc-hash",
|
||||
"text-size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -266,12 +219,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.25"
|
||||
@@ -299,10 +246,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xatu"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"src/ariadne",
|
||||
"src/nix-style-linter",
|
||||
"src/xatu",
|
||||
"src/machoke",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||
|
||||
rust_library(
|
||||
name = "nix_style_linter_lib",
|
||||
crate_name = "nix_style_linter",
|
||||
srcs = ["lib.rs"],
|
||||
deps = [
|
||||
"//third_party/crates:rnix",
|
||||
"//third_party/crates:rowan",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "nix-style-linter",
|
||||
crate_name = "nix_style_linter_cli",
|
||||
srcs = ["main.rs"],
|
||||
deps = [":nix_style_linter_lib"],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "lib_test",
|
||||
srcs = ["lib_test.rs"],
|
||||
deps = [":nix_style_linter_lib"],
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "nix-style-linter"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nix-style-linter"
|
||||
path = "main.rs"
|
||||
|
||||
[[test]]
|
||||
name = "lib_test"
|
||||
path = "lib_test.rs"
|
||||
|
||||
[dependencies]
|
||||
rnix = "0.14.0"
|
||||
rowan = "0.16.1"
|
||||
@@ -1,522 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rnix::SyntaxKind;
|
||||
use rnix::ast::{self, HasEntry};
|
||||
use rowan::ast::AstNode;
|
||||
|
||||
const PARSE_ERROR_RULE: &str = "parse-error";
|
||||
const LET_ORDER_RULE: &str = "let-order";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RunStatus {
|
||||
Clean,
|
||||
Diagnostics,
|
||||
ToolError,
|
||||
}
|
||||
|
||||
impl RunStatus {
|
||||
pub fn code(self) -> u8 {
|
||||
match self {
|
||||
RunStatus::Clean => 0,
|
||||
RunStatus::Diagnostics => 1,
|
||||
RunStatus::ToolError => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Diagnostic {
|
||||
pub path: PathBuf,
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
pub rule: &'static str,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
paths: &[PathBuf],
|
||||
stdout: &mut impl Write,
|
||||
stderr: &mut impl Write,
|
||||
) -> io::Result<RunStatus> {
|
||||
if paths.is_empty() {
|
||||
writeln!(stderr, "error: expected at least one path")?;
|
||||
return Ok(RunStatus::ToolError);
|
||||
}
|
||||
|
||||
let mut found_diagnostics = false;
|
||||
let mut found_tool_errors = false;
|
||||
|
||||
for path in paths {
|
||||
let path = resolve_workspace_path(path);
|
||||
match collect_nix_files(&path) {
|
||||
Ok(files) => {
|
||||
for file in files {
|
||||
match lint_file(&file) {
|
||||
Ok(diagnostics) => {
|
||||
for diagnostic in diagnostics {
|
||||
found_diagnostics = true;
|
||||
writeln!(stdout, "{diagnostic}")?;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
found_tool_errors = true;
|
||||
writeln!(
|
||||
stderr,
|
||||
"error: failed to read {path}: {error}",
|
||||
path = file.display()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
found_tool_errors = true;
|
||||
writeln!(stderr, "error: {error}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_tool_errors {
|
||||
Ok(RunStatus::ToolError)
|
||||
} else if found_diagnostics {
|
||||
Ok(RunStatus::Diagnostics)
|
||||
} else {
|
||||
Ok(RunStatus::Clean)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_workspace_path(path: &Path) -> PathBuf {
|
||||
if path.is_absolute() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
|
||||
if let Some(workspace) = std::env::var_os("BUILD_WORKSPACE_DIRECTORY") {
|
||||
return PathBuf::from(workspace).join(path);
|
||||
}
|
||||
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
pub fn lint_file(path: &Path) -> io::Result<Vec<Diagnostic>> {
|
||||
let source = fs::read_to_string(path)?;
|
||||
let parse = rnix::Root::parse(&source);
|
||||
let parse_diagnostics = parse_diagnostics(path, &source, parse.errors());
|
||||
|
||||
if !parse_diagnostics.is_empty() {
|
||||
return Ok(parse_diagnostics);
|
||||
}
|
||||
|
||||
Ok(let_order_diagnostics(path, &source, &parse.tree()))
|
||||
}
|
||||
|
||||
fn parse_diagnostics(
|
||||
path: &Path,
|
||||
source: &str,
|
||||
errors: &[rnix::ParseError],
|
||||
) -> Vec<Diagnostic> {
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| {
|
||||
let (line, column) = line_column(source, parse_error_offset(error, source.len()));
|
||||
Diagnostic {
|
||||
path: path.to_path_buf(),
|
||||
line,
|
||||
column,
|
||||
rule: PARSE_ERROR_RULE,
|
||||
message: error.to_string(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn let_order_diagnostics(path: &Path, source: &str, root: &ast::Root) -> Vec<Diagnostic> {
|
||||
root.syntax()
|
||||
.descendants()
|
||||
.filter_map(ast::LetIn::cast)
|
||||
.flat_map(|let_in| let_order_diagnostics_for_let(path, source, &let_in))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn let_order_diagnostics_for_let(
|
||||
path: &Path,
|
||||
source: &str,
|
||||
let_in: &ast::LetIn,
|
||||
) -> Vec<Diagnostic> {
|
||||
let bindings: Vec<_> = let_in
|
||||
.attrpath_values()
|
||||
.filter_map(|binding| plain_binding(source, &binding))
|
||||
.collect();
|
||||
let names: Vec<_> = bindings
|
||||
.iter()
|
||||
.map(|binding| binding.name.clone())
|
||||
.collect();
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
for (index, binding) in bindings.iter().enumerate() {
|
||||
let later_names: HashSet<_> = names.iter().skip(index + 1).cloned().collect();
|
||||
if let Some(referenced_name) =
|
||||
first_forward_reference(binding.value.syntax(), &later_names, &mut HashSet::new())
|
||||
{
|
||||
if is_suppressed(source, binding.line, LET_ORDER_RULE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
diagnostics.push(Diagnostic {
|
||||
path: path.to_path_buf(),
|
||||
line: binding.line,
|
||||
column: binding.column,
|
||||
rule: LET_ORDER_RULE,
|
||||
message: format!(
|
||||
"`{binding}` references later let binding `{referenced}`",
|
||||
binding = binding.name,
|
||||
referenced = referenced_name
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Binding {
|
||||
name: String,
|
||||
line: usize,
|
||||
column: usize,
|
||||
value: ast::Expr,
|
||||
}
|
||||
|
||||
fn plain_binding(source: &str, binding: &ast::AttrpathValue) -> Option<Binding> {
|
||||
let attr = plain_attr(binding.attrpath()?)?;
|
||||
let value = binding.value()?;
|
||||
let offset = usize::from(attr.text_range().start());
|
||||
let (line, column) = line_column(source, offset);
|
||||
|
||||
Some(Binding {
|
||||
name: attr.text().to_string(),
|
||||
line,
|
||||
column,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
fn plain_attrpath_name(binding: &ast::AttrpathValue) -> Option<String> {
|
||||
let attr = plain_attr(binding.attrpath()?)?;
|
||||
Some(attr.text().to_string())
|
||||
}
|
||||
|
||||
fn plain_attr(attrpath: ast::Attrpath) -> Option<rnix::SyntaxToken> {
|
||||
let mut attrs = attrpath.attrs();
|
||||
let attr = attrs.next()?;
|
||||
|
||||
if attrs.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match attr {
|
||||
ast::Attr::Ident(ident) => ident.ident_token(),
|
||||
ast::Attr::Dynamic(_) | ast::Attr::Str(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn first_forward_reference(
|
||||
node: &rnix::SyntaxNode,
|
||||
later_names: &HashSet<String>,
|
||||
shadowed: &mut HashSet<String>,
|
||||
) -> Option<String> {
|
||||
match node.kind() {
|
||||
SyntaxKind::NODE_LAMBDA => {
|
||||
return with_shadowed(shadowed, lambda_bound_names(node), |shadowed| {
|
||||
first_forward_reference_in_children(node, later_names, shadowed)
|
||||
});
|
||||
}
|
||||
SyntaxKind::NODE_LET_IN => {
|
||||
return with_shadowed(shadowed, plain_let_binding_names(node), |shadowed| {
|
||||
first_forward_reference_in_children(node, later_names, shadowed)
|
||||
});
|
||||
}
|
||||
SyntaxKind::NODE_ATTR_SET if rec_attrset_binds_names(node) => {
|
||||
return with_shadowed(shadowed, plain_attrset_binding_names(node), |shadowed| {
|
||||
first_forward_reference_in_children(node, later_names, shadowed)
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if is_reference_ident(node) {
|
||||
if let Some(name) = ident_name(node) {
|
||||
if later_names.contains(&name) && !shadowed.contains(&name) {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
first_forward_reference_in_children(node, later_names, shadowed)
|
||||
}
|
||||
|
||||
fn first_forward_reference_in_children(
|
||||
node: &rnix::SyntaxNode,
|
||||
later_names: &HashSet<String>,
|
||||
shadowed: &mut HashSet<String>,
|
||||
) -> Option<String> {
|
||||
node.children()
|
||||
.find_map(|child| first_forward_reference(&child, later_names, shadowed))
|
||||
}
|
||||
|
||||
fn with_shadowed<R>(
|
||||
shadowed: &mut HashSet<String>,
|
||||
names: Vec<String>,
|
||||
action: impl FnOnce(&mut HashSet<String>) -> R,
|
||||
) -> R {
|
||||
let inserted: Vec<_> = names
|
||||
.into_iter()
|
||||
.filter(|name| shadowed.insert(name.clone()))
|
||||
.collect();
|
||||
let result = action(shadowed);
|
||||
|
||||
for name in inserted {
|
||||
shadowed.remove(&name);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn is_reference_ident(node: &rnix::SyntaxNode) -> bool {
|
||||
if node.kind() != SyntaxKind::NODE_IDENT {
|
||||
return false;
|
||||
}
|
||||
|
||||
!matches!(
|
||||
node.parent().map(|parent| parent.kind()),
|
||||
Some(
|
||||
SyntaxKind::NODE_ATTRPATH
|
||||
| SyntaxKind::NODE_IDENT_PARAM
|
||||
| SyntaxKind::NODE_PAT_BIND
|
||||
| SyntaxKind::NODE_PAT_ENTRY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn ident_name(node: &rnix::SyntaxNode) -> Option<String> {
|
||||
ast::Ident::cast(node.clone())
|
||||
.and_then(|ident| ident.ident_token())
|
||||
.map(|token| token.text().to_string())
|
||||
}
|
||||
|
||||
fn lambda_bound_names(node: &rnix::SyntaxNode) -> Vec<String> {
|
||||
let Some(lambda) = ast::Lambda::cast(node.clone()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(param) = lambda.param() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
match param {
|
||||
ast::Param::IdentParam(param) => param
|
||||
.ident()
|
||||
.and_then(|ident| ident.ident_token())
|
||||
.map(|token| vec![token.text().to_string()])
|
||||
.unwrap_or_default(),
|
||||
ast::Param::Pattern(pattern) => {
|
||||
let mut names: Vec<_> = pattern
|
||||
.pat_entries()
|
||||
.filter_map(|entry| entry.ident())
|
||||
.filter_map(|ident| ident.ident_token())
|
||||
.map(|token| token.text().to_string())
|
||||
.collect();
|
||||
|
||||
if let Some(name) = pattern
|
||||
.pat_bind()
|
||||
.and_then(|bind| bind.ident())
|
||||
.and_then(|ident| ident.ident_token())
|
||||
.map(|token| token.text().to_string())
|
||||
{
|
||||
names.push(name);
|
||||
}
|
||||
|
||||
names
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plain_let_binding_names(node: &rnix::SyntaxNode) -> Vec<String> {
|
||||
ast::LetIn::cast(node.clone())
|
||||
.map(|let_in| {
|
||||
let_in
|
||||
.attrpath_values()
|
||||
.filter_map(|binding| plain_attrpath_name(&binding))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn rec_attrset_binds_names(node: &rnix::SyntaxNode) -> bool {
|
||||
ast::AttrSet::cast(node.clone())
|
||||
.and_then(|attrset| attrset.rec_token())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn plain_attrset_binding_names(node: &rnix::SyntaxNode) -> Vec<String> {
|
||||
ast::AttrSet::cast(node.clone())
|
||||
.map(|attrset| {
|
||||
attrset
|
||||
.attrpath_values()
|
||||
.filter_map(|binding| plain_attrpath_name(&binding))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_suppressed(source: &str, line: usize, rule: &str) -> bool {
|
||||
let Some(previous_line_index) = line.checked_sub(2) else {
|
||||
return false;
|
||||
};
|
||||
let Some(previous_line) = source.lines().nth(previous_line_index) else {
|
||||
return false;
|
||||
};
|
||||
let Some(comment) = previous_line.trim_start().strip_prefix('#') else {
|
||||
return false;
|
||||
};
|
||||
let Some(rest) = comment.trim().strip_prefix("nix-style-linter:") else {
|
||||
return false;
|
||||
};
|
||||
let Some((directive, reason)) = rest.trim().split_once("--") else {
|
||||
return false;
|
||||
};
|
||||
let mut words = directive.split_whitespace();
|
||||
|
||||
words.next() == Some("disable-next-line")
|
||||
&& words.any(|word| word == rule)
|
||||
&& !reason.trim().is_empty()
|
||||
}
|
||||
|
||||
fn collect_nix_files(path: &Path) -> Result<Vec<PathBuf>, InputError> {
|
||||
let metadata = fs::metadata(path).map_err(|source| InputError::Metadata {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
if metadata.is_file() {
|
||||
return Ok(is_nix_file(path)
|
||||
.then(|| path.to_path_buf())
|
||||
.into_iter()
|
||||
.collect());
|
||||
}
|
||||
|
||||
if metadata.is_dir() {
|
||||
let mut files = Vec::new();
|
||||
collect_nix_files_from_dir(path, &mut files)?;
|
||||
files.sort();
|
||||
return Ok(files);
|
||||
}
|
||||
|
||||
Err(InputError::UnsupportedPath(path.to_path_buf()))
|
||||
}
|
||||
|
||||
fn collect_nix_files_from_dir(path: &Path, files: &mut Vec<PathBuf>) -> Result<(), InputError> {
|
||||
let entries = fs::read_dir(path).map_err(|source| InputError::ReadDir {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|source| InputError::ReadDir {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
let entry_path = entry.path();
|
||||
let file_type = entry.file_type().map_err(|source| InputError::Metadata {
|
||||
path: entry_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
if file_type.is_dir() {
|
||||
collect_nix_files_from_dir(&entry_path, files)?;
|
||||
} else if file_type.is_file() && is_nix_file(&entry_path) {
|
||||
files.push(entry_path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_nix_file(path: &Path) -> bool {
|
||||
path.extension() == Some(OsStr::new("nix"))
|
||||
}
|
||||
|
||||
fn parse_error_offset(error: &rnix::ParseError, end_offset: usize) -> usize {
|
||||
match error {
|
||||
rnix::ParseError::Unexpected(range)
|
||||
| rnix::ParseError::UnexpectedExtra(range)
|
||||
| rnix::ParseError::UnexpectedWanted(_, range, _)
|
||||
| rnix::ParseError::UnexpectedDoubleBind(range)
|
||||
| rnix::ParseError::DuplicatedArgs(range, _) => usize::from(range.start()),
|
||||
rnix::ParseError::UnexpectedEOF
|
||||
| rnix::ParseError::UnexpectedEOFWanted(_)
|
||||
| rnix::ParseError::RecursionLimitExceeded => end_offset,
|
||||
_ => end_offset,
|
||||
}
|
||||
}
|
||||
|
||||
fn line_column(source: &str, offset: usize) -> (usize, usize) {
|
||||
let target = offset.min(source.len());
|
||||
let mut line = 1;
|
||||
let mut column = 1;
|
||||
|
||||
for (index, character) in source.char_indices() {
|
||||
if index >= target {
|
||||
break;
|
||||
}
|
||||
|
||||
if character == '\n' {
|
||||
line += 1;
|
||||
column = 1;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
|
||||
(line, column)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InputError {
|
||||
Metadata { path: PathBuf, source: io::Error },
|
||||
ReadDir { path: PathBuf, source: io::Error },
|
||||
UnsupportedPath(PathBuf),
|
||||
}
|
||||
|
||||
impl fmt::Display for InputError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
InputError::Metadata { path, source } => {
|
||||
write!(formatter, "failed to inspect {}: {source}", path.display())
|
||||
}
|
||||
InputError::ReadDir { path, source } => {
|
||||
write!(formatter, "failed to read directory {}: {source}", path.display())
|
||||
}
|
||||
InputError::UnsupportedPath(path) => {
|
||||
write!(formatter, "unsupported path type: {}", path.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use nix_style_linter::RunStatus;
|
||||
|
||||
static TEST_DIRECTORY_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn malformed_file_reports_parse_error() {
|
||||
let workspace = TestWorkspace::new("malformed_file_reports_parse_error");
|
||||
let path = workspace.write("broken.nix", "let x = ; in x\n");
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Diagnostics);
|
||||
assert_eq!(output.stderr, "");
|
||||
assert!(output.stdout.starts_with(&format!(
|
||||
"{}:1:9: parse-error: unexpected TOKEN_SEMICOLON at 8..9",
|
||||
workspace.path("broken.nix").display()
|
||||
)));
|
||||
assert!(!output.stdout.contains("let-order"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_reference_reports_let_order() {
|
||||
let workspace = TestWorkspace::new("forward_reference_reports_let_order");
|
||||
let path = workspace.write(
|
||||
"forward.nix",
|
||||
"let\n app = package;\n package = \"hello\";\nin app\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Diagnostics);
|
||||
assert_eq!(output.stderr, "");
|
||||
assert_eq!(
|
||||
output.stdout,
|
||||
format!(
|
||||
"{}:2:3: let-order: `app` references later let binding `package`\n",
|
||||
workspace.path("forward.nix").display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependency_ordered_bindings_exit_clean() {
|
||||
let workspace = TestWorkspace::new("dependency_ordered_bindings_exit_clean");
|
||||
let path = workspace.write(
|
||||
"ordered.nix",
|
||||
"let\n package = \"hello\";\n app = package;\nin app\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer_scope_reference_exits_clean() {
|
||||
let workspace = TestWorkspace::new("outer_scope_reference_exits_clean");
|
||||
let path = workspace.write(
|
||||
"outer.nix",
|
||||
"let\n package = \"hello\";\nin\nlet\n app = package;\nin app\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_lambda_parameter_shadows_later_binding() {
|
||||
let workspace = TestWorkspace::new("nested_lambda_parameter_shadows_later_binding");
|
||||
let path = workspace.write(
|
||||
"lambda.nix",
|
||||
"let\n call = package: package;\n package = \"hello\";\nin call\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_let_binding_shadows_later_binding() {
|
||||
let workspace = TestWorkspace::new("nested_let_binding_shadows_later_binding");
|
||||
let path = workspace.write(
|
||||
"nested-let.nix",
|
||||
"let\n app = let package = \"inner\"; in package;\n package = \"outer\";\nin app\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn earlier_sibling_constant_shared_by_multiple_bindings_exits_clean() {
|
||||
let workspace = TestWorkspace::new("earlier_sibling_constant_shared_by_multiple_bindings");
|
||||
let path = workspace.write(
|
||||
"shared.nix",
|
||||
"let\n package = \"hello\";\n app = package;\n shell = package;\nin [ app shell ]\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previous_line_suppression_disables_let_order() {
|
||||
let workspace = TestWorkspace::new("previous_line_suppression_disables_let_order");
|
||||
let path = workspace.write(
|
||||
"suppressed.nix",
|
||||
"let\n # nix-style-linter: disable-next-line let-order -- intentional recursion\n app = package;\n package = app;\nin app\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_suppression_does_not_disable_let_order() {
|
||||
let workspace = TestWorkspace::new("malformed_suppression_does_not_disable_let_order");
|
||||
let path = workspace.write(
|
||||
"unsuppressed.nix",
|
||||
"let\n # nix-style-linter: disable-next-line let-order\n app = package;\n package = \"hello\";\nin app\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Diagnostics);
|
||||
assert_eq!(
|
||||
output.stdout,
|
||||
format!(
|
||||
"{}:3:3: let-order: `app` references later let binding `package`\n",
|
||||
workspace.path("unsuppressed.nix").display()
|
||||
)
|
||||
);
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_binding_syntax_is_skipped() {
|
||||
let workspace = TestWorkspace::new("unsupported_binding_syntax_is_skipped");
|
||||
let path = workspace.write(
|
||||
"unsupported.nix",
|
||||
"let\n attrs.value = package;\n inherit package;\n package = \"hello\";\nin attrs\n",
|
||||
);
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_file_exits_clean() {
|
||||
let workspace = TestWorkspace::new("valid_file_exits_clean");
|
||||
let path = workspace.write("valid.nix", "let x = 1; in x\n");
|
||||
|
||||
let output = run_linter(&[path]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Clean);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_path_reports_tool_error() {
|
||||
let workspace = TestWorkspace::new("missing_path_reports_tool_error");
|
||||
let path = workspace.path("missing.nix");
|
||||
|
||||
let output = run_linter(&[path.clone()]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::ToolError);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert!(output.stderr.starts_with(&format!(
|
||||
"error: failed to inspect {}:",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_inputs_recurse_through_nix_files() {
|
||||
let workspace = TestWorkspace::new("directory_inputs_recurse_through_nix_files");
|
||||
workspace.write("ignored.txt", "let x = ; in x\n");
|
||||
workspace.write("nested/broken.nix", "let\n x = 1;\n y = ;\nin y\n");
|
||||
|
||||
let output = run_linter(&[workspace.root.clone()]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::Diagnostics);
|
||||
assert_eq!(output.stderr, "");
|
||||
assert!(output.stdout.starts_with(&format!(
|
||||
"{}:3:7: parse-error: unexpected TOKEN_SEMICOLON at 19..20",
|
||||
workspace.path("nested/broken.nix").display()
|
||||
)));
|
||||
assert!(!output.stdout.contains("ignored.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_invocation_reports_tool_error() {
|
||||
let output = run_linter(&[]);
|
||||
|
||||
assert_eq!(output.status, RunStatus::ToolError);
|
||||
assert_eq!(output.stdout, "");
|
||||
assert_eq!(output.stderr, "error: expected at least one path\n");
|
||||
}
|
||||
|
||||
struct LinterOutput {
|
||||
status: RunStatus,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
}
|
||||
|
||||
fn run_linter(paths: &[PathBuf]) -> LinterOutput {
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let status = nix_style_linter::run(paths, &mut stdout, &mut stderr).unwrap();
|
||||
|
||||
LinterOutput {
|
||||
status,
|
||||
stdout: String::from_utf8(stdout).unwrap(),
|
||||
stderr: String::from_utf8(stderr).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
struct TestWorkspace {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl TestWorkspace {
|
||||
fn new(name: &str) -> Self {
|
||||
let id = TEST_DIRECTORY_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"nix-style-linter-{process}-{name}-{id}",
|
||||
process = std::process::id()
|
||||
));
|
||||
|
||||
if root.exists() {
|
||||
fs::remove_dir_all(&root).unwrap();
|
||||
}
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
|
||||
Self { root }
|
||||
}
|
||||
|
||||
fn path(&self, relative: impl AsRef<Path>) -> PathBuf {
|
||||
self.root.join(relative)
|
||||
}
|
||||
|
||||
fn write(&self, relative: impl AsRef<Path>, contents: &str) -> PathBuf {
|
||||
let path = self.path(relative);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
fs::write(&path, contents).unwrap();
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestWorkspace {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.root).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let paths: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
|
||||
match nix_style_linter::run(&paths, &mut stdout, &mut stderr) {
|
||||
Ok(status) => ExitCode::from(status.code()),
|
||||
Err(error) => {
|
||||
eprintln!("error: internal error: {error}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||
|
||||
rust_library(
|
||||
name = "xatu_lib",
|
||||
crate_name = "xatu",
|
||||
srcs = ["lib.rs"],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "xatu",
|
||||
crate_name = "xatu_cli",
|
||||
srcs = ["main.rs"],
|
||||
deps = [":xatu_lib"],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "lib_test",
|
||||
srcs = ["lib_test.rs"],
|
||||
deps = [":xatu_lib"],
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "xatu"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "xatu"
|
||||
path = "main.rs"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,5 +0,0 @@
|
||||
pub const GREETING: &str = "Hello from Xatu";
|
||||
|
||||
pub fn greeting() -> &'static str {
|
||||
GREETING
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#[test]
|
||||
fn greeting_returns_hello_message() {
|
||||
assert_eq!(xatu::greeting(), "Hello from Xatu");
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("{}", xatu::greeting());
|
||||
}
|
||||
Reference in New Issue
Block a user