feat(xatu): add lint architecture #71
47
Cargo.lock
generated
47
Cargo.lock
generated
@@ -108,6 +108,18 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
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]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -164,6 +176,33 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -228,6 +267,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "text-size"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
@@ -261,6 +306,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"monorepo",
|
"monorepo",
|
||||||
|
"rnix",
|
||||||
|
"rowan",
|
||||||
"status",
|
"status",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ resolver = "3"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = "4"
|
clap = "4"
|
||||||
monorepo = { path = "src/monorepo" }
|
monorepo = { path = "src/monorepo" }
|
||||||
|
rnix = "=0.14.0"
|
||||||
|
rowan = "0.16"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
status = { path = "src/status" }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
status = { path = "src/status" }
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library")
|
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||||
|
|
||||||
rust_library(
|
rust_library(
|
||||||
name = "xatu_lib",
|
name = "xatu_lib",
|
||||||
crate_name = "xatu",
|
crate_name = "xatu",
|
||||||
srcs = ["xatu.rs"],
|
srcs = [
|
||||||
deps = ["//src/status:status"],
|
"ast.rs",
|
||||||
|
"file_store.rs",
|
||||||
|
"parser.rs",
|
||||||
|
"rules.rs",
|
||||||
|
"rules/empty_inherit.rs",
|
||||||
|
"xatu.rs",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//src/status:status",
|
||||||
|
"//third_party/crates:rnix",
|
||||||
|
"//third_party/crates/rowan-0.16.1:rowan",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "file_store_test",
|
||||||
|
srcs = ["file_store_test.rs"],
|
||||||
|
deps = [":xatu_lib"],
|
||||||
)
|
)
|
||||||
|
|
||||||
rust_binary(
|
rust_binary(
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ path = "xatu_main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
monorepo = { workspace = true }
|
monorepo = { workspace = true }
|
||||||
|
rnix = { workspace = true }
|
||||||
|
rowan = { workspace = true }
|
||||||
status = { workspace = true }
|
status = { workspace = true }
|
||||||
|
|||||||
36
src/xatu/ast.rs
Normal file
36
src/xatu/ast.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::FileId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Inherit {
|
||||||
|
pub attr_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum NodeData {
|
||||||
|
Inherit(Inherit),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct SourceRange {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Node {
|
||||||
|
pub range: SourceRange,
|
||||||
|
pub data: NodeData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ParseError {
|
||||||
|
pub range: SourceRange,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ParsedFile {
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub nodes: Vec<Node>,
|
||||||
|
pub errors: Vec<ParseError>,
|
||||||
|
}
|
||||||
33
src/xatu/file_store.rs
Normal file
33
src/xatu/file_store.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct FileId(usize);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SourceFile {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct FileStore {
|
||||||
|
files: Vec<SourceFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileStore {
|
||||||
|
pub fn add(&mut self, file: SourceFile) -> FileId {
|
||||||
|
let id = FileId(self.files.len());
|
||||||
|
self.files.push(file);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: FileId) -> &SourceFile {
|
||||||
|
&self.files[id.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<SourceFile>> for FileStore {
|
||||||
|
fn from(files: Vec<SourceFile>) -> Self {
|
||||||
|
Self { files }
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/xatu/file_store_test.rs
Normal file
33
src/xatu/file_store_test.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use xatu::{FileStore, SourceFile};
|
||||||
|
|
||||||
|
fn source_file(path: &str, source: &str) -> SourceFile {
|
||||||
|
SourceFile {
|
||||||
|
path: PathBuf::from(path),
|
||||||
|
source: source.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn added_file_can_be_retrieved_by_id() {
|
||||||
|
let mut store = FileStore::default();
|
||||||
|
|
||||||
|
let id = store.add(source_file("first.nix", "{ inherit foo; }"));
|
||||||
|
let file = store.get(id);
|
||||||
|
|
||||||
|
assert_eq!(PathBuf::from("first.nix"), file.path);
|
||||||
|
assert_eq!("{ inherit foo; }", file.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_ids_refer_to_their_added_files() {
|
||||||
|
let mut store = FileStore::default();
|
||||||
|
|
||||||
|
let first = store.add(source_file("first.nix", "first"));
|
||||||
|
let second = store.add(source_file("second.nix", "second"));
|
||||||
|
|
||||||
|
assert_ne!(first, second);
|
||||||
|
assert_eq!("first", store.get(first).source);
|
||||||
|
assert_eq!("second", store.get(second).source);
|
||||||
|
}
|
||||||
84
src/xatu/parser.rs
Normal file
84
src/xatu/parser.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use rowan::ast::AstNode;
|
||||||
|
|
||||||
|
use crate::{FileId, ast};
|
||||||
|
|
||||||
|
pub fn parse_file(file_id: FileId, source: &str) -> ast::ParsedFile {
|
||||||
|
let parse = rnix::Root::parse(source);
|
||||||
|
let source_len = source.len();
|
||||||
|
|
||||||
|
ast::ParsedFile {
|
||||||
|
file_id,
|
||||||
|
nodes: parse
|
||||||
|
.syntax()
|
||||||
|
.descendants()
|
||||||
|
.filter_map(convert_node)
|
||||||
|
.collect(),
|
||||||
|
errors: parse
|
||||||
|
.errors()
|
||||||
|
.iter()
|
||||||
|
.map(|error| convert_parse_error(error, source_len))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_parse_error(error: &rnix::ParseError, source_len: usize) -> ast::ParseError {
|
||||||
|
ast::ParseError {
|
||||||
|
range: parse_error_range(error, source_len),
|
||||||
|
message: error.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_error_range(error: &rnix::ParseError, source_len: usize) -> ast::SourceRange {
|
||||||
|
match error {
|
||||||
|
rnix::ParseError::Unexpected(range)
|
||||||
|
| rnix::ParseError::UnexpectedExtra(range)
|
||||||
|
| rnix::ParseError::UnexpectedWanted(_, range, _)
|
||||||
|
| rnix::ParseError::UnexpectedDoubleBind(range)
|
||||||
|
| rnix::ParseError::DuplicatedArgs(range, _) => (*range).into(),
|
||||||
|
|
||||||
|
rnix::ParseError::UnexpectedEOF
|
||||||
|
| rnix::ParseError::UnexpectedEOFWanted(_)
|
||||||
|
| rnix::ParseError::RecursionLimitExceeded => ast::SourceRange {
|
||||||
|
start: source_len,
|
||||||
|
end: source_len,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => ast::SourceRange {
|
||||||
|
start: source_len,
|
||||||
|
end: source_len,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_node(node: rnix::SyntaxNode) -> Option<ast::Node> {
|
||||||
|
match node.kind() {
|
||||||
|
rnix::SyntaxKind::NODE_INHERIT => convert_inherit(node),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_inherit(node: rnix::SyntaxNode) -> Option<ast::Node> {
|
||||||
|
let inherit = rnix::ast::Inherit::cast(node)?;
|
||||||
|
|
||||||
|
Some(ast::Node {
|
||||||
|
range: inherit.syntax().text_range().into(),
|
||||||
|
data: ast::NodeData::Inherit(inherit.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rnix::TextRange> for ast::SourceRange {
|
||||||
|
fn from(range: rnix::TextRange) -> Self {
|
||||||
|
Self {
|
||||||
|
start: usize::from(range.start()),
|
||||||
|
end: usize::from(range.end()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rnix::ast::Inherit> for ast::Inherit {
|
||||||
|
fn from(inherit: rnix::ast::Inherit) -> Self {
|
||||||
|
Self {
|
||||||
|
attr_count: inherit.attrs().count(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/xatu/rules.rs
Normal file
54
src/xatu/rules.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::FileId;
|
||||||
|
use crate::ast;
|
||||||
|
use crate::rules::empty_inherit::EmptyInherit;
|
||||||
|
|
||||||
|
pub mod empty_inherit;
|
||||||
|
|
||||||
|
pub struct Finding {
|
||||||
|
pub rule_id: &'static str,
|
||||||
|
pub range: ast::SourceRange,
|
||||||
|
pub message: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Diagnostic {
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub finding: Finding,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NodeRule {
|
||||||
|
type Node;
|
||||||
|
|
||||||
|
const ID: &'static str;
|
||||||
|
|
||||||
|
fn select(data: &ast::NodeData) -> Option<&Self::Node>;
|
||||||
|
fn check(node: &Self::Node) -> Option<&'static str>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_node_rule<R>(node: &ast::Node) -> Option<Finding>
|
||||||
|
where
|
||||||
|
R: NodeRule,
|
||||||
|
{
|
||||||
|
let typed_node = R::select(&node.data)?;
|
||||||
|
let message = R::check(typed_node)?;
|
||||||
|
|
||||||
|
Some(Finding {
|
||||||
|
rule_id: R::ID,
|
||||||
|
range: node.range,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_rules(file: &ast::ParsedFile) -> Vec<Diagnostic> {
|
||||||
|
file.nodes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|node| {
|
||||||
|
[run_node_rule::<EmptyInherit>(node)]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|finding| Diagnostic {
|
||||||
|
file_id: file.file_id,
|
||||||
|
finding,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
23
src/xatu/rules/empty_inherit.rs
Normal file
23
src/xatu/rules/empty_inherit.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use crate::{ast, rules::NodeRule};
|
||||||
|
|
||||||
|
pub struct EmptyInherit;
|
||||||
|
|
||||||
|
impl NodeRule for EmptyInherit {
|
||||||
|
type Node = ast::Inherit;
|
||||||
|
|
||||||
|
const ID: &'static str = "empty_inherit";
|
||||||
|
|
||||||
|
fn select(data: &ast::NodeData) -> Option<&Self::Node> {
|
||||||
|
match data {
|
||||||
|
ast::NodeData::Inherit(inherit) => Some(inherit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(node: &Self::Node) -> Option<&'static str> {
|
||||||
|
if node.attr_count == 0 {
|
||||||
|
Some("inherit statement has no attributes")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,29 @@
|
|||||||
use std::path::PathBuf;
|
mod ast;
|
||||||
|
mod file_store;
|
||||||
|
mod parser;
|
||||||
|
mod rules;
|
||||||
|
|
||||||
|
pub use file_store::{FileId, FileStore, SourceFile};
|
||||||
|
|
||||||
pub fn run(files: Vec<SourceFile>) -> status::Result<()> {
|
pub fn run(files: Vec<SourceFile>) -> status::Result<()> {
|
||||||
|
let mut store = FileStore::default();
|
||||||
|
|
||||||
for file in files {
|
for file in files {
|
||||||
println!("{}", file.path.display());
|
let file_id = store.add(file);
|
||||||
|
let file = store.get(file_id);
|
||||||
|
let parsed = parser::parse_file(file_id, &file.source);
|
||||||
|
|
||||||
|
for diagnostic in rules::run_rules(&parsed) {
|
||||||
|
println!(
|
||||||
|
"{}:{}..{}: {}: {}",
|
||||||
|
file.path.display(),
|
||||||
|
diagnostic.finding.range.start,
|
||||||
|
diagnostic.finding.range.end,
|
||||||
|
diagnostic.finding.rule_id,
|
||||||
|
diagnostic.finding.message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SourceFile {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub source: String,
|
|
||||||
}
|
|
||||||
|
|||||||
1
third_party/crates/crates.bzl
vendored
1
third_party/crates/crates.bzl
vendored
@@ -340,6 +340,7 @@ _NORMAL_DEPENDENCIES = {
|
|||||||
"src/xatu": {
|
"src/xatu": {
|
||||||
_COMMON_CONDITION: {
|
_COMMON_CONDITION: {
|
||||||
"clap": Label("//third_party/crates/clap-4.6.7:clap"),
|
"clap": Label("//third_party/crates/clap-4.6.7:clap"),
|
||||||
|
"rnix": Label("//third_party/crates/rnix-0.14.0:rnix"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user