code-moniker-workspace 0.7.1

Workspace model, ports, snapshots, linkage, and change analysis for code-moniker.
Documentation
use std::path::Path;

use code_moniker_core::core::code_graph::CodeGraph;
use code_moniker_core::core::moniker::{Moniker, MonikerBuilder};
use code_moniker_core::lang::{Lang, ts};

use crate::sources::CBuildContext;
use crate::tsconfig::TsResolution;

#[derive(Debug, Clone, Default)]
pub struct Context {
	pub c: CBuildContext,
	pub ts: TsResolution,
	pub project: Option<String>,
	pub srcset: Option<String>,
}

pub fn extract(lang: Lang, source: &str, path: &Path) -> CodeGraph {
	extract_with(lang, source, path, &Context::default())
}

pub fn source_root(lang: Lang, path: &Path, ctx: &Context) -> Option<Moniker> {
	let uri = path.to_str()?;
	let anchor = path_anchor(path, ctx);
	lang.file_root(uri, &anchor)
}

pub fn extract_with(lang: Lang, source: &str, path: &Path, ctx: &Context) -> CodeGraph {
	let uri = path.to_str().unwrap_or("single-file");
	let anchor = path_anchor(path, ctx);
	let deep = true;
	let mut graph = match lang {
		Lang::Ts => {
			let presets = ts::Presets {
				path_aliases: ctx.ts.aliases.clone(),
				sdk_profile: ctx.ts.sdk_profile_for(path).clone(),
				..ts::Presets::default()
			};
			ts::extract(uri, source, &anchor, deep, &presets)
		}
		Lang::Rs => code_moniker_core::lang::rs::extract_sdk(
			uri,
			source,
			&anchor,
			deep,
			&code_moniker_core::lang::rs::Presets::default(),
		),
		Lang::Java => code_moniker_core::lang::java::extract_sdk(
			uri,
			source,
			&anchor,
			deep,
			&code_moniker_core::lang::java::Presets::default(),
		),
		Lang::Python => code_moniker_core::lang::python::extract(
			uri,
			source,
			&anchor,
			deep,
			&code_moniker_core::lang::python::Presets::default(),
		),
		Lang::Go => code_moniker_core::lang::go::extract(
			uri,
			source,
			&anchor,
			deep,
			&code_moniker_core::lang::go::Presets::default(),
		),
		Lang::C => {
			let presets = ctx.c.extraction_presets();
			code_moniker_core::lang::c::extract(uri, source, &anchor, deep, &presets)
		}
		Lang::Cs => code_moniker_core::lang::cs::extract(
			uri,
			source,
			&anchor,
			deep,
			&code_moniker_core::lang::cs::Presets::default(),
		),
		Lang::Sql => code_moniker_core::lang::sql::extract(
			uri,
			source,
			&anchor,
			deep,
			&code_moniker_core::lang::sql::Presets::default(),
		),
	};
	graph.shrink_to_fit();
	graph
}

fn path_anchor(path: &Path, ctx: &Context) -> Moniker {
	let project = ctx.project.as_deref().map(str::as_bytes).unwrap_or(b".");
	let srcset = ctx
		.srcset
		.as_deref()
		.or_else(|| srcset(path))
		.map(str::as_bytes);
	anchor_moniker(project, srcset)
}

fn anchor_moniker(project: &[u8], srcset: Option<&[u8]>) -> Moniker {
	let mut b = MonikerBuilder::new();
	b.project(project);
	if let Some(srcset) = srcset {
		b.segment(b"srcset", srcset);
	}
	b.build()
}

fn srcset(path: &Path) -> Option<&'static str> {
	let parts: Vec<_> = path
		.components()
		.filter_map(|component| component.as_os_str().to_str())
		.collect();
	for window in parts.windows(2) {
		match window {
			["src", "main"] => return Some("main"),
			["src", "test"] | ["src", "tests"] => return Some("test"),
			_ => {}
		}
	}
	None
}

pub fn file_uri(path: &Path) -> String {
	let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
	format!("file://{}", abs.display())
}

#[cfg(test)]
mod tests {
	use super::*;
	use std::fs;

	use tempfile::tempdir;

	#[test]
	fn path_derived_roots_match_extracted_graph_roots() {
		let cases = [
			(Lang::Rs, "src/tools/mod.rs", ""),
			(Lang::Ts, "src/tools/read.ts", ""),
			(Lang::Python, "src/tools/read.py", ""),
			(Lang::Go, "src/tools/read.go", ""),
			(Lang::C, "src/tools/read.c", ""),
			(Lang::Cs, "src/tools/Read.cs", ""),
			(Lang::Sql, "db/tools/read.sql", ""),
			(
				Lang::Java,
				"src/main/java/app/tools/Read.java",
				"package app.tools; class Read {}",
			),
		];
		let ctx = Context::default();
		for (lang, path, source) in cases {
			let path = Path::new(path);
			let catalog_root = source_root(lang, path, &ctx)
				.unwrap_or_else(|| panic!("{} should expose a path root", lang.tag()));
			let graph = extract_with(lang, source, path, &ctx);
			assert_eq!(
				&catalog_root,
				graph.root(),
				"{} catalog root drifted from extraction",
				lang.tag()
			);
		}
	}

	#[test]
	fn typescript_extraction_uses_the_nearest_tsconfig_sdk_profile() {
		let temp = tempdir().expect("tempdir");
		let server = temp.path().join("server");
		let web = temp.path().join("web");
		fs::create_dir_all(&server).expect("server dir");
		fs::create_dir_all(&web).expect("web dir");
		fs::write(
			server.join("tsconfig.json"),
			r#"{"compilerOptions":{"target":"ES2022","lib":["ES2022"]}}"#,
		)
		.expect("server tsconfig");
		fs::write(
			web.join("tsconfig.json"),
			r#"{"compilerOptions":{"target":"ES2022","lib":["ES2022","DOM"]}}"#,
		)
		.expect("web tsconfig");
		let ctx = Context {
			ts: crate::tsconfig::load(temp.path()),
			..Context::default()
		};
		let source = "export function render() { document.body.replaceChildren(); return Promise.resolve(); }";

		let server_graph = extract_with(Lang::Ts, source, &server.join("main.ts"), &ctx);
		let web_graph = extract_with(Lang::Ts, source, &web.join("main.ts"), &ctx);
		let is_sdk_target = |graph: &CodeGraph, name: &[u8]| {
			graph.refs().any(|reference| {
				let segments = reference.target.as_view().segments().collect::<Vec<_>>();
				segments
					.first()
					.is_some_and(|segment| segment.kind == b"sdk")
					&& segments.last().is_some_and(|segment| segment.name == name)
			})
		};

		assert!(is_sdk_target(&web_graph, b"document"));
		assert!(is_sdk_target(&web_graph, b"replaceChildren"));
		assert!(!is_sdk_target(&server_graph, b"document"));
		assert!(!is_sdk_target(&server_graph, b"replaceChildren"));
		assert!(is_sdk_target(&server_graph, b"resolve"));
	}

	#[test]
	fn java_path_root_requires_a_standard_source_root() {
		let ctx = Context::default();
		assert!(source_root(Lang::Java, Path::new("fixtures/app/tools/Read.java"), &ctx).is_none());
	}

	#[test]
	fn java_path_root_keeps_java_package_segments_after_the_source_root() {
		let ctx = Context::default();
		let path = Path::new("src/main/java/com/acme/java/util/Read.java");
		let source = "package com.acme.java.util; class Read {}";
		let catalog_root = source_root(Lang::Java, path, &ctx).expect("standard Java path");
		let graph = extract_with(Lang::Java, source, path, &ctx);

		assert_eq!(&catalog_root, graph.root());
	}

	#[test]
	fn java_path_root_accepts_test_and_custom_source_sets() {
		let ctx = Context::default();
		for path in [
			"src/test/java/app/tools/Read.java",
			"src/integrationTest/java/app/tools/Read.java",
		] {
			let path = Path::new(path);
			let catalog_root =
				source_root(Lang::Java, path, &ctx).expect("standard Java source root");
			let graph = extract_with(Lang::Java, "package app.tools; class Read {}", path, &ctx);

			assert_eq!(&catalog_root, graph.root());
		}
	}

	#[test]
	fn java_path_root_exposes_package_misalignment() {
		let ctx = Context::default();
		let path = Path::new("src/main/java/app/tools/Read.java");
		let catalog_root = source_root(Lang::Java, path, &ctx).expect("standard Java path");
		let graph = extract_with(Lang::Java, "package other; class Read {}", path, &ctx);
		assert_ne!(
			&catalog_root,
			graph.root(),
			"a declared package mismatch must remain observable"
		);
	}
}