feat(xatu): add lexical scope graph

This commit is contained in:
2026-09-26 15:07:30 -04:00
parent 16ce11fc52
commit 4b388264ee
4 changed files with 262 additions and 0 deletions

View File

@@ -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 = [
@@ -26,6 +27,15 @@ rust_test(
deps = [":xatu_lib"],
)
rust_test(
name = "scope_graph_test",
srcs = ["scope_graph_test.rs"],
deps = [
":xatu_lib",
"//third_party/crates:googletest",
],
)
rust_test(
name = "xatu_test",
srcs = ["xatu_test.rs"],

172
src/xatu/scope_graph.rs Normal file
View File

@@ -0,0 +1,172 @@
#[derive(Debug)]
pub struct ScopeGraph {
scopes: Vec<Scope>,
declarations: Vec<Declaration>,
references: Vec<Reference>,
}
impl Default for ScopeGraph {
fn default() -> Self {
Self::new()
}
}
impl ScopeGraph {
pub fn new() -> Self {
Self {
scopes: vec![Scope::new(None)],
declarations: Vec::new(),
references: Vec::new(),
}
}
pub fn root_scope(&self) -> ScopeId {
ScopeId(0)
}
pub fn add_scope(&mut self, parent: ScopeId) -> ScopeId {
self.scope(parent);
let scope_id = ScopeId(self.scopes.len());
self.scopes.push(Scope::new(Some(parent)));
scope_id
}
pub fn add_declaration(&mut self, scope: ScopeId, name: impl Into<String>) -> DeclarationId {
let declaration_id = DeclarationId(self.declarations.len());
self.declarations.push(Declaration {
scope,
name: name.into(),
});
self.scope_mut(scope).declarations.push(declaration_id);
declaration_id
}
pub fn add_reference(&mut self, scope: ScopeId, name: impl Into<String>) -> ReferenceId {
let reference_id = ReferenceId(self.references.len());
self.references.push(Reference {
scope,
name: name.into(),
});
self.scope_mut(scope).references.push(reference_id);
reference_id
}
pub fn declaration_scope(&self, declaration: DeclarationId) -> ScopeId {
self.declaration(declaration).scope
}
pub fn reference_scope(&self, reference: ReferenceId) -> ScopeId {
self.reference(reference).scope
}
pub fn resolve(&self, reference: ReferenceId) -> Vec<Resolution> {
let reference = self.reference(reference);
let mut scope_id = reference.scope;
let mut steps = Vec::new();
loop {
let scope = self.scope(scope_id);
let declarations =
scope.declarations.iter().copied().filter(|declaration_id| {
self.declaration(*declaration_id).name == reference.name
});
let resolutions = declarations
.map(|declaration| {
let mut path = steps.clone();
path.push(PathStep::Declaration(declaration));
Resolution {
declaration,
path: ResolutionPath { steps: path },
}
})
.collect::<Vec<_>>();
if !resolutions.is_empty() {
return resolutions;
}
let Some(parent) = scope.parent else {
return Vec::new();
};
steps.push(PathStep::Parent);
scope_id = parent;
}
}
fn scope(&self, scope: ScopeId) -> &Scope {
&self.scopes[scope.0]
}
fn scope_mut(&mut self, scope: ScopeId) -> &mut Scope {
&mut self.scopes[scope.0]
}
fn declaration(&self, declaration: DeclarationId) -> &Declaration {
&self.declarations[declaration.0]
}
fn reference(&self, reference: ReferenceId) -> &Reference {
&self.references[reference.0]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ScopeId(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DeclarationId(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ReferenceId(usize);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolution {
pub declaration: DeclarationId,
pub path: ResolutionPath,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolutionPath {
pub steps: Vec<PathStep>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathStep {
Parent,
Declaration(DeclarationId),
}
#[derive(Debug)]
struct Scope {
parent: Option<ScopeId>,
declarations: Vec<DeclarationId>,
references: Vec<ReferenceId>,
}
impl Scope {
fn new(parent: Option<ScopeId>) -> Self {
Self {
parent,
declarations: Vec::new(),
references: Vec::new(),
}
}
}
#[derive(Debug)]
struct Declaration {
scope: ScopeId,
name: String,
}
#[derive(Debug)]
struct Reference {
scope: ScopeId,
name: String,
}

View File

@@ -0,0 +1,76 @@
use googletest::matchers::eq;
use googletest::{expect_that, gtest};
use xatu::{PathStep, Resolution, ResolutionPath, ScopeGraph};
#[gtest]
fn resolves_a_local_declaration() {
let mut graph = ScopeGraph::new();
let scope = graph.root_scope();
let declaration = graph.add_declaration(scope, "value");
let reference = graph.add_reference(scope, "value");
let expected = vec![resolution(declaration, vec![PathStep::Declaration(declaration)])];
expect_that!(graph.resolve(reference), eq(&expected));
}
#[gtest]
fn resolves_a_parent_declaration() {
let mut graph = ScopeGraph::new();
let parent = graph.root_scope();
let declaration = graph.add_declaration(parent, "value");
let child = graph.add_scope(parent);
let reference = graph.add_reference(child, "value");
let expected = vec![resolution(
declaration,
vec![PathStep::Parent, PathStep::Declaration(declaration)],
)];
expect_that!(graph.resolve(reference), eq(&expected));
}
#[gtest]
fn local_declarations_shadow_parent_declarations() {
let mut graph = ScopeGraph::new();
let parent = graph.root_scope();
graph.add_declaration(parent, "value");
let child = graph.add_scope(parent);
let declaration = graph.add_declaration(child, "value");
let reference = graph.add_reference(child, "value");
let expected = vec![resolution(declaration, vec![PathStep::Declaration(declaration)])];
expect_that!(graph.resolve(reference), eq(&expected));
}
#[gtest]
fn preserves_ambiguous_local_declarations() {
let mut graph = ScopeGraph::new();
let scope = graph.root_scope();
let first = graph.add_declaration(scope, "value");
let second = graph.add_declaration(scope, "value");
let reference = graph.add_reference(scope, "value");
let expected = vec![
resolution(first, vec![PathStep::Declaration(first)]),
resolution(second, vec![PathStep::Declaration(second)]),
];
expect_that!(graph.resolve(reference), eq(&expected));
}
#[gtest]
fn leaves_unresolved_references_without_resolutions() {
let mut graph = ScopeGraph::new();
let reference = graph.add_reference(graph.root_scope(), "value");
let expected = Vec::<Resolution>::new();
expect_that!(graph.resolve(reference), eq(&expected));
}
fn resolution(
declaration: xatu::DeclarationId,
steps: Vec<PathStep>,
) -> Resolution {
Resolution {
declaration,
path: ResolutionPath { steps },
}
}

View File

@@ -2,9 +2,13 @@ mod ast;
mod file_store;
mod parser;
mod rules;
mod scope_graph;
pub use file_store::{FileId, FileStore, SourceFile};
pub use rules::{Diagnostic, Finding};
pub use scope_graph::{
DeclarationId, PathStep, ReferenceId, Resolution, ResolutionPath, ScopeGraph, ScopeId,
};
#[derive(Debug)]
pub struct Report {