core/analyzers/orchestration.analyzer.ts
core/analyzers/orchestration.analyzer.ts is a file in GovLab Docs. 137 lines of code and 36 definitions.
import type { FunctionBody, OrchestrationFold, OrchestrationTarget, ProgramAnalysis } from "#types/code.types";
import type { DetectedProtocol } from "#types/graph.types";
import { firstDeclaration } from "#core/selectors/code.typescript.selector";
import { inPackageSources } from "#core/selectors/program.selector";
import ts from "typescript";
const MAX_MESSAGES = 50;
const MIN_PARTICIPANT_LEN = 3;
const MIN_ORCH_PARTICIPANTS = 2;
const MIN_ORCH_MESSAGES = 2;
const THIS = "this";
const foldTarget = function foldTarget(
fold: OrchestrationFold,
target: OrchestrationTarget,
fnName: string,
): OrchestrationFold {
const key = `${target.participant} ${target.method}`;
const skip =
target.participant === fnName ||
target.participant.length < MIN_PARTICIPANT_LEN ||
fold.msgKey.has(key) ||
fold.messages.length >= MAX_MESSAGES;
if (skip) {
return fold;
}
return {
messages: [...fold.messages, { async: true, text: target.method, to: target.participant }],
msgKey: new Set([...fold.msgKey, key]),
seen: new Set([...fold.seen, target.participant]),
};
};
const isNamedBinding = function isNamedBinding(node: ts.Node): node is ts.PropertyAssignment | ts.VariableDeclaration {
return (ts.isVariableDeclaration(node) || ts.isPropertyAssignment(node)) && ts.isIdentifier(node.name);
};
const boundFunction = function boundFunction(
node: ts.PropertyAssignment | ts.VariableDeclaration,
): FunctionBody | null {
const init = node.initializer;
if (!init || !(ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
return null;
}
return { body: init.body, name: node.name.getText() };
};
const functionBodyOf = function functionBodyOf(node: ts.Node): FunctionBody | null {
if (ts.isFunctionDeclaration(node) && node.name) {
return { body: node.body, name: node.name.text };
}
if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
return { body: node.body, name: node.name.text };
}
return isNamedBinding(node) ? boundFunction(node) : null;
};
const isTopLevelVariable = function isTopLevelVariable(decl: ts.Declaration): boolean {
return ts.isVariableDeclaration(decl) && ts.isSourceFile(decl.parent.parent.parent);
};
const isCallableDeclaration = function isCallableDeclaration(decl: ts.Declaration): boolean {
return (
ts.isFunctionDeclaration(decl) ||
ts.isImportSpecifier(decl) ||
ts.isImportClause(decl) ||
ts.isImportEqualsDeclaration(decl)
);
};
class ProtocolDetector {
readonly #checker: ts.TypeChecker;
public constructor(checker: ts.TypeChecker) {
this.#checker = checker;
}
public bestIn(node: ts.Node, best: DetectedProtocol | null): DetectedProtocol | null {
let current = this.#consider(node, best);
ts.forEachChild(node, (child) => {
current = this.bestIn(child, current);
});
return current;
}
#consider(node: ts.Node, best: DetectedProtocol | null): DetectedProtocol | null {
const fn = functionBodyOf(node);
if (!fn?.body || !ts.isBlock(fn.body)) {
return best;
}
const found = this.#orchestrationOf(fn.name, fn.body);
return found !== null && (best === null || found.messages.length > best.messages.length) ? found : best;
}
#orchestrationOf(fnName: string, body: ts.Block): DetectedProtocol | null {
const seed: OrchestrationFold = { messages: [], msgKey: new Set<string>(), seen: new Set<string>() };
const fold = this.#awaitTargets(body).reduce((acc, target) => foldTarget(acc, target, fnName), seed);
return fold.seen.size >= MIN_ORCH_PARTICIPANTS && fold.messages.length >= MIN_ORCH_MESSAGES
? { messages: fold.messages, participants: [...fold.seen], self: fnName }
: null;
}
#awaitTargets(body: ts.Block): OrchestrationTarget[] {
const out: OrchestrationTarget[] = [];
const visit = (child: ts.Node): void => {
const target = this.#targetOf(child);
if (target !== null) {
out.push(target);
}
ts.forEachChild(child, visit);
};
ts.forEachChild(body, visit);
return out;
}
#targetOf(child: ts.Node): OrchestrationTarget | null {
if (!ts.isAwaitExpression(child) || !ts.isCallExpression(child.expression)) {
return null;
}
return this.#awaitTarget(child.expression.expression);
}
#awaitTarget(callee: ts.Expression): OrchestrationTarget | null {
if (ts.isIdentifier(callee)) {
return this.#keepIdentifierCallee(callee) ? { method: callee.text, participant: callee.text } : null;
}
if (
ts.isPropertyAccessExpression(callee) &&
ts.isIdentifier(callee.expression) &&
callee.expression.text !== THIS &&
ts.isIdentifier(callee.name)
) {
return { method: callee.name.text, participant: callee.expression.text };
}
return null;
}
#keepIdentifierCallee(idNode: ts.Node): boolean {
const symbol = this.#checker.getSymbolAtLocation(idNode);
const decl = symbol ? firstDeclaration(symbol, this.#checker) : null;
if (decl === null || isCallableDeclaration(decl)) {
return true;
}
return isTopLevelVariable(decl);
}
}
export const detectProtocol = function detectProtocol(analysis: ProgramAnalysis): DetectedProtocol | null {
const detector = new ProtocolDetector(analysis.checker);
return inPackageSources(analysis.program, analysis.dirPosix).reduce<DetectedProtocol | null>(
(best, file) => detector.bestIn(file, best),
null,
);
};