feat(xatu): add lint architecture
This commit was merged in pull request #71.
This commit is contained in:
@@ -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(
|
||||
name = "xatu_lib",
|
||||
crate_name = "xatu",
|
||||
srcs = ["xatu.rs"],
|
||||
deps = ["//src/status:status"],
|
||||
srcs = [
|
||||
"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(
|
||||
|
||||
@@ -13,4 +13,6 @@ path = "xatu_main.rs"
|
||||
[dependencies]
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
monorepo = { workspace = true }
|
||||
rnix = { workspace = true }
|
||||
rowan = { 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<()> {
|
||||
let mut store = FileStore::default();
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SourceFile {
|
||||
pub path: PathBuf,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user