code-moniker-core 0.6.1

Core symbol-graph types and per-language extractors for code-moniker.
Documentation
use crate::core::code_graph::CodeGraph;
use crate::core::moniker::{Moniker, MonikerBuilder};
use crate::lang::ParsedDocument;
use crate::lang::sdk::{DiscoveredFile, GraphEmitter, ImportTable, ScopeTree};
use tree_sitter::Node;

mod builtins;
mod defs;
mod discover;
mod imports;
mod lombok;
mod refs;
mod symbols;
mod syntax;
mod type_resolution;

use discover::JavaDiscover;

use super::Presets;
use super::kinds;

pub fn extract(
	uri: &str,
	source: &str,
	document: &ParsedDocument,
	anchor: &Moniker,
	deep: bool,
	_presets: &Presets,
) -> CodeGraph {
	let root = document.primary().root_node();
	let pkg = read_package_name(root, source.as_bytes());
	let pieces = pkg
		.split('.')
		.filter(|piece| !piece.is_empty())
		.collect::<Vec<_>>();
	let module = compute_module_moniker(anchor, uri, &pieces);
	let discovered_parts = JavaDiscover::run(module.clone(), source.as_bytes(), deep, root);
	let discovered = DiscoveredFile::new(
		module,
		kinds::MODULE,
		discovered_parts.defs,
		ScopeTree::new(discovered_parts.root),
		ImportTable::default(),
	);
	GraphEmitter::emit(&discovered, &discovered_parts.refs)
		.unwrap_or_else(|err| panic!("Java SDK graph emission failed: {err}"))
}

fn compute_module_moniker(anchor: &Moniker, uri: &str, package_pieces: &[&str]) -> Moniker {
	let class_name = file_stem(uri);
	let mut builder = MonikerBuilder::from_view(anchor.as_view());
	builder.segment(crate::lang::kinds::LANG, b"java");
	for piece in package_pieces.iter().filter(|piece| !piece.is_empty()) {
		builder.segment(kinds::PACKAGE, piece.as_bytes());
	}
	builder.segment(kinds::MODULE, class_name.as_bytes());
	builder.build()
}

pub(super) fn standard_path_module_moniker(anchor: &Moniker, uri: &str) -> Option<Moniker> {
	let pieces = uri
		.split(['/', '\\'])
		.filter(|piece| !piece.is_empty() && *piece != ".")
		.collect::<Vec<_>>();
	let java_idx = pieces
		.windows(3)
		.position(|parts| parts[0] == "src" && parts[2] == "java")?
		+ 2;
	let (_, tail) = pieces.split_at(java_idx + 1);
	let (file, package_pieces) = tail.split_last()?;
	let class_name = file_stem(file);
	if class_name.is_empty() {
		return None;
	}
	let mut builder = MonikerBuilder::from_view(anchor.as_view());
	builder.segment(crate::lang::kinds::LANG, b"java");
	for piece in package_pieces {
		builder.segment(kinds::PACKAGE, piece.as_bytes());
	}
	builder.segment(kinds::MODULE, class_name.as_bytes());
	Some(builder.build())
}

fn file_stem(uri: &str) -> &str {
	let after_slash = uri.rsplit(['/', '\\']).next().unwrap_or(uri);
	after_slash.strip_suffix(".java").unwrap_or(after_slash)
}

fn read_package_name<'src>(root: Node<'_>, source: &'src [u8]) -> &'src str {
	let mut cursor = root.walk();
	for child in root.children(&mut cursor) {
		if child.kind() != "package_declaration" {
			continue;
		}
		let mut named_cursor = child.walk();
		for name in child.named_children(&mut named_cursor) {
			if let Ok(package) = name.utf8_text(source) {
				return package;
			}
		}
	}
	""
}

#[cfg(test)]
mod tests {
	use std::collections::HashSet;

	use super::*;

	#[test]
	fn standard_path_module_moniker_accepts_windows_separators() {
		let anchor = MonikerBuilder::new().project(b"app").build();
		let posix =
			standard_path_module_moniker(&anchor, "src/main/java/com/acme/java/util/Read.java")
				.expect("standard Java path");
		let windows_uri = r"src\main\java\com\acme\java\util\Read.java";
		let windows =
			standard_path_module_moniker(&anchor, windows_uri).expect("standard Windows Java path");
		let graph = crate::lang::java::extract(
			windows_uri,
			"package com.acme.java.util; class Read {}",
			&anchor,
			true,
			&Presets::default(),
		);

		assert_eq!(windows, posix);
		assert_eq!(&windows, graph.root());
	}

	#[test]
	fn local_types_are_defined_and_resolved_in_their_callable_scope() {
		let source = r#"
			package com.acme;
			class Records {
				void first() {
					record Local(int value) {}
					Local local = new Local(1);
				}
				void second() {
					record Local(String value) {}
					Local local = new Local("two");
				}
			}
		"#;
		let anchor = MonikerBuilder::new()
			.project(b"app")
			.segment(b"srcset", b"main")
			.build();
		let graph = crate::lang::java::extract(
			"src/main/java/com/acme/Records.java",
			source,
			&anchor,
			true,
			&Presets::default(),
		);
		let local_records = graph
			.defs()
			.filter(|def| {
				def.kind == kinds::RECORD
					&& def
						.moniker
						.as_view()
						.segments()
						.last()
						.is_some_and(|segment| segment.name == b"Local")
			})
			.map(|def| def.moniker.clone())
			.collect::<HashSet<_>>();

		assert_eq!(local_records.len(), 2, "{local_records:#?}");
		let local_type_targets = graph
			.refs()
			.filter(|reference| reference.kind == kinds::USES_TYPE)
			.filter(|reference| local_records.contains(&reference.target))
			.map(|reference| reference.target.clone())
			.collect::<HashSet<_>>();
		assert_eq!(local_type_targets, local_records);
	}

	#[test]
	fn qualified_inner_class_creation_keeps_its_outer_type_owner() {
		let source = r#"
			package com.acme;
			class Container {
				class Outer { class Inner {} }
				void create(Outer outer) {
					Outer.Inner inner = outer.new Inner();
				}
			}
		"#;
		let anchor = MonikerBuilder::new()
			.project(b"app")
			.segment(b"srcset", b"main")
			.build();
		let graph = crate::lang::java::extract(
			"src/main/java/com/acme/Nesting.java",
			source,
			&anchor,
			true,
			&Presets::default(),
		);
		let inner = MonikerBuilder::from_view(graph.root().as_view())
			.segment(kinds::CLASS, b"Container")
			.segment(kinds::CLASS, b"Outer")
			.segment(kinds::CLASS, b"Inner")
			.build();
		assert!(graph.contains(&inner));
		let qualified_targets = graph
			.refs()
			.filter(|reference| {
				(reference.kind == kinds::USES_TYPE || reference.kind == kinds::INSTANTIATES)
					&& reference.target.bind_match(&inner)
			})
			.count();

		assert_eq!(qualified_targets, 3);
	}

	#[test]
	fn local_reads_keep_the_declared_binding_kind() {
		let source = r#"
			class Locals {
				String read(String parameter) {
					String local = parameter;
					return local;
				}
			}
		"#;
		let anchor = MonikerBuilder::new().project(b"app").build();
		let graph =
			crate::lang::java::extract("Locals.java", source, &anchor, true, &Presets::default());
		let binding_targets = graph
			.refs()
			.filter(|reference| reference.kind == kinds::READS)
			.filter_map(|reference| reference.target.as_view().segments().last())
			.map(|segment| (segment.kind.to_vec(), segment.name.to_vec()))
			.collect::<HashSet<_>>();

		assert!(binding_targets.contains(&(kinds::PARAM.to_vec(), b"parameter".to_vec())));
		assert!(binding_targets.contains(&(kinds::LOCAL.to_vec(), b"local".to_vec())));
		assert!(!binding_targets.contains(&(kinds::PARAM.to_vec(), b"local".to_vec())));
	}
}