@workos/oagen 0.29.1 → 0.29.3
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/dist/cli/index.mjs +489 -30
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +2 -2
- package/dist/compat/index.mjs +1 -1
- package/dist/{differ-Co0fRniL.d.mts → differ-DHldxZMr.d.mts} +3 -3
- package/dist/differ-DHldxZMr.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +350 -22
- package/dist/index.mjs.map +1 -1
- package/dist/{overlay-jsSkFJ2g.mjs → overlay-CihWp6dQ.mjs} +140 -9
- package/dist/overlay-CihWp6dQ.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.mjs +350 -22
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +20 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +350 -22
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C6HB4OrC.mjs.map +1 -1
- package/dist/{types-DqKSnLBP.d.mts → types-yrBidOg3.d.mts} +24 -1
- package/dist/types-yrBidOg3.d.mts.map +1 -0
- package/dist/verify/index.d.mts +2 -2
- package/dist/verify/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/differ-Co0fRniL.d.mts.map +0 -1
- package/dist/overlay-jsSkFJ2g.mjs.map +0 -1
- package/dist/types-DqKSnLBP.d.mts.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -2013,6 +2013,30 @@ function extractInlineModel(name, schema) {
|
|
|
2013
2013
|
}
|
|
2014
2014
|
//#endregion
|
|
2015
2015
|
//#region src/parser/operations.ts
|
|
2016
|
+
/**
|
|
2017
|
+
* Resolve a raw parameter entry to a concrete Parameter Object, following
|
|
2018
|
+
* `$ref`s into `components/parameters` when present. Follows chains of refs
|
|
2019
|
+
* (a component that itself is a `$ref` to another component), with a cycle
|
|
2020
|
+
* guard for a spec that refs itself.
|
|
2021
|
+
*
|
|
2022
|
+
* Throws when a `$ref` can't be resolved so a malformed spec fails loudly at
|
|
2023
|
+
* parse time instead of silently losing the parameter (see `extractParams`,
|
|
2024
|
+
* which used to drop any entry with no `in` field — every `$ref` object has
|
|
2025
|
+
* none).
|
|
2026
|
+
*/
|
|
2027
|
+
function resolveParameterRef(raw, componentParameters, operationContext) {
|
|
2028
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
2029
|
+
let current = raw;
|
|
2030
|
+
while ("$ref" in current) {
|
|
2031
|
+
if (seenRefs.has(current.$ref)) throw new Error(`Circular parameter $ref chain starting at "${current.$ref}" in ${operationContext}.`);
|
|
2032
|
+
seenRefs.add(current.$ref);
|
|
2033
|
+
const refName = /^#\/components\/parameters\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2034
|
+
const resolved = refName ? componentParameters?.[refName] : void 0;
|
|
2035
|
+
if (!resolved) throw new Error(`Unresolved parameter $ref "${current.$ref}" in ${operationContext}: no matching entry under components/parameters.`);
|
|
2036
|
+
current = resolved;
|
|
2037
|
+
}
|
|
2038
|
+
return current;
|
|
2039
|
+
}
|
|
2016
2040
|
const HTTP_METHODS = [
|
|
2017
2041
|
"get",
|
|
2018
2042
|
"post",
|
|
@@ -2023,7 +2047,7 @@ const HTTP_METHODS = [
|
|
|
2023
2047
|
"options",
|
|
2024
2048
|
"trace"
|
|
2025
2049
|
];
|
|
2026
|
-
function extractOperations(paths, operationIdTransform, componentSchemas) {
|
|
2050
|
+
function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters) {
|
|
2027
2051
|
if (!paths) return {
|
|
2028
2052
|
services: [],
|
|
2029
2053
|
inlineModels: []
|
|
@@ -2036,7 +2060,7 @@ function extractOperations(paths, operationIdTransform, componentSchemas) {
|
|
|
2036
2060
|
const op = pathItem[method];
|
|
2037
2061
|
if (!op) continue;
|
|
2038
2062
|
const serviceName = inferServiceName(path, op.tags?.[0]);
|
|
2039
|
-
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas);
|
|
2063
|
+
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters);
|
|
2040
2064
|
inlineModels.push(...opModels);
|
|
2041
2065
|
const ops = serviceMap.get(serviceName) ?? [];
|
|
2042
2066
|
ops.push(operation);
|
|
@@ -2234,19 +2258,11 @@ function extractParameterGroups(op, allIRParams, operationContext) {
|
|
|
2234
2258
|
* the request body. Emitters use the group's presence to generate sum-type
|
|
2235
2259
|
* interfaces and custom JSON marshalling instead of flat optional fields.
|
|
2236
2260
|
*/
|
|
2237
|
-
function extractBodyParameterGroups(op, operationContext) {
|
|
2261
|
+
function extractBodyParameterGroups(op, operationContext, componentSchemas) {
|
|
2238
2262
|
const raw = op["x-mutually-exclusive-body-groups"];
|
|
2239
2263
|
if (!raw || typeof raw !== "object") return void 0;
|
|
2240
2264
|
const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
|
|
2241
|
-
const variantFieldSchemas =
|
|
2242
|
-
if (bodySchema) {
|
|
2243
|
-
for (const sub of bodySchema.allOf ?? []) for (const variant of sub.oneOf ?? []) if (variant.properties) {
|
|
2244
|
-
for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
|
|
2245
|
-
}
|
|
2246
|
-
for (const variant of bodySchema.oneOf ?? []) if (variant.properties) {
|
|
2247
|
-
for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2265
|
+
const variantFieldSchemas = collectVariantFieldSchemas(bodySchema, componentSchemas);
|
|
2250
2266
|
const groups = [];
|
|
2251
2267
|
for (const [groupName, groupDef] of Object.entries(raw)) {
|
|
2252
2268
|
if (!groupDef || typeof groupDef !== "object") continue;
|
|
@@ -2256,10 +2272,12 @@ function extractBodyParameterGroups(op, operationContext) {
|
|
|
2256
2272
|
const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
|
|
2257
2273
|
if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-body-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
|
|
2258
2274
|
const parameters = paramNames.map((pName) => {
|
|
2259
|
-
const
|
|
2275
|
+
const entry = variantFieldSchemas.get(pName);
|
|
2276
|
+
const fieldSchema = entry?.schema;
|
|
2277
|
+
if (!fieldSchema) console.warn(`[oagen] Warning: x-mutually-exclusive-body-groups.${groupName}.variants.${variantName} references "${pName}" in ${operationContext}, but no oneOf branch of the request body declares that property. Falling back to type: string.`);
|
|
2260
2278
|
return {
|
|
2261
2279
|
name: pName,
|
|
2262
|
-
type: fieldSchema ? schemaToTypeRef(fieldSchema, toPascalCase(pName)) : {
|
|
2280
|
+
type: fieldSchema ? schemaToTypeRef(fieldSchema, toPascalCase(pName), entry?.owner) : {
|
|
2263
2281
|
kind: "primitive",
|
|
2264
2282
|
type: "string"
|
|
2265
2283
|
},
|
|
@@ -2283,8 +2301,14 @@ function extractBodyParameterGroups(op, operationContext) {
|
|
|
2283
2301
|
}
|
|
2284
2302
|
return groups.length > 0 ? groups : void 0;
|
|
2285
2303
|
}
|
|
2286
|
-
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas) {
|
|
2287
|
-
const
|
|
2304
|
+
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters) {
|
|
2305
|
+
const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
2306
|
+
const paramsByIdentity = /* @__PURE__ */ new Map();
|
|
2307
|
+
for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
|
|
2308
|
+
const param = resolveParameterRef(raw, componentParameters, opLabel);
|
|
2309
|
+
paramsByIdentity.set(`${param.in}:${param.name}`, param);
|
|
2310
|
+
}
|
|
2311
|
+
const allParams = [...paramsByIdentity.values()];
|
|
2288
2312
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
2289
2313
|
const opContext = serviceName;
|
|
2290
2314
|
const pathParams = extractParams(allParams, "path", opContext, componentSchemas);
|
|
@@ -2308,15 +2332,13 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2308
2332
|
};
|
|
2309
2333
|
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
2310
2334
|
const security = extractOperationSecurity(op.security);
|
|
2311
|
-
const
|
|
2335
|
+
const queryParamGroups = extractParameterGroups(op, [
|
|
2312
2336
|
...pathParams,
|
|
2313
2337
|
...queryParams,
|
|
2314
2338
|
...headerParams,
|
|
2315
2339
|
...cookieParams
|
|
2316
|
-
];
|
|
2317
|
-
const
|
|
2318
|
-
const queryParamGroups = extractParameterGroups(op, allIRParams, opLabel);
|
|
2319
|
-
const bodyParamGroups = extractBodyParameterGroups(op, opLabel);
|
|
2340
|
+
], opLabel);
|
|
2341
|
+
const bodyParamGroups = extractBodyParameterGroups(op, opLabel, componentSchemas);
|
|
2320
2342
|
const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
|
|
2321
2343
|
return {
|
|
2322
2344
|
operation: {
|
|
@@ -2399,6 +2421,55 @@ function resolveParamSchemaRef(schema, componentSchemas) {
|
|
|
2399
2421
|
if (!match) return void 0;
|
|
2400
2422
|
return componentSchemas[match[1]];
|
|
2401
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Collect the property schemas of every `oneOf` branch reachable from a request
|
|
2426
|
+
* body schema, keyed by property name. These are the candidate members of a
|
|
2427
|
+
* mutually-exclusive body group.
|
|
2428
|
+
*
|
|
2429
|
+
* The bundler runs with `dereference: false`, so the body schema is very often
|
|
2430
|
+
* a bare `{ $ref: '#/components/schemas/XDto' }` and the `allOf`/`oneOf`
|
|
2431
|
+
* composition that actually declares the variant properties lives on the
|
|
2432
|
+
* component. Walking without resolving refs finds nothing, and every group
|
|
2433
|
+
* member then falls back to `string` — an SDK that compiles but types
|
|
2434
|
+
* object-valued members (e.g. a connection's `saml_options`) as plain strings.
|
|
2435
|
+
*
|
|
2436
|
+
* Composition keywords nest and may themselves be refs, so resolve and recurse
|
|
2437
|
+
* at each level. `seen` guards against a schema that (directly or transitively)
|
|
2438
|
+
* refs itself.
|
|
2439
|
+
*/
|
|
2440
|
+
function collectVariantFieldSchemas(schema, componentSchemas, owner = void 0, out = /* @__PURE__ */ new Map(), seen = /* @__PURE__ */ new Set()) {
|
|
2441
|
+
if (!schema) return out;
|
|
2442
|
+
const ref = schema.$ref;
|
|
2443
|
+
if (ref) {
|
|
2444
|
+
if (seen.has(ref)) return out;
|
|
2445
|
+
seen.add(ref);
|
|
2446
|
+
const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
|
|
2447
|
+
return collectVariantFieldSchemas(match && componentSchemas ? componentSchemas[match[1]] : void 0, componentSchemas, match ? resolveSchemaName(match[1]) : owner, out, seen);
|
|
2448
|
+
}
|
|
2449
|
+
for (const variant of schema.oneOf ?? []) {
|
|
2450
|
+
const variantRef = variant.$ref;
|
|
2451
|
+
const refMatch = variantRef ? /^#\/components\/schemas\/(.+)$/.exec(variantRef) : null;
|
|
2452
|
+
const resolved = resolveSchemaRefObject(variant, componentSchemas) ?? variant;
|
|
2453
|
+
const variantOwner = refMatch ? resolveSchemaName(refMatch[1]) : owner;
|
|
2454
|
+
for (const [name, fieldSchema] of Object.entries(resolved.properties ?? {})) if (fieldSchema) out.set(name, {
|
|
2455
|
+
schema: fieldSchema,
|
|
2456
|
+
owner: variantOwner
|
|
2457
|
+
});
|
|
2458
|
+
collectVariantFieldSchemas(variant, componentSchemas, owner, out, seen);
|
|
2459
|
+
}
|
|
2460
|
+
for (const sub of schema.allOf ?? []) collectVariantFieldSchemas(sub, componentSchemas, owner, out, seen);
|
|
2461
|
+
for (const sub of schema.anyOf ?? []) collectVariantFieldSchemas(sub, componentSchemas, owner, out, seen);
|
|
2462
|
+
return out;
|
|
2463
|
+
}
|
|
2464
|
+
/** Resolve `{ $ref: '#/components/schemas/X' }` to its component, else undefined. */
|
|
2465
|
+
function resolveSchemaRefObject(schema, componentSchemas) {
|
|
2466
|
+
if (!schema || !componentSchemas) return void 0;
|
|
2467
|
+
const ref = schema.$ref;
|
|
2468
|
+
if (!ref) return void 0;
|
|
2469
|
+
const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
|
|
2470
|
+
if (!match) return void 0;
|
|
2471
|
+
return componentSchemas[match[1]];
|
|
2472
|
+
}
|
|
2402
2473
|
function extractRequestBody(body, op, inlineModels) {
|
|
2403
2474
|
if (!body?.content) return {};
|
|
2404
2475
|
let encoding = "json";
|
|
@@ -2811,6 +2882,262 @@ function collectInlineEnumsFromOperations(services, enums) {
|
|
|
2811
2882
|
]) collectInlineEnumFromRef(param.type, enums, enumNames);
|
|
2812
2883
|
}
|
|
2813
2884
|
//#endregion
|
|
2885
|
+
//#region src/parser/parameter-group-names.ts
|
|
2886
|
+
/**
|
|
2887
|
+
* Assign `ParameterGroup.wrapperName` across the whole spec.
|
|
2888
|
+
*
|
|
2889
|
+
* Emitters derive a group's generated wrapper type name from the group name
|
|
2890
|
+
* alone, so two operations sharing a group name (create and update both
|
|
2891
|
+
* declare `protocol_options`) collapse onto a single type. That is only sound
|
|
2892
|
+
* while the two declarations carry the same members. Where they differ, one
|
|
2893
|
+
* operation ends up typed with the other's member — e.g. a connection update
|
|
2894
|
+
* forced to pass `CreateConnectionSAMLOptions`, which accepts three fields the
|
|
2895
|
+
* PATCH endpoint rejects.
|
|
2896
|
+
*
|
|
2897
|
+
* This pass fingerprints every group's members structurally and qualifies the
|
|
2898
|
+
* wrapper name with the operation name only for groups that genuinely diverge.
|
|
2899
|
+
* Groups whose declarations agree keep the bare group name, so wrapper types
|
|
2900
|
+
* already published in the SDKs are never renamed.
|
|
2901
|
+
*/
|
|
2902
|
+
function assignParameterGroupWrapperNames(spec) {
|
|
2903
|
+
const modelMap = new Map(spec.models.map((m) => [m.name, m]));
|
|
2904
|
+
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
2905
|
+
const fingerprints = /* @__PURE__ */ new Map();
|
|
2906
|
+
const occurrences = [];
|
|
2907
|
+
for (const service of spec.services) for (const op of service.operations) for (const group of op.parameterGroups ?? []) {
|
|
2908
|
+
const fp = fingerprintGroup(group, modelMap, enumMap);
|
|
2909
|
+
let seen = fingerprints.get(group.name);
|
|
2910
|
+
if (!seen) {
|
|
2911
|
+
seen = /* @__PURE__ */ new Set();
|
|
2912
|
+
fingerprints.set(group.name, seen);
|
|
2913
|
+
}
|
|
2914
|
+
seen.add(fp);
|
|
2915
|
+
occurrences.push({
|
|
2916
|
+
service,
|
|
2917
|
+
op,
|
|
2918
|
+
group,
|
|
2919
|
+
fingerprint: fp
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
for (const { op, group } of occurrences) group.wrapperName = (fingerprints.get(group.name)?.size ?? 0) > 1 ? `${op.name}_${group.name}` : group.name;
|
|
2923
|
+
qualifyResidualCollisions(occurrences);
|
|
2924
|
+
reconcileSharedGroups(occurrences.map((o) => o.group));
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Break the collisions the operation-qualified name can still leave behind.
|
|
2928
|
+
*
|
|
2929
|
+
* `${op.name}_${group.name}` is not a unique key, for two separate reasons:
|
|
2930
|
+
*
|
|
2931
|
+
* - `disambiguateOperationNames` makes operation names unique only *within* a
|
|
2932
|
+
* service, so two services that each own an operation named `create`, both
|
|
2933
|
+
* declaring a `password` group with different members, both land on
|
|
2934
|
+
* `create_password`.
|
|
2935
|
+
* - Within one service it deliberately leaves same-name operations alone when
|
|
2936
|
+
* they share a path (see the `uniquePaths.size <= 1` guard — "same path,
|
|
2937
|
+
* different methods"), so `PUT` and `PATCH` on one path both stay `update`
|
|
2938
|
+
* and a service prefix alone still produces one name for both.
|
|
2939
|
+
*
|
|
2940
|
+
* Either way `reconcileSharedGroups` would then fuse two incompatible
|
|
2941
|
+
* declarations into a single type — the exact failure this pass exists to
|
|
2942
|
+
* prevent — and it does not drop the surplus member, so the wrapper would
|
|
2943
|
+
* expose a field one of the operations rejects.
|
|
2944
|
+
*
|
|
2945
|
+
* So escalate in rounds, adding a qualifier at a time and re-testing what still
|
|
2946
|
+
* collides. `(service, path, method)` identifies an operation uniquely, and a
|
|
2947
|
+
* same-name collision within a service implies a shared path, so service then
|
|
2948
|
+
* method is sufficient to separate every case.
|
|
2949
|
+
*
|
|
2950
|
+
* Each round only touches names that *still* group differing fingerprints, so a
|
|
2951
|
+
* name whose sharers agree is never renamed: sharing one wrapper is exactly what
|
|
2952
|
+
* should happen there, and renaming it would move a type the SDKs already
|
|
2953
|
+
* publish. On a spec where no qualified name collides — the WorkOS spec today —
|
|
2954
|
+
* the whole pass is a no-op.
|
|
2955
|
+
*/
|
|
2956
|
+
function qualifyResidualCollisions(occurrences) {
|
|
2957
|
+
const qualifiers = [(o) => o.service.name, (o) => o.op.httpMethod];
|
|
2958
|
+
for (const qualify of qualifiers) for (const sharers of collidingCandidates(occurrences)) {
|
|
2959
|
+
const movable = sharers.filter((o) => o.group.wrapperName !== o.group.name);
|
|
2960
|
+
for (const o of movable) o.group.wrapperName = `${qualify(o)}_${o.group.wrapperName ?? o.group.name}`;
|
|
2961
|
+
}
|
|
2962
|
+
separateRemainingByStructure(occurrences);
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Terminal round: separate whatever the operation attributes could not.
|
|
2966
|
+
*
|
|
2967
|
+
* No combination of operation attributes is unique by construction. Two
|
|
2968
|
+
* operations in one service can share a name, a method *and* differ only by
|
|
2969
|
+
* path — `normalizeOperationIdForNaming` strips the `[N]` suffix, so a NestJS
|
|
2970
|
+
* controller method bound to several routes derives one name for all of them.
|
|
2971
|
+
* The WorkOS spec already contains such a pair (`Authorization`'s
|
|
2972
|
+
* `listRoleAssignments` on two paths); it declares no parameter groups today,
|
|
2973
|
+
* so nothing is wrong in the SDKs, but the shape is present rather than
|
|
2974
|
+
* hypothetical. Adding `path` as a fourth qualifier would only move the
|
|
2975
|
+
* boundary again.
|
|
2976
|
+
*
|
|
2977
|
+
* So stop enumerating attributes and separate by the thing that actually
|
|
2978
|
+
* matters: a name still fusing distinct structures gets one wrapper per
|
|
2979
|
+
* distinct fingerprint. That makes "same `wrapperName` implies same structure"
|
|
2980
|
+
* hold unconditionally, which is the invariant every emitter relies on.
|
|
2981
|
+
*
|
|
2982
|
+
* Suffixes are keyed on the fingerprint, not on a per-occurrence counter, so
|
|
2983
|
+
* declarations that agree keep sharing a wrapper.
|
|
2984
|
+
*
|
|
2985
|
+
* Ordering is by first appearance, not by sorted fingerprint: the structure
|
|
2986
|
+
* declared first keeps the unsuffixed name. Sorting by fingerprint would be
|
|
2987
|
+
* equally deterministic but less stable in the way that matters — adding a
|
|
2988
|
+
* member to one operation changes its fingerprint and could swap which of two
|
|
2989
|
+
* published wrappers holds the bare name. Under first-appearance order only
|
|
2990
|
+
* reordering the operations themselves can shift a name, which is both rarer
|
|
2991
|
+
* and visible in the spec diff.
|
|
2992
|
+
*
|
|
2993
|
+
* Either way this is a no-op wherever an earlier round already sufficed —
|
|
2994
|
+
* including the WorkOS spec, which never reaches this round at all.
|
|
2995
|
+
*/
|
|
2996
|
+
function separateRemainingByStructure(occurrences) {
|
|
2997
|
+
const taken = new Set(occurrences.map((o) => o.group.wrapperName ?? o.group.name));
|
|
2998
|
+
for (const sharers of collidingCandidates(occurrences)) {
|
|
2999
|
+
const base = sharers[0].group.wrapperName ?? sharers[0].group.name;
|
|
3000
|
+
const nameFor = /* @__PURE__ */ new Map();
|
|
3001
|
+
let n = 2;
|
|
3002
|
+
for (const o of sharers) {
|
|
3003
|
+
if (nameFor.has(o.fingerprint)) continue;
|
|
3004
|
+
if (nameFor.size === 0) {
|
|
3005
|
+
nameFor.set(o.fingerprint, base);
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
while (taken.has(`${base}_${n}`)) n++;
|
|
3009
|
+
const name = `${base}_${n}`;
|
|
3010
|
+
taken.add(name);
|
|
3011
|
+
nameFor.set(o.fingerprint, name);
|
|
3012
|
+
n++;
|
|
3013
|
+
}
|
|
3014
|
+
for (const o of sharers) o.group.wrapperName = nameFor.get(o.fingerprint);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* Candidate wrapper names shared by declarations that do not agree — the only
|
|
3019
|
+
* ones an escalation round may rename.
|
|
3020
|
+
*/
|
|
3021
|
+
function collidingCandidates(occurrences) {
|
|
3022
|
+
const byCandidate = /* @__PURE__ */ new Map();
|
|
3023
|
+
for (const o of occurrences) {
|
|
3024
|
+
const key = o.group.wrapperName ?? o.group.name;
|
|
3025
|
+
const list = byCandidate.get(key);
|
|
3026
|
+
if (list) list.push(o);
|
|
3027
|
+
else byCandidate.set(key, [o]);
|
|
3028
|
+
}
|
|
3029
|
+
return [...byCandidate.values()].filter((sharers) => sharers.length > 1 && new Set(sharers.map((o) => o.fingerprint)).size > 1);
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Make every group that shares a `wrapperName` describe literally the same
|
|
3033
|
+
* type.
|
|
3034
|
+
*
|
|
3035
|
+
* Deciding two declarations may share one wrapper is only half the job. The
|
|
3036
|
+
* fingerprint deliberately ignores nullability and member order, so sharers can
|
|
3037
|
+
* still disagree on both — and each emitter renders the wrapper from whichever
|
|
3038
|
+
* operation it happens to reach first while rendering call sites from their own
|
|
3039
|
+
* operation. Kotlin showed the consequence: `ParentResource.ById.id` emitted as
|
|
3040
|
+
* `String` from one operation, while the Java-friendly overload built from
|
|
3041
|
+
* another passed a `String?` into it, and the SDK stopped compiling.
|
|
3042
|
+
*
|
|
3043
|
+
* So normalize the two axes the fingerprint waived:
|
|
3044
|
+
* - a member the sharers disagree on settles as non-nullable. Widening to
|
|
3045
|
+
* nullable instead reads as the friendlier choice, but the nullable form
|
|
3046
|
+
* doesn't survive every emitter's group paths (Kotlin's query dispatch
|
|
3047
|
+
* builds a `Pair<String, String>` from the member, which a `String?` can't
|
|
3048
|
+
* satisfy), and non-nullable is what all sharers carried before group
|
|
3049
|
+
* member types were resolved at all — so it changes no published surface.
|
|
3050
|
+
* A member every sharer agrees is nullable stays nullable.
|
|
3051
|
+
*
|
|
3052
|
+
* The cost: an operation whose spec really does accept null for a member
|
|
3053
|
+
* (`PATCH` on a resource may clear `parent_resource_id`) can't express that
|
|
3054
|
+
* through the shared wrapper. It never could — the member used to be a flat
|
|
3055
|
+
* `string` everywhere — so nothing regresses, but the limitation is real and
|
|
3056
|
+
* removing it means letting that group's wrapper split per operation.
|
|
3057
|
+
* - member order follows the first sharer, so positional constructors and
|
|
3058
|
+
* their call sites agree.
|
|
3059
|
+
*/
|
|
3060
|
+
function reconcileSharedGroups(groups) {
|
|
3061
|
+
const byWrapper = /* @__PURE__ */ new Map();
|
|
3062
|
+
for (const group of groups) {
|
|
3063
|
+
const key = group.wrapperName ?? group.name;
|
|
3064
|
+
const list = byWrapper.get(key);
|
|
3065
|
+
if (list) list.push(group);
|
|
3066
|
+
else byWrapper.set(key, [group]);
|
|
3067
|
+
}
|
|
3068
|
+
for (const sharers of byWrapper.values()) {
|
|
3069
|
+
if (sharers.length < 2) continue;
|
|
3070
|
+
for (const variant of sharers[0].variants) {
|
|
3071
|
+
const peers = sharers.slice(1).map((g) => g.variants.find((v) => v.name === variant.name)).filter((v) => v !== void 0);
|
|
3072
|
+
if (peers.length === 0) continue;
|
|
3073
|
+
for (const member of variant.parameters) {
|
|
3074
|
+
const all = [member, ...peers.map((p) => p.parameters.find((q) => q.name === member.name)).filter(isParameter)];
|
|
3075
|
+
if (all.every((p) => p.type.kind === "nullable")) continue;
|
|
3076
|
+
for (const p of all) if (p.type.kind === "nullable") p.type = p.type.inner;
|
|
3077
|
+
}
|
|
3078
|
+
const canonical = variant.parameters.map((p) => p.name);
|
|
3079
|
+
for (const peer of peers) peer.parameters.sort((a, b) => {
|
|
3080
|
+
const ia = canonical.indexOf(a.name);
|
|
3081
|
+
const ib = canonical.indexOf(b.name);
|
|
3082
|
+
if (ia === -1 && ib === -1) return 0;
|
|
3083
|
+
if (ia === -1) return 1;
|
|
3084
|
+
if (ib === -1) return -1;
|
|
3085
|
+
return ia - ib;
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function isParameter(p) {
|
|
3091
|
+
return p !== void 0;
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* Structural fingerprint of a group: its variants, their members, and each
|
|
3095
|
+
* member's resolved shape. Two declarations with the same fingerprint can
|
|
3096
|
+
* safely share one generated wrapper type.
|
|
3097
|
+
*/
|
|
3098
|
+
function fingerprintGroup(group, modelMap, enumMap) {
|
|
3099
|
+
return group.variants.map((v) => {
|
|
3100
|
+
const members = [...v.parameters].sort((a, b) => a.name.localeCompare(b.name)).map((p) => `${p.name}:${fingerprintType(p.type, modelMap, enumMap, /* @__PURE__ */ new Set())}`);
|
|
3101
|
+
const optional = [...v.optionalParameters ?? []].sort();
|
|
3102
|
+
return `${v.name}(${members.join(",")})[${optional.join(",")}]`;
|
|
3103
|
+
}).join("|");
|
|
3104
|
+
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Fingerprint a type by shape rather than by name.
|
|
3107
|
+
*
|
|
3108
|
+
* Names cannot be compared directly because the parser synthesizes a separate
|
|
3109
|
+
* name per declaration site for inline schemas: `create_user` and `update_user`
|
|
3110
|
+
* each get their own `PasswordHashType` enum, spelled `CreateUserPasswordHashType`
|
|
3111
|
+
* and `UpdateUserPasswordHashType`, though their values are identical. Comparing
|
|
3112
|
+
* names would report divergence and rename a wrapper for no reason.
|
|
3113
|
+
*
|
|
3114
|
+
* Nullability is also stripped: an update declaring a member nullable while
|
|
3115
|
+
* create declares it plain is not a reason to split the wrapper type.
|
|
3116
|
+
*/
|
|
3117
|
+
function fingerprintType(ref, modelMap, enumMap, visiting) {
|
|
3118
|
+
switch (ref.kind) {
|
|
3119
|
+
case "nullable": return fingerprintType(ref.inner, modelMap, enumMap, visiting);
|
|
3120
|
+
case "primitive": return `p:${ref.type}`;
|
|
3121
|
+
case "literal": return `l:${typeof ref.value}:${String(ref.value)}`;
|
|
3122
|
+
case "array": return `a<${fingerprintType(ref.items, modelMap, enumMap, visiting)}>`;
|
|
3123
|
+
case "map": return `m<${fingerprintType(ref.valueType, modelMap, enumMap, visiting)}>`;
|
|
3124
|
+
case "union": return `u<${ref.variants.map((v) => fingerprintType(v, modelMap, enumMap, visiting)).sort().join(",")}>`;
|
|
3125
|
+
case "enum": {
|
|
3126
|
+
const e = enumMap.get(ref.name);
|
|
3127
|
+
if (!e) return `e:${ref.name}`;
|
|
3128
|
+
return `e<${e.values.map((v) => `${typeof v.value}:${String(v.value)}`).sort().join(",")}>`;
|
|
3129
|
+
}
|
|
3130
|
+
case "model": {
|
|
3131
|
+
const m = modelMap.get(ref.name);
|
|
3132
|
+
if (!m) return `M:${ref.name}`;
|
|
3133
|
+
if (visiting.has(ref.name)) return `M:${ref.name}`;
|
|
3134
|
+
const next = new Set(visiting).add(ref.name);
|
|
3135
|
+
return `M<${[...m.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}${f.required ? "!" : "?"}:${fingerprintType(f.type, modelMap, enumMap, next)}`).join(",")}>`;
|
|
3136
|
+
}
|
|
3137
|
+
default: return "unknown";
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
//#endregion
|
|
2814
3141
|
//#region src/parser/normalize-model-refs.ts
|
|
2815
3142
|
function validateModelRefs(spec) {
|
|
2816
3143
|
const knownNames = /* @__PURE__ */ new Set();
|
|
@@ -2840,7 +3167,7 @@ async function parseSpec(specPath, options) {
|
|
|
2840
3167
|
const version = spec.openapi ?? "";
|
|
2841
3168
|
if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
|
|
2842
3169
|
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
2843
|
-
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas);
|
|
3170
|
+
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters);
|
|
2844
3171
|
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
2845
3172
|
const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
|
|
2846
3173
|
clearSchemaNameTransform();
|
|
@@ -2865,6 +3192,7 @@ async function parseSpec(specPath, options) {
|
|
|
2865
3192
|
auth,
|
|
2866
3193
|
sdk: defaultSdkBehavior()
|
|
2867
3194
|
};
|
|
3195
|
+
assignParameterGroupWrapperNames(result);
|
|
2868
3196
|
validateModelRefs(result);
|
|
2869
3197
|
return result;
|
|
2870
3198
|
}
|
|
@@ -8165,7 +8493,10 @@ function apiSurfaceToSnapshot(surface) {
|
|
|
8165
8493
|
}
|
|
8166
8494
|
return {
|
|
8167
8495
|
schemaVersion: "1",
|
|
8168
|
-
source: {
|
|
8496
|
+
source: {
|
|
8497
|
+
extractedAt: surface.extractedAt,
|
|
8498
|
+
language
|
|
8499
|
+
},
|
|
8169
8500
|
policies: getDefaultPolicy(language),
|
|
8170
8501
|
symbols
|
|
8171
8502
|
};
|
|
@@ -8395,10 +8726,138 @@ function severityMeetsThreshold(severity, threshold) {
|
|
|
8395
8726
|
//#endregion
|
|
8396
8727
|
//#region src/compat/classify.ts
|
|
8397
8728
|
/**
|
|
8729
|
+
* Normalize a type annotation string to a canonical form before equality
|
|
8730
|
+
* comparison. Emitters evolve their annotation style across releases (e.g.
|
|
8731
|
+
* Python's PEP 604 `X | None` replacing `Optional[X]`, PEP 585 lowercase
|
|
8732
|
+
* generics, or dropping quotes from forward refs); such changes are cosmetic
|
|
8733
|
+
* and must not register as breaking type changes. Only Python needs this
|
|
8734
|
+
* today — other languages encode optionality with a single stable spelling
|
|
8735
|
+
* (`*Foo`, `Option<T>`, `T?`, `T.nilable(Foo)`) so they use exact comparison.
|
|
8736
|
+
* String-literal *values* inside `Literal[...]` are preserved verbatim so a
|
|
8737
|
+
* genuine change to a wire value stays visible. When `language` is absent
|
|
8738
|
+
* (older snapshots) no normalization is applied, preserving prior behaviour.
|
|
8739
|
+
*/
|
|
8740
|
+
function normalizeType(type, language) {
|
|
8741
|
+
if (!type) return type;
|
|
8742
|
+
if (language !== "python") return type;
|
|
8743
|
+
return canonicalizePythonType(type);
|
|
8744
|
+
}
|
|
8745
|
+
/** PEP 585 capitalized builtins that map to their lowercase spellings, so
|
|
8746
|
+
* `List[str]` and `list[str]` compare equal. */
|
|
8747
|
+
const PEP585_BUILTINS = {
|
|
8748
|
+
List: "list",
|
|
8749
|
+
Dict: "dict",
|
|
8750
|
+
Tuple: "tuple",
|
|
8751
|
+
Set: "set",
|
|
8752
|
+
FrozenSet: "frozenset",
|
|
8753
|
+
Type: "type"
|
|
8754
|
+
};
|
|
8755
|
+
/** Recursively canonicalize a Python annotation. Structural forms are
|
|
8756
|
+
* normalized (`Optional[X]`→`X | None`, `Union[...]`→` | ` joins, PEP 585
|
|
8757
|
+
* builtins, quoted forward refs, parenthesized unions), but `Literal[...]`
|
|
8758
|
+
* arguments are preserved verbatim — their quotes and inner whitespace are
|
|
8759
|
+
* part of the wire value, not cosmetic. */
|
|
8760
|
+
function canonicalizePythonType(s) {
|
|
8761
|
+
const t = s.trim();
|
|
8762
|
+
if (t.length === 0) return t;
|
|
8763
|
+
if (t.startsWith("(") && t.endsWith(")")) return canonicalizePythonType(t.slice(1, -1));
|
|
8764
|
+
if (t.startsWith("Literal[") && t.endsWith("]")) return `Literal[${splitTopLevel(t.slice(8, -1), ",").map((p) => p.trim()).filter((p) => p.length > 0).map(canonicalizeLiteralArg).join(", ")}]`;
|
|
8765
|
+
if (t.startsWith("Optional[") && t.endsWith("]")) return `${canonicalizePythonType(t.slice(9, -1))} | None`;
|
|
8766
|
+
if (t.startsWith("Union[") && t.endsWith("]")) return splitTopLevel(t.slice(6, -1), ",").map((p) => p.trim()).filter((p) => p.length > 0).map(canonicalizePythonType).join(" | ");
|
|
8767
|
+
if (t.includes("|")) {
|
|
8768
|
+
const arms = splitTopLevel(t, "|").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
8769
|
+
if (arms.length > 1) return arms.map(canonicalizePythonType).join(" | ");
|
|
8770
|
+
}
|
|
8771
|
+
const open = t.indexOf("[");
|
|
8772
|
+
if (open !== -1 && t.endsWith("]")) {
|
|
8773
|
+
const outer = t.slice(0, open).trim();
|
|
8774
|
+
const inner = t.slice(open + 1, -1);
|
|
8775
|
+
return `${PEP585_BUILTINS[outer] ?? outer}[${splitTopLevel(inner, ",").map((p) => p.trim()).filter((p) => p.length > 0).map(canonicalizePythonType).join(", ")}]`;
|
|
8776
|
+
}
|
|
8777
|
+
if (t.startsWith("\"") && t.endsWith("\"") && t.length >= 2 || t.startsWith("'") && t.endsWith("'") && t.length >= 2) return t.slice(1, -1);
|
|
8778
|
+
return PEP585_BUILTINS[t] ?? t;
|
|
8779
|
+
}
|
|
8780
|
+
/** Canonicalize one `Literal[...]` argument so semantically identical string
|
|
8781
|
+
* values compare equal regardless of quote style or escape form. The value
|
|
8782
|
+
* is decoded (backslash escapes processed) then re-encoded in a canonical
|
|
8783
|
+
* double-quoted form, so `Literal['say "hi"']` and `Literal["say \\"hi\\""]`
|
|
8784
|
+
* both canonicalize to `"say \"hi\""`. Bare (unquoted) arguments — e.g. an
|
|
8785
|
+
* enum name or a number — are returned unchanged. */
|
|
8786
|
+
function canonicalizeLiteralArg(arg) {
|
|
8787
|
+
const a = arg.trim();
|
|
8788
|
+
if (a.length < 2) return a;
|
|
8789
|
+
let quote = null;
|
|
8790
|
+
if (a.startsWith("\"") && a.endsWith("\"")) quote = "\"";
|
|
8791
|
+
else if (a.startsWith("'") && a.endsWith("'")) quote = "'";
|
|
8792
|
+
if (!quote) return a;
|
|
8793
|
+
return `"${encodeDoubleQuoted(decodeStringLiteral(a.slice(1, -1)))}"`;
|
|
8794
|
+
}
|
|
8795
|
+
/** Decode the backslash escapes inside a Python string literal's content.
|
|
8796
|
+
* Only the delimiter-relevant escapes (`\\`, `\'`, `\"`) are processed;
|
|
8797
|
+
* other sequences (`\n`, `\t`, `\u…`) are kept verbatim since they're
|
|
8798
|
+
* written identically regardless of quote style and so compare equal already. */
|
|
8799
|
+
function decodeStringLiteral(content) {
|
|
8800
|
+
let out = "";
|
|
8801
|
+
for (let i = 0; i < content.length; i++) {
|
|
8802
|
+
const c = content[i];
|
|
8803
|
+
if (c === "\\" && i + 1 < content.length) {
|
|
8804
|
+
const next = content[i + 1];
|
|
8805
|
+
if (next === "\\" || next === "\"" || next === "'") {
|
|
8806
|
+
out += next;
|
|
8807
|
+
i++;
|
|
8808
|
+
continue;
|
|
8809
|
+
}
|
|
8810
|
+
out += c + next;
|
|
8811
|
+
i++;
|
|
8812
|
+
continue;
|
|
8813
|
+
}
|
|
8814
|
+
out += c;
|
|
8815
|
+
}
|
|
8816
|
+
return out;
|
|
8817
|
+
}
|
|
8818
|
+
/** Re-encode a decoded value as a canonical double-quoted Python string. */
|
|
8819
|
+
function encodeDoubleQuoted(value) {
|
|
8820
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
8821
|
+
}
|
|
8822
|
+
/** Split on a separator that sits at bracket-depth 0 and outside string
|
|
8823
|
+
* literals, so commas/pipes inside `Literal["a,b"]` or `"x|y"` are not split.
|
|
8824
|
+
* An escaped matching quote (`\"` inside a double-quoted string) does not
|
|
8825
|
+
* close the string: only a quote preceded by an even number of backslashes
|
|
8826
|
+
* (zero counts) is a real closing delimiter. */
|
|
8827
|
+
function splitTopLevel(s, sep) {
|
|
8828
|
+
const parts = [];
|
|
8829
|
+
let depth = 0;
|
|
8830
|
+
let inStr = null;
|
|
8831
|
+
let start = 0;
|
|
8832
|
+
for (let i = 0; i < s.length; i++) {
|
|
8833
|
+
const c = s[i];
|
|
8834
|
+
if (inStr) {
|
|
8835
|
+
if (c === inStr) {
|
|
8836
|
+
let precedingBackslashes = 0;
|
|
8837
|
+
for (let j = i - 1; j >= 0 && s[j] === "\\"; j--) precedingBackslashes++;
|
|
8838
|
+
if (precedingBackslashes % 2 === 0) inStr = null;
|
|
8839
|
+
}
|
|
8840
|
+
continue;
|
|
8841
|
+
}
|
|
8842
|
+
if (c === "\"" || c === "'") {
|
|
8843
|
+
inStr = c;
|
|
8844
|
+
continue;
|
|
8845
|
+
}
|
|
8846
|
+
if (c === "[") depth++;
|
|
8847
|
+
else if (c === "]") depth--;
|
|
8848
|
+
else if (c === sep && depth === 0) {
|
|
8849
|
+
parts.push(s.slice(start, i));
|
|
8850
|
+
start = i + 1;
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
parts.push(s.slice(start));
|
|
8854
|
+
return parts;
|
|
8855
|
+
}
|
|
8856
|
+
/**
|
|
8398
8857
|
* Classify changes between a baseline and candidate symbol.
|
|
8399
8858
|
* Returns one or more classified changes for the diff.
|
|
8400
8859
|
*/
|
|
8401
|
-
function classifySymbolChanges(baseline, candidate, policy) {
|
|
8860
|
+
function classifySymbolChanges(baseline, candidate, policy, language) {
|
|
8402
8861
|
const changes = [];
|
|
8403
8862
|
const specRef = baseline.schemaName ?? candidate?.schemaName;
|
|
8404
8863
|
if (!candidate) {
|
|
@@ -8422,8 +8881,8 @@ function classifySymbolChanges(baseline, candidate, policy) {
|
|
|
8422
8881
|
policy,
|
|
8423
8882
|
specRef
|
|
8424
8883
|
}));
|
|
8425
|
-
if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy, specRef));
|
|
8426
|
-
if (baseline.returns && candidate.returns && baseline.returns.name !== candidate.returns.name) changes.push(makeChange({
|
|
8884
|
+
if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy, specRef, language));
|
|
8885
|
+
if (baseline.returns && candidate.returns && normalizeType(baseline.returns.name, language) !== normalizeType(candidate.returns.name, language)) changes.push(makeChange({
|
|
8427
8886
|
category: "return_type_changed",
|
|
8428
8887
|
symbol: baseline.fqName,
|
|
8429
8888
|
old: { returnType: baseline.returns.name },
|
|
@@ -8441,7 +8900,7 @@ function classifySymbolChanges(baseline, candidate, policy) {
|
|
|
8441
8900
|
policy,
|
|
8442
8901
|
specRef
|
|
8443
8902
|
}));
|
|
8444
|
-
if (baseline.typeRef && candidate.typeRef && baseline.typeRef.name !== candidate.typeRef.name) changes.push(makeChange({
|
|
8903
|
+
if (baseline.typeRef && candidate.typeRef && normalizeType(baseline.typeRef.name, language) !== normalizeType(candidate.typeRef.name, language)) changes.push(makeChange({
|
|
8445
8904
|
category: "field_type_changed",
|
|
8446
8905
|
symbol: baseline.fqName,
|
|
8447
8906
|
old: { type: baseline.typeRef.name },
|
|
@@ -8463,7 +8922,7 @@ function classifySymbolChanges(baseline, candidate, policy) {
|
|
|
8463
8922
|
/**
|
|
8464
8923
|
* Classify parameter-level changes between two symbol versions.
|
|
8465
8924
|
*/
|
|
8466
|
-
function classifyParameterChanges(baseline, candidate, policy, specRef) {
|
|
8925
|
+
function classifyParameterChanges(baseline, candidate, policy, specRef, language) {
|
|
8467
8926
|
const changes = [];
|
|
8468
8927
|
const baseParams = baseline.parameters ?? [];
|
|
8469
8928
|
const candParams = candidate.parameters ?? [];
|
|
@@ -8512,7 +8971,7 @@ function classifyParameterChanges(baseline, candidate, policy, specRef) {
|
|
|
8512
8971
|
policy,
|
|
8513
8972
|
specRef
|
|
8514
8973
|
}));
|
|
8515
|
-
if (baseParam.type.name !== candParam.type.name) changes.push(makeChange({
|
|
8974
|
+
if (normalizeType(baseParam.type.name, language) !== normalizeType(candParam.type.name, language)) changes.push(makeChange({
|
|
8516
8975
|
category: "parameter_type_narrowed",
|
|
8517
8976
|
symbol: baseline.fqName,
|
|
8518
8977
|
old: {
|
|
@@ -8869,7 +9328,7 @@ function diffSnapshots(baseline, candidate, policy) {
|
|
|
8869
9328
|
for (const baseSym of baseline.symbols) {
|
|
8870
9329
|
if (isServiceWrapperConstructor(baseSym, serviceAccessors)) continue;
|
|
8871
9330
|
const candSym = candById.get(baseSym.id) ?? candByFqName.get(baseSym.fqName);
|
|
8872
|
-
changes.push(...classifySymbolChanges(baseSym, candSym, effectivePolicy));
|
|
9331
|
+
changes.push(...classifySymbolChanges(baseSym, candSym, effectivePolicy, baseline.source.language));
|
|
8873
9332
|
}
|
|
8874
9333
|
for (const candSym of candidate.symbols) if (!baseByFqName.has(candSym.fqName)) {
|
|
8875
9334
|
if (isServiceWrapperConstructor(candSym, serviceAccessors)) continue;
|