use std::collections::HashSet;
use std::path::Path;
use crate::core::{CodeNode, NodeKind, Visibility};
use crate::graph::CodeGraph;
use petgraph::graph::NodeIndex;
use regex::Regex;
pub struct EntryPointDetector<'a> {
graph: &'a CodeGraph,
rules: crate::config::ResolvedEntryPointRules,
serde_targets: HashSet<String>, }
impl<'a> EntryPointDetector<'a> {
pub fn new(graph: &'a CodeGraph) -> Self {
let serde_targets = Self::build_serde_target_index(graph);
Self {
graph,
rules: crate::config::ResolvedEntryPointRules::with_defaults(),
serde_targets,
}
}
pub fn with_rules(graph: &'a CodeGraph, rules: crate::config::ResolvedEntryPointRules) -> Self {
let serde_targets = Self::build_serde_target_index(graph);
Self {
graph,
rules,
serde_targets,
}
}
fn build_serde_target_index(graph: &'a CodeGraph) -> HashSet<String> {
let mut targets = HashSet::new();
for (_, node) in graph.nodes() {
for attr in &node.attributes {
if attr.starts_with("serde_default:")
|| attr.starts_with("serde_serialize_with:")
|| attr.starts_with("serde_deserialize_with:")
{
if let Some(fn_name) = attr.split(':').nth(1) {
targets.insert(fn_name.to_string());
}
}
}
}
targets
}
pub fn detect_production_entry_points(&self) -> HashSet<NodeIndex> {
let mut entries = HashSet::new();
entries.extend(self.graph.entry_points());
for (idx, node) in self.graph.nodes() {
if self.is_production_entry(node) {
entries.insert(idx);
}
}
entries
}
pub fn detect_test_entry_points(&self) -> HashSet<NodeIndex> {
let mut entries = HashSet::new();
entries.extend(self.graph.test_entry_points());
for (idx, node) in self.graph.nodes() {
if self.is_test_entry(node) {
entries.insert(idx);
}
}
entries
}
fn is_production_entry(&self, node: &CodeNode) -> bool {
self.is_main_function(node)
|| self.is_module_entry(node)
|| self.is_exported_entry(node)
|| self.is_framework_entry(node)
|| self.is_python_dunder(node)
|| self.is_swift_coding_keys(node)
|| self.is_nextjs_app_router_entry(node)
}
fn is_main_function(&self, node: &CodeNode) -> bool {
matches!(
node.name.as_str(),
"main"
| "__main__"
| "Main"
| "app"
| "run"
| "start"
| "init"
| "handler"
| "lambda_handler"
)
}
fn is_module_entry(&self, node: &CodeNode) -> bool {
node.name.starts_with("<module:")
}
fn is_exported_entry(&self, node: &CodeNode) -> bool {
let has_explicit_visibility = matches!(
node.language,
crate::core::Language::Rust
| crate::core::Language::Java
| crate::core::Language::CSharp
| crate::core::Language::Go
| crate::core::Language::Kotlin
| crate::core::Language::Scala
);
has_explicit_visibility
&& node.visibility == Visibility::Public
&& matches!(
node.kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::AsyncFunction
| NodeKind::AsyncMethod
| NodeKind::Struct
| NodeKind::Trait
| NodeKind::Class
| NodeKind::Interface
| NodeKind::Enum
)
}
fn is_framework_entry(&self, node: &CodeNode) -> bool {
if node.language == crate::core::Language::Python
&& node.attributes.iter().any(|attr| {
matches!(
attr.as_str(),
"property" | "staticmethod" | "classmethod" | "abstractmethod"
)
})
{
return true;
}
if node.language == crate::core::Language::Swift
&& node.attributes.iter().any(|attr| {
matches!(
attr.as_str(),
"objc"
| "IBAction"
| "IBOutlet"
| "IBDesignable"
| "IBInspectable"
| "NSManaged"
)
})
{
return true;
}
let has_framework_attr = node.attributes.iter().any(|attr| {
self.rules.matches_attribute(attr)
|| attr.starts_with("dataclass(")
|| attr.starts_with("attr.s")
|| attr.starts_with("attr.attrs")
});
if has_framework_attr {
return true;
}
if self.serde_targets.contains(&node.name) {
return true;
}
if self.rules.matches_function(&node.name) {
return true;
}
let name = &node.name;
name.starts_with("app.get")
|| name.starts_with("app.post")
|| name.starts_with("app.put")
|| name.starts_with("app.delete")
|| name.starts_with("app.route")
|| name.starts_with("router.get")
|| name.starts_with("router.post")
|| name.starts_with("router.put")
|| name.starts_with("router.delete")
|| name.starts_with("r.GET")
|| name.starts_with("r.POST")
|| name.starts_with("e.GET")
|| name.starts_with("e.POST")
}
fn is_python_dunder(&self, node: &CodeNode) -> bool {
node.language == crate::core::Language::Python
&& node.name.starts_with("__")
&& node.name.ends_with("__")
&& node.name.len() > 4
&& matches!(
node.kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::AsyncFunction
| NodeKind::AsyncMethod
)
}
fn is_swift_coding_keys(&self, node: &CodeNode) -> bool {
node.language == crate::core::Language::Swift && node.name == "CodingKeys"
}
fn is_nextjs_app_router_entry(&self, node: &CodeNode) -> bool {
if !matches!(
node.language,
crate::core::Language::TypeScript | crate::core::Language::JavaScript
) {
return false;
}
let stem = std::path::Path::new(&node.location.file)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("");
let is_convention_file = matches!(
stem,
"page"
| "layout"
| "error"
| "loading"
| "not-found"
| "template"
| "default"
| "route"
| "middleware"
);
is_convention_file && node.visibility == Visibility::Public
}
fn is_test_entry(&self, node: &CodeNode) -> bool {
if node.is_test {
return true;
}
if Self::is_test_file(&node.location.file) {
return true;
}
let name = &node.name;
name.starts_with("test_")
|| name.starts_with("Test")
|| name.ends_with("_test")
|| name.starts_with("it_")
|| name.starts_with("should_")
|| name.starts_with("spec_")
}
pub fn is_test_file(path: &str) -> bool {
let normalized = path.replace('\\', "/").to_lowercase();
for seg in normalized.split('/') {
if matches!(
seg,
"tests"
| "test"
| "__tests__"
| "spec"
| "__mocks__"
| "mocks"
| "test-utils"
| "testutils"
| "testing"
| "benches"
| "benchmarks"
) {
return true;
}
}
if let Some(filename) = normalized.rsplit('/').next() {
if filename.starts_with("test_") {
return true;
}
if let Some(stem) = filename.rsplit_once('.').map(|(s, _)| s) {
if stem.ends_with("_test")
|| stem.ends_with("_tests")
|| stem.ends_with(".test")
|| stem.ends_with(".spec")
|| stem.ends_with("_spec")
{
return true;
}
if stem.starts_with("setup") && stem.contains("test") {
return true;
}
}
}
false
}
}
pub fn detect_config_entry_points(root: &Path, graph: &CodeGraph) -> HashSet<NodeIndex> {
let mut entries = HashSet::new();
let mut entry_files: HashSet<String> = HashSet::new();
let mut api_module_files: HashSet<String> = HashSet::new();
let walker = ignore::WalkBuilder::new(root)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.hidden(true)
.max_depth(Some(10))
.build();
let re_entrypoint_line = Regex::new(r#"(?i)^(?:ENTRYPOINT|CMD)\s+(.+)$"#).ok();
let re_quoted_arg = Regex::new(r#""([^"]+)""#).ok();
let re_docker_build_context =
Regex::new(r#"(?:build:\s*(?:context:\s*)?|build:\s*\n\s*context:\s*)([^\s\n]+)"#).ok();
for entry in walker.flatten() {
let path = entry.path();
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
match file_name.as_str() {
"Dockerfile" => {
if let Ok(content) = std::fs::read_to_string(path) {
extract_dockerfile_entries(
&content,
path,
&re_entrypoint_line,
&re_quoted_arg,
&mut entry_files,
);
}
}
name if name.starts_with("docker-compose")
&& (name.ends_with(".yml") || name.ends_with(".yaml")) =>
{
if let Ok(content) = std::fs::read_to_string(path) {
extract_docker_compose_entries(
&content,
path,
&re_docker_build_context,
&mut entry_files,
);
}
}
"package.json" => {
if let Ok(content) = std::fs::read_to_string(path) {
extract_package_json_entries(&content, path, &mut entry_files);
extract_package_json_public_api_modules(&content, path, &mut api_module_files);
}
}
"cdk.json" => {
if let Ok(content) = std::fs::read_to_string(path) {
extract_cdk_json_entries(&content, path, &mut entry_files);
}
}
_ => {}
}
}
for entry_file in &entry_files {
for (idx, node) in graph.nodes() {
let node_file = &node.location.file;
if node_file.ends_with(entry_file) || entry_file.ends_with(node_file) {
if node.name.starts_with("<module:") {
entries.insert(idx);
continue;
}
if node.visibility == Visibility::Public {
entries.insert(idx);
}
}
}
}
for api_file in &api_module_files {
for (idx, node) in graph.nodes() {
let node_file = node.location.file.replace('\\', "/");
if (node_file.ends_with(api_file) || api_file.ends_with(&node_file))
&& node.visibility == Visibility::Public
&& matches!(
node.kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::AsyncFunction
| NodeKind::AsyncMethod
| NodeKind::Class
)
{
entries.insert(idx);
}
}
}
detect_python_public_api_entries(root, graph, &mut entries);
detect_python_script_entries(root, &mut entry_files);
for entry_file in &entry_files {
for (idx, node) in graph.nodes() {
let node_file = &node.location.file;
if (node_file.ends_with(entry_file) || entry_file.ends_with(node_file))
&& (node.name.starts_with("<module:") || node.visibility == Visibility::Public)
{
entries.insert(idx);
}
}
}
entries
}
fn extract_dockerfile_entries(
content: &str,
dockerfile_path: &Path,
re_entrypoint_line: &Option<Regex>,
re_quoted_arg: &Option<Regex>,
entry_files: &mut HashSet<String>,
) {
let dir = dockerfile_path.parent().unwrap_or(Path::new("."));
let skip_commands = [
"node",
"python",
"python3",
"java",
"npm",
"yarn",
"pnpm",
"sh",
"bash",
"/bin/sh",
"/bin/bash",
"/usr/bin/python",
"/usr/bin/python3",
"/usr/local/bin/node",
"/usr/local/bin/python",
];
for line in content.lines() {
let trimmed = line.trim();
if let Some(ref re_line) = re_entrypoint_line {
if let Some(caps) = re_line.captures(trimmed) {
let args_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let args: Vec<String> = if let Some(ref re_q) = re_quoted_arg {
re_q.captures_iter(args_str)
.filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
.collect()
} else {
args_str.split_whitespace().map(|s| s.to_string()).collect()
};
for arg in &args {
if skip_commands.contains(&arg.as_str()) {
continue;
}
let resolved = dir.join(arg);
entry_files.insert(resolved.to_string_lossy().to_string());
entry_files.insert(arg.clone());
}
}
}
}
}
fn extract_docker_compose_entries(
content: &str,
compose_path: &Path,
re_build_context: &Option<Regex>,
entry_files: &mut HashSet<String>,
) {
let dir = compose_path.parent().unwrap_or(Path::new("."));
if let Some(ref re) = re_build_context {
for caps in re.captures_iter(content) {
if let Some(m) = caps.get(1) {
let context = m.as_str().trim_matches('\"').trim_matches('\'');
let context_dir = dir.join(context);
for entry_name in &[
"main.py",
"app.py",
"index.js",
"index.ts",
"main.go",
"main.rs",
"server.js",
"server.ts",
] {
let entry_path = context_dir.join(entry_name);
if entry_path.exists() {
entry_files.insert(entry_path.to_string_lossy().to_string());
}
}
let pkg_json = context_dir.join("package.json");
if pkg_json.exists() {
if let Ok(pkg_content) = std::fs::read_to_string(&pkg_json) {
extract_package_json_entries(&pkg_content, &pkg_json, entry_files);
}
}
}
}
}
}
fn extract_package_json_entries(content: &str, pkg_path: &Path, entry_files: &mut HashSet<String>) {
let dir = pkg_path.parent().unwrap_or(Path::new("."));
if let Ok(json) = serde_json::from_str::<serde_json::Value>(content) {
if let Some(main) = json.get("main").and_then(|v| v.as_str()) {
let resolved = dir.join(main);
entry_files.insert(resolved.to_string_lossy().to_string());
entry_files.insert(main.to_string());
}
if let Some(bin) = json.get("bin") {
match bin {
serde_json::Value::String(s) => {
let resolved = dir.join(s.as_str());
entry_files.insert(resolved.to_string_lossy().to_string());
entry_files.insert(s.clone());
}
serde_json::Value::Object(map) => {
for (_, v) in map {
if let Some(s) = v.as_str() {
let resolved = dir.join(s);
entry_files.insert(resolved.to_string_lossy().to_string());
entry_files.insert(s.to_string());
}
}
}
_ => {}
}
}
if let Some(scripts) = json.get("scripts").and_then(|v| v.as_object()) {
if let Some(start) = scripts.get("start").and_then(|v| v.as_str()) {
let parts: Vec<&str> = start.split_whitespace().collect();
for part in &parts {
if part.ends_with(".js")
|| part.ends_with(".ts")
|| part.ends_with(".py")
|| part.ends_with(".mjs")
{
let resolved = dir.join(part);
entry_files.insert(resolved.to_string_lossy().to_string());
entry_files.insert(part.to_string());
}
}
}
}
}
}
fn extract_package_json_public_api_modules(
content: &str,
pkg_path: &Path,
api_module_files: &mut HashSet<String>,
) {
let dir = pkg_path.parent().unwrap_or(Path::new("."));
let json = match serde_json::from_str::<serde_json::Value>(content) {
Ok(v) => v,
Err(_) => return,
};
let mut raw_paths: Vec<String> = Vec::new();
if let Some(exports) = json.get("exports") {
collect_exports_paths(exports, &mut raw_paths);
}
if let Some(module) = json.get("module").and_then(|v| v.as_str()) {
raw_paths.push(module.to_string());
}
for key in &["types", "typings"] {
if let Some(types_path) = json.get(*key).and_then(|v| v.as_str()) {
raw_paths.push(types_path.to_string());
}
}
for raw in &raw_paths {
let cleaned = raw.strip_prefix("./").unwrap_or(raw);
if cleaned.is_empty() {
continue;
}
let resolved = dir.join(cleaned);
api_module_files.insert(resolved.to_string_lossy().replace('\\', "/"));
api_module_files.insert(cleaned.replace('\\', "/"));
if cleaned.starts_with("dist/") || cleaned.starts_with("dist\\") {
let src_variant = format!("src/{}", &cleaned[5..]);
let resolved_src = dir.join(&src_variant);
api_module_files.insert(resolved_src.to_string_lossy().replace('\\', "/"));
api_module_files.insert(src_variant);
}
if cleaned.ends_with(".js") || cleaned.ends_with(".mjs") || cleaned.ends_with(".cjs") {
let ext_start = cleaned.rfind('.').unwrap();
let ts_variant = format!("{}.ts", &cleaned[..ext_start]);
let resolved_ts = dir.join(&ts_variant);
api_module_files.insert(resolved_ts.to_string_lossy().replace('\\', "/"));
api_module_files.insert(ts_variant.clone());
let tsx_variant = format!("{}.tsx", &cleaned[..ext_start]);
let resolved_tsx = dir.join(&tsx_variant);
api_module_files.insert(resolved_tsx.to_string_lossy().replace('\\', "/"));
api_module_files.insert(tsx_variant);
}
if let Some(stripped) = cleaned.strip_suffix(".d.ts") {
let ts_variant = format!("{}.ts", stripped);
let resolved_ts = dir.join(&ts_variant);
api_module_files.insert(resolved_ts.to_string_lossy().replace('\\', "/"));
api_module_files.insert(ts_variant);
}
}
}
fn collect_exports_paths(value: &serde_json::Value, paths: &mut Vec<String>) {
match value {
serde_json::Value::String(s) => {
paths.push(s.clone());
}
serde_json::Value::Object(map) => {
for (key, val) in map {
if key == "default"
|| key == "import"
|| key == "require"
|| key == "types"
|| key == "node"
|| key == "browser"
|| key.starts_with('.')
{
collect_exports_paths(val, paths);
}
}
}
_ => {}
}
}
fn extract_cdk_json_entries(content: &str, cdk_path: &Path, entry_files: &mut HashSet<String>) {
let dir = cdk_path.parent().unwrap_or(Path::new("."));
if let Ok(json) = serde_json::from_str::<serde_json::Value>(content) {
if let Some(app) = json.get("app").and_then(|v| v.as_str()) {
let parts: Vec<&str> = app.split_whitespace().collect();
for part in &parts {
if part.ends_with(".ts")
|| part.ends_with(".js")
|| part.ends_with(".py")
|| part.ends_with(".mjs")
{
let resolved = dir.join(part);
entry_files.insert(resolved.to_string_lossy().to_string());
entry_files.insert(part.to_string());
}
}
}
}
}
fn detect_python_public_api_entries(
root: &Path,
graph: &CodeGraph,
entries: &mut HashSet<NodeIndex>,
) {
let re_all = Regex::new(r#"__all__\s*=\s*\[([^\]]*)\]"#).ok();
let re_all_name = Regex::new(r#"["'](\w+)["']"#).ok();
let re_reexport = Regex::new(r#"from\s+\.[\w.]*\s+import\s+(.+)"#).ok();
let walker = ignore::WalkBuilder::new(root)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.hidden(true)
.max_depth(Some(10))
.build();
for entry in walker.flatten() {
let path = entry.path();
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
if file_name != "__init__.py" {
continue;
}
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let mut public_names: HashSet<String> = HashSet::new();
let joined = content.replace('\n', " ");
if let Some(ref re) = re_all {
if let Some(caps) = re.captures(&joined) {
if let (Some(list_content), Some(ref re_name)) = (caps.get(1), &re_all_name) {
for name_cap in re_name.captures_iter(list_content.as_str()) {
if let Some(name) = name_cap.get(1) {
public_names.insert(name.as_str().to_string());
}
}
}
}
}
if let Some(ref re) = re_reexport {
for caps in re.captures_iter(&content) {
if let Some(imports_str) = caps.get(1) {
for name in imports_str.as_str().split(',') {
let trimmed = name.trim();
let actual_name = if let Some((_orig, alias)) = trimmed.split_once(" as ") {
alias.trim()
} else {
trimmed
};
if !actual_name.is_empty()
&& !actual_name.starts_with('(')
&& !actual_name.starts_with('#')
{
public_names.insert(actual_name.to_string());
}
}
}
}
}
if !public_names.is_empty() {
let init_dir = path.parent().unwrap_or(Path::new("."));
let init_dir_str = init_dir.to_string_lossy();
for (idx, node) in graph.nodes() {
if node.language != crate::core::Language::Python {
continue;
}
if (node.location.file.contains(&*init_dir_str)
|| init_dir_str.contains(&node.location.file)
|| is_in_package_dir(&node.location.file, &init_dir_str))
&& public_names.contains(&node.name)
{
entries.insert(idx);
}
}
}
}
}
fn is_in_package_dir(file_path: &str, dir_path: &str) -> bool {
let file_normalized = file_path.replace('\\', "/");
let dir_normalized = dir_path.replace('\\', "/");
if let Some(file_dir) = file_normalized.rsplit_once('/').map(|(d, _)| d) {
file_dir.ends_with(&dir_normalized) || dir_normalized.ends_with(file_dir)
} else {
false
}
}
fn detect_python_script_entries(root: &Path, entry_files: &mut HashSet<String>) {
let pyproject_path = root.join("pyproject.toml");
if let Ok(content) = std::fs::read_to_string(&pyproject_path) {
if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
if let Some(scripts) = toml_value
.get("project")
.and_then(|p| p.get("scripts"))
.and_then(|s| s.as_table())
{
for (_cmd, val) in scripts {
if let Some(entry) = val.as_str() {
if let Some(file) = python_entry_to_file(entry) {
entry_files.insert(file);
}
}
}
}
if let Some(scripts) = toml_value
.get("tool")
.and_then(|t| t.get("poetry"))
.and_then(|p| p.get("scripts"))
.and_then(|s| s.as_table())
{
for (_cmd, val) in scripts {
if let Some(entry) = val.as_str() {
if let Some(file) = python_entry_to_file(entry) {
entry_files.insert(file);
}
}
}
}
}
}
let setup_path = root.join("setup.py");
if let Ok(content) = std::fs::read_to_string(&setup_path) {
let re = Regex::new(r#"["'][\w-]+\s*=\s*([\w.]+):(\w+)["']"#).ok();
if let Some(ref re) = re {
for caps in re.captures_iter(&content) {
if let Some(module_path) = caps.get(1) {
let file = module_path.as_str().replace('.', "/") + ".py";
entry_files.insert(file);
}
}
}
}
}
fn python_entry_to_file(entry: &str) -> Option<String> {
let module_part = entry.split(':').next()?;
if module_part.is_empty() {
return None;
}
Some(module_part.replace('.', "/") + ".py")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Language, SourceLocation};
fn make_node(name: &str, kind: NodeKind, vis: Visibility) -> CodeNode {
CodeNode::new(
name.to_string(),
kind,
SourceLocation::new("test.py".to_string(), 1, 10, 0, 0),
Language::Python,
vis,
)
}
#[test]
fn test_detect_main_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node("main", NodeKind::Function, Visibility::Public));
graph.add_node(make_node("helper", NodeKind::Function, Visibility::Private));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(!entries.is_empty());
}
#[test]
fn test_detect_test_entries() {
let mut graph = CodeGraph::new();
graph.add_node(make_node(
"test_foo",
NodeKind::Function,
Visibility::Public,
));
graph.add_node(make_node("helper", NodeKind::Function, Visibility::Private));
let detector = EntryPointDetector::new(&graph);
let test_entries = detector.detect_test_entry_points();
assert!(!test_entries.is_empty());
}
fn make_node_with_attrs(
name: &str,
kind: NodeKind,
vis: Visibility,
attrs: Vec<&str>,
) -> CodeNode {
CodeNode::new(
name.to_string(),
kind,
SourceLocation::new("test.py".to_string(), 1, 10, 0, 0),
Language::Python,
vis,
)
.with_attributes(attrs.into_iter().map(|s| s.to_string()).collect())
}
#[test]
fn test_spring_bean_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"myService",
NodeKind::Function,
Visibility::Public,
vec!["Bean"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Spring @Bean should be detected as entry point"
);
}
#[test]
fn test_spring_controller_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"UserController",
NodeKind::Class,
Visibility::Public,
vec!["RestController"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Spring @RestController should be detected as entry point"
);
}
#[test]
fn test_spring_scheduled_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"runCleanup",
NodeKind::Function,
Visibility::Public,
vec!["Scheduled"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Spring @Scheduled should be detected as entry point"
);
}
#[test]
fn test_aspnet_http_verbs_are_framework_entries() {
for attr in &[
"HttpGet",
"HttpPost",
"HttpPut",
"HttpDelete",
"ApiController",
] {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"action",
NodeKind::Function,
Visibility::Public,
vec![attr],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"ASP.NET attribute '{}' should be detected as entry point",
attr
);
}
}
#[test]
fn test_fastapi_name_pattern_is_framework_entry() {
for name in &["app.get", "app.post", "app.put", "app.delete", "app.route"] {
let mut graph = CodeGraph::new();
graph.add_node(make_node(name, NodeKind::Function, Visibility::Public));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"FastAPI/Flask name pattern '{}' should be detected as entry point",
name
);
}
}
#[test]
fn test_express_name_pattern_is_framework_entry() {
for name in &["router.get", "router.post", "router.put", "router.delete"] {
let mut graph = CodeGraph::new();
graph.add_node(make_node(name, NodeKind::Function, Visibility::Public));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Express name pattern '{}' should be detected as entry point",
name
);
}
}
#[test]
fn test_go_gin_echo_name_pattern_is_framework_entry() {
for name in &["r.GET", "r.POST", "e.GET", "e.POST"] {
let mut graph = CodeGraph::new();
graph.add_node(make_node(name, NodeKind::Function, Visibility::Public));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Go Gin/Echo name pattern '{}' should be detected as entry point",
name
);
}
}
#[test]
fn test_request_mapping_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"handleRequest",
NodeKind::Function,
Visibility::Public,
vec!["RequestMapping"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Spring @RequestMapping should be detected as entry point"
);
}
#[test]
fn test_post_construct_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"initialize",
NodeKind::Function,
Visibility::Public,
vec!["PostConstruct"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Spring @PostConstruct should be detected as entry point"
);
}
#[test]
fn test_serde_default_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"Config",
NodeKind::Struct,
Visibility::Public,
vec!["serde_default:default_page_size"],
));
graph.add_node(CodeNode::new(
"default_page_size".to_string(),
NodeKind::Function,
SourceLocation::new("config.rs".to_string(), 20, 25, 0, 0),
Language::Rust,
Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
let entry_names: Vec<String> = entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"default_page_size".to_string()),
"Serde-referenced function should be detected as entry point, got: {:?}",
entry_names
);
}
#[test]
fn test_serde_serialize_with_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"serialize_date",
NodeKind::Function,
Visibility::Private,
vec!["serde_serialize_with:serialize_date"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"serde_serialize_with function should be detected as entry point"
);
}
#[test]
fn test_impl_from_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"from".to_string(),
NodeKind::Function,
SourceLocation::new("types.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Public,
)
.with_attributes(vec!["impl_trait:From".to_string()]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"impl From function should be detected as entry point"
);
}
#[test]
fn test_impl_display_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"fmt".to_string(),
NodeKind::Function,
SourceLocation::new("types.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Public,
)
.with_attributes(vec!["impl_trait:Display".to_string()]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"impl Display function should be detected as entry point"
);
}
#[test]
fn test_impl_default_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"default".to_string(),
NodeKind::Function,
SourceLocation::new("types.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Public,
)
.with_attributes(vec!["impl_trait:Default".to_string()]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"impl Default function should be detected as entry point"
);
}
#[test]
fn test_plain_impl_method_not_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"helper".to_string(),
NodeKind::Function,
SourceLocation::new("types.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"Plain impl method should not be framework entry"
);
}
#[test]
fn test_python_dunder_is_entry_point() {
for name in &[
"__call__",
"__repr__",
"__getattr__",
"__init__",
"__enter__",
"__exit__",
] {
let mut graph = CodeGraph::new();
graph.add_node(make_node(name, NodeKind::Function, Visibility::Public));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Python dunder '{}' should be detected as entry point",
name
);
}
}
#[test]
fn test_rust_dunder_not_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"__call__".to_string(),
NodeKind::Function,
SourceLocation::new("test.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"Rust '__call__' should NOT be a Python dunder entry point. Entries: {:?}",
entries
);
}
#[test]
fn test_python_short_dunder_not_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(make_node("__", NodeKind::Function, Visibility::Public));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"'__' should NOT be a dunder entry point"
);
}
#[test]
fn test_python_property_is_framework_entry() {
for attr in &["property", "staticmethod", "classmethod", "abstractmethod"] {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"my_method",
NodeKind::Function,
Visibility::Public,
vec![attr],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Python @{} should be detected as entry point",
attr
);
}
}
#[test]
fn test_rust_property_attr_not_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"my_method".to_string(),
NodeKind::Function,
SourceLocation::new("test.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Private,
)
.with_attributes(vec!["property".to_string()]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"Rust function with 'property' attr should NOT be a Python decorator entry point"
);
}
fn make_swift_node(name: &str, kind: NodeKind, vis: Visibility) -> CodeNode {
CodeNode::new(
name.to_string(),
kind,
SourceLocation::new("test.swift".to_string(), 1, 10, 0, 0),
Language::Swift,
vis,
)
}
fn make_swift_node_with_attrs(
name: &str,
kind: NodeKind,
vis: Visibility,
attrs: Vec<&str>,
) -> CodeNode {
CodeNode::new(
name.to_string(),
kind,
SourceLocation::new("test.swift".to_string(), 1, 10, 0, 0),
Language::Swift,
vis,
)
.with_attributes(attrs.into_iter().map(|s| s.to_string()).collect())
}
#[test]
fn test_swift_coding_keys_is_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(make_swift_node(
"CodingKeys",
NodeKind::Struct, Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Swift CodingKeys should be detected as entry point"
);
}
#[test]
fn test_rust_coding_keys_not_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"CodingKeys".to_string(),
NodeKind::Struct,
SourceLocation::new("test.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"Rust 'CodingKeys' should NOT be a Swift entry point. Entries: {:?}",
entries
);
}
#[test]
fn test_swift_objc_is_framework_entry() {
for attr in &[
"objc",
"IBAction",
"IBOutlet",
"IBDesignable",
"IBInspectable",
"NSManaged",
] {
let mut graph = CodeGraph::new();
graph.add_node(make_swift_node_with_attrs(
"someMethod",
NodeKind::Function,
Visibility::Public,
vec![attr],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Swift @{} should be detected as entry point",
attr
);
}
}
#[test]
fn test_plain_function_not_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node(
"helper_function",
NodeKind::Function,
Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"A plain private function should not be detected as entry point"
);
}
#[test]
fn test_impl_custom_trait_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"validate".to_string(),
NodeKind::Function,
SourceLocation::new("types.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Public,
)
.with_attributes(vec!["impl_trait:Validate".to_string()]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"impl Validate (custom trait) should be detected as entry point"
);
}
#[test]
fn test_impl_arbitrary_trait_is_framework_entry() {
for trait_name in &[
"MyAppHandler",
"CustomSerializer",
"ProtobufMessage",
"Validator",
] {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"method".to_string(),
NodeKind::Function,
SourceLocation::new("types.rs".to_string(), 1, 10, 0, 0),
Language::Rust,
Visibility::Public,
)
.with_attributes(vec![format!("impl_trait:{}", trait_name)]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"impl {} should be detected as entry point",
trait_name
);
}
}
#[test]
fn test_extends_attribute_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(
CodeNode::new(
"speak".to_string(),
NodeKind::Function,
SourceLocation::new("dog.py".to_string(), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
)
.with_attributes(vec!["extends:Animal".to_string()]),
);
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Method with extends:Animal attribute should be detected as entry point"
);
}
#[test]
fn test_detect_config_entry_points_dockerfile() {
use std::io::Write;
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let dockerfile_path = dir.path().join("Dockerfile");
let mut f = std::fs::File::create(&dockerfile_path).unwrap();
writeln!(f, "FROM python:3.9").unwrap();
writeln!(f, "COPY . /app").unwrap();
writeln!(f, "CMD [\"python\", \"main.py\"]").unwrap();
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"<module:main.py>".to_string(),
NodeKind::Function,
SourceLocation::new("main.py".to_string(), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"main".to_string(),
NodeKind::Function,
SourceLocation::new("main.py".to_string(), 5, 10, 0, 0),
Language::Python,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names
.iter()
.any(|n| n == "main" || n.contains("main.py")),
"Dockerfile CMD should detect main.py as entry, got: {:?}",
entry_names
);
}
#[test]
fn test_detect_config_entry_points_package_json() {
use std::io::Write;
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let pkg_path = dir.path().join("package.json");
let mut f = std::fs::File::create(&pkg_path).unwrap();
writeln!(
f,
r#"{{"main": "src/index.js", "scripts": {{"start": "node src/server.js"}}}}"#
)
.unwrap();
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"<module:src/index.js>".to_string(),
NodeKind::Function,
SourceLocation::new("src/index.js".to_string(), 1, 10, 0, 0),
Language::JavaScript,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"<module:src/server.js>".to_string(),
NodeKind::Function,
SourceLocation::new("src/server.js".to_string(), 1, 10, 0, 0),
Language::JavaScript,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
!config_entries.is_empty(),
"package.json should detect entry points, got: {:?}",
entry_names
);
}
#[test]
fn test_python_dataclass_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"User",
NodeKind::Class,
Visibility::Public,
vec!["dataclass"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Python @dataclass should be detected as entry point"
);
}
#[test]
fn test_java_lombok_annotations_are_framework_entries() {
for attr in &[
"Data",
"Getter",
"Setter",
"Builder",
"NoArgsConstructor",
"AllArgsConstructor",
"Value",
] {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"UserDto",
NodeKind::Class,
Visibility::Public,
vec![attr],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Java Lombok @{} should be detected as entry point",
attr
);
}
}
#[test]
fn test_jpa_entity_is_framework_entry() {
for attr in &["Entity", "Table", "MappedSuperclass", "Embeddable"] {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"UserEntity",
NodeKind::Class,
Visibility::Public,
vec![attr],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"JPA @{} should be detected as entry point",
attr
);
}
}
#[test]
fn test_csharp_serialization_attrs_are_framework_entries() {
for attr in &[
"Serializable",
"DataContract",
"DataMember",
"JsonConverter",
"ProtoContract",
] {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"UserModel",
NodeKind::Class,
Visibility::Public,
vec![attr],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"C# [{}] should be detected as entry point",
attr
);
}
}
#[test]
fn test_kotlin_parcelize_is_framework_entry() {
let mut graph = CodeGraph::new();
graph.add_node(make_node_with_attrs(
"UserParcel",
NodeKind::Class,
Visibility::Public,
vec!["Parcelize"],
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Kotlin @Parcelize should be detected as entry point"
);
}
#[test]
fn test_nextjs_app_router_page_is_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"Page".to_string(),
NodeKind::Function,
SourceLocation::new("app/dashboard/page.tsx".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Next.js App Router page.tsx export should be detected as entry point"
);
}
#[test]
fn test_nextjs_app_router_layout_is_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"RootLayout".to_string(),
NodeKind::Function,
SourceLocation::new("app/layout.tsx".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Next.js App Router layout.tsx export should be detected as entry point"
);
}
#[test]
fn test_nextjs_app_router_route_handler_is_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"GET".to_string(),
NodeKind::Function,
SourceLocation::new("app/api/users/route.ts".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
!entries.is_empty(),
"Next.js App Router route.ts GET handler should be detected as entry point"
);
}
#[test]
fn test_nextjs_private_function_in_page_not_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"helperFunction".to_string(),
NodeKind::Function,
SourceLocation::new("app/page.tsx".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Private,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"Private function in page.tsx should NOT be an entry point"
);
}
#[test]
fn test_nextjs_non_convention_file_not_entry_point() {
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"MyComponent".to_string(),
NodeKind::Function,
SourceLocation::new("app/components/Button.tsx".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
let detector = EntryPointDetector::new(&graph);
let entries = detector.detect_production_entry_points();
assert!(
entries.is_empty(),
"Exported function in non-convention file should NOT be an entry point"
);
}
#[test]
fn test_package_json_exports_public_api() {
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"exports": {"." : "./src/index.ts"}}"#,
)
.unwrap();
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"createClient".to_string(),
NodeKind::Function,
SourceLocation::new("src/index.ts".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"internalHelper".to_string(),
NodeKind::Function,
SourceLocation::new("src/index.ts".to_string(), 20, 25, 0, 0),
Language::TypeScript,
Visibility::Private,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"createClient".to_string()),
"Public function in exports module should be entry point. Got: {:?}",
entry_names
);
assert!(
!entry_names.contains(&"internalHelper".to_string()),
"Private function in exports module should NOT be entry point. Got: {:?}",
entry_names
);
}
#[test]
fn test_package_json_module_field_public_api() {
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"module": "./src/lib.ts"}"#,
)
.unwrap();
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"exportedUtil".to_string(),
NodeKind::Function,
SourceLocation::new("src/lib.ts".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"exportedUtil".to_string()),
"Public function in 'module' field target should be entry point. Got: {:?}",
entry_names
);
}
#[test]
fn test_package_json_exports_nested_object() {
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"exports": {".": {"import": "./src/index.ts", "require": "./src/index.cjs"}, "./utils": "./src/utils.ts"}}"#,
)
.unwrap();
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"mainExport".to_string(),
NodeKind::Function,
SourceLocation::new("src/index.ts".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"utilExport".to_string(),
NodeKind::Function,
SourceLocation::new("src/utils.ts".to_string(), 1, 10, 0, 0),
Language::TypeScript,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"mainExport".to_string()),
"Main export from nested exports should be entry point. Got: {:?}",
entry_names
);
assert!(
entry_names.contains(&"utilExport".to_string()),
"Subpath export from exports should be entry point. Got: {:?}",
entry_names
);
}
#[test]
fn test_python_init_all_public_api() {
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let pkg_dir = dir.path().join("mypackage");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(
pkg_dir.join("__init__.py"),
r#"__all__ = ["public_func", "MyClass"]"#,
)
.unwrap();
let mut graph = CodeGraph::new();
let pkg_dir_str = pkg_dir.to_string_lossy().to_string();
graph.add_node(CodeNode::new(
"public_func".to_string(),
NodeKind::Function,
SourceLocation::new(format!("{}/core.py", pkg_dir_str), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"MyClass".to_string(),
NodeKind::Class,
SourceLocation::new(format!("{}/models.py", pkg_dir_str), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"_internal_func".to_string(),
NodeKind::Function,
SourceLocation::new(format!("{}/core.py", pkg_dir_str), 20, 25, 0, 0),
Language::Python,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"public_func".to_string()),
"__all__ public_func should be entry point. Got: {:?}",
entry_names
);
assert!(
entry_names.contains(&"MyClass".to_string()),
"__all__ MyClass should be entry point. Got: {:?}",
entry_names
);
assert!(
!entry_names.contains(&"_internal_func".to_string()),
"_internal_func NOT in __all__ should not be entry point. Got: {:?}",
entry_names
);
}
#[test]
fn test_python_init_reexport_public_api() {
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let pkg_dir = dir.path().join("mypackage");
std::fs::create_dir_all(&pkg_dir).unwrap();
std::fs::write(
pkg_dir.join("__init__.py"),
"from .core import MyClass, helper_func\nfrom .utils import format_data\n",
)
.unwrap();
let mut graph = CodeGraph::new();
let pkg_dir_str = pkg_dir.to_string_lossy().to_string();
graph.add_node(CodeNode::new(
"MyClass".to_string(),
NodeKind::Class,
SourceLocation::new(format!("{}/core.py", pkg_dir_str), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"helper_func".to_string(),
NodeKind::Function,
SourceLocation::new(format!("{}/core.py", pkg_dir_str), 20, 25, 0, 0),
Language::Python,
Visibility::Public,
));
graph.add_node(CodeNode::new(
"format_data".to_string(),
NodeKind::Function,
SourceLocation::new(format!("{}/utils.py", pkg_dir_str), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"MyClass".to_string()),
"Re-exported MyClass should be entry point. Got: {:?}",
entry_names
);
assert!(
entry_names.contains(&"format_data".to_string()),
"Re-exported format_data should be entry point. Got: {:?}",
entry_names
);
}
#[test]
fn test_python_pyproject_scripts_entry() {
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("pyproject.toml"),
r#"
[project.scripts]
mycli = "mypackage.cli:main"
"#,
)
.unwrap();
let mut graph = CodeGraph::new();
graph.add_node(CodeNode::new(
"main".to_string(),
NodeKind::Function,
SourceLocation::new("mypackage/cli.py".to_string(), 1, 10, 0, 0),
Language::Python,
Visibility::Public,
));
let config_entries = detect_config_entry_points(dir.path(), &graph);
let entry_names: Vec<String> = config_entries
.iter()
.filter_map(|idx| graph.get_node(*idx).map(|n| n.name.clone()))
.collect();
assert!(
entry_names.contains(&"main".to_string()),
"pyproject.toml console_scripts entry should be detected. Got: {:?}",
entry_names
);
}
}