vouchington-tooling 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -16
- package/dist/api-contract-discovery/header-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/protocol-body-status.d.mts +2 -0
- package/dist/api-contract-discovery/protocol-body-status.mjs +4 -0
- package/dist/api-contract-discovery/protocol-callback-argument-bindings.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-callback-argument-bindings.mjs +33 -0
- package/dist/api-contract-discovery/protocol-callback-invocation.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-callback-invocation.mjs +103 -0
- package/dist/api-contract-discovery/protocol-callback-mutations.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-callback-mutations.mjs +42 -0
- package/dist/api-contract-discovery/protocol-callback-registration.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-callback-registration.mjs +27 -0
- package/dist/api-contract-discovery/protocol-callback-values.d.mts +17 -0
- package/dist/api-contract-discovery/protocol-callback-values.mjs +114 -0
- package/dist/api-contract-discovery/protocol-context-alias.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-context-alias.mjs +20 -0
- package/dist/api-contract-discovery/protocol-contract-extraction.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-contract-extraction.mjs +87 -0
- package/dist/api-contract-discovery/protocol-contract-keys.d.mts +1 -0
- package/dist/api-contract-discovery/protocol-contract-keys.mjs +8 -0
- package/dist/api-contract-discovery/protocol-contract-registry.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-contract-registry.mjs +133 -0
- package/dist/api-contract-discovery/protocol-executable-path.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-executable-path.mjs +64 -0
- package/dist/api-contract-discovery/protocol-execution-path.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-execution-path.mjs +16 -0
- package/dist/api-contract-discovery/protocol-http-association.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-http-association.mjs +97 -0
- package/dist/api-contract-discovery/protocol-http-body-kinds.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-body-kinds.mjs +19 -0
- package/dist/api-contract-discovery/protocol-http-context-callers.d.mts +6 -0
- package/dist/api-contract-discovery/protocol-http-context-callers.mjs +34 -0
- package/dist/api-contract-discovery/protocol-http-context.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-http-context.mjs +108 -0
- package/dist/api-contract-discovery/protocol-http-emission.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-emission.mjs +76 -0
- package/dist/api-contract-discovery/protocol-http-implicit-taint.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-http-implicit-taint.mjs +26 -0
- package/dist/api-contract-discovery/protocol-http-registration.d.mts +12 -0
- package/dist/api-contract-discovery/protocol-http-registration.mjs +53 -0
- package/dist/api-contract-discovery/protocol-marker-analysis.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-marker-analysis.mjs +50 -0
- package/dist/api-contract-discovery/protocol-opaque-callback.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-opaque-callback.mjs +107 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +72 -0
- package/dist/api-contract-discovery/protocol-platform-mutation-targets.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-platform-mutation-targets.mjs +89 -0
- package/dist/api-contract-discovery/protocol-platform-writes.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-platform-writes.mjs +92 -0
- package/dist/api-contract-discovery/protocol-requested-keys.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-requested-keys.mjs +45 -0
- package/dist/api-contract-discovery/protocol-sse-callers.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-sse-callers.mjs +67 -0
- package/dist/api-contract-discovery/protocol-sse-emission.d.mts +11 -0
- package/dist/api-contract-discovery/protocol-sse-emission.mjs +61 -0
- package/dist/api-contract-discovery/protocol-sse-feasible-status.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-sse-feasible-status.mjs +24 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +70 -0
- package/dist/api-contract-discovery/protocol-sse-status.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-sse-status.mjs +84 -0
- package/dist/api-contract-discovery/protocol-sse-write-access.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-write-access.mjs +42 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +6 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +66 -0
- package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +36 -0
- package/dist/api-contract-discovery/protocol-sse-write-resolution.d.mts +14 -0
- package/dist/api-contract-discovery/protocol-sse-write-resolution.mjs +79 -0
- package/dist/api-contract-discovery/protocol-status-dominance.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-status-dominance.mjs +35 -0
- package/dist/api-contract-discovery/protocol-write-receiver.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-write-receiver.mjs +79 -0
- package/dist/api-contract-discovery/query-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/registered-route-binding-writes.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-binding-writes.mjs +30 -0
- package/dist/api-contract-discovery/registered-route-catalog.mjs +11 -7
- package/dist/api-contract-discovery/registered-route-factory-returns.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-factory-returns.mjs +29 -0
- package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +5 -1
- package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +79 -35
- package/dist/api-contract-discovery/registered-route-runtime-parameters.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-runtime-parameters.mjs +5 -0
- package/dist/api-contract-discovery/registered-route-terminal-error.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-terminal-error.mjs +98 -0
- package/dist/api-contract-discovery/request-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/response-contract-implicit.mjs +28 -10
- package/dist/api-contract-discovery/response-contract-registry.mjs +14 -1
- package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +1 -1
- package/dist/api-contract-discovery/response-contract-route-analysis.mjs +22 -4
- package/dist/api-contract-discovery/response-contract-status.d.mts +1 -1
- package/dist/api-contract-discovery/response-contract-status.mjs +10 -8
- package/dist/api-contract-discovery/response-contract-types.d.mts +7 -0
- package/dist/cli/index.mjs +0 -3
- package/dist/cli/parse-forwarded-commands.d.mts +0 -3
- package/dist/cli/parse-forwarded-commands.mjs +0 -2
- package/dist/cli/parse.d.mts +0 -3
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +0 -5
- package/dist/contract-schema/contract-schema-bounded-array.d.mts +1 -3
- package/dist/contract-schema/contract-schema-bounded-array.mjs +5 -0
- package/dist/contract-schema/contract-schema-object-tuple.mjs +7 -5
- package/dist/contract-schema/contract-schema-type-extractor.mjs +13 -0
- package/dist/contract-schema/contract-schema-validator.mjs +2 -2
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/openapi-document/build-openapi-catalog-sse.d.mts +4 -0
- package/dist/openapi-document/build-openapi-catalog-sse.mjs +27 -0
- package/dist/openapi-document/build-openapi-document.mjs +5 -2
- package/dist/openapi-document/build-openapi-response.d.mts +2 -2
- package/dist/openapi-document/build-openapi-response.mjs +33 -3
- package/dist/openapi-document/build-openapi-sse-events.d.mts +11 -0
- package/dist/openapi-document/build-openapi-sse-events.mjs +46 -0
- package/dist/openapi-document/contract-schema-intersection-merge.mjs +11 -11
- package/dist/openapi-document/contract-schema-to-openapi.mjs +3 -3
- package/dist/openapi-document/openapi-route-helpers.mjs +1 -3
- package/dist/openapi-document/openapi-types.d.mts +3 -0
- package/dist/openapi-document/operation-types.d.mts +7 -0
- package/package.json +3 -8
- package/dist/agent-harness-config/apply.d.mts +0 -4
- package/dist/agent-harness-config/apply.mjs +0 -72
- package/dist/agent-harness-config/cli.d.mts +0 -10
- package/dist/agent-harness-config/cli.mjs +0 -100
- package/dist/agent-harness-config/index.d.mts +0 -4
- package/dist/agent-harness-config/index.mjs +0 -3
- package/dist/agent-harness-config/merge-json.d.mts +0 -5
- package/dist/agent-harness-config/merge-json.mjs +0 -62
- package/dist/agent-harness-config/merge-toml.d.mts +0 -6
- package/dist/agent-harness-config/merge-toml.mjs +0 -132
- package/dist/agent-harness-config/paths.d.mts +0 -3
- package/dist/agent-harness-config/paths.mjs +0 -117
- package/dist/agent-harness-config/policy.d.mts +0 -14
- package/dist/agent-harness-config/policy.mjs +0 -51
- package/dist/agent-harness-config/prerequisites.d.mts +0 -2
- package/dist/agent-harness-config/prerequisites.mjs +0 -67
- package/dist/agent-harness-config/toml-value.d.mts +0 -6
- package/dist/agent-harness-config/toml-value.mjs +0 -69
- package/dist/agent-harness-config/types.d.mts +0 -90
- package/dist/agent-harness-config/types.mjs +0 -2
- package/docs/agent-harness-config.md +0 -134
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
|
|
3
|
+
import { isSupportedProtocolCallback } from './protocol-callback-invocation.mjs';
|
|
4
|
+
import { potentiallyExecuted } from './protocol-executable-path.mjs';
|
|
5
|
+
import { enclosingFunction } from './protocol-marker-analysis.mjs';
|
|
6
|
+
import { sseCallerScope } from './protocol-sse-callers.mjs';
|
|
7
|
+
import { resolveSseStatus } from './protocol-sse-status.mjs';
|
|
8
|
+
import { writeReceiver } from './protocol-write-receiver.mjs';
|
|
9
|
+
export function sseEmission(call, checker, calls, binding, bindings) {
|
|
10
|
+
const write = call.parent;
|
|
11
|
+
if (!ts.isCallExpression(write) ||
|
|
12
|
+
!ts.isPropertyAccessExpression(write.expression) ||
|
|
13
|
+
write.expression.name.text !== 'write' ||
|
|
14
|
+
write.arguments[0] !== call)
|
|
15
|
+
throw new Error('apiSseFrame must be the frame written by its route');
|
|
16
|
+
const receiver = writeReceiver(write, checker);
|
|
17
|
+
if (!receiver)
|
|
18
|
+
throw new Error('SSE frame requires a bound stream receiver');
|
|
19
|
+
if (receiver.mutableAlias)
|
|
20
|
+
throw new Error('SSE frame uses a mutable stream alias');
|
|
21
|
+
let fn = enclosingFunction(call);
|
|
22
|
+
let context;
|
|
23
|
+
const callbacks = [];
|
|
24
|
+
const functions = [];
|
|
25
|
+
while (fn) {
|
|
26
|
+
functions.push(fn);
|
|
27
|
+
if (!potentiallyExecuted(fn) || !potentiallyExecuted(call))
|
|
28
|
+
throw new Error('SSE frame is not on an executable callback path');
|
|
29
|
+
if (fn.asteriskToken)
|
|
30
|
+
throw new Error('SSE frame is in a deferred generator');
|
|
31
|
+
if (!isSupportedProtocolCallback(fn, checker))
|
|
32
|
+
throw new Error('SSE frame is in an uninvoked callback');
|
|
33
|
+
if (ts.isFunctionDeclaration(fn)) {
|
|
34
|
+
const name = runtimeParameters(fn)[0]?.name;
|
|
35
|
+
if (name && ts.isIdentifier(name))
|
|
36
|
+
context = checker.getSymbolAtLocation(name);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
let value = fn;
|
|
40
|
+
while (ts.isPropertyAssignment(value.parent) || ts.isObjectLiteralExpression(value.parent))
|
|
41
|
+
value = value.parent;
|
|
42
|
+
if (ts.isCallExpression(value.parent) || ts.isNewExpression(value.parent))
|
|
43
|
+
callbacks.push(value.parent);
|
|
44
|
+
if ((ts.isCallExpression(value.parent) &&
|
|
45
|
+
ts.isPropertyAccessExpression(value.parent.expression) &&
|
|
46
|
+
['get', 'post', 'put', 'patch', 'delete'].includes(value.parent.expression.name.text)) ||
|
|
47
|
+
(ts.isVariableDeclaration(fn.parent) && !enclosingFunction(fn))) {
|
|
48
|
+
const name = runtimeParameters(fn)[0]?.name;
|
|
49
|
+
if (name && ts.isIdentifier(name))
|
|
50
|
+
context = checker.getSymbolAtLocation(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
fn = enclosingFunction(fn);
|
|
54
|
+
}
|
|
55
|
+
const caller = sseCallerScope(functions, calls, binding, bindings, checker);
|
|
56
|
+
const anchors = [write, ...callbacks];
|
|
57
|
+
const paths = caller ? caller.paths.map((path) => [...anchors, ...path]) : [anchors];
|
|
58
|
+
const contexts = caller?.contexts ?? new Set(context ? [context] : []);
|
|
59
|
+
const status = resolveSseStatus(paths, [...new Set([...functions, ...(caller?.functions ?? [])])], contexts, checker);
|
|
60
|
+
return { write, receiver, status };
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
/** Excludes a sibling branch or a statement that cannot precede this anchor. */
|
|
3
|
+
export function statusCanPrecede(setter, anchor) {
|
|
4
|
+
const ancestors = new Set();
|
|
5
|
+
for (let node = anchor; node; node = node.parent)
|
|
6
|
+
ancestors.add(node);
|
|
7
|
+
for (let node = setter; node.parent; node = node.parent) {
|
|
8
|
+
const parent = node.parent;
|
|
9
|
+
if (ts.isIfStatement(parent) && parent.elseStatement) {
|
|
10
|
+
const opposite = parent.thenStatement === node ? parent.elseStatement : parent.thenStatement;
|
|
11
|
+
if (ancestors.has(opposite))
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (ts.isBlock(parent) && ancestors.has(parent)) {
|
|
15
|
+
let statement = anchor;
|
|
16
|
+
while (statement.parent !== parent)
|
|
17
|
+
statement = statement.parent;
|
|
18
|
+
if (parent.statements.indexOf(node) >
|
|
19
|
+
parent.statements.indexOf(statement))
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type HandlerBindings, type RouteBinding } from './response-contract-route-analysis.mts';
|
|
3
|
+
import { type WriteReceiver } from './protocol-write-receiver.mts';
|
|
4
|
+
export type SseRouteWrites = {
|
|
5
|
+
receivers: WriteReceiver[];
|
|
6
|
+
keys: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Reports unmarked writes on executable or opaque escaping paths to a selected frame's stream. */
|
|
9
|
+
export declare function rejectRawSseWrites(files: readonly ts.SourceFile[], checker: ts.TypeChecker, bindings: HandlerBindings, framedWrites: ReadonlySet<ts.CallExpression>, routes: ReadonlyMap<string, SseRouteWrites>, reject: (node: ts.CallExpression, binding: RouteBinding, keys: readonly string[]) => void): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { potentiallyExecuted } from './protocol-executable-path.mjs';
|
|
3
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
4
|
+
import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
|
|
5
|
+
import { enclosingRouteBinding, visit, } from './response-contract-route-analysis.mjs';
|
|
6
|
+
import { expressionReceiver, sameWriteReceiver, } from './protocol-write-receiver.mjs';
|
|
7
|
+
import { sseWriteInvocation } from './protocol-sse-write-access.mjs';
|
|
8
|
+
import { isSourceLevelMutation, mutationAffectsSelectedStream, sseWriteMutation, } from './protocol-sse-write-mutations.mjs';
|
|
9
|
+
import { actualReceivers, helperBindings, routeKey } from './protocol-sse-write-helpers.mjs';
|
|
10
|
+
/** Reports unmarked writes on executable or opaque escaping paths to a selected frame's stream. */
|
|
11
|
+
export function rejectRawSseWrites(files, checker, bindings, framedWrites, routes, reject) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const mutations = [];
|
|
14
|
+
for (const file of files)
|
|
15
|
+
visit(file, (node) => {
|
|
16
|
+
if (ts.isCallExpression(node))
|
|
17
|
+
calls.push(node);
|
|
18
|
+
const mutation = sseWriteMutation(node);
|
|
19
|
+
if (mutation)
|
|
20
|
+
mutations.push(mutation);
|
|
21
|
+
});
|
|
22
|
+
for (const node of calls) {
|
|
23
|
+
if (framedWrites.has(node)) {
|
|
24
|
+
const binding = enclosingRouteBinding(node, checker, bindings, false);
|
|
25
|
+
const route = binding && routes.get(routeKey(binding));
|
|
26
|
+
const source = node.getSourceFile();
|
|
27
|
+
if (binding &&
|
|
28
|
+
route &&
|
|
29
|
+
mutationAffectsSelectedStream(mutations, route.receivers, checker, (mutationNode) => {
|
|
30
|
+
if (mutationNode.getSourceFile() === source && isSourceLevelMutation(mutationNode))
|
|
31
|
+
return true;
|
|
32
|
+
const candidate = enclosingRouteBinding(mutationNode, checker, bindings, false);
|
|
33
|
+
return !!candidate && routeKey(candidate) === routeKey(binding);
|
|
34
|
+
}, (expression) => {
|
|
35
|
+
const receiver = expressionReceiver(expression, checker);
|
|
36
|
+
return receiver && actualReceivers(receiver, binding, calls, checker, bindings);
|
|
37
|
+
}, (receiver) => actualReceivers(receiver, binding, calls, checker, bindings)))
|
|
38
|
+
reject(node, binding, route.keys);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const access = sseWriteInvocation(node, checker);
|
|
42
|
+
if (!access || !access.rawBytes || !potentiallyExecuted(node))
|
|
43
|
+
continue;
|
|
44
|
+
const proven = executableProtocolPath(node, checker) || opaqueProtocolCallbackPath(node, checker);
|
|
45
|
+
const helpers = helperBindings(node, calls, checker, bindings);
|
|
46
|
+
if (!proven && !helpers.length)
|
|
47
|
+
continue;
|
|
48
|
+
const binding = proven ? enclosingRouteBinding(node, checker, bindings, false) : undefined;
|
|
49
|
+
const candidates = binding ? [binding] : helpers;
|
|
50
|
+
for (const candidate of candidates) {
|
|
51
|
+
const route = routes.get(routeKey(candidate));
|
|
52
|
+
if (!route)
|
|
53
|
+
continue;
|
|
54
|
+
if (!access.receiver) {
|
|
55
|
+
reject(node, candidate, route.keys);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const receiver = expressionReceiver(access.receiver, checker);
|
|
59
|
+
if (!receiver) {
|
|
60
|
+
reject(node, candidate, route.keys);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const actual = actualReceivers(receiver, candidate, calls, checker, bindings);
|
|
64
|
+
const framed = route.receivers.flatMap((value) => actualReceivers(value, candidate, calls, checker, bindings));
|
|
65
|
+
if (actual.some((value) => value === undefined ||
|
|
66
|
+
framed.some((frame) => frame === undefined || sameWriteReceiver(frame, value))))
|
|
67
|
+
reject(node, candidate, route.keys);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { resolveEmissionStatus } from './response-contract-status.mts';
|
|
3
|
+
export declare function resolveSseStatus(paths: readonly (readonly (ts.CallExpression | ts.NewExpression)[])[], functions: readonly ts.FunctionLikeDeclaration[], contexts: ReadonlySet<ts.Symbol>, checker: ts.TypeChecker): ReturnType<typeof resolveEmissionStatus>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
3
|
+
import { unsupportedContextAlias, unsupportedContextAssignment } from './protocol-context-alias.mjs';
|
|
4
|
+
import { contextResponseMethod } from './protocol-http-emission.mjs';
|
|
5
|
+
import { enclosingFunction, unwrapExpression } from './protocol-marker-analysis.mjs';
|
|
6
|
+
import { expressionReceiver } from './protocol-write-receiver.mjs';
|
|
7
|
+
import { statusCanPrecede } from './protocol-sse-feasible-status.mjs';
|
|
8
|
+
import { statusDominatesEmission, unconditionalStatusSetter } from './protocol-status-dominance.mjs';
|
|
9
|
+
import { visit } from './response-contract-route-analysis.mjs';
|
|
10
|
+
import { resolveEmissionStatus } from './response-contract-status.mjs';
|
|
11
|
+
import { statusForbidsBody } from './protocol-body-status.mjs';
|
|
12
|
+
export function resolveSseStatus(paths, functions, contexts, checker) {
|
|
13
|
+
const matches = (expression) => [...contexts].some((context) => contextResponseMethod(expression, context, checker) === 'setStatus');
|
|
14
|
+
const setters = new Set();
|
|
15
|
+
for (const fn of functions)
|
|
16
|
+
visit(fn, (node) => {
|
|
17
|
+
if (!executableProtocolPath(node, checker))
|
|
18
|
+
return;
|
|
19
|
+
if ([...contexts].some((context) => (ts.isVariableDeclaration(node) && unsupportedContextAlias(node, context, checker)) ||
|
|
20
|
+
unsupportedContextAssignment(node, context, checker)))
|
|
21
|
+
throw new Error('SSE context has an unsupported mutable or destructured alias');
|
|
22
|
+
if (ts.isCallExpression(node) && indirectSetter(node.expression, contexts, checker))
|
|
23
|
+
throw new Error('SSE context uses an unsupported indirect status setter');
|
|
24
|
+
if (ts.isCallExpression(node) && matches(node.expression))
|
|
25
|
+
setters.add(node);
|
|
26
|
+
});
|
|
27
|
+
const statuses = paths.map((path) => {
|
|
28
|
+
for (const setter of setters) {
|
|
29
|
+
const anchors = path.filter((anchor) => enclosingFunction(anchor) === enclosingFunction(setter));
|
|
30
|
+
if ((!anchors.length || anchors.some((anchor) => statusCanPrecede(setter, anchor))) &&
|
|
31
|
+
!anchors.some((anchor) => statusDominatesEmission(anchor, new Set([setter]))))
|
|
32
|
+
throw new Error('SSE status does not dominate its frame emission');
|
|
33
|
+
}
|
|
34
|
+
for (const anchor of path) {
|
|
35
|
+
const setter = nearestSetter(anchor, setters);
|
|
36
|
+
if (setter) {
|
|
37
|
+
const status = resolveEmissionStatus(setter, matches);
|
|
38
|
+
if (status.unavailableReason)
|
|
39
|
+
throw new Error(status.unavailableReason);
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { statusKnowledge: 'default' };
|
|
44
|
+
});
|
|
45
|
+
if (statuses.every((status) => status.statusKnowledge === 'default'))
|
|
46
|
+
return { statusKnowledge: 'default' };
|
|
47
|
+
const codes = [...new Set(statuses.flatMap((status) => status.statusCodes ?? [200]))].toSorted((a, b) => a - b);
|
|
48
|
+
if (codes.some((code) => !Number.isInteger(code) || code < 100 || code > 599))
|
|
49
|
+
throw new Error('SSE status must be an integer from 100 through 599');
|
|
50
|
+
if (codes.some(statusForbidsBody))
|
|
51
|
+
throw new Error('SSE HTTP status forbids response content');
|
|
52
|
+
return { statusKnowledge: 'explicit', statusCodes: codes };
|
|
53
|
+
}
|
|
54
|
+
function indirectSetter(expression, contexts, checker) {
|
|
55
|
+
const access = unwrapExpression(expression);
|
|
56
|
+
const name = ts.isPropertyAccessExpression(access)
|
|
57
|
+
? access.name.text
|
|
58
|
+
: ts.isElementAccessExpression(access) && ts.isStringLiteral(access.argumentExpression)
|
|
59
|
+
? access.argumentExpression.text
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!name || !['call', 'apply', 'bind'].includes(name))
|
|
62
|
+
return false;
|
|
63
|
+
const target = access.expression;
|
|
64
|
+
if ([...contexts].some((context) => contextResponseMethod(target, context, checker) === 'setStatus'))
|
|
65
|
+
return true;
|
|
66
|
+
const receiver = expressionReceiver(target, checker);
|
|
67
|
+
return !!receiver && contexts.has(receiver.root) && receiver.path[0] === 'setStatus';
|
|
68
|
+
}
|
|
69
|
+
function nearestSetter(anchor, setters) {
|
|
70
|
+
let current = anchor;
|
|
71
|
+
while (current.parent && !ts.isFunctionLike(current.parent)) {
|
|
72
|
+
const parent = current.parent;
|
|
73
|
+
if (ts.isBlock(parent)) {
|
|
74
|
+
const index = parent.statements.indexOf(current);
|
|
75
|
+
for (const statement of parent.statements.slice(0, index).toReversed()) {
|
|
76
|
+
const setter = unconditionalStatusSetter(statement, setters);
|
|
77
|
+
if (setter)
|
|
78
|
+
return setter;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
current = parent;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
type WriteInvocation = {
|
|
3
|
+
receiver: ts.Expression | undefined;
|
|
4
|
+
method: string | undefined;
|
|
5
|
+
rawBytes: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Resolves direct and indirect SSE writes without losing their actual receiver. */
|
|
8
|
+
export declare function sseWriteInvocation(call: ts.CallExpression, checker: ts.TypeChecker): WriteInvocation | undefined;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { hasRawBytes, indirectPayload, reflectApplyInvocation, writeAccess, } from './protocol-sse-write-resolution.mjs';
|
|
3
|
+
/** Resolves direct and indirect SSE writes without losing their actual receiver. */
|
|
4
|
+
export function sseWriteInvocation(call, checker) {
|
|
5
|
+
const reflected = reflectApplyInvocation(call, checker);
|
|
6
|
+
if (reflected)
|
|
7
|
+
return reflected;
|
|
8
|
+
const access = writeAccess(call.expression);
|
|
9
|
+
if (!access)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (access.method === 'pipe')
|
|
12
|
+
return call.arguments[0]
|
|
13
|
+
? { receiver: call.arguments[0], method: 'write', rawBytes: true }
|
|
14
|
+
: undefined;
|
|
15
|
+
if (access.method === 'bind') {
|
|
16
|
+
const target = writeAccess(access.receiver);
|
|
17
|
+
if (target?.method !== 'write' && target?.method !== 'end')
|
|
18
|
+
return undefined;
|
|
19
|
+
return { receiver: call.arguments[0], method: target.method, rawBytes: true };
|
|
20
|
+
}
|
|
21
|
+
if (access.method !== 'call' && access.method !== 'apply')
|
|
22
|
+
return { ...access, rawBytes: hasRawBytes(access.method, call.arguments, checker, false) };
|
|
23
|
+
const target = writeAccess(access.receiver);
|
|
24
|
+
const receiver = call.arguments[0];
|
|
25
|
+
if (!receiver)
|
|
26
|
+
return undefined;
|
|
27
|
+
const payload = indirectPayload(access.method, call, checker);
|
|
28
|
+
if (target?.method === 'pipe') {
|
|
29
|
+
const destination = payload?.[0];
|
|
30
|
+
return {
|
|
31
|
+
receiver: destination,
|
|
32
|
+
method: 'write',
|
|
33
|
+
rawBytes: payload === undefined || destination !== undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const method = target?.method === 'write' || target?.method === 'end' ? target.method : undefined;
|
|
37
|
+
return {
|
|
38
|
+
receiver,
|
|
39
|
+
method,
|
|
40
|
+
rawBytes: payload === undefined || hasRawBytes(method, payload, checker, true),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type HandlerBindings, type RouteBinding } from './response-contract-route-analysis.mts';
|
|
3
|
+
import { type WriteReceiver } from './protocol-write-receiver.mts';
|
|
4
|
+
export declare function routeKey(binding: RouteBinding): string;
|
|
5
|
+
export declare function helperBindings(node: ts.Node, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings): RouteBinding[];
|
|
6
|
+
export declare function actualReceivers(receiver: WriteReceiver, binding: RouteBinding, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings, active?: Set<ts.Symbol>): (WriteReceiver | undefined)[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { hasBindingWrite } from './registered-route-binding-writes.mjs';
|
|
3
|
+
import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
|
|
4
|
+
import { enclosingFunction } from './protocol-marker-analysis.mjs';
|
|
5
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
6
|
+
import { enclosingRouteBinding, } from './response-contract-route-analysis.mjs';
|
|
7
|
+
import { expressionReceiver } from './protocol-write-receiver.mjs';
|
|
8
|
+
export function routeKey(binding) {
|
|
9
|
+
return `${binding.method}:${binding.routeTemplate}`;
|
|
10
|
+
}
|
|
11
|
+
function implementationDeclaration(declaration, checker) {
|
|
12
|
+
if (!declaration)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration))
|
|
15
|
+
return declaration;
|
|
16
|
+
if (ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)) {
|
|
17
|
+
if (declaration.body)
|
|
18
|
+
return declaration;
|
|
19
|
+
const symbol = declaration.name && checker.getSymbolAtLocation(declaration.name);
|
|
20
|
+
return symbol?.declarations?.find((candidate) => (ts.isFunctionDeclaration(candidate) || ts.isMethodDeclaration(candidate)) &&
|
|
21
|
+
!!candidate.body);
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function helperBindings(node, calls, checker, bindings) {
|
|
26
|
+
const fn = enclosingFunction(node);
|
|
27
|
+
if (!fn || fn.asteriskToken)
|
|
28
|
+
return [];
|
|
29
|
+
return calls.flatMap((call) => {
|
|
30
|
+
if (implementationDeclaration(checker.getResolvedSignature(call)?.declaration, checker) !== fn ||
|
|
31
|
+
!executableProtocolPath(call, checker))
|
|
32
|
+
return [];
|
|
33
|
+
const binding = enclosingRouteBinding(call, checker, bindings, false);
|
|
34
|
+
return binding ? [binding] : [];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function actualReceivers(receiver, binding, calls, checker, bindings, active = new Set()) {
|
|
38
|
+
const declaration = receiver.root.valueDeclaration;
|
|
39
|
+
if (receiver.mutableAlias ||
|
|
40
|
+
(declaration &&
|
|
41
|
+
(ts.isBindingElement(declaration) ||
|
|
42
|
+
(ts.isVariableDeclaration(declaration) &&
|
|
43
|
+
!(declaration.parent.flags & ts.NodeFlags.Const)) ||
|
|
44
|
+
(ts.isParameter(declaration) && hasBindingWrite(declaration, checker)))))
|
|
45
|
+
return [undefined];
|
|
46
|
+
const fn = declaration && enclosingFunction(declaration);
|
|
47
|
+
if (!declaration || !ts.isParameter(declaration) || !fn)
|
|
48
|
+
return [receiver];
|
|
49
|
+
if (active.has(receiver.root))
|
|
50
|
+
return [undefined];
|
|
51
|
+
const index = runtimeParameters(fn).indexOf(declaration);
|
|
52
|
+
const actuals = calls.flatMap((call) => {
|
|
53
|
+
if (implementationDeclaration(checker.getResolvedSignature(call)?.declaration, checker) !== fn ||
|
|
54
|
+
!executableProtocolPath(call, checker))
|
|
55
|
+
return [];
|
|
56
|
+
const callBinding = enclosingRouteBinding(call, checker, bindings, false);
|
|
57
|
+
if (!callBinding || routeKey(callBinding) !== routeKey(binding))
|
|
58
|
+
return [];
|
|
59
|
+
const argument = call.arguments[index] ?? declaration.initializer;
|
|
60
|
+
const actual = argument && expressionReceiver(argument, checker);
|
|
61
|
+
if (!actual || receiver.path.length)
|
|
62
|
+
return [undefined];
|
|
63
|
+
return actualReceivers(actual, binding, calls, checker, bindings, new Set(active).add(receiver.root));
|
|
64
|
+
});
|
|
65
|
+
return actuals.length ? actuals : [receiver];
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type WriteReceiver } from './protocol-write-receiver.mts';
|
|
3
|
+
export type SseWriteMutation = {
|
|
4
|
+
node: ts.Node;
|
|
5
|
+
receiver: ts.Expression;
|
|
6
|
+
};
|
|
7
|
+
export declare function sseWriteMutation(node: ts.Node): SseWriteMutation | undefined;
|
|
8
|
+
export declare function mutationAffectsSelectedStream(mutations: readonly SseWriteMutation[], frameReceivers: readonly WriteReceiver[], checker: ts.TypeChecker, isSameRoute: (node: ts.Node) => boolean, resolveActual: (expression: ts.Expression) => readonly (WriteReceiver | undefined)[] | undefined, resolveFrame: (receiver: WriteReceiver) => readonly (WriteReceiver | undefined)[]): boolean;
|
|
9
|
+
export declare function isSourceLevelMutation(node: ts.Node): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
3
|
+
import { sameWriteReceiver } from './protocol-write-receiver.mjs';
|
|
4
|
+
import { writeAccess } from './protocol-sse-write-resolution.mjs';
|
|
5
|
+
export function sseWriteMutation(node) {
|
|
6
|
+
if (ts.isBinaryExpression(node) && isAssignment(node.operatorToken.kind)) {
|
|
7
|
+
const access = writeAccess(node.left);
|
|
8
|
+
if (access && (access.method === 'write' || access.method === 'end'))
|
|
9
|
+
return { node, receiver: access.receiver };
|
|
10
|
+
}
|
|
11
|
+
if (ts.isDeleteExpression(node)) {
|
|
12
|
+
const access = writeAccess(node.expression);
|
|
13
|
+
if (access && (access.method === 'write' || access.method === 'end'))
|
|
14
|
+
return { node, receiver: access.receiver };
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
function isAssignment(kind) {
|
|
19
|
+
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
|
20
|
+
}
|
|
21
|
+
export function mutationAffectsSelectedStream(mutations, frameReceivers, checker, isSameRoute, resolveActual, resolveFrame) {
|
|
22
|
+
const framed = frameReceivers.flatMap(resolveFrame);
|
|
23
|
+
return mutations.some(({ node, receiver }) => {
|
|
24
|
+
if ((!isSourceLevelMutation(node) && !executableProtocolPath(node, checker)) ||
|
|
25
|
+
!isSameRoute(node))
|
|
26
|
+
return false;
|
|
27
|
+
const actual = resolveActual(receiver);
|
|
28
|
+
if (!actual)
|
|
29
|
+
return true;
|
|
30
|
+
return actual.some((value) => value === undefined ||
|
|
31
|
+
framed.some((frame) => frame === undefined || sameWriteReceiver(frame, value)));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function isSourceLevelMutation(node) {
|
|
35
|
+
return ts.isExpressionStatement(node.parent) && ts.isSourceFile(node.parent.parent);
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
export type WriteAccess = {
|
|
3
|
+
receiver: ts.Expression;
|
|
4
|
+
method: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
export type WriteInvocation = {
|
|
7
|
+
receiver: ts.Expression | undefined;
|
|
8
|
+
method: string | undefined;
|
|
9
|
+
rawBytes: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function writeAccess(expression: ts.Expression): WriteAccess | undefined;
|
|
12
|
+
export declare function reflectApplyInvocation(call: ts.CallExpression, checker: ts.TypeChecker): WriteInvocation | undefined;
|
|
13
|
+
export declare function indirectPayload(method: 'call' | 'apply', call: ts.CallExpression, checker: ts.TypeChecker): readonly ts.Expression[] | undefined;
|
|
14
|
+
export declare function hasRawBytes(method: string | undefined, payload: readonly ts.Expression[] | undefined, checker: ts.TypeChecker, indirect: boolean): boolean;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { unwrapExpression } from './protocol-marker-analysis.mjs';
|
|
3
|
+
export function writeAccess(expression) {
|
|
4
|
+
expression = unwrapExpression(expression);
|
|
5
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
6
|
+
return { receiver: expression.expression, method: expression.name.text };
|
|
7
|
+
if (!ts.isElementAccessExpression(expression))
|
|
8
|
+
return undefined;
|
|
9
|
+
const method = unwrapExpression(expression.argumentExpression);
|
|
10
|
+
return {
|
|
11
|
+
receiver: expression.expression,
|
|
12
|
+
method: ts.isStringLiteralLike(method) ? method.text : undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function reflectApplyInvocation(call, checker) {
|
|
16
|
+
const expression = unwrapExpression(call.expression);
|
|
17
|
+
if (!ts.isPropertyAccessExpression(expression) ||
|
|
18
|
+
expression.name.text !== 'apply' ||
|
|
19
|
+
!ts.isIdentifier(expression.expression) ||
|
|
20
|
+
expression.expression.text !== 'Reflect')
|
|
21
|
+
return undefined;
|
|
22
|
+
const defaultApply = checker
|
|
23
|
+
.getSymbolAtLocation(expression.name)
|
|
24
|
+
?.declarations?.some((declaration) => declaration.getSourceFile().fileName.endsWith('/lib.es2015.reflect.d.ts'));
|
|
25
|
+
if (!defaultApply)
|
|
26
|
+
return undefined;
|
|
27
|
+
const target = call.arguments[0] && writeAccess(call.arguments[0]);
|
|
28
|
+
const receiver = call.arguments[1];
|
|
29
|
+
const payload = applyPayload(call.arguments[2], checker);
|
|
30
|
+
if (target?.method === 'pipe') {
|
|
31
|
+
const destination = payload?.[0];
|
|
32
|
+
return {
|
|
33
|
+
receiver: destination,
|
|
34
|
+
method: 'write',
|
|
35
|
+
rawBytes: payload === undefined || destination !== undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const method = target?.method === 'write' || target?.method === 'end' ? target.method : undefined;
|
|
39
|
+
return {
|
|
40
|
+
receiver,
|
|
41
|
+
method,
|
|
42
|
+
rawBytes: payload === undefined || hasRawBytes(method, payload, checker, true),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function indirectPayload(method, call, checker) {
|
|
46
|
+
if (method === 'call')
|
|
47
|
+
return call.arguments.slice(1);
|
|
48
|
+
return applyPayload(call.arguments[1], checker);
|
|
49
|
+
}
|
|
50
|
+
function applyPayload(argument, checker) {
|
|
51
|
+
if (!argument)
|
|
52
|
+
return [];
|
|
53
|
+
const value = unwrapExpression(argument);
|
|
54
|
+
if (isNullish(argument, checker))
|
|
55
|
+
return [];
|
|
56
|
+
if (!ts.isArrayLiteralExpression(value) || value.elements.some(ts.isSpreadElement))
|
|
57
|
+
return undefined;
|
|
58
|
+
return value.elements.filter(ts.isExpression);
|
|
59
|
+
}
|
|
60
|
+
function isNullish(expression, checker) {
|
|
61
|
+
const value = unwrapExpression(expression);
|
|
62
|
+
if (value.kind === ts.SyntaxKind.NullKeyword)
|
|
63
|
+
return true;
|
|
64
|
+
if (ts.isIdentifier(value) && value.text === 'undefined')
|
|
65
|
+
return true;
|
|
66
|
+
const flags = checker.getTypeAtLocation(expression).flags;
|
|
67
|
+
return !!(flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null));
|
|
68
|
+
}
|
|
69
|
+
export function hasRawBytes(method, payload, checker, indirect) {
|
|
70
|
+
if (payload === undefined || method === undefined)
|
|
71
|
+
return true;
|
|
72
|
+
if (method === 'write')
|
|
73
|
+
return !indirect || payload.length > 0;
|
|
74
|
+
if (method !== 'end' || !payload[0])
|
|
75
|
+
return false;
|
|
76
|
+
const type = checker.getTypeAtLocation(payload[0]);
|
|
77
|
+
return (!(type.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) &&
|
|
78
|
+
type.getCallSignatures().length === 0);
|
|
79
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
/** Requires the last potentially preceding proven status setter to belong to this response. */
|
|
3
|
+
export declare function statusDominatesEmission(emission: ts.CallExpression | ts.NewExpression, statuses: ReadonlySet<ts.CallExpression>, allStatuses?: ReadonlySet<ts.CallExpression>): boolean;
|
|
4
|
+
/** Finds the last setter through unconditional blocks, excluding conditional compound statements. */
|
|
5
|
+
export declare function unconditionalStatusSetter(statement: ts.Statement, statuses: ReadonlySet<ts.CallExpression>): ts.CallExpression | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
/** Requires the last potentially preceding proven status setter to belong to this response. */
|
|
3
|
+
export function statusDominatesEmission(emission, statuses, allStatuses = statuses) {
|
|
4
|
+
let current = emission;
|
|
5
|
+
while (current.parent && !ts.isFunctionLike(current.parent)) {
|
|
6
|
+
const parent = current.parent;
|
|
7
|
+
if (ts.isBlock(parent)) {
|
|
8
|
+
const index = parent.statements.indexOf(current);
|
|
9
|
+
for (const statement of parent.statements.slice(0, index).toReversed())
|
|
10
|
+
if (containsStatus(statement, allStatuses)) {
|
|
11
|
+
const setter = unconditionalStatusSetter(statement, allStatuses);
|
|
12
|
+
return !!setter && statuses.has(setter);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
current = parent;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/** Finds the last setter through unconditional blocks, excluding conditional compound statements. */
|
|
20
|
+
export function unconditionalStatusSetter(statement, statuses) {
|
|
21
|
+
if (ts.isBlock(statement)) {
|
|
22
|
+
for (const child of statement.statements.toReversed())
|
|
23
|
+
if (containsStatus(child, statuses))
|
|
24
|
+
return unconditionalStatusSetter(child, statuses);
|
|
25
|
+
}
|
|
26
|
+
else if (ts.isExpressionStatement(statement) &&
|
|
27
|
+
ts.isCallExpression(statement.expression) &&
|
|
28
|
+
statuses.has(statement.expression))
|
|
29
|
+
return statement.expression;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function containsStatus(node, statuses) {
|
|
33
|
+
return ((ts.isCallExpression(node) && statuses.has(node)) ||
|
|
34
|
+
(ts.forEachChild(node, (child) => containsStatus(child, statuses)) ?? false));
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
export type WriteReceiver = {
|
|
3
|
+
root: ts.Symbol;
|
|
4
|
+
path: readonly string[];
|
|
5
|
+
mutableAlias?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function writeReceiver(call: ts.CallExpression, checker: ts.TypeChecker): WriteReceiver | undefined;
|
|
8
|
+
export declare function sameWriteReceiver(left: WriteReceiver, right: WriteReceiver | undefined): boolean;
|
|
9
|
+
export declare function expressionReceiver(expression: ts.Expression, checker: ts.TypeChecker, active?: Set<ts.Symbol>): WriteReceiver | undefined;
|