feat(xatu): add lexical scope graph
This commit is contained in:
@@ -11,6 +11,7 @@ rust_library(
|
|||||||
"rules/manual_inherit.rs",
|
"rules/manual_inherit.rs",
|
||||||
"rules/manual_inherit_from.rs",
|
"rules/manual_inherit_from.rs",
|
||||||
"rules/unquoted_uri.rs",
|
"rules/unquoted_uri.rs",
|
||||||
|
"scope_graph.rs",
|
||||||
"xatu.rs",
|
"xatu.rs",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
@@ -26,6 +27,15 @@ rust_test(
|
|||||||
deps = [":xatu_lib"],
|
deps = [":xatu_lib"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "scope_graph_test",
|
||||||
|
srcs = ["scope_graph_test.rs"],
|
||||||
|
deps = [
|
||||||
|
":xatu_lib",
|
||||||
|
"//third_party/crates:googletest",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
rust_test(
|
rust_test(
|
||||||
name = "xatu_test",
|
name = "xatu_test",
|
||||||
srcs = ["xatu_test.rs"],
|
srcs = ["xatu_test.rs"],
|
||||||
|
|||||||
172
src/xatu/scope_graph.rs
Normal file
172
src/xatu/scope_graph.rs
Normal 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,
|
||||||
|
}
|
||||||
76
src/xatu/scope_graph_test.rs
Normal file
76
src/xatu/scope_graph_test.rs
Normal 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 },
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,13 @@ mod ast;
|
|||||||
mod file_store;
|
mod file_store;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod rules;
|
mod rules;
|
||||||
|
mod scope_graph;
|
||||||
|
|
||||||
pub use file_store::{FileId, FileStore, SourceFile};
|
pub use file_store::{FileId, FileStore, SourceFile};
|
||||||
pub use rules::{Diagnostic, Finding};
|
pub use rules::{Diagnostic, Finding};
|
||||||
|
pub use scope_graph::{
|
||||||
|
DeclarationId, PathStep, ReferenceId, Resolution, ResolutionPath, ScopeGraph, ScopeId,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
|
|||||||
Reference in New Issue
Block a user