@formspec/build 0.1.0-alpha.33 → 0.1.0-alpha.35

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.
@@ -1677,6 +1677,9 @@ function extractDefaultValueAnnotation(initializer, file = "") {
1677
1677
  function isObjectType(type) {
1678
1678
  return !!(type.flags & ts3.TypeFlags.Object);
1679
1679
  }
1680
+ function isIntersectionType(type) {
1681
+ return !!(type.flags & ts3.TypeFlags.Intersection);
1682
+ }
1680
1683
  function isTypeReference(type) {
1681
1684
  return !!(type.flags & ts3.TypeFlags.Object) && !!(type.objectFlags & ts3.ObjectFlags.Reference);
1682
1685
  }
@@ -1697,10 +1700,11 @@ function makeParseOptions(extensionRegistry, fieldType, checker, subjectType, ho
1697
1700
  ...hostType !== void 0 && { hostType }
1698
1701
  };
1699
1702
  }
1700
- function createAnalyzerMetadataPolicy(input) {
1703
+ function createAnalyzerMetadataPolicy(input, discriminator) {
1701
1704
  return {
1702
1705
  raw: input,
1703
- normalized: normalizeMetadataPolicy(input)
1706
+ normalized: normalizeMetadataPolicy(input),
1707
+ discriminator
1704
1708
  };
1705
1709
  }
1706
1710
  function resolveNodeMetadata(metadataPolicy, declarationKind, logicalName, node, checker, extensionRegistry, buildContext) {
@@ -1739,8 +1743,11 @@ function resolveNodeMetadata(metadataPolicy, declarationKind, logicalName, node,
1739
1743
  }
1740
1744
  return resolvedMetadata;
1741
1745
  }
1742
- function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry, metadataPolicy) {
1743
- const normalizedMetadataPolicy = createAnalyzerMetadataPolicy(metadataPolicy);
1746
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry, metadataPolicy, discriminatorOptions) {
1747
+ const normalizedMetadataPolicy = createAnalyzerMetadataPolicy(
1748
+ metadataPolicy,
1749
+ discriminatorOptions
1750
+ );
1744
1751
  const name = classDecl.name?.text ?? "AnonymousClass";
1745
1752
  const fields = [];
1746
1753
  const fieldLayouts = [];
@@ -1821,8 +1828,11 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry, meta
1821
1828
  staticMethods
1822
1829
  };
1823
1830
  }
1824
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry, metadataPolicy) {
1825
- const normalizedMetadataPolicy = createAnalyzerMetadataPolicy(metadataPolicy);
1831
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry, metadataPolicy, discriminatorOptions) {
1832
+ const normalizedMetadataPolicy = createAnalyzerMetadataPolicy(
1833
+ metadataPolicy,
1834
+ discriminatorOptions
1835
+ );
1826
1836
  const name = interfaceDecl.name.text;
1827
1837
  const fields = [];
1828
1838
  const typeRegistry = {};
@@ -1890,19 +1900,31 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1890
1900
  staticMethods: []
1891
1901
  };
1892
1902
  }
1893
- function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry, metadataPolicy) {
1894
- if (!ts3.isTypeLiteralNode(typeAlias.type)) {
1903
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry, metadataPolicy, discriminatorOptions) {
1904
+ const members = getObjectLikeTypeAliasMembers(typeAlias.type);
1905
+ if (members === null) {
1895
1906
  const sourceFile = typeAlias.getSourceFile();
1896
1907
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
1897
1908
  const kindDesc = ts3.SyntaxKind[typeAlias.type.kind] ?? "unknown";
1898
1909
  return {
1899
1910
  ok: false,
1900
- error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
1911
+ error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object-like type alias (found ${kindDesc})`
1901
1912
  };
1902
1913
  }
1903
- const typeLiteral = typeAlias.type;
1904
- const normalizedMetadataPolicy = createAnalyzerMetadataPolicy(metadataPolicy);
1914
+ const normalizedMetadataPolicy = createAnalyzerMetadataPolicy(
1915
+ metadataPolicy,
1916
+ discriminatorOptions
1917
+ );
1905
1918
  const name = typeAlias.name.text;
1919
+ const duplicatePropertyNames = findDuplicateObjectLikeTypeAliasPropertyNames(members);
1920
+ if (duplicatePropertyNames.length > 0) {
1921
+ const sourceFile = typeAlias.getSourceFile();
1922
+ const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
1923
+ return {
1924
+ ok: false,
1925
+ error: `Type alias "${name}" at line ${String(line + 1)} contains duplicate property names across object-like members: ${duplicatePropertyNames.join(", ")}`
1926
+ };
1927
+ }
1906
1928
  const fields = [];
1907
1929
  const typeRegistry = {};
1908
1930
  const diagnostics = [];
@@ -1915,7 +1937,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry,
1915
1937
  const annotations = [...typeAliasDoc.annotations];
1916
1938
  diagnostics.push(...typeAliasDoc.diagnostics);
1917
1939
  const visiting = /* @__PURE__ */ new Set();
1918
- for (const member of typeLiteral.members) {
1940
+ for (const member of members) {
1919
1941
  if (ts3.isPropertySignature(member)) {
1920
1942
  const fieldNode = analyzeInterfacePropertyToIR(
1921
1943
  member,
@@ -2024,15 +2046,43 @@ function isNullishSemanticType(type) {
2024
2046
  }
2025
2047
  return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
2026
2048
  }
2027
- function isStringLikeSemanticType(type) {
2049
+ function isStringLikeSemanticType(type, checker, seen = /* @__PURE__ */ new Set()) {
2050
+ if (seen.has(type)) {
2051
+ return false;
2052
+ }
2053
+ seen.add(type);
2028
2054
  if (type.flags & ts3.TypeFlags.StringLike) {
2029
2055
  return true;
2030
2056
  }
2031
2057
  if (type.isUnion()) {
2032
- return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
2058
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member, checker, seen));
2059
+ }
2060
+ const baseConstraint = checker.getBaseConstraintOfType(type);
2061
+ if (baseConstraint !== void 0 && baseConstraint !== type) {
2062
+ return isStringLikeSemanticType(baseConstraint, checker, seen);
2033
2063
  }
2034
2064
  return false;
2035
2065
  }
2066
+ function getObjectLikeTypeAliasMembers(typeNode) {
2067
+ if (ts3.isParenthesizedTypeNode(typeNode)) {
2068
+ return getObjectLikeTypeAliasMembers(typeNode.type);
2069
+ }
2070
+ if (ts3.isTypeLiteralNode(typeNode)) {
2071
+ return [...typeNode.members];
2072
+ }
2073
+ if (ts3.isIntersectionTypeNode(typeNode)) {
2074
+ const members = [];
2075
+ for (const intersectionMember of typeNode.types) {
2076
+ const resolvedMembers = getObjectLikeTypeAliasMembers(intersectionMember);
2077
+ if (resolvedMembers === null) {
2078
+ return null;
2079
+ }
2080
+ members.push(...resolvedMembers);
2081
+ }
2082
+ return members;
2083
+ }
2084
+ return null;
2085
+ }
2036
2086
  function extractDiscriminatorDirective(node, file, diagnostics) {
2037
2087
  const discriminatorTags = getLeadingParsedTags(node).filter(
2038
2088
  (tag) => tag.normalizedTagName === "discriminator"
@@ -2139,7 +2189,7 @@ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
2139
2189
  );
2140
2190
  return null;
2141
2191
  }
2142
- if (!isStringLikeSemanticType(property.type)) {
2192
+ if (!isStringLikeSemanticType(property.type, checker)) {
2143
2193
  diagnostics.push(
2144
2194
  makeAnalysisDiagnostic(
2145
2195
  "TYPE_MISMATCH",
@@ -2166,8 +2216,8 @@ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typ
2166
2216
  const localTypeParameter = node.typeParameters?.[typeParameterIndex];
2167
2217
  return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
2168
2218
  }
2169
- function resolveLiteralDiscriminatorPropertyValue(boundType, fieldName, checker, provenance, diagnostics) {
2170
- const propertySymbol = boundType.getProperty(fieldName);
2219
+ function resolveLiteralDiscriminatorPropertyValue(boundType, propertyName, checker, provenance, diagnostics) {
2220
+ const propertySymbol = boundType.getProperty(propertyName);
2171
2221
  if (propertySymbol === void 0) {
2172
2222
  return void 0;
2173
2223
  }
@@ -2198,6 +2248,9 @@ function resolveLiteralDiscriminatorPropertyValue(boundType, fieldName, checker,
2198
2248
  }
2199
2249
  return void 0;
2200
2250
  }
2251
+ function getDiscriminatorIdentityPropertyNames(fieldName) {
2252
+ return fieldName === "object" ? ["object"] : [fieldName, "object"];
2253
+ }
2201
2254
  function resolveDiscriminatorApiName(boundType, checker, metadataPolicy) {
2202
2255
  const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
2203
2256
  if (declaration === null) {
@@ -2218,6 +2271,10 @@ function resolveDiscriminatorApiName(boundType, checker, metadataPolicy) {
2218
2271
  );
2219
2272
  return metadata?.apiName;
2220
2273
  }
2274
+ function applyDiscriminatorApiNamePrefix(value, discriminatorOptions) {
2275
+ const prefix = discriminatorOptions?.apiNamePrefix;
2276
+ return prefix === void 0 || prefix === "" ? value : `${prefix}${value}`;
2277
+ }
2221
2278
  function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
2222
2279
  if (seen.has(type)) {
2223
2280
  return null;
@@ -2272,22 +2329,27 @@ function resolveDiscriminatorValue(boundType, fieldName, checker, provenance, di
2272
2329
  return null;
2273
2330
  }
2274
2331
  }
2275
- const literalIdentityValue = resolveLiteralDiscriminatorPropertyValue(
2276
- boundType,
2277
- fieldName,
2278
- checker,
2279
- provenance,
2280
- diagnostics
2281
- );
2282
- if (literalIdentityValue !== void 0) {
2283
- return literalIdentityValue;
2332
+ for (const identityPropertyName of getDiscriminatorIdentityPropertyNames(fieldName)) {
2333
+ const literalIdentityValue = resolveLiteralDiscriminatorPropertyValue(
2334
+ boundType,
2335
+ identityPropertyName,
2336
+ checker,
2337
+ provenance,
2338
+ diagnostics
2339
+ );
2340
+ if (literalIdentityValue === null) {
2341
+ return null;
2342
+ }
2343
+ if (literalIdentityValue !== void 0) {
2344
+ return literalIdentityValue;
2345
+ }
2284
2346
  }
2285
2347
  const apiName = resolveDiscriminatorApiName(boundType, checker, metadataPolicy);
2286
2348
  if (apiName?.source === "explicit") {
2287
- return apiName.value;
2349
+ return applyDiscriminatorApiNamePrefix(apiName.value, metadataPolicy.discriminator);
2288
2350
  }
2289
2351
  if (apiName?.source === "inferred") {
2290
- return apiName.value;
2352
+ return applyDiscriminatorApiNamePrefix(apiName.value, metadataPolicy.discriminator);
2291
2353
  }
2292
2354
  diagnostics.push(
2293
2355
  makeAnalysisDiagnostic(
@@ -2350,15 +2412,20 @@ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
2350
2412
  return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
2351
2413
  }
2352
2414
  function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy, extensionRegistry, diagnostics) {
2353
- const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2354
- if (typeNode === void 0) {
2415
+ const sourceTypeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2416
+ if (sourceTypeNode === void 0) {
2355
2417
  return [];
2356
2418
  }
2357
- const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2358
- if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
2419
+ const unwrapParentheses = (typeNode) => ts3.isParenthesizedTypeNode(typeNode) ? unwrapParentheses(typeNode.type) : typeNode;
2420
+ const directTypeNode = unwrapParentheses(sourceTypeNode);
2421
+ const referenceTypeNode = ts3.isTypeReferenceNode(directTypeNode) ? directTypeNode : (() => {
2422
+ const resolvedTypeNode = resolveAliasedTypeNode(directTypeNode, checker);
2423
+ return ts3.isTypeReferenceNode(resolvedTypeNode) ? resolvedTypeNode : null;
2424
+ })();
2425
+ if (referenceTypeNode?.typeArguments === void 0) {
2359
2426
  return [];
2360
2427
  }
2361
- return resolvedTypeNode.typeArguments.map((argumentNode) => {
2428
+ return referenceTypeNode.typeArguments.map((argumentNode) => {
2362
2429
  const argumentType = checker.getTypeFromTypeNode(argumentNode);
2363
2430
  return {
2364
2431
  tsType: argumentType,
@@ -2472,10 +2539,10 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
2472
2539
  };
2473
2540
  }
2474
2541
  function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
2475
- if (!ts3.isIdentifier(prop.name)) {
2542
+ const name = getAnalyzableObjectLikePropertyName(prop.name);
2543
+ if (name === null) {
2476
2544
  return null;
2477
2545
  }
2478
- const name = prop.name.text;
2479
2546
  const tsType = checker.getTypeAtLocation(prop);
2480
2547
  const optional = prop.questionToken !== void 0;
2481
2548
  const provenance = provenanceForNode(prop, file);
@@ -2531,6 +2598,31 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
2531
2598
  provenance
2532
2599
  };
2533
2600
  }
2601
+ function findDuplicateObjectLikeTypeAliasPropertyNames(members) {
2602
+ const seen = /* @__PURE__ */ new Set();
2603
+ const duplicates = /* @__PURE__ */ new Set();
2604
+ for (const member of members) {
2605
+ if (!ts3.isPropertySignature(member)) {
2606
+ continue;
2607
+ }
2608
+ const name = getAnalyzableObjectLikePropertyName(member.name);
2609
+ if (name === null) {
2610
+ continue;
2611
+ }
2612
+ if (seen.has(name)) {
2613
+ duplicates.add(name);
2614
+ continue;
2615
+ }
2616
+ seen.add(name);
2617
+ }
2618
+ return [...duplicates].sort();
2619
+ }
2620
+ function getAnalyzableObjectLikePropertyName(name) {
2621
+ if (!ts3.isIdentifier(name)) {
2622
+ return null;
2623
+ }
2624
+ return name.text;
2625
+ }
2534
2626
  function applyEnumMemberDisplayNames(type, annotations) {
2535
2627
  if (!annotations.some(
2536
2628
  (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
@@ -2723,6 +2815,23 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2723
2815
  diagnostics
2724
2816
  );
2725
2817
  }
2818
+ if (isIntersectionType(type)) {
2819
+ const sourceTypeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2820
+ const resolvedSourceTypeNode = sourceTypeNode === void 0 ? void 0 : resolveAliasedTypeNode(sourceTypeNode, checker);
2821
+ if (resolvedSourceTypeNode !== void 0 && getObjectLikeTypeAliasMembers(resolvedSourceTypeNode) !== null) {
2822
+ return resolveObjectType(
2823
+ type,
2824
+ checker,
2825
+ file,
2826
+ typeRegistry,
2827
+ visiting,
2828
+ sourceNode,
2829
+ metadataPolicy,
2830
+ extensionRegistry,
2831
+ diagnostics
2832
+ );
2833
+ }
2834
+ }
2726
2835
  if (isObjectType(type)) {
2727
2836
  return resolveObjectType(
2728
2837
  type,
@@ -2809,9 +2918,10 @@ function shouldEmitPrimitiveAliasDefinition(typeNode, checker) {
2809
2918
  const resolved = checker.getTypeFromTypeNode(aliasDecl.type);
2810
2919
  return !!(resolved.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null));
2811
2920
  }
2812
- function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, metadataPolicy = createAnalyzerMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2921
+ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, metadataPolicy = createAnalyzerMetadataPolicy(void 0), extensionRegistry, diagnostics, visitedAliases = /* @__PURE__ */ new Set()) {
2813
2922
  const nestedAliasDecl = type.aliasSymbol?.declarations?.find(ts3.isTypeAliasDeclaration);
2814
- if (nestedAliasDecl !== void 0) {
2923
+ if (nestedAliasDecl !== void 0 && !visitedAliases.has(nestedAliasDecl)) {
2924
+ visitedAliases.add(nestedAliasDecl);
2815
2925
  return resolveAliasedPrimitiveTarget(
2816
2926
  checker.getTypeFromTypeNode(nestedAliasDecl.type),
2817
2927
  checker,
@@ -2820,9 +2930,25 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
2820
2930
  visiting,
2821
2931
  metadataPolicy,
2822
2932
  extensionRegistry,
2823
- diagnostics
2933
+ diagnostics,
2934
+ visitedAliases
2824
2935
  );
2825
2936
  }
2937
+ if (type.flags & ts3.TypeFlags.String) {
2938
+ return { kind: "primitive", primitiveKind: "string" };
2939
+ }
2940
+ if (type.flags & ts3.TypeFlags.Number) {
2941
+ return { kind: "primitive", primitiveKind: "number" };
2942
+ }
2943
+ if (type.flags & (ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral)) {
2944
+ return { kind: "primitive", primitiveKind: "bigint" };
2945
+ }
2946
+ if (type.flags & ts3.TypeFlags.Boolean) {
2947
+ return { kind: "primitive", primitiveKind: "boolean" };
2948
+ }
2949
+ if (type.flags & ts3.TypeFlags.Null) {
2950
+ return { kind: "primitive", primitiveKind: "null" };
2951
+ }
2826
2952
  return resolveTypeNode(
2827
2953
  type,
2828
2954
  checker,
@@ -3102,7 +3228,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNo
3102
3228
  };
3103
3229
  }
3104
3230
  }
3105
- const recordNode = tryResolveRecordType(
3231
+ const recordNode = isObjectType(type) ? tryResolveRecordType(
3106
3232
  type,
3107
3233
  checker,
3108
3234
  file,
@@ -3111,7 +3237,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNo
3111
3237
  metadataPolicy,
3112
3238
  extensionRegistry,
3113
3239
  collectedDiagnostics
3114
- );
3240
+ ) : null;
3115
3241
  if (recordNode) {
3116
3242
  visiting.delete(type);
3117
3243
  if (registryTypeName !== void 0 && shouldRegisterNamedType) {
@@ -3308,9 +3434,10 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
3308
3434
  );
3309
3435
  }
3310
3436
  const typeAliasDecl = declarations.find(ts3.isTypeAliasDeclaration);
3311
- if (typeAliasDecl && ts3.isTypeLiteralNode(typeAliasDecl.type)) {
3437
+ const typeAliasMembers = typeAliasDecl === void 0 ? null : getObjectLikeTypeAliasMembers(typeAliasDecl.type);
3438
+ if (typeAliasDecl && typeAliasMembers !== null) {
3312
3439
  return buildFieldNodeInfoMap(
3313
- typeAliasDecl.type.members,
3440
+ typeAliasMembers,
3314
3441
  checker,
3315
3442
  file,
3316
3443
  typeRegistry,
@@ -3601,7 +3728,7 @@ function findInterfaceByName(sourceFile, interfaceName) {
3601
3728
  function findTypeAliasByName(sourceFile, aliasName) {
3602
3729
  return findNodeByName(sourceFile, aliasName, ts4.isTypeAliasDeclaration, (n) => n.name.text);
3603
3730
  }
3604
- function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry, metadataPolicy) {
3731
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry, metadataPolicy, discriminatorOptions) {
3605
3732
  const analysisFilePath = path.resolve(filePath);
3606
3733
  const classDecl = findClassByName(ctx.sourceFile, typeName);
3607
3734
  if (classDecl !== null) {
@@ -3610,7 +3737,8 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
3610
3737
  ctx.checker,
3611
3738
  analysisFilePath,
3612
3739
  extensionRegistry,
3613
- metadataPolicy
3740
+ metadataPolicy,
3741
+ discriminatorOptions
3614
3742
  );
3615
3743
  }
3616
3744
  const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
@@ -3620,7 +3748,8 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
3620
3748
  ctx.checker,
3621
3749
  analysisFilePath,
3622
3750
  extensionRegistry,
3623
- metadataPolicy
3751
+ metadataPolicy,
3752
+ discriminatorOptions
3624
3753
  );
3625
3754
  }
3626
3755
  const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
@@ -3630,7 +3759,8 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
3630
3759
  ctx.checker,
3631
3760
  analysisFilePath,
3632
3761
  extensionRegistry,
3633
- metadataPolicy
3762
+ metadataPolicy,
3763
+ discriminatorOptions
3634
3764
  );
3635
3765
  if (result.ok) {
3636
3766
  return result.analysis;