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

@@ -0,0 +1,164 @@
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Router.Common.Frames;
using StellaOps.Router.Common.Models;
using Xunit;
namespace StellaOps.Microservice.Tests;
public sealed class RequestDispatcherTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
[Fact]
public async Task DispatchAsync_WhenEndpointNotFound_Returns404()
{
var registry = new EndpointRegistry();
var services = new ServiceCollection();
using var provider = services.BuildServiceProvider();
var dispatcher = new RequestDispatcher(
registry,
provider,
CreateLogger<RequestDispatcher>());
var response = await dispatcher.DispatchAsync(
new RequestFrame
{
RequestId = "req-404",
Method = "GET",
Path = "/missing",
Payload = ReadOnlyMemory<byte>.Empty
},
CancellationToken.None);
response.StatusCode.Should().Be(404);
Encoding.UTF8.GetString(response.Payload.Span).Should().Be("Not Found");
}
[Fact]
public async Task DispatchAsync_WhenBodyEmpty_BindsFromPathAndQueryParameters()
{
var registry = new EndpointRegistry();
registry.Register(new EndpointDescriptor
{
ServiceName = "inventory",
Version = "1.0.0",
Method = "GET",
Path = "/items/{id}",
HandlerType = typeof(GetItemHandler)
});
var services = new ServiceCollection();
services.AddTransient<GetItemHandler>();
using var provider = services.BuildServiceProvider();
var dispatcher = new RequestDispatcher(
registry,
provider,
CreateLogger<RequestDispatcher>(),
jsonOptions: JsonOptions);
var response = await dispatcher.DispatchAsync(
new RequestFrame
{
RequestId = "req-params",
Method = "GET",
Path = "/items/123?filter=active",
Payload = ReadOnlyMemory<byte>.Empty
},
CancellationToken.None);
response.StatusCode.Should().Be(200);
response.Headers.Should().ContainKey("Content-Type");
var dto = JsonSerializer.Deserialize<GetItemResponse>(response.Payload.Span, JsonOptions);
dto.Should().NotBeNull();
dto!.Id.Should().Be(123);
dto.Filter.Should().Be("active");
}
[Fact]
public async Task DispatchAsync_WhenBodyPresent_PathAndQueryOverrideJsonProperties()
{
var registry = new EndpointRegistry();
registry.Register(new EndpointDescriptor
{
ServiceName = "inventory",
Version = "1.0.0",
Method = "POST",
Path = "/items/{id}",
HandlerType = typeof(GetItemHandler)
});
var services = new ServiceCollection();
services.AddTransient<GetItemHandler>();
using var provider = services.BuildServiceProvider();
var dispatcher = new RequestDispatcher(
registry,
provider,
CreateLogger<RequestDispatcher>(),
jsonOptions: JsonOptions);
var body = JsonSerializer.SerializeToUtf8Bytes(
new GetItemRequest { Id = 999, Filter = "fromBody" },
JsonOptions);
var response = await dispatcher.DispatchAsync(
new RequestFrame
{
RequestId = "req-body",
Method = "POST",
Path = "/items/123?filter=active",
Payload = body
},
CancellationToken.None);
response.StatusCode.Should().Be(200);
var dto = JsonSerializer.Deserialize<GetItemResponse>(response.Payload.Span, JsonOptions);
dto.Should().NotBeNull();
dto!.Id.Should().Be(123);
dto.Filter.Should().Be("active");
}
private static ILogger<T> CreateLogger<T>()
{
var factory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
return factory.CreateLogger<T>();
}
private sealed class GetItemRequest
{
public int Id { get; set; }
public string? Filter { get; set; }
}
private sealed class GetItemResponse
{
public int Id { get; set; }
public string? Filter { get; set; }
}
private sealed class GetItemHandler : IStellaEndpoint<GetItemRequest, GetItemResponse>
{
public Task<GetItemResponse> HandleAsync(GetItemRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new GetItemResponse
{
Id = request.Id,
Filter = request.Filter
});
}
}
}