@apifuse/provider-sdk 2.2.0-beta.53 → 2.2.0-beta.55
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/CHANGELOG.md +8 -0
- package/dist/cli/migrate-operation-declaration.d.ts +5 -1
- package/dist/cli/migrate-operation-declaration.js +645 -46
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/trace.d.ts +7 -0
- package/dist/runtime/trace.js +24 -1
- package/dist/server/serve-implementation.d.ts +4 -0
- package/dist/server/serve-implementation.js +576 -321
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +2 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +19 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +2 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +18 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +13 -0
- package/src/cli/migrate-operation-declaration.ts +844 -49
- package/src/runtime/auth-flow.ts +3 -0
- package/src/runtime/trace.ts +37 -1
- package/src/server/serve-implementation.ts +759 -586
- package/src/types.ts +1 -0
|
@@ -69,6 +69,8 @@ export type OperationDeclarationRefusalReason =
|
|
|
69
69
|
| "codemod_syntax"
|
|
70
70
|
| "missing_english_locale"
|
|
71
71
|
| "operation_id_unresolved"
|
|
72
|
+
| "factory_operation_id_ambiguous"
|
|
73
|
+
| "no_operations_discovered"
|
|
72
74
|
| "examples_conflict"
|
|
73
75
|
| "invalid_locale_key"
|
|
74
76
|
| "locale_todo_conflict";
|
|
@@ -122,6 +124,23 @@ type ResolvedMember = {
|
|
|
122
124
|
readonly property: TS.ObjectLiteralElementLike;
|
|
123
125
|
readonly initializer?: TS.Expression;
|
|
124
126
|
readonly fromSpread?: boolean;
|
|
127
|
+
readonly source: TS.SourceFile;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type StaticObjectReference = {
|
|
131
|
+
readonly object: TS.ObjectLiteralExpression;
|
|
132
|
+
readonly source: TS.SourceFile;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
type StaticObjectResolver = (
|
|
136
|
+
expression: TS.Expression,
|
|
137
|
+
source: TS.SourceFile,
|
|
138
|
+
) => StaticObjectReference | undefined;
|
|
139
|
+
|
|
140
|
+
type RepositoryMigrationContext = {
|
|
141
|
+
readonly operationSites?: ReadonlyMap<number, string>;
|
|
142
|
+
readonly excludedBindings?: ReadonlySet<string>;
|
|
143
|
+
readonly staticObjectResolver?: StaticObjectResolver;
|
|
125
144
|
};
|
|
126
145
|
|
|
127
146
|
type OperationSite = {
|
|
@@ -147,6 +166,15 @@ export function migrateOperationDeclaration(
|
|
|
147
166
|
sourceText: string,
|
|
148
167
|
fileName: string,
|
|
149
168
|
options: OperationDeclarationMigrationOptions = {},
|
|
169
|
+
): OperationDeclarationMigration {
|
|
170
|
+
return migrateOperationDeclarationInternal(sourceText, fileName, options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function migrateOperationDeclarationInternal(
|
|
174
|
+
sourceText: string,
|
|
175
|
+
fileName: string,
|
|
176
|
+
options: OperationDeclarationMigrationOptions,
|
|
177
|
+
context: RepositoryMigrationContext = {},
|
|
150
178
|
): OperationDeclarationMigration {
|
|
151
179
|
const source = parseSource(fileName, sourceText);
|
|
152
180
|
const parseError = firstSyntaxError(source);
|
|
@@ -159,7 +187,14 @@ export function migrateOperationDeclaration(
|
|
|
159
187
|
|
|
160
188
|
const constObjects = collectModuleConstObjects(source);
|
|
161
189
|
const constArrays = collectModuleConstArrays(source);
|
|
162
|
-
const discovery = discoverOperationSites(
|
|
190
|
+
const discovery = discoverOperationSites(
|
|
191
|
+
source,
|
|
192
|
+
fileName,
|
|
193
|
+
constObjects,
|
|
194
|
+
options.operationIds,
|
|
195
|
+
context.operationSites,
|
|
196
|
+
context.excludedBindings,
|
|
197
|
+
);
|
|
163
198
|
if (discovery.refusals.length > 0) {
|
|
164
199
|
return { status: "refused", refusals: discovery.refusals };
|
|
165
200
|
}
|
|
@@ -175,6 +210,7 @@ export function migrateOperationDeclaration(
|
|
|
175
210
|
constObjects,
|
|
176
211
|
constArrays,
|
|
177
212
|
options.localeFiles ?? [],
|
|
213
|
+
context.staticObjectResolver,
|
|
178
214
|
);
|
|
179
215
|
if ("refusal" in plan) {
|
|
180
216
|
refusals.push(plan.refusal);
|
|
@@ -241,6 +277,7 @@ function planOperationMigration(
|
|
|
241
277
|
constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
|
|
242
278
|
constArrays: ReadonlyMap<string, TS.ArrayLiteralExpression>,
|
|
243
279
|
localeFiles: readonly string[],
|
|
280
|
+
staticObjectResolver?: StaticObjectResolver,
|
|
244
281
|
): PlannedOperation | { readonly refusal: OperationDeclarationRefusal } {
|
|
245
282
|
if (site.object.properties.some(ts.isSpreadAssignment)) {
|
|
246
283
|
return {
|
|
@@ -252,7 +289,14 @@ function planOperationMigration(
|
|
|
252
289
|
),
|
|
253
290
|
};
|
|
254
291
|
}
|
|
255
|
-
const expanded = expandMembers(
|
|
292
|
+
const expanded = expandMembers(
|
|
293
|
+
site.object,
|
|
294
|
+
source,
|
|
295
|
+
constObjects,
|
|
296
|
+
fileName,
|
|
297
|
+
site.operationKey,
|
|
298
|
+
staticObjectResolver,
|
|
299
|
+
);
|
|
256
300
|
if ("refusal" in expanded) return expanded;
|
|
257
301
|
|
|
258
302
|
const top = indexMembers(expanded.members, fileName, site.operationKey);
|
|
@@ -272,8 +316,13 @@ function planOperationMigration(
|
|
|
272
316
|
for (const containerName of ["annotations", "toolRouter", "docs"] as const) {
|
|
273
317
|
const member = top.byName.get(containerName);
|
|
274
318
|
if (member === undefined) continue;
|
|
275
|
-
const
|
|
276
|
-
|
|
319
|
+
const resolvedObject = resolveObjectInitializer(
|
|
320
|
+
member,
|
|
321
|
+
source,
|
|
322
|
+
constObjects,
|
|
323
|
+
staticObjectResolver,
|
|
324
|
+
);
|
|
325
|
+
if (resolvedObject === undefined) {
|
|
277
326
|
return {
|
|
278
327
|
refusal: refusal(
|
|
279
328
|
fileName,
|
|
@@ -283,7 +332,14 @@ function planOperationMigration(
|
|
|
283
332
|
),
|
|
284
333
|
};
|
|
285
334
|
}
|
|
286
|
-
const members = expandMembers(
|
|
335
|
+
const members = expandMembers(
|
|
336
|
+
resolvedObject.object,
|
|
337
|
+
resolvedObject.source,
|
|
338
|
+
constObjects,
|
|
339
|
+
fileName,
|
|
340
|
+
site.operationKey,
|
|
341
|
+
staticObjectResolver,
|
|
342
|
+
);
|
|
287
343
|
if ("refusal" in members) return members;
|
|
288
344
|
const indexed = indexMembers(members.members, fileName, site.operationKey);
|
|
289
345
|
if ("refusal" in indexed) return indexed;
|
|
@@ -350,19 +406,12 @@ function planOperationMigration(
|
|
|
350
406
|
annotations.get("timeoutMs"),
|
|
351
407
|
fileName,
|
|
352
408
|
site.operationKey,
|
|
353
|
-
source,
|
|
354
409
|
"execution_conflict",
|
|
355
410
|
);
|
|
356
411
|
if ("refusal" in timeout) return timeout;
|
|
357
412
|
if (timeout.insert !== undefined) addInsertion(insertions, "annotations", timeout.insert);
|
|
358
413
|
|
|
359
|
-
const connection = resolveConnectionMode(
|
|
360
|
-
top.byName,
|
|
361
|
-
toolRouter,
|
|
362
|
-
fileName,
|
|
363
|
-
site.operationKey,
|
|
364
|
-
source,
|
|
365
|
-
);
|
|
414
|
+
const connection = resolveConnectionMode(top.byName, toolRouter, fileName, site.operationKey);
|
|
366
415
|
if ("refusal" in connection) return connection;
|
|
367
416
|
if (connection.insert !== undefined) {
|
|
368
417
|
addInsertion(insertions, "toolRouter", connection.insert);
|
|
@@ -374,7 +423,6 @@ function planOperationMigration(
|
|
|
374
423
|
toolRouter.get("connectionExternalRefParam"),
|
|
375
424
|
fileName,
|
|
376
425
|
site.operationKey,
|
|
377
|
-
source,
|
|
378
426
|
"connection_mode_conflict",
|
|
379
427
|
);
|
|
380
428
|
if ("refusal" in externalRef) return externalRef;
|
|
@@ -388,7 +436,6 @@ function planOperationMigration(
|
|
|
388
436
|
risk.value,
|
|
389
437
|
fileName,
|
|
390
438
|
site.operationKey,
|
|
391
|
-
source,
|
|
392
439
|
);
|
|
393
440
|
if ("refusal" in approval) return approval;
|
|
394
441
|
if (approval.removeTop !== undefined) removals.push(approval.removeTop);
|
|
@@ -401,7 +448,6 @@ function planOperationMigration(
|
|
|
401
448
|
docs.get(field),
|
|
402
449
|
fileName,
|
|
403
450
|
site.operationKey,
|
|
404
|
-
source,
|
|
405
451
|
"locale_key_conflict",
|
|
406
452
|
);
|
|
407
453
|
if ("refusal" in merged) return merged;
|
|
@@ -668,7 +714,6 @@ function resolveConnectionMode(
|
|
|
668
714
|
toolRouter: ReadonlyMap<string, ResolvedMember>,
|
|
669
715
|
fileName: string,
|
|
670
716
|
operationKey: string,
|
|
671
|
-
source: TS.SourceFile,
|
|
672
717
|
): { readonly insert?: string } | { readonly refusal: OperationDeclarationRefusal } {
|
|
673
718
|
const flat = top.get("connectionMode");
|
|
674
719
|
const nested = toolRouter.get("connectionMode");
|
|
@@ -695,7 +740,7 @@ function resolveConnectionMode(
|
|
|
695
740
|
}
|
|
696
741
|
if (flat !== undefined) return {};
|
|
697
742
|
if (nested !== undefined) {
|
|
698
|
-
return { insert: memberText(nested, "connectionMode"
|
|
743
|
+
return { insert: memberText(nested, "connectionMode") };
|
|
699
744
|
}
|
|
700
745
|
|
|
701
746
|
const required = toolRouter.get("requiresConnection");
|
|
@@ -716,7 +761,6 @@ function resolveApproval(
|
|
|
716
761
|
riskClass: string,
|
|
717
762
|
fileName: string,
|
|
718
763
|
operationKey: string,
|
|
719
|
-
source: TS.SourceFile,
|
|
720
764
|
):
|
|
721
765
|
| { readonly insert?: string; readonly removeTop?: TS.ObjectLiteralElementLike }
|
|
722
766
|
| { readonly refusal: OperationDeclarationRefusal } {
|
|
@@ -748,7 +792,7 @@ function resolveApproval(
|
|
|
748
792
|
return flat === undefined ? {} : { removeTop: flat.property };
|
|
749
793
|
}
|
|
750
794
|
if (flat !== undefined) return {};
|
|
751
|
-
return { insert: memberText(selected, "approval"
|
|
795
|
+
return { insert: memberText(selected, "approval") };
|
|
752
796
|
}
|
|
753
797
|
|
|
754
798
|
function defaultApprovalPolicy(riskClass: string): string {
|
|
@@ -763,11 +807,10 @@ function mergeFlatAndNested(
|
|
|
763
807
|
nested: ResolvedMember | undefined,
|
|
764
808
|
fileName: string,
|
|
765
809
|
operationKey: string,
|
|
766
|
-
source: TS.SourceFile,
|
|
767
810
|
reason: "locale_key_conflict" | "connection_mode_conflict" | "execution_conflict",
|
|
768
811
|
): { readonly insert?: string } | { readonly refusal: OperationDeclarationRefusal } {
|
|
769
812
|
if (nested === undefined) return {};
|
|
770
|
-
if (flat === undefined) return { insert: memberText(nested, field
|
|
813
|
+
if (flat === undefined) return { insert: memberText(nested, field) };
|
|
771
814
|
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
772
815
|
if (same === undefined) {
|
|
773
816
|
return nonLiteral(
|
|
@@ -777,6 +820,11 @@ function mergeFlatAndNested(
|
|
|
777
820
|
);
|
|
778
821
|
}
|
|
779
822
|
if (!same) {
|
|
823
|
+
// A shared imported docs template is intentionally lower-precedence than
|
|
824
|
+
// an operation's explicit flat locale key. Keep direct-vs-direct
|
|
825
|
+
// conflicts fail-closed, but do not let a spread default replace the
|
|
826
|
+
// operation-specific key during flattening.
|
|
827
|
+
if (reason === "locale_key_conflict" && nested.fromSpread === true) return {};
|
|
780
828
|
return {
|
|
781
829
|
refusal: refusal(
|
|
782
830
|
fileName,
|
|
@@ -936,7 +984,11 @@ function resolveOperationLocaleNamespace(
|
|
|
936
984
|
site: OperationSite,
|
|
937
985
|
): { readonly namespace: string } | { readonly refusal: OperationDeclarationRefusal } {
|
|
938
986
|
const authoredNamespaces = new Set<string>();
|
|
939
|
-
|
|
987
|
+
const directMembers = members.filter(
|
|
988
|
+
(member) => member !== undefined && member.fromSpread !== true,
|
|
989
|
+
);
|
|
990
|
+
const namespaceMembers = directMembers.length > 0 ? directMembers : members;
|
|
991
|
+
for (const member of namespaceMembers) {
|
|
940
992
|
const localeKey = literalString(member?.initializer);
|
|
941
993
|
if (localeKey === undefined) continue;
|
|
942
994
|
const segments = localeKey.split(".");
|
|
@@ -956,6 +1008,10 @@ function resolveOperationLocaleNamespace(
|
|
|
956
1008
|
}
|
|
957
1009
|
const authored = authoredNamespaces.values().next().value;
|
|
958
1010
|
if (authored !== undefined) return { namespace: authored };
|
|
1011
|
+
// planExamples proves the id before using this placeholder to construct a
|
|
1012
|
+
// locale key. Avoid validating an unproven binding such as <anonymous> as
|
|
1013
|
+
// though it were an authored operation id.
|
|
1014
|
+
if (!site.operationIdProven) return { namespace: site.operationKey };
|
|
959
1015
|
|
|
960
1016
|
try {
|
|
961
1017
|
return { namespace: operationIdToLocaleNamespace(site.operationKey) };
|
|
@@ -989,13 +1045,18 @@ function discoverOperationSites(
|
|
|
989
1045
|
fileName: string,
|
|
990
1046
|
constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
|
|
991
1047
|
operationIds: ReadonlyMap<string, string> | undefined,
|
|
1048
|
+
operationSites: ReadonlyMap<number, string> | undefined,
|
|
1049
|
+
excludedBindings: ReadonlySet<string> | undefined,
|
|
992
1050
|
): {
|
|
993
1051
|
readonly sites: OperationSite[];
|
|
994
1052
|
readonly refusals: OperationDeclarationRefusal[];
|
|
995
1053
|
} {
|
|
996
1054
|
const sitesByStart = new Map<number, OperationSite>();
|
|
997
1055
|
const localIds = new Map(operationIds ?? []);
|
|
1056
|
+
const localExcludedBindings = new Set(excludedBindings ?? []);
|
|
998
1057
|
const refusals: OperationDeclarationRefusal[] = [];
|
|
1058
|
+
const operationFactories = collectSimpleOperationFactories(source);
|
|
1059
|
+
const factoryCallIds = new Map<string, Set<string>>();
|
|
999
1060
|
|
|
1000
1061
|
for (const map of collectOperationsMaps(source, constObjects, fileName, refusals)) {
|
|
1001
1062
|
for (const property of map.properties) {
|
|
@@ -1029,6 +1090,18 @@ function discoverOperationSites(
|
|
|
1029
1090
|
});
|
|
1030
1091
|
continue;
|
|
1031
1092
|
}
|
|
1093
|
+
if (
|
|
1094
|
+
ts.isCallExpression(unwrapped) &&
|
|
1095
|
+
ts.isIdentifier(unwrapped.expression) &&
|
|
1096
|
+
operationFactories.has(unwrapped.expression.text) &&
|
|
1097
|
+
!localExcludedBindings.has(unwrapped.expression.text)
|
|
1098
|
+
) {
|
|
1099
|
+
const factoryName = unwrapped.expression.text;
|
|
1100
|
+
const operationIdsForFactory = factoryCallIds.get(factoryName) ?? new Set<string>();
|
|
1101
|
+
operationIdsForFactory.add(key);
|
|
1102
|
+
factoryCallIds.set(factoryName, operationIdsForFactory);
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1032
1105
|
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
1033
1106
|
sitesByStart.set(unwrapped.getStart(source), {
|
|
1034
1107
|
object: unwrapped,
|
|
@@ -1038,11 +1111,41 @@ function discoverOperationSites(
|
|
|
1038
1111
|
}
|
|
1039
1112
|
}
|
|
1040
1113
|
}
|
|
1114
|
+
for (const [factoryName, operationIdsForFactory] of factoryCallIds) {
|
|
1115
|
+
const ids = [...operationIdsForFactory];
|
|
1116
|
+
if (ids.length === 1) {
|
|
1117
|
+
const operationId = ids[0];
|
|
1118
|
+
if (operationId !== undefined) localIds.set(factoryName, operationId);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
localExcludedBindings.add(factoryName);
|
|
1122
|
+
refusals.push(
|
|
1123
|
+
refusal(
|
|
1124
|
+
fileName,
|
|
1125
|
+
factoryName,
|
|
1126
|
+
"factory_operation_id_ambiguous",
|
|
1127
|
+
`Factory ${factoryName} is registered under multiple operation ids: ${ids
|
|
1128
|
+
.map((id) => JSON.stringify(id))
|
|
1129
|
+
.join(", ")}. A shared operation body cannot own one examples locale namespace.`,
|
|
1130
|
+
),
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1041
1133
|
|
|
1042
1134
|
const visit = (node: TS.Node): void => {
|
|
1135
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1136
|
+
const indexedOperationKey = operationSites?.get(node.getStart(source));
|
|
1137
|
+
if (indexedOperationKey !== undefined) {
|
|
1138
|
+
sitesByStart.set(node.getStart(source), {
|
|
1139
|
+
object: node,
|
|
1140
|
+
operationKey: indexedOperationKey,
|
|
1141
|
+
operationIdProven: true,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1043
1145
|
if (ts.isCallExpression(node) && isOperationHelperCall(node)) {
|
|
1044
1146
|
const argument = operationArgument(node);
|
|
1045
1147
|
const bindingName = enclosingBindingName(node);
|
|
1148
|
+
if (bindingName !== undefined && localExcludedBindings.has(bindingName)) return;
|
|
1046
1149
|
const operationKey =
|
|
1047
1150
|
(bindingName === undefined ? undefined : localIds.get(bindingName)) ??
|
|
1048
1151
|
bindingName ??
|
|
@@ -1086,6 +1189,35 @@ function discoverOperationSites(
|
|
|
1086
1189
|
};
|
|
1087
1190
|
}
|
|
1088
1191
|
|
|
1192
|
+
function collectSimpleOperationFactories(source: TS.SourceFile): ReadonlySet<string> {
|
|
1193
|
+
const factories = new Set<string>();
|
|
1194
|
+
for (const statement of source.statements) {
|
|
1195
|
+
if (
|
|
1196
|
+
!ts.isFunctionDeclaration(statement) ||
|
|
1197
|
+
statement.name === undefined ||
|
|
1198
|
+
statement.body === undefined ||
|
|
1199
|
+
statement.body.statements.length !== 1
|
|
1200
|
+
) {
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
const returned = statement.body.statements[0];
|
|
1204
|
+
if (!ts.isReturnStatement(returned) || returned.expression === undefined) continue;
|
|
1205
|
+
const expression = unwrapExpression(returned.expression);
|
|
1206
|
+
if (
|
|
1207
|
+
expression !== undefined &&
|
|
1208
|
+
ts.isCallExpression(expression) &&
|
|
1209
|
+
isOperationHelperCall(expression)
|
|
1210
|
+
) {
|
|
1211
|
+
const argument = operationArgument(expression);
|
|
1212
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
1213
|
+
if (object !== undefined && ts.isObjectLiteralExpression(object)) {
|
|
1214
|
+
factories.add(statement.name.text);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return factories;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1089
1221
|
function collectOperationsMaps(
|
|
1090
1222
|
source: TS.SourceFile,
|
|
1091
1223
|
constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
|
|
@@ -1232,6 +1364,9 @@ function enclosingBindingName(node: TS.Node): string | undefined {
|
|
|
1232
1364
|
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
|
|
1233
1365
|
return current.name.text;
|
|
1234
1366
|
}
|
|
1367
|
+
if (ts.isFunctionDeclaration(current) && current.name !== undefined) {
|
|
1368
|
+
return current.name.text;
|
|
1369
|
+
}
|
|
1235
1370
|
if (ts.isPropertyAssignment(current)) return staticPropertyName(current.name);
|
|
1236
1371
|
if (ts.isExportAssignment(current)) return "default";
|
|
1237
1372
|
current = current.parent;
|
|
@@ -1317,9 +1452,11 @@ function expandArrayElements(
|
|
|
1317
1452
|
|
|
1318
1453
|
function expandMembers(
|
|
1319
1454
|
object: TS.ObjectLiteralExpression,
|
|
1455
|
+
source: TS.SourceFile,
|
|
1320
1456
|
constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
|
|
1321
1457
|
fileName: string,
|
|
1322
1458
|
operationKey: string,
|
|
1459
|
+
staticObjectResolver?: StaticObjectResolver,
|
|
1323
1460
|
seen = new Set<TS.ObjectLiteralExpression>(),
|
|
1324
1461
|
): { readonly members: ResolvedMember[] } | { readonly refusal: OperationDeclarationRefusal } {
|
|
1325
1462
|
if (seen.has(object)) {
|
|
@@ -1329,22 +1466,23 @@ function expandMembers(
|
|
|
1329
1466
|
const members: ResolvedMember[] = [];
|
|
1330
1467
|
for (const property of object.properties) {
|
|
1331
1468
|
if (ts.isSpreadAssignment(property)) {
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
: undefined;
|
|
1469
|
+
const spreadObject = resolveStaticObjectExpression(
|
|
1470
|
+
property.expression,
|
|
1471
|
+
source,
|
|
1472
|
+
constObjects,
|
|
1473
|
+
staticObjectResolver,
|
|
1474
|
+
);
|
|
1339
1475
|
if (spreadObject === undefined) {
|
|
1340
|
-
members.push({ name: "<spread>", property });
|
|
1476
|
+
members.push({ name: "<spread>", property, source });
|
|
1341
1477
|
continue;
|
|
1342
1478
|
}
|
|
1343
1479
|
const expanded = expandMembers(
|
|
1344
|
-
spreadObject,
|
|
1480
|
+
spreadObject.object,
|
|
1481
|
+
spreadObject.source,
|
|
1345
1482
|
constObjects,
|
|
1346
1483
|
fileName,
|
|
1347
1484
|
operationKey,
|
|
1485
|
+
staticObjectResolver,
|
|
1348
1486
|
new Set(seen),
|
|
1349
1487
|
);
|
|
1350
1488
|
if ("refusal" in expanded) return expanded;
|
|
@@ -1356,7 +1494,7 @@ function expandMembers(
|
|
|
1356
1494
|
return nonLiteral(fileName, operationKey, "A declaration member uses a computed name.");
|
|
1357
1495
|
}
|
|
1358
1496
|
const initializer = propertyValue(property);
|
|
1359
|
-
members.push({ name, property, initializer });
|
|
1497
|
+
members.push({ name, property, initializer, source });
|
|
1360
1498
|
}
|
|
1361
1499
|
return { members };
|
|
1362
1500
|
}
|
|
@@ -1405,7 +1543,7 @@ function indexMembersFromObject(
|
|
|
1405
1543
|
if (name === undefined || initializer === undefined) {
|
|
1406
1544
|
return nonLiteral(fileName, operationKey, "Example members must be literal properties.");
|
|
1407
1545
|
}
|
|
1408
|
-
members.push({ name, property, initializer });
|
|
1546
|
+
members.push({ name, property, initializer, source: object.getSourceFile() });
|
|
1409
1547
|
}
|
|
1410
1548
|
const indexed = indexMembers(members, fileName, operationKey);
|
|
1411
1549
|
if ("refusal" in indexed) return indexed;
|
|
@@ -1414,12 +1552,36 @@ function indexMembersFromObject(
|
|
|
1414
1552
|
|
|
1415
1553
|
function resolveObjectInitializer(
|
|
1416
1554
|
member: ResolvedMember,
|
|
1555
|
+
rootSource: TS.SourceFile,
|
|
1417
1556
|
constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
if (initializer === undefined) return undefined;
|
|
1421
|
-
|
|
1422
|
-
|
|
1557
|
+
staticObjectResolver?: StaticObjectResolver,
|
|
1558
|
+
): StaticObjectReference | undefined {
|
|
1559
|
+
if (member.initializer === undefined) return undefined;
|
|
1560
|
+
return resolveStaticObjectExpression(
|
|
1561
|
+
member.initializer,
|
|
1562
|
+
member.source,
|
|
1563
|
+
constObjects,
|
|
1564
|
+
staticObjectResolver,
|
|
1565
|
+
rootSource,
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function resolveStaticObjectExpression(
|
|
1570
|
+
expression: TS.Expression,
|
|
1571
|
+
source: TS.SourceFile,
|
|
1572
|
+
constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
|
|
1573
|
+
staticObjectResolver?: StaticObjectResolver,
|
|
1574
|
+
rootSource: TS.SourceFile = source,
|
|
1575
|
+
): StaticObjectReference | undefined {
|
|
1576
|
+
const unwrapped = unwrapExpression(expression);
|
|
1577
|
+
if (unwrapped === undefined) return undefined;
|
|
1578
|
+
if (ts.isObjectLiteralExpression(unwrapped)) return { object: unwrapped, source };
|
|
1579
|
+
const externallyResolved = staticObjectResolver?.(unwrapped, source);
|
|
1580
|
+
if (externallyResolved !== undefined) return externallyResolved;
|
|
1581
|
+
if (source === rootSource && ts.isIdentifier(unwrapped)) {
|
|
1582
|
+
const object = constObjects.get(unwrapped.text);
|
|
1583
|
+
if (object !== undefined) return { object, source };
|
|
1584
|
+
}
|
|
1423
1585
|
return undefined;
|
|
1424
1586
|
}
|
|
1425
1587
|
|
|
@@ -1521,9 +1683,9 @@ function literalValue(expression: TS.Expression | undefined): unknown | typeof N
|
|
|
1521
1683
|
return NOT_LITERAL;
|
|
1522
1684
|
}
|
|
1523
1685
|
|
|
1524
|
-
function memberText(member: ResolvedMember, name: string
|
|
1686
|
+
function memberText(member: ResolvedMember, name: string): string {
|
|
1525
1687
|
if (member.initializer === undefined) return name;
|
|
1526
|
-
return `${name}: ${member.initializer.getText(source)}`;
|
|
1688
|
+
return `${name}: ${member.initializer.getText(member.source)}`;
|
|
1527
1689
|
}
|
|
1528
1690
|
|
|
1529
1691
|
function addInsertion(
|
|
@@ -1738,6 +1900,10 @@ export type OperationDeclarationRepositoryResult =
|
|
|
1738
1900
|
readonly status: "refused";
|
|
1739
1901
|
readonly providerRoot: string;
|
|
1740
1902
|
readonly refusals: readonly OperationDeclarationRefusal[];
|
|
1903
|
+
/** Operations that were independently migratable before repository-atomic refusal. */
|
|
1904
|
+
readonly operationCount: number;
|
|
1905
|
+
readonly changedFiles: readonly string[];
|
|
1906
|
+
readonly localeTodoCount: number;
|
|
1741
1907
|
};
|
|
1742
1908
|
|
|
1743
1909
|
/** Run the file transform repository-wide, committing writes only if every file is provable. */
|
|
@@ -1748,19 +1914,28 @@ export function migrateOperationDeclarationRepository(
|
|
|
1748
1914
|
const providerRoot = resolve(providerRootInput);
|
|
1749
1915
|
const sourceFiles = collectSourceFiles(providerRoot);
|
|
1750
1916
|
const localeFiles = collectLocaleFiles(providerRoot);
|
|
1751
|
-
const
|
|
1917
|
+
const repositoryIndex = buildRepositoryOperationIndex(sourceFiles, providerRoot);
|
|
1752
1918
|
const pendingWrites = new Map<string, string>();
|
|
1753
1919
|
const changedFiles: string[] = [];
|
|
1754
1920
|
const todos: LocaleTodo[] = [];
|
|
1755
|
-
const refusals: OperationDeclarationRefusal[] = [];
|
|
1921
|
+
const refusals: OperationDeclarationRefusal[] = [...repositoryIndex.refusals];
|
|
1756
1922
|
let operationCount = 0;
|
|
1757
1923
|
|
|
1758
1924
|
for (const sourcePath of sourceFiles) {
|
|
1759
1925
|
const relativePath = slash(relative(providerRoot, sourcePath));
|
|
1760
|
-
const result =
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1926
|
+
const result = migrateOperationDeclarationInternal(
|
|
1927
|
+
readFileSync(sourcePath, "utf8"),
|
|
1928
|
+
relativePath,
|
|
1929
|
+
{
|
|
1930
|
+
operationIds: repositoryIndex.operationIds.get(sourcePath),
|
|
1931
|
+
localeFiles,
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
operationSites: repositoryIndex.operationSites.get(sourcePath),
|
|
1935
|
+
excludedBindings: repositoryIndex.excludedBindings.get(sourcePath),
|
|
1936
|
+
staticObjectResolver: repositoryIndex.staticObjectResolverFor(sourcePath),
|
|
1937
|
+
},
|
|
1938
|
+
);
|
|
1764
1939
|
if (result.status === "refused") {
|
|
1765
1940
|
refusals.push(...result.refusals);
|
|
1766
1941
|
continue;
|
|
@@ -1772,11 +1947,32 @@ export function migrateOperationDeclarationRepository(
|
|
|
1772
1947
|
todos.push(...result.localeTodos);
|
|
1773
1948
|
}
|
|
1774
1949
|
}
|
|
1950
|
+
if (repositoryIndex.declarations.length > 0 && repositoryIndex.discoveredCount === 0) {
|
|
1951
|
+
for (const declaration of repositoryIndex.declarations) {
|
|
1952
|
+
refusals.push(
|
|
1953
|
+
refusal(
|
|
1954
|
+
slash(relative(providerRoot, declaration.path)),
|
|
1955
|
+
"<operations>",
|
|
1956
|
+
"no_operations_discovered",
|
|
1957
|
+
`Provider construct ${declaration.construct} declares operations via unresolved initializer ${JSON.stringify(
|
|
1958
|
+
declaration.initializer.getText(declaration.initializer.getSourceFile()),
|
|
1959
|
+
)}, but repository-wide discovery found zero operation sites.`,
|
|
1960
|
+
),
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1775
1964
|
|
|
1776
1965
|
const todoConflict = findLocaleTodoConflict(todos);
|
|
1777
1966
|
if (todoConflict !== undefined) refusals.push(todoConflict);
|
|
1778
1967
|
if (refusals.length > 0) {
|
|
1779
|
-
return {
|
|
1968
|
+
return {
|
|
1969
|
+
status: "refused",
|
|
1970
|
+
providerRoot,
|
|
1971
|
+
refusals,
|
|
1972
|
+
operationCount,
|
|
1973
|
+
changedFiles,
|
|
1974
|
+
localeTodoCount: todos.length,
|
|
1975
|
+
};
|
|
1780
1976
|
}
|
|
1781
1977
|
for (const [path, code] of renderLocaleCatalogWrites(providerRoot, todos)) {
|
|
1782
1978
|
pendingWrites.set(path, code);
|
|
@@ -1968,6 +2164,601 @@ function collectLocaleFiles(root: string): string[] {
|
|
|
1968
2164
|
.sort();
|
|
1969
2165
|
}
|
|
1970
2166
|
|
|
2167
|
+
type LocatedExpression = {
|
|
2168
|
+
readonly path: string;
|
|
2169
|
+
readonly source: TS.SourceFile;
|
|
2170
|
+
readonly expression: TS.Expression;
|
|
2171
|
+
readonly bindingName?: string;
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
type StaticResolution =
|
|
2175
|
+
| { readonly status: "resolved"; readonly value: LocatedExpression }
|
|
2176
|
+
| { readonly status: "missing" }
|
|
2177
|
+
| { readonly status: "refused"; readonly detail: string };
|
|
2178
|
+
|
|
2179
|
+
type ProviderOperationsDeclaration = {
|
|
2180
|
+
readonly path: string;
|
|
2181
|
+
readonly construct: string;
|
|
2182
|
+
readonly initializer: TS.Expression;
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
type RepositoryOperationIndex = {
|
|
2186
|
+
readonly operationIds: Map<string, Map<string, string>>;
|
|
2187
|
+
readonly operationSites: Map<string, Map<number, string>>;
|
|
2188
|
+
readonly excludedBindings: Map<string, Set<string>>;
|
|
2189
|
+
readonly refusals: OperationDeclarationRefusal[];
|
|
2190
|
+
readonly declarations: ProviderOperationsDeclaration[];
|
|
2191
|
+
readonly discoveredCount: number;
|
|
2192
|
+
readonly staticObjectResolverFor: (path: string) => StaticObjectResolver;
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
function buildRepositoryOperationIndex(
|
|
2196
|
+
sourceFiles: readonly string[],
|
|
2197
|
+
providerRoot: string,
|
|
2198
|
+
): RepositoryOperationIndex {
|
|
2199
|
+
const sources = new Map<string, TS.SourceFile>();
|
|
2200
|
+
for (const path of sourceFiles) {
|
|
2201
|
+
const source = parseSource(path, readFileSync(path, "utf8"));
|
|
2202
|
+
if (firstSyntaxError(source) === undefined) sources.set(path, source);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
const operationIds = buildOperationIdIndex(sourceFiles);
|
|
2206
|
+
const operationSites = new Map<string, Map<number, string>>();
|
|
2207
|
+
const excludedBindings = new Map<string, Set<string>>();
|
|
2208
|
+
const refusals: OperationDeclarationRefusal[] = [];
|
|
2209
|
+
const declarations: ProviderOperationsDeclaration[] = [];
|
|
2210
|
+
const indexedProviderProperties = new Set<string>();
|
|
2211
|
+
const factoryIds = new Map<string, { path: string; name: string; ids: Set<string> }>();
|
|
2212
|
+
const bindingCandidates = new Map<string, { path: string; name: string; ids: Set<string> }>();
|
|
2213
|
+
let discoveredCount = 0;
|
|
2214
|
+
|
|
2215
|
+
const relativePath = (path: string): string => slash(relative(providerRoot, path));
|
|
2216
|
+
const resolutionRefusal = (path: string, operationKey: string, detail: string): void => {
|
|
2217
|
+
refusals.push(refusal(relativePath(path), operationKey, "non_literal", detail));
|
|
2218
|
+
};
|
|
2219
|
+
|
|
2220
|
+
const resolveExport = (
|
|
2221
|
+
path: string,
|
|
2222
|
+
exportedName: string,
|
|
2223
|
+
active: ReadonlySet<string>,
|
|
2224
|
+
): StaticResolution => {
|
|
2225
|
+
const key = `${path}\0export\0${exportedName}`;
|
|
2226
|
+
if (active.has(key)) {
|
|
2227
|
+
return {
|
|
2228
|
+
status: "refused",
|
|
2229
|
+
detail: `Static relative export cycle while resolving ${JSON.stringify(exportedName)} from ${relativePath(path)}.`,
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
const source = sources.get(path);
|
|
2233
|
+
if (source === undefined) return { status: "missing" };
|
|
2234
|
+
const nextActive = new Set(active);
|
|
2235
|
+
nextActive.add(key);
|
|
2236
|
+
const explicit: Array<() => StaticResolution> = [];
|
|
2237
|
+
const exportStars: string[] = [];
|
|
2238
|
+
|
|
2239
|
+
for (const statement of source.statements) {
|
|
2240
|
+
if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
|
|
2241
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2242
|
+
if (
|
|
2243
|
+
ts.isIdentifier(declaration.name) &&
|
|
2244
|
+
declaration.name.text === exportedName &&
|
|
2245
|
+
declaration.initializer !== undefined
|
|
2246
|
+
) {
|
|
2247
|
+
const initializer = declaration.initializer;
|
|
2248
|
+
const bindingName = declaration.name.text;
|
|
2249
|
+
explicit.push(() => ({
|
|
2250
|
+
status: "resolved",
|
|
2251
|
+
value: {
|
|
2252
|
+
path,
|
|
2253
|
+
source,
|
|
2254
|
+
expression: initializer,
|
|
2255
|
+
bindingName,
|
|
2256
|
+
},
|
|
2257
|
+
}));
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (
|
|
2262
|
+
ts.isExportAssignment(statement) &&
|
|
2263
|
+
!statement.isExportEquals &&
|
|
2264
|
+
exportedName === "default"
|
|
2265
|
+
) {
|
|
2266
|
+
explicit.push(() => ({
|
|
2267
|
+
status: "resolved",
|
|
2268
|
+
value: { path, source, expression: statement.expression },
|
|
2269
|
+
}));
|
|
2270
|
+
}
|
|
2271
|
+
if (!ts.isExportDeclaration(statement)) continue;
|
|
2272
|
+
const specifier =
|
|
2273
|
+
statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier)
|
|
2274
|
+
? statement.moduleSpecifier.text
|
|
2275
|
+
: undefined;
|
|
2276
|
+
if (statement.exportClause === undefined) {
|
|
2277
|
+
if (specifier !== undefined) exportStars.push(specifier);
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (!ts.isNamedExports(statement.exportClause)) continue;
|
|
2281
|
+
for (const element of statement.exportClause.elements) {
|
|
2282
|
+
if (element.name.text !== exportedName) continue;
|
|
2283
|
+
const localName = element.propertyName?.text ?? element.name.text;
|
|
2284
|
+
explicit.push(() => {
|
|
2285
|
+
if (specifier === undefined) {
|
|
2286
|
+
return resolveIdentifier(path, localName, nextActive);
|
|
2287
|
+
}
|
|
2288
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
2289
|
+
if (target.status !== "resolved") return target;
|
|
2290
|
+
return resolveExport(target.path, localName, nextActive);
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (explicit.length > 1) {
|
|
2296
|
+
return {
|
|
2297
|
+
status: "refused",
|
|
2298
|
+
detail: `Export ${JSON.stringify(exportedName)} is ambiguous in ${relativePath(path)}.`,
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
if (explicit.length === 1) return explicit[0]?.() ?? { status: "missing" };
|
|
2302
|
+
|
|
2303
|
+
const resolvedStars: LocatedExpression[] = [];
|
|
2304
|
+
let firstStarRefusal: StaticResolution | undefined;
|
|
2305
|
+
for (const specifier of exportStars) {
|
|
2306
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
2307
|
+
if (target.status !== "resolved") {
|
|
2308
|
+
if (target.status === "refused") firstStarRefusal ??= target;
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
const candidate = resolveExport(target.path, exportedName, nextActive);
|
|
2312
|
+
if (candidate.status === "resolved") resolvedStars.push(candidate.value);
|
|
2313
|
+
else if (candidate.status === "refused") firstStarRefusal ??= candidate;
|
|
2314
|
+
}
|
|
2315
|
+
const origins = new Set(
|
|
2316
|
+
resolvedStars.map((item) => `${item.path}\0${item.expression.getStart(item.source)}`),
|
|
2317
|
+
);
|
|
2318
|
+
if (origins.size > 1) {
|
|
2319
|
+
return {
|
|
2320
|
+
status: "refused",
|
|
2321
|
+
detail: `Export ${JSON.stringify(exportedName)} is ambiguous across relative re-exports from ${relativePath(path)}.`,
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
if (firstStarRefusal !== undefined) return firstStarRefusal;
|
|
2325
|
+
const resolved = resolvedStars[0];
|
|
2326
|
+
return resolved === undefined ? { status: "missing" } : { status: "resolved", value: resolved };
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
const resolveIdentifier = (
|
|
2330
|
+
path: string,
|
|
2331
|
+
name: string,
|
|
2332
|
+
active: ReadonlySet<string>,
|
|
2333
|
+
): StaticResolution => {
|
|
2334
|
+
const source = sources.get(path);
|
|
2335
|
+
if (source === undefined) return { status: "missing" };
|
|
2336
|
+
for (const statement of source.statements) {
|
|
2337
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
2338
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
2339
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2340
|
+
if (
|
|
2341
|
+
ts.isIdentifier(declaration.name) &&
|
|
2342
|
+
declaration.name.text === name &&
|
|
2343
|
+
declaration.initializer !== undefined
|
|
2344
|
+
) {
|
|
2345
|
+
return {
|
|
2346
|
+
status: "resolved",
|
|
2347
|
+
value: {
|
|
2348
|
+
path,
|
|
2349
|
+
source,
|
|
2350
|
+
expression: declaration.initializer,
|
|
2351
|
+
bindingName: name,
|
|
2352
|
+
},
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
for (const statement of source.statements) {
|
|
2358
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
let exportedName: string | undefined;
|
|
2362
|
+
if (statement.importClause?.name?.text === name) exportedName = "default";
|
|
2363
|
+
const bindings = statement.importClause?.namedBindings;
|
|
2364
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)) {
|
|
2365
|
+
for (const element of bindings.elements) {
|
|
2366
|
+
if (element.name.text === name) {
|
|
2367
|
+
exportedName = element.propertyName?.text ?? element.name.text;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (exportedName === undefined) continue;
|
|
2372
|
+
const specifier = statement.moduleSpecifier.text;
|
|
2373
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
2374
|
+
if (target.status !== "resolved") return target;
|
|
2375
|
+
return resolveExport(target.path, exportedName, active);
|
|
2376
|
+
}
|
|
2377
|
+
return { status: "missing" };
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
const resolveLocatedExpression = (
|
|
2381
|
+
located: LocatedExpression,
|
|
2382
|
+
active: ReadonlySet<string>,
|
|
2383
|
+
): StaticResolution => {
|
|
2384
|
+
const expression = unwrapExpression(located.expression);
|
|
2385
|
+
if (expression === undefined) return { status: "missing" };
|
|
2386
|
+
if (!ts.isIdentifier(expression)) {
|
|
2387
|
+
return { status: "resolved", value: { ...located, expression } };
|
|
2388
|
+
}
|
|
2389
|
+
const key = `${located.path}\0identifier\0${expression.text}`;
|
|
2390
|
+
if (active.has(key)) {
|
|
2391
|
+
return {
|
|
2392
|
+
status: "refused",
|
|
2393
|
+
detail: `Static identifier cycle while resolving ${JSON.stringify(expression.text)} in ${relativePath(located.path)}.`,
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
const nextActive = new Set(active);
|
|
2397
|
+
nextActive.add(key);
|
|
2398
|
+
const resolved = resolveIdentifier(located.path, expression.text, nextActive);
|
|
2399
|
+
return resolved.status === "resolved"
|
|
2400
|
+
? resolveLocatedExpression(resolved.value, nextActive)
|
|
2401
|
+
: resolved;
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
type RegistryEntry = { readonly operationId: string; readonly value: LocatedExpression };
|
|
2405
|
+
const flattenRegistry = (
|
|
2406
|
+
locatedInput: LocatedExpression,
|
|
2407
|
+
active: ReadonlySet<string>,
|
|
2408
|
+
): { readonly entries: Map<string, RegistryEntry> } | { readonly detail: string } => {
|
|
2409
|
+
const resolved = resolveLocatedExpression(locatedInput, active);
|
|
2410
|
+
if (resolved.status === "refused") return { detail: resolved.detail };
|
|
2411
|
+
if (resolved.status === "missing") {
|
|
2412
|
+
return {
|
|
2413
|
+
detail: `Operations initializer ${JSON.stringify(locatedInput.expression.getText(locatedInput.source))} is not a static local or relative-imported object.`,
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
const expression = unwrapExpression(resolved.value.expression);
|
|
2417
|
+
if (expression === undefined || !ts.isObjectLiteralExpression(expression)) {
|
|
2418
|
+
return {
|
|
2419
|
+
detail: `Operations initializer ${JSON.stringify(resolved.value.expression.getText(resolved.value.source))} is not a static object literal.`,
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
const cycleKey = `${resolved.value.path}\0object\0${expression.getStart(resolved.value.source)}`;
|
|
2423
|
+
if (active.has(cycleKey)) {
|
|
2424
|
+
return {
|
|
2425
|
+
detail: `Static operations registry spread cycle reaches ${relativePath(resolved.value.path)}.`,
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
const nextActive = new Set(active);
|
|
2429
|
+
nextActive.add(cycleKey);
|
|
2430
|
+
const entries = new Map<string, RegistryEntry>();
|
|
2431
|
+
for (const property of expression.properties) {
|
|
2432
|
+
if (ts.isSpreadAssignment(property)) {
|
|
2433
|
+
const spread = flattenRegistry(
|
|
2434
|
+
{
|
|
2435
|
+
path: resolved.value.path,
|
|
2436
|
+
source: resolved.value.source,
|
|
2437
|
+
expression: property.expression,
|
|
2438
|
+
},
|
|
2439
|
+
nextActive,
|
|
2440
|
+
);
|
|
2441
|
+
if ("detail" in spread) return spread;
|
|
2442
|
+
for (const [operationId, entry] of spread.entries) entries.set(operationId, entry);
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
const operationId = staticPropertyName(property.name);
|
|
2446
|
+
const value = propertyValue(property);
|
|
2447
|
+
if (operationId === undefined) {
|
|
2448
|
+
return {
|
|
2449
|
+
detail: `Operations registry in ${relativePath(resolved.value.path)} uses a computed key.`,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
if (value === undefined) {
|
|
2453
|
+
return { detail: `Operation ${JSON.stringify(operationId)} has a non-static initializer.` };
|
|
2454
|
+
}
|
|
2455
|
+
entries.set(operationId, {
|
|
2456
|
+
operationId,
|
|
2457
|
+
value: {
|
|
2458
|
+
path: resolved.value.path,
|
|
2459
|
+
source: resolved.value.source,
|
|
2460
|
+
expression: value,
|
|
2461
|
+
},
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
return { entries };
|
|
2465
|
+
};
|
|
2466
|
+
|
|
2467
|
+
const addBindingCandidate = (path: string, name: string, operationId: string): void => {
|
|
2468
|
+
const key = `${path}\0${name}`;
|
|
2469
|
+
const candidate = bindingCandidates.get(key) ?? { path, name, ids: new Set<string>() };
|
|
2470
|
+
candidate.ids.add(operationId);
|
|
2471
|
+
bindingCandidates.set(key, candidate);
|
|
2472
|
+
};
|
|
2473
|
+
const addObjectSite = (
|
|
2474
|
+
path: string,
|
|
2475
|
+
source: TS.SourceFile,
|
|
2476
|
+
object: TS.ObjectLiteralExpression,
|
|
2477
|
+
operationId: string,
|
|
2478
|
+
): void => {
|
|
2479
|
+
const sites = operationSites.get(path) ?? new Map<number, string>();
|
|
2480
|
+
const start = object.getStart(source);
|
|
2481
|
+
const previous = sites.get(start);
|
|
2482
|
+
if (previous !== undefined && previous !== operationId) {
|
|
2483
|
+
resolutionRefusal(
|
|
2484
|
+
path,
|
|
2485
|
+
operationId,
|
|
2486
|
+
`One static operation object is registered under both ${JSON.stringify(previous)} and ${JSON.stringify(operationId)}.`,
|
|
2487
|
+
);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
sites.set(start, operationId);
|
|
2491
|
+
operationSites.set(path, sites);
|
|
2492
|
+
discoveredCount += 1;
|
|
2493
|
+
};
|
|
2494
|
+
|
|
2495
|
+
const classifyEntry = (entry: RegistryEntry): void => {
|
|
2496
|
+
const resolved = resolveLocatedExpression(entry.value, new Set());
|
|
2497
|
+
if (resolved.status === "refused") {
|
|
2498
|
+
resolutionRefusal(entry.value.path, entry.operationId, resolved.detail);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
if (resolved.status === "missing") {
|
|
2502
|
+
resolutionRefusal(
|
|
2503
|
+
entry.value.path,
|
|
2504
|
+
entry.operationId,
|
|
2505
|
+
`Operation initializer ${JSON.stringify(entry.value.expression.getText(entry.value.source))} is not statically resolvable.`,
|
|
2506
|
+
);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
const expression = unwrapExpression(resolved.value.expression);
|
|
2510
|
+
if (expression === undefined) return;
|
|
2511
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
2512
|
+
addObjectSite(resolved.value.path, resolved.value.source, expression, entry.operationId);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (ts.isCallExpression(expression) && isOperationHelperCall(expression)) {
|
|
2516
|
+
const argument = operationArgument(expression);
|
|
2517
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
2518
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object)) {
|
|
2519
|
+
resolutionRefusal(
|
|
2520
|
+
resolved.value.path,
|
|
2521
|
+
entry.operationId,
|
|
2522
|
+
"Operation helper argument must be a static object literal.",
|
|
2523
|
+
);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (resolved.value.bindingName !== undefined) {
|
|
2527
|
+
addBindingCandidate(resolved.value.path, resolved.value.bindingName, entry.operationId);
|
|
2528
|
+
}
|
|
2529
|
+
addObjectSite(resolved.value.path, resolved.value.source, object, entry.operationId);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
if (
|
|
2533
|
+
ts.isCallExpression(expression) &&
|
|
2534
|
+
ts.isIdentifier(expression.expression) &&
|
|
2535
|
+
collectSimpleOperationFactories(resolved.value.source).has(expression.expression.text)
|
|
2536
|
+
) {
|
|
2537
|
+
const name = expression.expression.text;
|
|
2538
|
+
const key = `${resolved.value.path}\0${name}`;
|
|
2539
|
+
const factory = factoryIds.get(key) ?? {
|
|
2540
|
+
path: resolved.value.path,
|
|
2541
|
+
name,
|
|
2542
|
+
ids: new Set<string>(),
|
|
2543
|
+
};
|
|
2544
|
+
factory.ids.add(entry.operationId);
|
|
2545
|
+
factoryIds.set(key, factory);
|
|
2546
|
+
discoveredCount += 1;
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
resolutionRefusal(
|
|
2550
|
+
resolved.value.path,
|
|
2551
|
+
entry.operationId,
|
|
2552
|
+
`Operation initializer ${JSON.stringify(expression.getText(resolved.value.source))} is not a raw object, operation helper, or simple same-file factory call.`,
|
|
2553
|
+
);
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
const inspectProviderProperty = (
|
|
2557
|
+
path: string,
|
|
2558
|
+
source: TS.SourceFile,
|
|
2559
|
+
node: TS.PropertyAssignment,
|
|
2560
|
+
construct: string,
|
|
2561
|
+
): void => {
|
|
2562
|
+
const key = `${path}\0${node.getStart(source)}`;
|
|
2563
|
+
if (indexedProviderProperties.has(key)) return;
|
|
2564
|
+
indexedProviderProperties.add(key);
|
|
2565
|
+
declarations.push({ path, construct, initializer: node.initializer });
|
|
2566
|
+
const flattened = flattenRegistry({ path, source, expression: node.initializer }, new Set());
|
|
2567
|
+
if ("detail" in flattened) {
|
|
2568
|
+
resolutionRefusal(path, "<operations>", flattened.detail);
|
|
2569
|
+
} else {
|
|
2570
|
+
for (const entry of flattened.entries.values()) classifyEntry(entry);
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
|
|
2574
|
+
for (const [path, source] of sources) {
|
|
2575
|
+
const constObjects = collectModuleConstObjects(source);
|
|
2576
|
+
const visit = (node: TS.Node): void => {
|
|
2577
|
+
if (
|
|
2578
|
+
ts.isPropertyAssignment(node) &&
|
|
2579
|
+
staticPropertyName(node.name) === "operations" &&
|
|
2580
|
+
isProviderOperationsProperty(node, constObjects)
|
|
2581
|
+
) {
|
|
2582
|
+
inspectProviderProperty(path, source, node, providerConstructName(node));
|
|
2583
|
+
}
|
|
2584
|
+
if (ts.isCallExpression(node)) {
|
|
2585
|
+
const construct = providerCallConstruct(node);
|
|
2586
|
+
if (construct !== undefined) {
|
|
2587
|
+
for (const argument of node.arguments) {
|
|
2588
|
+
const resolved = resolveLocatedExpression(
|
|
2589
|
+
{ path, source, expression: argument },
|
|
2590
|
+
new Set(),
|
|
2591
|
+
);
|
|
2592
|
+
if (resolved.status !== "resolved") continue;
|
|
2593
|
+
const object = unwrapExpression(resolved.value.expression);
|
|
2594
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object)) continue;
|
|
2595
|
+
for (const property of object.properties) {
|
|
2596
|
+
if (
|
|
2597
|
+
ts.isPropertyAssignment(property) &&
|
|
2598
|
+
staticPropertyName(property.name) === "operations"
|
|
2599
|
+
) {
|
|
2600
|
+
inspectProviderProperty(
|
|
2601
|
+
resolved.value.path,
|
|
2602
|
+
resolved.value.source,
|
|
2603
|
+
property,
|
|
2604
|
+
construct,
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
ts.forEachChild(node, visit);
|
|
2612
|
+
};
|
|
2613
|
+
visit(source);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
for (const candidate of bindingCandidates.values()) {
|
|
2617
|
+
const ids = [...candidate.ids];
|
|
2618
|
+
const indexed = operationIds.get(candidate.path) ?? new Map<string, string>();
|
|
2619
|
+
if (ids.length === 1) {
|
|
2620
|
+
const operationId = ids[0];
|
|
2621
|
+
if (operationId !== undefined) indexed.set(candidate.name, operationId);
|
|
2622
|
+
} else {
|
|
2623
|
+
indexed.delete(candidate.name);
|
|
2624
|
+
const excluded = excludedBindings.get(candidate.path) ?? new Set<string>();
|
|
2625
|
+
excluded.add(candidate.name);
|
|
2626
|
+
excludedBindings.set(candidate.path, excluded);
|
|
2627
|
+
}
|
|
2628
|
+
operationIds.set(candidate.path, indexed);
|
|
2629
|
+
}
|
|
2630
|
+
for (const factory of factoryIds.values()) {
|
|
2631
|
+
const indexed = operationIds.get(factory.path) ?? new Map<string, string>();
|
|
2632
|
+
const ids = [...factory.ids];
|
|
2633
|
+
if (ids.length === 1) {
|
|
2634
|
+
const operationId = ids[0];
|
|
2635
|
+
if (operationId !== undefined) indexed.set(factory.name, operationId);
|
|
2636
|
+
} else {
|
|
2637
|
+
indexed.delete(factory.name);
|
|
2638
|
+
const excluded = excludedBindings.get(factory.path) ?? new Set<string>();
|
|
2639
|
+
excluded.add(factory.name);
|
|
2640
|
+
excludedBindings.set(factory.path, excluded);
|
|
2641
|
+
refusals.push(
|
|
2642
|
+
refusal(
|
|
2643
|
+
relativePath(factory.path),
|
|
2644
|
+
factory.name,
|
|
2645
|
+
"factory_operation_id_ambiguous",
|
|
2646
|
+
`Factory ${factory.name} is registered under multiple operation ids: ${ids
|
|
2647
|
+
.map((id) => JSON.stringify(id))
|
|
2648
|
+
.join(", ")}. A shared operation body cannot own one examples locale namespace.`,
|
|
2649
|
+
),
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
operationIds.set(factory.path, indexed);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
const resolveStaticObject = (
|
|
2656
|
+
expressionInput: TS.Expression,
|
|
2657
|
+
path: string,
|
|
2658
|
+
active = new Set<string>(),
|
|
2659
|
+
): StaticObjectReference | undefined => {
|
|
2660
|
+
const expression = unwrapExpression(expressionInput);
|
|
2661
|
+
if (expression === undefined) return undefined;
|
|
2662
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
2663
|
+
const source = sources.get(path) ?? expression.getSourceFile();
|
|
2664
|
+
return { object: expression, source };
|
|
2665
|
+
}
|
|
2666
|
+
if (ts.isIdentifier(expression)) {
|
|
2667
|
+
const key = `${path}\0member\0${expression.text}`;
|
|
2668
|
+
if (active.has(key)) return undefined;
|
|
2669
|
+
const nextActive = new Set(active);
|
|
2670
|
+
nextActive.add(key);
|
|
2671
|
+
const resolved = resolveIdentifier(path, expression.text, nextActive);
|
|
2672
|
+
return resolved.status === "resolved"
|
|
2673
|
+
? resolveStaticObject(resolved.value.expression, resolved.value.path, nextActive)
|
|
2674
|
+
: undefined;
|
|
2675
|
+
}
|
|
2676
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
2677
|
+
const owner = resolveStaticObject(expression.expression, path, active);
|
|
2678
|
+
if (owner === undefined) return undefined;
|
|
2679
|
+
for (const property of owner.object.properties) {
|
|
2680
|
+
if (ts.isSpreadAssignment(property)) continue;
|
|
2681
|
+
if (staticPropertyName(property.name) !== expression.name.text) continue;
|
|
2682
|
+
const value = propertyValue(property);
|
|
2683
|
+
if (value === undefined) return undefined;
|
|
2684
|
+
return resolveStaticObject(value, owner.source.fileName, active);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return undefined;
|
|
2688
|
+
};
|
|
2689
|
+
|
|
2690
|
+
return {
|
|
2691
|
+
operationIds,
|
|
2692
|
+
operationSites,
|
|
2693
|
+
excludedBindings,
|
|
2694
|
+
refusals,
|
|
2695
|
+
declarations,
|
|
2696
|
+
discoveredCount,
|
|
2697
|
+
staticObjectResolverFor: (currentPath) => (expression, source) =>
|
|
2698
|
+
resolveStaticObject(expression, sources.has(source.fileName) ? source.fileName : currentPath),
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
function hasExportModifier(node: TS.Node): boolean {
|
|
2703
|
+
return (
|
|
2704
|
+
ts.canHaveModifiers(node) &&
|
|
2705
|
+
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
|
|
2706
|
+
true
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
function resolveStaticModule(
|
|
2711
|
+
containingPath: string,
|
|
2712
|
+
specifier: string,
|
|
2713
|
+
sources: ReadonlyMap<string, TS.SourceFile>,
|
|
2714
|
+
):
|
|
2715
|
+
| { readonly status: "resolved"; readonly path: string }
|
|
2716
|
+
| Exclude<StaticResolution, { status: "resolved" }> {
|
|
2717
|
+
if (!specifier.startsWith(".")) {
|
|
2718
|
+
return {
|
|
2719
|
+
status: "refused",
|
|
2720
|
+
detail: `Static operation discovery refuses non-relative import ${JSON.stringify(specifier)}.`,
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
const path = resolveLocalModule(containingPath, specifier, sources);
|
|
2724
|
+
if (path === undefined) {
|
|
2725
|
+
return {
|
|
2726
|
+
status: "refused",
|
|
2727
|
+
detail: `Static relative import ${JSON.stringify(specifier)} from ${containingPath} could not be resolved.`,
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
return { status: "resolved", path };
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
function providerConstructName(node: TS.PropertyAssignment): string {
|
|
2734
|
+
let current: TS.Node = node;
|
|
2735
|
+
while (current.parent !== undefined) {
|
|
2736
|
+
const parent = current.parent;
|
|
2737
|
+
if (ts.isCallExpression(parent)) {
|
|
2738
|
+
return providerCallConstruct(parent) ?? parent.expression.getText();
|
|
2739
|
+
}
|
|
2740
|
+
current = parent;
|
|
2741
|
+
}
|
|
2742
|
+
return "provider declaration";
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function providerCallConstruct(call: TS.CallExpression): string | undefined {
|
|
2746
|
+
if (
|
|
2747
|
+
ts.isIdentifier(call.expression) &&
|
|
2748
|
+
/(?:defineProvider|Provider)\b/.test(call.expression.text)
|
|
2749
|
+
) {
|
|
2750
|
+
return call.expression.text;
|
|
2751
|
+
}
|
|
2752
|
+
if (
|
|
2753
|
+
ts.isCallExpression(call.expression) &&
|
|
2754
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
2755
|
+
/\bdefineProvider\b/.test(call.expression.expression.text)
|
|
2756
|
+
) {
|
|
2757
|
+
return call.expression.expression.text;
|
|
2758
|
+
}
|
|
2759
|
+
return undefined;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
1971
2762
|
function buildOperationIdIndex(sourceFiles: readonly string[]): Map<string, Map<string, string>> {
|
|
1972
2763
|
const result = new Map<string, Map<string, string>>();
|
|
1973
2764
|
const idsByPath = new Map<string, Map<string, string | null>>();
|
|
@@ -2155,7 +2946,11 @@ function resolveLocalModule(
|
|
|
2155
2946
|
): string | undefined {
|
|
2156
2947
|
if (!specifier.startsWith(".")) return undefined;
|
|
2157
2948
|
const base = resolve(dirname(containingPath), specifier);
|
|
2158
|
-
|
|
2949
|
+
const emittedExtensionSource = /\.(?:c|m)?js$/.test(base)
|
|
2950
|
+
? base.replace(/\.(?:c|m)?js$/, ".ts")
|
|
2951
|
+
: undefined;
|
|
2952
|
+
for (const candidate of [base, emittedExtensionSource, `${base}.ts`, join(base, "index.ts")]) {
|
|
2953
|
+
if (candidate === undefined) continue;
|
|
2159
2954
|
if (sources.has(candidate)) return candidate;
|
|
2160
2955
|
}
|
|
2161
2956
|
return undefined;
|