init: archlens — architecture diagram generator
Some checks failed
CI / Check / Test (push) Failing after 1m24s
Some checks failed
CI / Check / Test (push) Failing after 1m24s
Hex arch + DDD, tree-sitter parsing, Mermaid/ASCII output. Supports Rust + Python. 92 tests. CI, diff, --check for staleness detection.
This commit is contained in:
17
crates/adapters/tree-sitter/Cargo.toml
Normal file
17
crates/adapters/tree-sitter/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "archlens-tree-sitter"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
tree-sitter-python.workspace = true
|
||||
tree-sitter-c-sharp.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
5
crates/adapters/tree-sitter/src/lib.rs
Normal file
5
crates/adapters/tree-sitter/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod python;
|
||||
mod rust;
|
||||
mod tree_sitter_analyzer;
|
||||
|
||||
pub use tree_sitter_analyzer::TreeSitterAnalyzer;
|
||||
337
crates/adapters/tree-sitter/src/python/mod.rs
Normal file
337
crates/adapters/tree-sitter/src/python/mod.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use tree_sitter::{Node, Parser};
|
||||
|
||||
use archlens_domain::{
|
||||
AnalysisResult, AnalysisWarning, CodeElement, CodeElementKind, DomainError, FilePath,
|
||||
Relationship, RelationshipKind,
|
||||
};
|
||||
|
||||
pub fn analyze(source: &str, file_path: &FilePath) -> Result<AnalysisResult, DomainError> {
|
||||
let mut parser = Parser::new();
|
||||
parser
|
||||
.set_language(&tree_sitter_python::LANGUAGE.into())
|
||||
.map_err(|e| DomainError::AnalysisError(e.to_string()))?;
|
||||
|
||||
let tree = parser
|
||||
.parse(source, None)
|
||||
.ok_or_else(|| DomainError::AnalysisError("failed to parse".to_string()))?;
|
||||
|
||||
let mut elements = Vec::new();
|
||||
let mut relationships = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut type_names: HashSet<String> = HashSet::new();
|
||||
|
||||
let root = tree.root_node();
|
||||
collect_classes(
|
||||
&root,
|
||||
source,
|
||||
file_path,
|
||||
&mut elements,
|
||||
&mut type_names,
|
||||
&mut relationships,
|
||||
&mut warnings,
|
||||
);
|
||||
collect_imports(&root, source, file_path, &mut relationships);
|
||||
|
||||
let relationships = relationships
|
||||
.into_iter()
|
||||
.map(|r| r.with_source_file(file_path.clone()))
|
||||
.collect();
|
||||
|
||||
Ok(AnalysisResult::new(elements, relationships, warnings))
|
||||
}
|
||||
|
||||
fn collect_classes(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
file_path: &FilePath,
|
||||
elements: &mut Vec<CodeElement>,
|
||||
type_names: &mut HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
warnings: &mut Vec<AnalysisWarning>,
|
||||
) {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
if child.kind() != "class_definition" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(name_node) = child.child_by_field_name("name") else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let name = &source[name_node.byte_range()];
|
||||
let line = child.start_position().row + 1;
|
||||
|
||||
match CodeElement::new(name, CodeElementKind::Class, file_path.clone(), line) {
|
||||
Ok(element) => {
|
||||
type_names.insert(name.to_string());
|
||||
elements.push(element);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Ok(w) = AnalysisWarning::new(file_path.clone(), line, &e.to_string()) {
|
||||
warnings.push(w);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(superclasses) = child.child_by_field_name("superclasses") {
|
||||
collect_inheritance(&superclasses, source, name, type_names, relationships);
|
||||
}
|
||||
|
||||
if let Some(body) = child.child_by_field_name("body") {
|
||||
collect_typed_fields(&body, source, name, type_names, relationships);
|
||||
collect_constructor_params(&body, source, name, type_names, relationships);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_inheritance(
|
||||
superclasses: &Node,
|
||||
source: &str,
|
||||
class_name: &str,
|
||||
_type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
let mut cursor = superclasses.walk();
|
||||
for child in superclasses.children(&mut cursor) {
|
||||
if child.kind() == "identifier" {
|
||||
let base_name = &source[child.byte_range()];
|
||||
if !is_python_builtin(base_name)
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(class_name, base_name, RelationshipKind::Inheritance)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PYTHON_BUILTINS: &[&str] = &[
|
||||
"str",
|
||||
"int",
|
||||
"float",
|
||||
"bool",
|
||||
"bytes",
|
||||
"list",
|
||||
"dict",
|
||||
"set",
|
||||
"tuple",
|
||||
"None",
|
||||
"type",
|
||||
"object",
|
||||
"Exception",
|
||||
"BaseException",
|
||||
"Optional",
|
||||
"Any",
|
||||
"Union",
|
||||
"List",
|
||||
"Dict",
|
||||
"Set",
|
||||
"Tuple",
|
||||
"Callable",
|
||||
"Sequence",
|
||||
"Mapping",
|
||||
"Iterable",
|
||||
"Iterator",
|
||||
"Generator",
|
||||
"Coroutine",
|
||||
"AsyncGenerator",
|
||||
"ClassVar",
|
||||
"Final",
|
||||
"Literal",
|
||||
"TypeVar",
|
||||
"Generic",
|
||||
"Protocol",
|
||||
"runtime_checkable",
|
||||
"Self",
|
||||
];
|
||||
|
||||
fn is_python_builtin(name: &str) -> bool {
|
||||
PYTHON_BUILTINS.contains(&name)
|
||||
}
|
||||
|
||||
const STDLIB_MODULES: &[&str] = &[
|
||||
"os",
|
||||
"sys",
|
||||
"typing",
|
||||
"logging",
|
||||
"json",
|
||||
"re",
|
||||
"io",
|
||||
"abc",
|
||||
"collections",
|
||||
"datetime",
|
||||
"enum",
|
||||
"functools",
|
||||
"hashlib",
|
||||
"http",
|
||||
"importlib",
|
||||
"inspect",
|
||||
"itertools",
|
||||
"math",
|
||||
"pathlib",
|
||||
"pickle",
|
||||
"random",
|
||||
"shutil",
|
||||
"signal",
|
||||
"socket",
|
||||
"string",
|
||||
"subprocess",
|
||||
"tempfile",
|
||||
"threading",
|
||||
"time",
|
||||
"traceback",
|
||||
"unittest",
|
||||
"urllib",
|
||||
"uuid",
|
||||
"warnings",
|
||||
"contextlib",
|
||||
"dataclasses",
|
||||
"copy",
|
||||
"struct",
|
||||
"base64",
|
||||
"csv",
|
||||
"glob",
|
||||
"operator",
|
||||
"textwrap",
|
||||
"asyncio",
|
||||
"concurrent",
|
||||
"multiprocessing",
|
||||
];
|
||||
|
||||
fn is_external_import(module: &str) -> bool {
|
||||
let top = module.split('.').next().unwrap_or(module);
|
||||
if STDLIB_MODULES.contains(&top) {
|
||||
return true;
|
||||
}
|
||||
if top.starts_with('_') {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn collect_imports(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
file_path: &FilePath,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
let file_name = std::path::Path::new(file_path.as_str())
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
match child.kind() {
|
||||
"import_statement" => {
|
||||
let mut name_cursor = child.walk();
|
||||
for name_child in child.children(&mut name_cursor) {
|
||||
if name_child.kind() == "dotted_name" {
|
||||
let module = &source[name_child.byte_range()];
|
||||
if !is_external_import(module)
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(file_name, module, RelationshipKind::Import)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"import_from_statement" => {
|
||||
if let Some(module_node) = child.child_by_field_name("module_name") {
|
||||
let module = &source[module_node.byte_range()];
|
||||
if !is_external_import(module)
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(file_name, module, RelationshipKind::Import)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_constructor_params(
|
||||
body: &Node,
|
||||
source: &str,
|
||||
class_name: &str,
|
||||
_type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
let mut cursor = body.walk();
|
||||
for child in body.children(&mut cursor) {
|
||||
if child.kind() != "function_definition" {
|
||||
continue;
|
||||
}
|
||||
let Some(fn_name) = child.child_by_field_name("name") else {
|
||||
continue;
|
||||
};
|
||||
if &source[fn_name.byte_range()] != "__init__" {
|
||||
continue;
|
||||
}
|
||||
let Some(params) = child.child_by_field_name("parameters") else {
|
||||
continue;
|
||||
};
|
||||
let mut param_cursor = params.walk();
|
||||
for param in params.children(&mut param_cursor) {
|
||||
if param.kind() == "typed_parameter"
|
||||
&& let Some(type_node) = param.child_by_field_name("type")
|
||||
{
|
||||
let type_text = &source[type_node.byte_range()];
|
||||
let base_type = type_text.split('[').next().unwrap_or(type_text).trim();
|
||||
if base_type != class_name
|
||||
&& !base_type.is_empty()
|
||||
&& !is_python_builtin(base_type)
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(class_name, base_type, RelationshipKind::Composition)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_typed_fields(
|
||||
body: &Node,
|
||||
source: &str,
|
||||
class_name: &str,
|
||||
type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
collect_typed_fields_recursive(body, source, class_name, type_names, relationships);
|
||||
}
|
||||
|
||||
fn collect_typed_fields_recursive(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
class_name: &str,
|
||||
_type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
if (child.kind() == "assignment" || child.kind() == "typed_assignment")
|
||||
&& let Some(type_node) = child.child_by_field_name("type")
|
||||
{
|
||||
let type_text = &source[type_node.byte_range()];
|
||||
let base_type = type_text.split('[').next().unwrap_or(type_text).trim();
|
||||
if base_type != class_name
|
||||
&& !base_type.is_empty()
|
||||
&& !is_python_builtin(base_type)
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(class_name, base_type, RelationshipKind::Composition)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
|
||||
collect_typed_fields_recursive(&child, source, class_name, _type_names, relationships);
|
||||
}
|
||||
}
|
||||
349
crates/adapters/tree-sitter/src/rust/mod.rs
Normal file
349
crates/adapters/tree-sitter/src/rust/mod.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use tree_sitter::{Node, Parser};
|
||||
|
||||
const RUST_PRIMITIVES: &[&str] = &[
|
||||
"bool",
|
||||
"char",
|
||||
"str",
|
||||
"String",
|
||||
"u8",
|
||||
"u16",
|
||||
"u32",
|
||||
"u64",
|
||||
"u128",
|
||||
"usize",
|
||||
"i8",
|
||||
"i16",
|
||||
"i32",
|
||||
"i64",
|
||||
"i128",
|
||||
"isize",
|
||||
"f32",
|
||||
"f64",
|
||||
"Vec",
|
||||
"Option",
|
||||
"Result",
|
||||
"Box",
|
||||
"Rc",
|
||||
"Arc",
|
||||
"HashMap",
|
||||
"HashSet",
|
||||
"BTreeMap",
|
||||
"BTreeSet",
|
||||
"PhantomData",
|
||||
"Pin",
|
||||
"Cow",
|
||||
"Self",
|
||||
];
|
||||
|
||||
use archlens_domain::{
|
||||
AnalysisResult, AnalysisWarning, CodeElement, CodeElementKind, DomainError, FilePath,
|
||||
Relationship, RelationshipKind, Visibility,
|
||||
};
|
||||
|
||||
pub fn analyze(source: &str, file_path: &FilePath) -> Result<AnalysisResult, DomainError> {
|
||||
let mut parser = Parser::new();
|
||||
parser
|
||||
.set_language(&tree_sitter_rust::LANGUAGE.into())
|
||||
.map_err(|e| DomainError::AnalysisError(e.to_string()))?;
|
||||
|
||||
let tree = parser
|
||||
.parse(source, None)
|
||||
.ok_or_else(|| DomainError::AnalysisError("failed to parse".to_string()))?;
|
||||
|
||||
let mut elements = Vec::new();
|
||||
let mut relationships = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut type_names: HashSet<String> = HashSet::new();
|
||||
|
||||
let root = tree.root_node();
|
||||
collect_types(
|
||||
&root,
|
||||
source,
|
||||
file_path,
|
||||
&mut elements,
|
||||
&mut type_names,
|
||||
&mut warnings,
|
||||
);
|
||||
collect_relationships(
|
||||
&root,
|
||||
source,
|
||||
&type_names,
|
||||
&mut relationships,
|
||||
&mut warnings,
|
||||
);
|
||||
collect_mod_declarations(&root, source, file_path, &mut relationships);
|
||||
collect_use_imports(&root, source, file_path, &mut relationships);
|
||||
|
||||
let relationships = relationships
|
||||
.into_iter()
|
||||
.map(|r| r.with_source_file(file_path.clone()))
|
||||
.collect();
|
||||
|
||||
Ok(AnalysisResult::new(elements, relationships, warnings))
|
||||
}
|
||||
|
||||
fn collect_types(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
file_path: &FilePath,
|
||||
elements: &mut Vec<CodeElement>,
|
||||
type_names: &mut HashSet<String>,
|
||||
warnings: &mut Vec<AnalysisWarning>,
|
||||
) {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
let (kind, name_field) = match child.kind() {
|
||||
"struct_item" => (CodeElementKind::Struct, "name"),
|
||||
"enum_item" => (CodeElementKind::Enum, "name"),
|
||||
"trait_item" => (CodeElementKind::Trait, "name"),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if let Some(name_node) = child.child_by_field_name(name_field) {
|
||||
let name = &source[name_node.byte_range()];
|
||||
let line = child.start_position().row + 1;
|
||||
let visibility = detect_visibility(&child, source);
|
||||
|
||||
match CodeElement::new(name, kind, file_path.clone(), line) {
|
||||
Ok(element) => {
|
||||
let fields = extract_fields(&child, source);
|
||||
let methods = extract_methods(node, source, name);
|
||||
let element = element
|
||||
.with_visibility(visibility)
|
||||
.with_fields(fields)
|
||||
.with_methods(methods);
|
||||
type_names.insert(name.to_string());
|
||||
elements.push(element);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Ok(w) = AnalysisWarning::new(file_path.clone(), line, &e.to_string()) {
|
||||
warnings.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_relationships(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
warnings: &mut Vec<AnalysisWarning>,
|
||||
) {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
match child.kind() {
|
||||
"struct_item" => {
|
||||
if let Some(name_node) = child.child_by_field_name("name") {
|
||||
let struct_name = source[name_node.byte_range()].to_string();
|
||||
collect_field_compositions(
|
||||
&child,
|
||||
source,
|
||||
&struct_name,
|
||||
type_names,
|
||||
relationships,
|
||||
);
|
||||
}
|
||||
}
|
||||
"impl_item" => {
|
||||
collect_trait_impl(&child, source, type_names, relationships, warnings);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_field_compositions(
|
||||
struct_node: &Node,
|
||||
source: &str,
|
||||
struct_name: &str,
|
||||
_type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
if let Some(body) = struct_node.child_by_field_name("body") {
|
||||
let mut cursor = body.walk();
|
||||
for field in body.children(&mut cursor) {
|
||||
if field.kind() == "field_declaration"
|
||||
&& let Some(type_node) = field.child_by_field_name("type")
|
||||
{
|
||||
let type_text = extract_base_type(&type_node, source);
|
||||
if type_text != struct_name
|
||||
&& !type_text.is_empty()
|
||||
&& !RUST_PRIMITIVES.contains(&type_text.as_str())
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(struct_name, &type_text, RelationshipKind::Composition)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_trait_impl(
|
||||
impl_node: &Node,
|
||||
source: &str,
|
||||
_type_names: &HashSet<String>,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
_warnings: &mut Vec<AnalysisWarning>,
|
||||
) {
|
||||
let trait_node = impl_node.child_by_field_name("trait");
|
||||
let type_node = impl_node.child_by_field_name("type");
|
||||
|
||||
if let (Some(trait_n), Some(type_n)) = (trait_node, type_node) {
|
||||
let trait_name = extract_base_type(&trait_n, source);
|
||||
let type_name = extract_base_type(&type_n, source);
|
||||
|
||||
if !trait_name.is_empty()
|
||||
&& !type_name.is_empty()
|
||||
&& !RUST_PRIMITIVES.contains(&trait_name.as_str())
|
||||
&& !RUST_PRIMITIVES.contains(&type_name.as_str())
|
||||
&& let Ok(rel) =
|
||||
Relationship::new(&type_name, &trait_name, RelationshipKind::Inheritance)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_fields(node: &Node, source: &str) -> Vec<String> {
|
||||
let mut fields = Vec::new();
|
||||
if let Some(body) = node.child_by_field_name("body") {
|
||||
let mut cursor = body.walk();
|
||||
for child in body.children(&mut cursor) {
|
||||
if child.kind() == "field_declaration" {
|
||||
let name = child
|
||||
.child_by_field_name("name")
|
||||
.map(|n| &source[n.byte_range()]);
|
||||
let ty = child
|
||||
.child_by_field_name("type")
|
||||
.map(|n| extract_base_type(&n, source));
|
||||
if let (Some(name), Some(ty)) = (name, ty) {
|
||||
fields.push(format!("{name}: {ty}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
fn extract_methods(root: &Node, source: &str, type_name: &str) -> Vec<String> {
|
||||
let mut methods = Vec::new();
|
||||
let mut cursor = root.walk();
|
||||
for child in root.children(&mut cursor) {
|
||||
if child.kind() != "impl_item" {
|
||||
continue;
|
||||
}
|
||||
if child.child_by_field_name("trait").is_some() {
|
||||
continue;
|
||||
}
|
||||
let type_node = child.child_by_field_name("type");
|
||||
if let Some(tn) = type_node {
|
||||
let impl_name = extract_base_type(&tn, source);
|
||||
if impl_name != type_name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(body) = child.child_by_field_name("body") {
|
||||
let mut body_cursor = body.walk();
|
||||
for item in body.children(&mut body_cursor) {
|
||||
if item.kind() == "function_item"
|
||||
&& let Some(name_node) = item.child_by_field_name("name")
|
||||
{
|
||||
let fn_name = &source[name_node.byte_range()];
|
||||
let vis = if detect_visibility(&item, source) == Visibility::Public {
|
||||
"+"
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
methods.push(format!("{vis}{fn_name}()"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
methods
|
||||
}
|
||||
|
||||
fn collect_mod_declarations(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
file_path: &FilePath,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
let file_name = std::path::Path::new(file_path.as_str())
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
if child.kind() != "mod_item" {
|
||||
continue;
|
||||
}
|
||||
if child.child_by_field_name("body").is_some() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name_node) = child.child_by_field_name("name") {
|
||||
let mod_name = &source[name_node.byte_range()];
|
||||
let target = format!("crate::{mod_name}");
|
||||
if let Ok(rel) = Relationship::new(file_name, &target, RelationshipKind::Import) {
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_use_imports(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
file_path: &FilePath,
|
||||
relationships: &mut Vec<Relationship>,
|
||||
) {
|
||||
let file_name = std::path::Path::new(file_path.as_str())
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
if child.kind() != "use_declaration" {
|
||||
continue;
|
||||
}
|
||||
if let Some(arg) = child.child_by_field_name("argument") {
|
||||
let text = &source[arg.byte_range()];
|
||||
let path = text
|
||||
.split('{')
|
||||
.next()
|
||||
.unwrap_or(text)
|
||||
.trim_end_matches("::");
|
||||
|
||||
if (path.starts_with("crate::") || path.starts_with("super::"))
|
||||
&& let Ok(rel) = Relationship::new(file_name, path, RelationshipKind::Import)
|
||||
{
|
||||
relationships.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_base_type(node: &Node, source: &str) -> String {
|
||||
let text = &source[node.byte_range()];
|
||||
text.split('<').next().unwrap_or(text).trim().to_string()
|
||||
}
|
||||
|
||||
fn detect_visibility(node: &Node, source: &str) -> Visibility {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
if child.kind() == "visibility_modifier" {
|
||||
let text = &source[child.byte_range()];
|
||||
if text.contains("pub") {
|
||||
return Visibility::Public;
|
||||
}
|
||||
}
|
||||
}
|
||||
Visibility::Private
|
||||
}
|
||||
30
crates/adapters/tree-sitter/src/tree_sitter_analyzer.rs
Normal file
30
crates/adapters/tree-sitter/src/tree_sitter_analyzer.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use archlens_domain::{AnalysisResult, DomainError, Language, SourceFile, ports::SourceAnalyzer};
|
||||
|
||||
use crate::{python, rust};
|
||||
|
||||
pub struct TreeSitterAnalyzer;
|
||||
|
||||
impl Default for TreeSitterAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeSitterAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAnalyzer for TreeSitterAnalyzer {
|
||||
fn analyze_file(&self, file: &SourceFile) -> Result<AnalysisResult, DomainError> {
|
||||
let source = std::fs::read_to_string(file.path().as_str())
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))?;
|
||||
|
||||
match file.language() {
|
||||
Language::Rust => rust::analyze(&source, file.path()),
|
||||
Language::Python => python::analyze(&source, file.path()),
|
||||
Language::CSharp => Ok(AnalysisResult::empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
138
crates/adapters/tree-sitter/tests/python_analyzer_tests.rs
Normal file
138
crates/adapters/tree-sitter/tests/python_analyzer_tests.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use archlens_domain::{
|
||||
CodeElementKind, FilePath, Language, RelationshipKind, SourceFile, ports::SourceAnalyzer,
|
||||
};
|
||||
use archlens_tree_sitter::TreeSitterAnalyzer;
|
||||
|
||||
fn analyze_python(source: &str, filename: &str) -> archlens_domain::AnalysisResult {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join(filename);
|
||||
std::fs::write(&file_path, source).unwrap();
|
||||
|
||||
let analyzer = TreeSitterAnalyzer::new();
|
||||
let source_file = SourceFile::new(
|
||||
FilePath::new(file_path.to_str().unwrap()).unwrap(),
|
||||
Language::Python,
|
||||
);
|
||||
analyzer.analyze_file(&source_file).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_python_class() {
|
||||
let result = analyze_python("class Order:\n pass\n", "order.py");
|
||||
|
||||
assert_eq!(result.elements().len(), 1);
|
||||
assert_eq!(result.elements()[0].name(), "Order");
|
||||
assert_eq!(result.elements()[0].kind(), CodeElementKind::Class);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_python_inheritance() {
|
||||
let source = "class Animal:\n pass\n\nclass Dog(Animal):\n pass\n";
|
||||
let result = analyze_python(source, "animals.py");
|
||||
|
||||
assert_eq!(result.elements().len(), 2);
|
||||
|
||||
let inheritance: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Inheritance)
|
||||
.collect();
|
||||
|
||||
assert_eq!(inheritance.len(), 1);
|
||||
assert_eq!(inheritance[0].source(), "Dog");
|
||||
assert_eq!(inheritance[0].target(), "Animal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_composition_from_type_annotated_fields() {
|
||||
let source = "class Address:\n pass\n\nclass User:\n def __init__(self):\n self.address: Address = Address()\n";
|
||||
let result = analyze_python(source, "user.py");
|
||||
|
||||
let composition: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Composition)
|
||||
.collect();
|
||||
|
||||
assert_eq!(composition.len(), 1);
|
||||
assert_eq!(composition[0].source(), "User");
|
||||
assert_eq!(composition[0].target(), "Address");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_import_from_import_statement() {
|
||||
let source = "import os\nfrom commons.src.schema import BaseModel\n";
|
||||
let result = analyze_python(source, "service.py");
|
||||
|
||||
let imports: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Import)
|
||||
.collect();
|
||||
|
||||
assert!(imports.iter().any(|r| r.target() == "commons.src.schema"));
|
||||
assert!(
|
||||
!imports.iter().any(|r| r.target() == "os"),
|
||||
"stdlib should be filtered"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_relative_imports_from_init() {
|
||||
let source = "from .schema import BaseModel\nfrom .client import ApiClient\n";
|
||||
let result = analyze_python(source, "__init__.py");
|
||||
|
||||
let imports: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Import)
|
||||
.collect();
|
||||
|
||||
assert_eq!(imports.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_import_from_plain_import() {
|
||||
let source = "import commons.utils\n";
|
||||
let result = analyze_python(source, "service.py");
|
||||
|
||||
let imports: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Import)
|
||||
.collect();
|
||||
|
||||
assert!(imports.iter().any(|r| r.target() == "commons.utils"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_composition_from_constructor_params() {
|
||||
let source = "class Config:\n pass\n\nclass Service:\n def __init__(self, config: Config):\n pass\n";
|
||||
let result = analyze_python(source, "service.py");
|
||||
|
||||
let composition: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Composition)
|
||||
.collect();
|
||||
|
||||
assert_eq!(composition.len(), 1);
|
||||
assert_eq!(composition[0].source(), "Service");
|
||||
assert_eq!(composition[0].target(), "Config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_composition_from_class_level_annotations() {
|
||||
let source = "class Gad:\n pass\n\nclass Definition:\n gad: Gad\n name: str\n";
|
||||
let result = analyze_python(source, "models.py");
|
||||
|
||||
let composition: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Composition)
|
||||
.collect();
|
||||
|
||||
assert_eq!(composition.len(), 1);
|
||||
assert_eq!(composition[0].source(), "Definition");
|
||||
assert_eq!(composition[0].target(), "Gad");
|
||||
}
|
||||
130
crates/adapters/tree-sitter/tests/rust_analyzer_tests.rs
Normal file
130
crates/adapters/tree-sitter/tests/rust_analyzer_tests.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use archlens_domain::{
|
||||
CodeElementKind, FilePath, Language, RelationshipKind, SourceFile, ports::SourceAnalyzer,
|
||||
};
|
||||
use archlens_tree_sitter::TreeSitterAnalyzer;
|
||||
|
||||
fn analyze_rust(source: &str, filename: &str) -> archlens_domain::AnalysisResult {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join(filename);
|
||||
std::fs::write(&file_path, source).unwrap();
|
||||
|
||||
let analyzer = TreeSitterAnalyzer::new();
|
||||
let source_file = SourceFile::new(
|
||||
FilePath::new(file_path.to_str().unwrap()).unwrap(),
|
||||
Language::Rust,
|
||||
);
|
||||
analyzer.analyze_file(&source_file).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_rust_struct() {
|
||||
let result = analyze_rust("pub struct Order {\n id: u64,\n}", "order.rs");
|
||||
|
||||
assert_eq!(result.elements().len(), 1);
|
||||
assert_eq!(result.elements()[0].name(), "Order");
|
||||
assert_eq!(result.elements()[0].kind(), CodeElementKind::Struct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_rust_enum() {
|
||||
let result = analyze_rust(
|
||||
"pub enum Status {\n Active,\n Inactive,\n}",
|
||||
"status.rs",
|
||||
);
|
||||
|
||||
assert_eq!(result.elements().len(), 1);
|
||||
assert_eq!(result.elements()[0].name(), "Status");
|
||||
assert_eq!(result.elements()[0].kind(), CodeElementKind::Enum);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_rust_trait() {
|
||||
let result = analyze_rust("pub trait Repository {\n fn find(&self);\n}", "repo.rs");
|
||||
|
||||
assert_eq!(result.elements().len(), 1);
|
||||
assert_eq!(result.elements()[0].name(), "Repository");
|
||||
assert_eq!(result.elements()[0].kind(), CodeElementKind::Trait);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_composition_from_struct_fields() {
|
||||
let source =
|
||||
"pub struct Order {\n id: u64,\n}\npub struct OrderService {\n order: Order,\n}";
|
||||
let result = analyze_rust(source, "service.rs");
|
||||
|
||||
assert_eq!(result.elements().len(), 2);
|
||||
assert_eq!(result.relationships().len(), 1);
|
||||
assert_eq!(result.relationships()[0].source(), "OrderService");
|
||||
assert_eq!(result.relationships()[0].target(), "Order");
|
||||
assert_eq!(
|
||||
result.relationships()[0].kind(),
|
||||
RelationshipKind::Composition
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_inheritance_from_trait_impl() {
|
||||
let source = "pub trait Printable {}\npub struct Order {}\nimpl Printable for Order {}";
|
||||
let result = analyze_rust(source, "order.rs");
|
||||
|
||||
let inheritance: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Inheritance)
|
||||
.collect();
|
||||
|
||||
assert_eq!(inheritance.len(), 1);
|
||||
assert_eq!(inheritance[0].source(), "Order");
|
||||
assert_eq!(inheritance[0].target(), "Printable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_use_imports() {
|
||||
let source =
|
||||
"use crate::domain::Order;\nuse crate::ports::Repository;\n\npub struct Service {}";
|
||||
let result = analyze_rust(source, "service.rs");
|
||||
|
||||
let imports: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Import)
|
||||
.collect();
|
||||
|
||||
assert!(imports.iter().any(|r| r.target() == "crate::domain::Order"));
|
||||
assert!(
|
||||
imports
|
||||
.iter()
|
||||
.any(|r| r.target() == "crate::ports::Repository")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_std_and_external_crate_imports() {
|
||||
let source =
|
||||
"use std::collections::HashMap;\nuse serde::Serialize;\nuse crate::models::Order;\n";
|
||||
let result = analyze_rust(source, "lib.rs");
|
||||
|
||||
let imports: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Import)
|
||||
.collect();
|
||||
|
||||
assert_eq!(imports.len(), 1);
|
||||
assert_eq!(imports[0].target(), "crate::models::Order");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_mod_declarations() {
|
||||
let source = "mod models;\nmod services;\npub struct App {}";
|
||||
let result = analyze_rust(source, "lib.rs");
|
||||
|
||||
let imports: Vec<_> = result
|
||||
.relationships()
|
||||
.iter()
|
||||
.filter(|r| r.kind() == RelationshipKind::Import)
|
||||
.collect();
|
||||
|
||||
assert!(imports.iter().any(|r| r.target() == "crate::models"));
|
||||
assert!(imports.iter().any(|r| r.target() == "crate::services"));
|
||||
}
|
||||
Reference in New Issue
Block a user