feat: add stella-callgraph-node for JavaScript/TypeScript call graph extraction

- Implemented a new tool `stella-callgraph-node` that extracts call graphs from JavaScript/TypeScript projects using Babel AST.
- Added command-line interface with options for JSON output and help.
- Included functionality to analyze project structure, detect functions, and build call graphs.
- Created a package.json file for dependency management.

feat: introduce stella-callgraph-python for Python call graph extraction

- Developed `stella-callgraph-python` to extract call graphs from Python projects using AST analysis.
- Implemented command-line interface with options for JSON output and verbose logging.
- Added framework detection to identify popular web frameworks and their entry points.
- Created an AST analyzer to traverse Python code and extract function definitions and calls.
- Included requirements.txt for project dependencies.

chore: add framework detection for Python projects

- Implemented framework detection logic to identify frameworks like Flask, FastAPI, Django, and others based on project files and import patterns.
- Enhanced the AST analyzer to recognize entry points based on decorators and function definitions.
This commit is contained in:
master
2025-12-19 18:11:59 +02:00
parent 951a38d561
commit 8779e9226f
130 changed files with 19011 additions and 422 deletions

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Router.Common.Models;
@@ -12,6 +13,7 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
private readonly StellaMicroserviceOptions _options;
private readonly ILogger<GeneratedEndpointDiscoveryProvider> _logger;
private readonly ReflectionEndpointDiscoveryProvider _reflectionFallback;
private readonly IServiceProviderIsService? _serviceProviderIsService;
private const string GeneratedProviderTypeName = "StellaOps.Microservice.Generated.GeneratedEndpointProvider";
@@ -20,11 +22,16 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
/// </summary>
public GeneratedEndpointDiscoveryProvider(
StellaMicroserviceOptions options,
ILogger<GeneratedEndpointDiscoveryProvider> logger)
ILogger<GeneratedEndpointDiscoveryProvider> logger,
IServiceProviderIsService? serviceProviderIsService = null)
{
_options = options;
_logger = logger;
_reflectionFallback = new ReflectionEndpointDiscoveryProvider(options);
_serviceProviderIsService = serviceProviderIsService;
_reflectionFallback = new ReflectionEndpointDiscoveryProvider(
options,
assemblies: null,
serviceProviderIsService: serviceProviderIsService);
}
/// <inheritdoc />
@@ -65,33 +72,38 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
{
try
{
// Look in the entry assembly first
var entryAssembly = Assembly.GetEntryAssembly();
var providerType = entryAssembly?.GetType(GeneratedProviderTypeName);
if (providerType != null)
static Type? GetProviderType(Assembly? assembly)
{
return (IGeneratedEndpointProvider)Activator.CreateInstance(providerType)!;
if (assembly is null) return null;
var type = assembly.GetType(GeneratedProviderTypeName);
if (type is null) return null;
return typeof(IGeneratedEndpointProvider).IsAssignableFrom(type) ? type : null;
}
// Also check the calling assembly
var callingAssembly = Assembly.GetCallingAssembly();
providerType = callingAssembly.GetType(GeneratedProviderTypeName);
var providerTypes = new List<Type>();
if (providerType != null)
// Look in the entry and calling assemblies first.
var entryProviderType = GetProviderType(Assembly.GetEntryAssembly());
if (entryProviderType is not null)
{
return (IGeneratedEndpointProvider)Activator.CreateInstance(providerType)!;
providerTypes.Add(entryProviderType);
}
// Check all loaded assemblies
var callingProviderType = GetProviderType(Assembly.GetCallingAssembly());
if (callingProviderType is not null)
{
providerTypes.Add(callingProviderType);
}
// Check all loaded assemblies (integration tests may host multiple microservices in one process).
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
providerType = assembly.GetType(GeneratedProviderTypeName);
if (providerType != null)
var providerType = GetProviderType(assembly);
if (providerType is not null)
{
return (IGeneratedEndpointProvider)Activator.CreateInstance(providerType)!;
providerTypes.Add(providerType);
}
}
catch
@@ -99,6 +111,78 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
// Ignore assembly loading errors
}
}
providerTypes = providerTypes.Distinct().ToList();
if (providerTypes.Count == 0)
{
return null;
}
// If multiple providers exist (e.g., test process hosts several microservices), pick the provider that
// best matches the currently-registered handler types in DI.
if (_serviceProviderIsService is not null && providerTypes.Count > 1)
{
IGeneratedEndpointProvider? best = null;
var bestScore = 0;
foreach (var providerType in providerTypes)
{
IGeneratedEndpointProvider? providerInstance;
try
{
providerInstance =
(IGeneratedEndpointProvider?)Activator.CreateInstance(providerType, nonPublic: true);
}
catch
{
continue;
}
if (providerInstance is null)
{
continue;
}
var score = 0;
try
{
foreach (var handlerType in providerInstance.GetHandlerTypes())
{
if (_serviceProviderIsService.IsService(handlerType))
{
score++;
}
}
}
catch
{
score = 0;
}
if (score > bestScore)
{
bestScore = score;
best = providerInstance;
}
}
if (best is not null && bestScore > 0)
{
_logger.LogDebug(
"Selected generated endpoint provider {ProviderType} (matched {MatchCount} handlers)",
best.GetType().FullName,
bestScore);
return best;
}
}
// Deterministic fallback: choose the provider type with the lowest assembly name.
var selectedProviderType = providerTypes
.OrderBy(t => t.Assembly.GetName().Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(t => t.FullName, StringComparer.Ordinal)
.First();
return (IGeneratedEndpointProvider)Activator.CreateInstance(selectedProviderType, nonPublic: true)!;
}
catch (Exception ex)
{

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Router.Common.Models;
namespace StellaOps.Microservice;
@@ -10,16 +11,21 @@ public sealed class ReflectionEndpointDiscoveryProvider : IEndpointDiscoveryProv
{
private readonly StellaMicroserviceOptions _options;
private readonly IEnumerable<Assembly> _assemblies;
private readonly IServiceProviderIsService? _serviceProviderIsService;
/// <summary>
/// Initializes a new instance of the <see cref="ReflectionEndpointDiscoveryProvider"/> class.
/// </summary>
/// <param name="options">The microservice options.</param>
/// <param name="assemblies">The assemblies to scan for endpoints.</param>
public ReflectionEndpointDiscoveryProvider(StellaMicroserviceOptions options, IEnumerable<Assembly>? assemblies = null)
public ReflectionEndpointDiscoveryProvider(
StellaMicroserviceOptions options,
IEnumerable<Assembly>? assemblies = null,
IServiceProviderIsService? serviceProviderIsService = null)
{
_options = options;
_assemblies = assemblies ?? AppDomain.CurrentDomain.GetAssemblies();
_serviceProviderIsService = serviceProviderIsService;
}
/// <inheritdoc />
@@ -42,6 +48,11 @@ public sealed class ReflectionEndpointDiscoveryProvider : IEndpointDiscoveryProv
$"Type {type.FullName} has [StellaEndpoint] but does not implement IStellaEndpoint.");
}
if (_serviceProviderIsService is not null && !_serviceProviderIsService.IsService(type))
{
continue;
}
var claims = attribute.RequiredClaims
.Select(c => new ClaimRequirement { Type = c })
.ToList();
@@ -54,7 +65,8 @@ public sealed class ReflectionEndpointDiscoveryProvider : IEndpointDiscoveryProv
Path = attribute.Path,
DefaultTimeout = TimeSpan.FromSeconds(attribute.TimeoutSeconds),
SupportsStreaming = attribute.SupportsStreaming,
RequiringClaims = claims
RequiringClaims = claims,
HandlerType = type
};
endpoints.Add(descriptor);