diff --git a/src/xatu/BUILD b/src/xatu/BUILD index f60733b..ae529fc 100644 --- a/src/xatu/BUILD +++ b/src/xatu/BUILD @@ -11,6 +11,7 @@ rust_library( "rules/manual_inherit.rs", "rules/manual_inherit_from.rs", "rules/unquoted_uri.rs", + "scope_graph.rs", "xatu.rs", ], deps = [ diff --git a/src/xatu/ast.rs b/src/xatu/ast.rs index 1deb464..c24b52c 100644 --- a/src/xatu/ast.rs +++ b/src/xatu/ast.rs @@ -3,6 +3,7 @@ use crate::FileId; #[derive(Debug)] pub struct ParsedFile { pub file_id: FileId, + pub root: Expr, pub nodes: Vec, pub tokens: Vec, pub errors: Vec, @@ -33,6 +34,7 @@ pub enum NodeData { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Binding { + pub range: SourceRange, pub key: Ident, pub value: Expr, } @@ -46,6 +48,16 @@ pub struct Ident { pub enum Expr { Ident(Ident), Select(Select), + Let { + bindings: Vec, + body: Box, + }, + AttrSet(Vec), + Lambda { + parameters: Vec, + body: Box, + }, + Other(Vec), } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/xatu/parser.rs b/src/xatu/parser.rs index 61545c3..cf60b40 100644 --- a/src/xatu/parser.rs +++ b/src/xatu/parser.rs @@ -1,4 +1,4 @@ -use rnix::ast::Attr; +use rnix::ast::{Attr, HasEntry}; use rowan::ast::AstNode; use crate::{FileId, ast}; @@ -7,10 +7,14 @@ pub fn parse_file(file_id: FileId, source: &str) -> ast::ParsedFile { let parse = rnix::Root::parse(source); let source_len = source.len(); - let (nodes, tokens) = convert_syntax(parse.syntax()); + let syntax = parse.syntax(); + let root = rnix::ast::Root::cast(syntax.clone()).expect("parser root is a root node"); + let root = convert_expr(root.expr().expect("parser root has an expression")); + let (nodes, tokens) = convert_syntax(syntax); ast::ParsedFile { file_id, + root, nodes, tokens, errors: parse @@ -35,6 +39,70 @@ fn convert_syntax(syntax: rnix::SyntaxNode) -> (Vec, Vec) (nodes, tokens) } +fn convert_expr(expr: rnix::ast::Expr) -> ast::Expr { + match expr { + rnix::ast::Expr::Ident(ident) => ast::Expr::Ident(ident.into()), + rnix::ast::Expr::LetIn(let_in) => ast::Expr::Let { + bindings: let_in + .attrpath_values() + .filter_map(convert_binding_ast) + .collect(), + body: Box::new(convert_expr( + let_in.body().expect("let expressions have bodies"), + )), + }, + rnix::ast::Expr::AttrSet(attr_set) => ast::Expr::AttrSet( + attr_set + .attrpath_values() + .filter_map(convert_binding_ast) + .collect(), + ), + rnix::ast::Expr::Lambda(lambda) => ast::Expr::Lambda { + parameters: lambda_parameters(&lambda), + body: Box::new(convert_expr(lambda.body().expect("lambdas have bodies"))), + }, + expr => ast::Expr::Other( + expr.syntax() + .children() + .filter_map(rnix::ast::Expr::cast) + .map(convert_expr) + .collect(), + ), + } +} + +fn convert_binding_ast(binding: rnix::ast::AttrpathValue) -> Option { + let mut attrs = binding.attrpath()?.attrs(); + let Attr::Ident(key) = attrs.next()? else { + return None; + }; + + if attrs.next().is_some() { + return None; + } + + Some(ast::Binding { + range: binding.syntax().text_range().into(), + key: key.into(), + value: convert_expr(binding.value()?), + }) +} + +fn lambda_parameters(lambda: &rnix::ast::Lambda) -> Vec { + match lambda.param() { + Some(rnix::ast::Param::IdentParam(parameter)) => { + parameter.ident().map(Into::into).into_iter().collect() + } + Some(rnix::ast::Param::Pattern(pattern)) => pattern + .pat_entries() + .filter_map(|entry| entry.ident()) + .chain(pattern.pat_bind().and_then(|binding| binding.ident())) + .map(Into::into) + .collect(), + None => Vec::new(), + } +} + fn convert_node(node: rnix::SyntaxNode) -> Option { match node.kind() { rnix::SyntaxKind::NODE_ATTRPATH_VALUE => convert_binding(node), @@ -69,7 +137,11 @@ impl TryFrom for ast::Binding { let value = binding.value().ok_or(())?.try_into()?; - Ok(Self { key, value }) + Ok(Self { + range: binding.syntax().text_range().into(), + key, + value, + }) } } diff --git a/src/xatu/scope_graph.rs b/src/xatu/scope_graph.rs new file mode 100644 index 0000000..6795458 --- /dev/null +++ b/src/xatu/scope_graph.rs @@ -0,0 +1,194 @@ +use crate::ast::{self, ParsedFile}; + +#[derive(Debug)] +pub(crate) struct ScopeGraph { + nodes: Vec, + edges: Vec, + lets: Vec, +} + +impl ScopeGraph { + pub(crate) fn from_file(file: &ParsedFile) -> Self { + let mut graph = Self { + nodes: Vec::new(), + edges: Vec::new(), + lets: Vec::new(), + }; + let root = graph.add_scope(None); + graph.build_expr(&file.root, root); + graph + } + + pub(crate) fn let_scopes(&self) -> &[LetScope] { + &self.lets + } + + fn build_expr(&mut self, expr: &ast::Expr, scope: NodeId) -> Vec { + match expr { + ast::Expr::Ident(ident) => vec![self.add_reference(&ident.name, scope)], + ast::Expr::Let { bindings, body } => self.build_let(bindings, body, scope), + ast::Expr::AttrSet(bindings) => bindings + .iter() + .flat_map(|binding| self.build_expr(&binding.value, scope)) + .collect(), + ast::Expr::Lambda { parameters, body } => { + let scope = self.add_scope(Some(scope)); + for parameter in parameters { + self.add_declaration(¶meter.name, scope); + } + self.build_expr(body, scope) + } + ast::Expr::Other(expressions) => expressions + .iter() + .flat_map(|expression| self.build_expr(expression, scope)) + .collect(), + ast::Expr::Select(_) => Vec::new(), + } + } + + fn build_let( + &mut self, + bindings: &[ast::Binding], + body: &ast::Expr, + parent: NodeId, + ) -> Vec { + let scope = self.add_scope(Some(parent)); + let mut graph_bindings = bindings + .iter() + .map(|binding| Binding { + declaration: self.add_declaration(&binding.key.name, scope), + name: binding.key.name.clone(), + references: Vec::new(), + range: binding.range, + }) + .collect::>(); + + for (binding, graph_binding) in bindings.iter().zip(&mut graph_bindings) { + graph_binding.references = self.build_expr(&binding.value, scope); + } + + let entry_references = self.build_expr(body, scope); + let references = graph_bindings + .iter() + .flat_map(|binding| binding.references.iter().copied()) + .chain(entry_references.iter().copied()) + .collect(); + self.lets.push(LetScope { + bindings: graph_bindings, + entry_references, + }); + + references + } + + pub(crate) fn resolve(&self, reference: NodeId) -> Vec { + let Node::Reference(name) = &self.nodes[reference.0] else { + return Vec::new(); + }; + let Some(mut scope) = self.edge_target(reference, Node::is_scope) else { + return Vec::new(); + }; + + loop { + let declarations = self + .edge_targets(scope, |node| matches!(node, Node::Declaration(declaration) if declaration.name == *name)) + .collect::>(); + if !declarations.is_empty() { + return declarations; + } + + let Some(parent) = self.edge_target(scope, Node::is_scope) else { + return Vec::new(); + }; + scope = parent; + } + } + + fn add_scope(&mut self, parent: Option) -> NodeId { + let scope = self.add_node(Node::Scope); + if let Some(parent) = parent { + self.add_edge(scope, parent); + } + scope + } + + fn add_declaration(&mut self, name: &str, scope: NodeId) -> NodeId { + let declaration = self.add_node(Node::Declaration(Declaration { + name: name.to_owned(), + })); + self.add_edge(scope, declaration); + declaration + } + + fn add_reference(&mut self, name: &str, scope: NodeId) -> NodeId { + let reference = self.add_node(Node::Reference(name.to_owned())); + self.add_edge(reference, scope); + reference + } + + fn add_node(&mut self, node: Node) -> NodeId { + let id = NodeId(self.nodes.len()); + self.nodes.push(node); + id + } + + fn add_edge(&mut self, from: NodeId, to: NodeId) { + self.edges.push(Edge { from, to }); + } + + fn edge_target(&self, from: NodeId, predicate: fn(&Node) -> bool) -> Option { + self.edge_targets(from, predicate).next() + } + + fn edge_targets<'graph>( + &'graph self, + from: NodeId, + predicate: impl Fn(&Node) -> bool + 'graph, + ) -> impl Iterator + 'graph { + self.edges + .iter() + .filter(move |edge| edge.from == from && predicate(&self.nodes[edge.to.0])) + .map(|edge| edge.to) + } +} + +#[derive(Debug)] +enum Node { + Scope, + Declaration(Declaration), + Reference(String), +} + +impl Node { + fn is_scope(&self) -> bool { + matches!(self, Self::Scope) + } +} + +#[derive(Debug)] +struct Declaration { + name: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct NodeId(usize); + +#[derive(Debug)] +struct Edge { + from: NodeId, + to: NodeId, +} + +#[derive(Debug)] +pub(crate) struct LetScope { + pub(crate) bindings: Vec, + pub(crate) entry_references: Vec, +} + +#[derive(Debug)] +pub(crate) struct Binding { + pub(crate) declaration: NodeId, + pub(crate) name: String, + pub(crate) references: Vec, + pub(crate) range: ast::SourceRange, +} diff --git a/src/xatu/xatu.rs b/src/xatu/xatu.rs index bc8b40f..4c450f7 100644 --- a/src/xatu/xatu.rs +++ b/src/xatu/xatu.rs @@ -2,6 +2,7 @@ mod ast; mod file_store; mod parser; mod rules; +mod scope_graph; pub use file_store::{FileId, FileStore, SourceFile}; pub use rules::{Diagnostic, Finding};