feat(resolution): bridge MediatR Send/Publish to its IRequestHandler.Handle
MediatR decouples a _mediator.Send(x)/.Publish(x) call from the Handle method that runs it, linked by the request/notification TYPE (the IRequestHandler<X,…> generic), usually across files in a Clean Architecture layout — so flows dead-end at the mediator call and the agent reads to find the handler. mediatrDispatchEdges bridges each dispatch -> the matching handler's Handle. Same two-pass, type-keyed shape as the Spring synthesizer, with two C#-specific twists found by probing: - C# method nodes carry NO signature (csharp.ts defines no getSignature), so Pass 1 reads the request type from the handler CLASS base-list source (`: IRequestHandler<X,…>` first generic arg) and binds the class's Handle. - The dominant .NET idiom is VARIABLE-passed, not inline `Send(new X)` — eShop has zero genuine inline MediatR sends. So Pass 2 resolves the sent type from the argument three ways within the enclosing method: inline `new X(…)`, a local `var v = new X(…)` (backward scan), or a parameter/local declared `X v`. Two precision gates: the receiver must be mediator-ish (mediator/sender/ publisher — excludes MAUI MessagingCenter.Send, HttpClient.Send) AND the resolved type must have a handler (so a same-named non-request DTO is never bridged). Handles the IdentifiedCommand<T,R> wrapper and void IRequestHandler<T>. Surfaces as `dynamic: mediatr dispatch` via the generic synth-edge fallback. Validated 100% precision on two grep-confirmed repos: jasontaylordev/ CleanArchitecture (small, 9 edges, inline + param forms) and dotnet/eShop (medium, 9 edges, 0 false positives, variable-passed + IdentifiedCommand + the CancelOrderCommand DTO-collision correctly avoided); 0 on the Newtonsoft.Json control. Node-stable (pure edge synth). Suite 1619 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8591ea5993
commit
d1381e11f6
@@ -17,6 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- `codegraph_explore` now connects React data-fetching flows built on **RTK Query** (Redux Toolkit's `createApi`). An endpoint defined inside `createApi({ endpoints })` and the `useGetXQuery` / `useUpdateYMutation` hook it generates were both invisible to analysis — so "what does this component fetch?" or "where does `useGetThingQuery` get its data?" dead-ended, because the hook, the endpoint, and the component had nothing linking them. CodeGraph now indexes each endpoint and each generated hook as real symbols and wires the path `component → useGetXQuery → getX → queryFn`, so the flow resolves in one explore call instead of reading the API slice by hand. Both the arrow (`endpoints: build => ({ … })`) and method (`endpoints(builder) { return { … } }`) styles are recognized, along with the `useLazyGetXQuery` variant; hand-written hooks of a similar name are left untouched.
|
||||
- `codegraph_explore` now follows **Celery** task dispatch in Python. A `send_email.delay(...)` or `send_email.apply_async(...)` call now links to the `@shared_task` / `@app.task` function it runs — typically defined in a different module (`tasks.py`) from where it's triggered (a view or service) — so "what actually happens when this is dispatched?" traces from the call site straight into the task body instead of stopping at the `.delay()` line. Both decorator dialects are recognized (bare `@shared_task` and the arg'd `@app.task(bind=True, …)` form), including the module-qualified `tasks.invalidate_cache.apply_async()` call style. It stays precise: a `.delay()` on something that isn't a Celery task is never mislinked, so a project that doesn't use Celery is unaffected.
|
||||
- `codegraph_explore` now follows **Spring application events** in Java. A `publishEvent(new OrderShippedEvent(...))` call now links to every `@EventListener` that handles that event — usually in a different class — so "what reacts when this is published?" traces from the publisher straight into each listener method instead of dead-ending at `publishEvent(...)`. The link is by event type, and all the common listener styles are recognized: a `@EventListener` typed on its parameter, the `@EventListener(SomeEvent.class)` form, `@TransactionalEventListener`, and the older `implements ApplicationListener<SomeEvent>`. One event fans out to all its listeners, and a plain Spring app with no event bus is unaffected.
|
||||
- `codegraph_explore` now follows **MediatR** request and notification dispatch in C#/.NET. A `_mediator.Send(command)` or `_mediator.Publish(notification)` call now links to the `Handle` method of the matching `IRequestHandler<>` / `INotificationHandler<>` — usually in a different file in a Clean Architecture layout — so "what handles this command?" traces from the controller straight into the handler instead of stopping at the mediator call. The sent type is recognized whether it's constructed inline (`Send(new GetFooQuery())`), built into a local first (`var cmd = new …; Send(cmd)`), or passed in as a parameter, and it's matched by type — so a `MessagingCenter.Send(...)` or a same-named DTO that isn't a request is never mislinked, and a project without MediatR is unaffected.
|
||||
|
||||
- `codegraph_explore` now surfaces the right code in large multi-layer projects. When you ask a backend-flow question in a repo that pairs an API server with a big frontend that mirrors the same domain words — say an `app/` admin UI sitting over an `api/` server — the server-side file that genuinely matches several of your query's terms is no longer pushed out of the results by the larger, more interconnected frontend layer. A file corroborated by two or more distinct query terms is now kept in the answer even when a denser unrelated layer would otherwise crowd it out, so "how does X read items / handle the request" returns the service or handler that does the work instead of a wall of frontend views. Single-layer projects are unaffected; set `CODEGRAPH_RANK_NO_MULTITERM=1` to revert to the previous ranking.
|
||||
- Impact and blast-radius analysis for TypeScript, JavaScript, Go, Python, Rust, Ruby, C, Java, C#, PHP, Scala, Kotlin, Swift, Dart, and Pascal/Delphi now understands the readers of a constant. When you change a file-scope, package-level, module-level, or class-level constant — a config object, a lookup table, a shared constant — the other symbols in that file that read it now show up as affected, where before they were invisible (impact only followed calls, imports, and inheritance, so a constant's consumers looked like "nothing depends on this"). This makes `codegraph impact`, and the impact trail in `codegraph_explore`/`codegraph_node`, catch the "change this table, break its readers" class of change. It's on by default and adds no nodes to your graph; bundled/minified files and ambiguously-shadowed names are skipped to keep results precise. Set `CODEGRAPH_VALUE_REFS=0` to turn it off.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* MediatR request/notification dispatch bridge (C#/.NET).
|
||||
*
|
||||
* MediatR decouples a `_mediator.Send(x)` / `_mediator.Publish(x)` call from the `Handle`
|
||||
* method that runs it, linked by the request/notification TYPE (the `IRequestHandler<T,…>`
|
||||
* generic). This bridges each mediator dispatch → the `Handle` of the matching handler.
|
||||
* The sent type is resolved from the argument three ways — inline `new X(...)`, a local
|
||||
* `var v = new X(...)`, and a parameter/local declared `X v` — and precision rests on two
|
||||
* gates proven here: the receiver must be mediator-ish (a `MessagingCenter.Send` is ignored),
|
||||
* and the type must have a handler (an `IRequest` with no handler is never bridged). Covers
|
||||
* `IRequest<T>`, void `IRequest` (single-arg `IRequestHandler<T>`), and `INotification`.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
describe('mediatr-dispatch synthesizer', () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mediatr-dispatch-')); });
|
||||
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
const write = (rel: string, body: string) => {
|
||||
const p = path.join(dir, rel);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, body);
|
||||
};
|
||||
|
||||
it('bridges Send/Publish to the matching Handle across inline, local, and param arg forms', async () => {
|
||||
write('Requests.cs', `namespace Shop;
|
||||
using MediatR;
|
||||
public record GetThingsQuery : IRequest<ThingsVm>;
|
||||
public record CreateThingCommand(string Name) : IRequest<int>;
|
||||
public record DeleteThingCommand(int Id) : IRequest;
|
||||
public record ThingDeletedNotification(int Id) : INotification;
|
||||
public class UnhandledCommand : IRequest<int> { }
|
||||
`);
|
||||
write('Handlers.cs', `namespace Shop;
|
||||
using MediatR;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
public class GetThingsQueryHandler : IRequestHandler<GetThingsQuery, ThingsVm> {
|
||||
public Task<ThingsVm> Handle(GetThingsQuery request, CancellationToken ct) => Task.FromResult(new ThingsVm());
|
||||
}
|
||||
public class CreateThingCommandHandler : IRequestHandler<CreateThingCommand, int> {
|
||||
public Task<int> Handle(CreateThingCommand request, CancellationToken ct) => Task.FromResult(1);
|
||||
}
|
||||
public class DeleteThingCommandHandler : IRequestHandler<DeleteThingCommand> {
|
||||
public Task Handle(DeleteThingCommand request, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
public class ThingDeletedNotificationHandler : INotificationHandler<ThingDeletedNotification> {
|
||||
public Task Handle(ThingDeletedNotification notification, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
`);
|
||||
write('ThingsController.cs', `namespace Shop;
|
||||
using MediatR;
|
||||
using System.Threading.Tasks;
|
||||
public class ThingsController {
|
||||
private readonly ISender _mediator;
|
||||
public ThingsController(ISender mediator) { _mediator = mediator; }
|
||||
|
||||
public async Task GetThings() {
|
||||
var vm = await _mediator.Send(new GetThingsQuery());
|
||||
}
|
||||
public async Task Create(CreateThingCommand command) {
|
||||
var id = await _mediator.Send(command);
|
||||
}
|
||||
public async Task Delete(int id) {
|
||||
var command = new DeleteThingCommand(id);
|
||||
await _mediator.Send(command);
|
||||
}
|
||||
public async Task Notify(int id) {
|
||||
await _mediator.Publish(new ThingDeletedNotification(id));
|
||||
}
|
||||
public async Task Bogus() {
|
||||
await _mediator.Send(new UnhandledCommand());
|
||||
}
|
||||
public void ViaMessagingCenter() {
|
||||
MessagingCenter.Send(this, "evt", new CreateThingCommand("x"));
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const db = (cg as any).db.db;
|
||||
|
||||
const edges = db
|
||||
.prepare(
|
||||
`SELECT s.name source, t.name target, json_extract(e.metadata,'$.via') via
|
||||
FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
|
||||
WHERE json_extract(e.metadata,'$.synthesizedBy') = 'mediatr-dispatch'`
|
||||
)
|
||||
.all();
|
||||
|
||||
// Four bridged dispatches: inline (GetThings, Notify), param-typed (Create), local var (Delete).
|
||||
expect(edges.map((r: any) => r.source).sort()).toEqual(['Create', 'Delete', 'GetThings', 'Notify']);
|
||||
expect([...new Set(edges.map((r: any) => r.via))].sort()).toEqual([
|
||||
'CreateThingCommand', 'DeleteThingCommand', 'GetThingsQuery', 'ThingDeletedNotification',
|
||||
]);
|
||||
// Every target is a Handle method.
|
||||
expect(edges.every((r: any) => r.target === 'Handle')).toBe(true);
|
||||
// PRECISION: an IRequest with no handler is never bridged; a non-mediator .Send is ignored.
|
||||
expect(edges.some((r: any) => r.via === 'UnhandledCommand')).toBe(false);
|
||||
expect(edges.some((r: any) => r.source === 'ViaMessagingCenter')).toBe(false);
|
||||
|
||||
cg.close?.();
|
||||
});
|
||||
|
||||
it('produces no edges in a C# project with no MediatR (clean control)', async () => {
|
||||
write('Service.cs', `namespace Shop;
|
||||
public class Service {
|
||||
private readonly IRepo _repo;
|
||||
public Service(IRepo repo) { _repo = repo; }
|
||||
public string Find(string id) => _repo.Get(id);
|
||||
}
|
||||
`);
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const db = (cg as any).db.db;
|
||||
const count = db
|
||||
.prepare(`SELECT count(*) c FROM edges WHERE json_extract(metadata,'$.synthesizedBy') = 'mediatr-dispatch'`)
|
||||
.get();
|
||||
expect(count.c).toBe(0);
|
||||
cg.close?.();
|
||||
});
|
||||
});
|
||||
@@ -63,7 +63,7 @@ Status legend (matches the playbook): ✅ done+validated · 🟡 shipped but und
|
||||
|
||||
| Shape | Ecosystem | Anchor | Mechanism | Status |
|
||||
|---|---|---|---|---|
|
||||
| **MediatR / CQRS** | .NET | `IRequest<T>` → `IRequestHandler<TReq,T>` by the generic request type; `_mediator.Send(new GetFooQuery())` → handler | S (generic-type-keyed) | 🔬 named a frontier in CLAUDE.md, but it's statically keyable via the generic — worth a real attempt |
|
||||
| **MediatR / CQRS** | .NET | `_mediator.Send(x)`/`.Publish(x)` → the `Handle` of `IRequestHandler<X,…>`/`INotificationHandler<X>` by request type | S (type-keyed, 2-pass + arg resolution) | ✅ **SHIPPED (2026-06-20)** — `synthesizedBy:'mediatr-dispatch'` (`mediatrDispatchEdges`). Same 2-pass type-keyed shape as Spring, with a twist: **C# method nodes have NO `signature`** (csharp.ts defines no `getSignature`), so Pass 1 reads the request type from the handler **class base-list source** (`: IRequestHandler<X,…>` — first generic arg), not a param signature, and binds the class's `Handle` method. **The dominant .NET idiom is VARIABLE-passed, not inline** — eShop had **0** genuine inline `Send(new X)` (every send is `mediator.Send(command)`), so Pass 2 RESOLVES the sent type from the argument three ways within the enclosing method: inline `new X(…)`, a local `var v = new X(…)` (backward scan, wins), or a parameter/local declared `X v`. **Two precision gates:** (1) receiver must be mediator-ish (`/mediator|sender|publisher/i` — excludes MAUI `MessagingCenter.Send`, `HttpClient.Send`), (2) resolved type must be in the handler map (so eShop's same-named `CancelOrderCommand` DTO in ClientApp, which has no handler, is never bridged). Handles the `IdentifiedCommand<T,R>` wrapper (sent + handled at that layer) and void single-arg `IRequestHandler<T>`. **100% precision: jasontaylordev/CleanArchitecture (small, 9 edges, inline + param forms) + dotnet/eShop (medium, 9 edges, 0 FP, variable-passed + IdentifiedCommand + DTO-collision avoided); 0 on the Newtonsoft.Json control.** Node-stable (pure edge synth). Surfaces `dynamic: mediatr dispatch`. `+ mediatr-dispatch-synthesizer.test.ts`. **Deferred (recall):** generic `_mediator.Publish(domainEvent)` over a collection (concrete type erased at the publish site — eShop's DDD AddDomainEvent fan-out), `record`-positional or factory-built args whose type isn't a `new X`/param, the `ICommandHandler<T>` facade indirection (modular-monolith). |
|
||||
| **Celery** | Python | `@shared_task`/`@app.task`/`@<app>.task`/`@task` def + `.delay()`/`.apply_async()` call → task body | S (decorator-gated name) | ✅ **SHIPPED (2026-06-20)** — `synthesizedBy:'celery-dispatch'` (`celeryDispatchEdges`). Link the enclosing fn at each `.delay(`/`.apply_async(` site → the task fn. Precision rests on the DECORATOR gate: the dispatched name must resolve to a Python `function` carrying a task decorator, read from the source lines ABOVE its `def` (the def's own startLine excludes the decorator; no `decorates` edge exists — `@shared_task` is an unresolved external import). `kind==='function'` filter drops the same-named test-method collision (`consume_file`). Canvas forms (`group(t).delay()`, `t.s()`/`.si()`) have no single identifier before `.delay` → skipped, not mis-bridged. Cross-module name collision → same-file preference else bail. **100% precision: paperless-ngx (small, `@shared_task`, 31 edges, 31/31 real), pretix (medium, `@app.task`, 63 edges across 21 tasks, 0/21 FP); 0 on the httpie control (no Celery).** Node-stable (pure edge synth, no extraction change). Surfaces as `dynamic: celery dispatch @site` via the generic fallback. `+ celery-dispatch-synthesizer.test.ts`. **Deferred (recall):** canvas dispatch, class-based `Task` subclasses, `app.send_task('dotted.name')` string dispatch, aliased imports (`import send_email as s; s.delay()`). |
|
||||
| **Sidekiq** | Ruby | `class W; include Sidekiq::Job; def perform; end` + `W.perform_async(...)` → `perform` | S (class→perform) | ⬜ — the Ruby sibling of Celery; build next-to-it (grep-confirm ≥2 `perform_async` repos). |
|
||||
| **Spring events** | Java | `publishEvent(new XEvent(…))` → `@EventListener`/`@TransactionalEventListener`/`ApplicationListener<X>` by event type | S (type-keyed, 2-pass) | ✅ **SHIPPED (2026-06-20)** — `synthesizedBy:'spring-event'` (`springEventEdges`). Pass 1 builds `Map<eventType, listenerMethod[]>` — listeners are `@EventListener`/`@TransactionalEventListener` methods (event type = the first param type off the node `signature`, or the `@EventListener(X.class)` value form) + `class … implements ApplicationListener<X>` `onApplicationEvent` methods (name + file `ApplicationListener<` gate). Pass 2 links each `publishEvent(new XEvent(…))` site's enclosing method → every listener of XEvent. **KEY Java fact:** a method node's range INCLUDES its leading annotations (`startLine` = first `@…` line, NOT the `public void` decl), so the annotation gate scans DOWNWARD from startLine, bounded to consecutive `@`-lines (no bleed into an adjacent method). Keyed by EXACT type name, no name resolution — precision is structural (param type ↔ `new X` type). Multi-line `publishEvent(\n new X(…))` handled (`\s*` spans newlines). **100% precision: halo (medium, 1254 java, 33 edges across 24 events, 0 publisher/listener FP, all 3 listener forms + fan-out) + thombergs/code-examples (4 edges incl. the `@TransactionalEventListener` form halo lacks); 0 on the gson control (no Spring).** Node-stable (pure edge synth). Surfaces `dynamic: spring event @site`. `+ spring-event-synthesizer.test.ts`. **Deferred (recall):** `publishEvent(bareVar)` (needs the var's declared type), Spring's listener-return-value re-publish, `@DomainEvents`/`AbstractAggregateRoot.registerEvent`, generic `PayloadApplicationEvent<X>` params. |
|
||||
@@ -88,6 +88,7 @@ Status legend (matches the playbook): ✅ done+validated · 🟡 shipped but und
|
||||
| Vuex dispatch | `vuex-dispatch` | ✅ **shipped (2026-06-20)** — string `dispatch('ns/action')`/`commit('M')` → handler; 100% precision element-admin (55) / vue-admin-template (12) / d2-admin (63), 0 on Redux controls. |
|
||||
| Celery | `celery-dispatch` | ✅ **shipped (2026-06-20)** — `.delay()`/`.apply_async()` → `@shared_task`/`@app.task` body; 100% precision paperless-ngx (31) / pretix (63 across 21 tasks), 0 on httpie control. Decorator-gated via source above the `def`. |
|
||||
| Spring events | `spring-event` | ✅ **shipped (2026-06-20)** — `publishEvent(new XEvent)` → `@EventListener`/`@TransactionalEventListener`/`ApplicationListener<X>` by event type; 100% precision halo (33 across 24 events) / code-examples (4), 0 on gson control. Type-keyed 2-pass, no name resolution. |
|
||||
| MediatR | `mediatr-dispatch` | ✅ **shipped (2026-06-20)** — `_mediator.Send(x)`/`.Publish(x)` → the `Handle` of `IRequestHandler<X>`/`INotificationHandler<X>` by request type; 100% precision jasontaylor (9) / eShop (9, variable-passed), 0 on Newtonsoft control. Type from class base-list (C# has no signature) + arg resolved inline/local/param; receiver + handler-map gates. |
|
||||
| (see playbook §6 / `callback-synthesizer.ts` for the other ~20 channels) | | |
|
||||
|
||||
### redux-thunk follow-ups (found by the n>1 validation — this is exactly what it's for)
|
||||
|
||||
@@ -2301,13 +2301,128 @@ function springEventEdges(ctx: ResolutionContext): Edge[] {
|
||||
return edges;
|
||||
}
|
||||
|
||||
// ── MediatR request/notification dispatch (C#/.NET) ───────────────────────────
|
||||
// MediatR decouples a Send/Publish call site from its Handle method through a mediator,
|
||||
// linked by the request/notification TYPE (the IRequestHandler<T,…> generic):
|
||||
// // CancelOrderCommandHandler.cs — the handler
|
||||
// public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, bool> {
|
||||
// public async Task<bool> Handle(CancelOrderCommand request, CancellationToken ct) { … }
|
||||
// // some controller — the dispatch (usually a DIFFERENT file)
|
||||
// var command = new CancelOrderCommand(orderId); await _mediator.Send(command);
|
||||
// Bridge it: link the enclosing method at each mediator `.Send(x)`/`.Publish(x)` site → the
|
||||
// `Handle` method of the handler for x's type. The sent type is resolved from the argument —
|
||||
// inline `new X(…)`, a local `var v = new X(…)`, or a parameter/local declared `X v` — bounded
|
||||
// to the enclosing method. Precision rests on TWO gates: the receiver must be mediator-ish
|
||||
// (`mediator`/`sender`/`publisher`, so MAUI `MessagingCenter.Send` is ignored) AND the resolved
|
||||
// type must be a known handler request type (so a same-named non-request DTO is never bridged).
|
||||
// C# has no `signature` on method nodes, so the handler's request type is read from the class
|
||||
// base-list source (`: IRequestHandler<X,…>`), not a param signature.
|
||||
const MEDIATR_HANDLER_BASE_RE = /(?:IRequestHandler|INotificationHandler)\s*<\s*([A-Za-z_]\w*)/;
|
||||
const MEDIATR_DISPATCH_RE = /([A-Za-z_][\w.]*)\s*\.\s*(?:Send|Publish)\s*\(\s*(new\s+[A-Z]\w*|[A-Za-z_]\w*)/g;
|
||||
const MEDIATR_RECEIVER_RE = /(?:mediator|sender|publisher)/i;
|
||||
const MEDIATR_CS_EXT = /\.cs$/;
|
||||
const MEDIATR_FANOUT_CAP = 80;
|
||||
const MEDIATR_HANDLER_DECL_LOOKAHEAD = 4; // lines from a class startLine to find a wrapped base list
|
||||
|
||||
/** The type sent at a MediatR `.Send(arg)`/`.Publish(arg)` site: an inline `new X(…)`, else
|
||||
* `arg` as an identifier resolved within the enclosing method — a `… arg = new X(…)` assignment
|
||||
* (wins), or a parameter/local declared `X arg`. Returns null when the type can't be seen. */
|
||||
function resolveMediatrArgType(arg: string, lines: string[], methodStart: number, dispatchLine: number): string | null {
|
||||
const inl = /^new\s+([A-Z]\w*)/.exec(arg);
|
||||
if (inl) return inl[1]!;
|
||||
if (!/^[A-Za-z_]\w*$/.test(arg)) return null;
|
||||
const assignRe = new RegExp(`\\b${arg}\\b\\s*=\\s*new\\s+([A-Z]\\w*)`);
|
||||
const declRe = new RegExp(`\\b([A-Z]\\w*)\\b\\s+${arg}\\b`);
|
||||
let declType: string | null = null;
|
||||
for (let i = Math.max(0, methodStart - 1); i < dispatchLine && i < lines.length; i++) {
|
||||
const ln = lines[i] ?? '';
|
||||
const a = assignRe.exec(ln);
|
||||
if (a) return a[1]!; // an explicit `arg = new X` is the most specific — take it
|
||||
if (!declType) {
|
||||
const d = declRe.exec(ln);
|
||||
if (d) declType = d[1]!; // a `X arg` declaration — remember, but keep scanning for an assignment
|
||||
}
|
||||
}
|
||||
return declType;
|
||||
}
|
||||
|
||||
function mediatrDispatchEdges(ctx: ResolutionContext): Edge[] {
|
||||
// Pass 1 — request/notification type → the Handle method of each handler class.
|
||||
const handlers = new Map<string, Node[]>();
|
||||
for (const file of ctx.getAllFiles()) {
|
||||
if (!MEDIATR_CS_EXT.test(file)) continue;
|
||||
const content = ctx.readFile(file);
|
||||
if (!content || (!content.includes('IRequestHandler<') && !content.includes('INotificationHandler<'))) continue;
|
||||
const lines = content.split('\n');
|
||||
const nodesInFile = ctx.getNodesInFile(file);
|
||||
for (const cls of nodesInFile) {
|
||||
if (cls.kind !== 'class') continue;
|
||||
const decl = lines.slice(cls.startLine - 1, cls.startLine - 1 + MEDIATR_HANDLER_DECL_LOOKAHEAD).join('\n');
|
||||
const m = MEDIATR_HANDLER_BASE_RE.exec(decl);
|
||||
if (!m) continue;
|
||||
const type = m[1]!;
|
||||
const end = cls.endLine ?? cls.startLine;
|
||||
const handle = nodesInFile.find(
|
||||
(n) => n.kind === 'method' && n.name === 'Handle' && n.startLine >= cls.startLine && n.startLine <= end
|
||||
);
|
||||
if (!handle) continue;
|
||||
let arr = handlers.get(type);
|
||||
if (!arr) { arr = []; handlers.set(type, arr); }
|
||||
arr.push(handle);
|
||||
}
|
||||
}
|
||||
if (!handlers.size) return [];
|
||||
|
||||
// Pass 2 — link each mediator-ish .Send(x)/.Publish(x) → the handler of x's type.
|
||||
const edges: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const file of ctx.getAllFiles()) {
|
||||
if (!MEDIATR_CS_EXT.test(file)) continue;
|
||||
const content = ctx.readFile(file);
|
||||
if (!content || (!content.includes('.Send(') && !content.includes('.Publish('))) continue;
|
||||
const safe = stripCommentsForRegex(content, 'csharp');
|
||||
const safeLines = safe.split('\n');
|
||||
const nodesInFile = ctx.getNodesInFile(file);
|
||||
MEDIATR_DISPATCH_RE.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
let added = 0;
|
||||
while ((m = MEDIATR_DISPATCH_RE.exec(safe)) && added < MEDIATR_FANOUT_CAP) {
|
||||
if (!MEDIATR_RECEIVER_RE.test(m[1]!)) continue; // not a mediator (MessagingCenter, HttpClient, …)
|
||||
const line = safe.slice(0, m.index).split('\n').length;
|
||||
const disp = enclosingFn(nodesInFile, line);
|
||||
if (!disp) continue;
|
||||
const type = resolveMediatrArgType(m[2]!, safeLines, disp.startLine, line);
|
||||
if (!type) continue;
|
||||
const targets = handlers.get(type);
|
||||
if (!targets) continue;
|
||||
for (const target of targets) {
|
||||
if (target.id === disp.id) continue;
|
||||
const key = `${disp.id}>${target.id}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
edges.push({
|
||||
source: disp.id,
|
||||
target: target.id,
|
||||
kind: 'calls',
|
||||
line,
|
||||
provenance: 'heuristic',
|
||||
metadata: { synthesizedBy: 'mediatr-dispatch', via: type, registeredAt: `${file}:${line}` },
|
||||
});
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
||||
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
||||
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
|
||||
* Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
|
||||
* generated-hook → endpoint + Pinia useStore().action() + Vuex string dispatch +
|
||||
* Celery task .delay()/.apply_async() → task body + Spring publishEvent → @EventListener).
|
||||
* Celery task .delay()/.apply_async() → task body + Spring publishEvent → @EventListener +
|
||||
* MediatR Send/Publish → IRequestHandler/INotificationHandler).
|
||||
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
||||
*/
|
||||
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
||||
@@ -2352,6 +2467,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
||||
const vuexEdges = vuexDispatchEdges(ctx);
|
||||
const celeryEdges = celeryDispatchEdges(ctx);
|
||||
const springEdges = springEventEdges(ctx);
|
||||
const mediatrEdges = mediatrDispatchEdges(ctx);
|
||||
|
||||
const merged: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -2382,6 +2498,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
||||
...vuexEdges,
|
||||
...celeryEdges,
|
||||
...springEdges,
|
||||
...mediatrEdges,
|
||||
]) {
|
||||
const key = `${e.source}>${e.target}`;
|
||||
if (seen.has(key)) continue;
|
||||
|
||||
Reference in New Issue
Block a user