瀏覽代碼

Fix LinkNode paths for getTypeManifestVisitor (#284)

This PR fixes an issue in the JavaScript `getTypeManifestVisitors` where complex link node paths would be incorrectly resolved due to the fact that the `NodeStack` would follow in invalid path in the tree. The new methods to save and restore `NodePaths` inside the `NodeStack` help us fix this.
Loris Leiva 1 年之前
父節點
當前提交
d1bab68c1c

+ 6 - 0
.changeset/dirty-onions-arrive.md

@@ -0,0 +1,6 @@
+---
+'@codama/renderers-js-umi': minor
+'@codama/renderers-js': minor
+---
+
+Fix LinkNode paths for JavaScript `getTypeManifestVisitors`

+ 1 - 0
packages/renderers-js-umi/src/getRenderMapVisitor.ts

@@ -96,6 +96,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
             linkables,
             nonScalarEnums,
             parentName,
+            stack,
         });
     const typeManifestVisitor = getTypeManifestVisitor();
     const resolvedInstructionInputVisitor = getResolvedInstructionInputsVisitor();

+ 2 - 2
packages/renderers-js-umi/src/getTypeManifestVisitor.ts

@@ -64,11 +64,12 @@ export function getTypeManifestVisitor(input: {
     linkables: LinkableDictionary;
     nonScalarEnums: CamelCaseString[];
     parentName?: { loose: string; strict: string };
+    stack?: NodeStack;
 }) {
     const { linkables, nonScalarEnums, customAccountData, customInstructionData, getImportFrom } = input;
     let parentName = input.parentName ?? null;
     let parentSize: NumberTypeNode | number | null = null;
-    const stack = new NodeStack();
+    const stack = input.stack ?? new NodeStack();
 
     return pipe(
         staticVisitor(
@@ -428,7 +429,6 @@ export function getTypeManifestVisitor(input: {
                     const variantName = pascalCase(node.variant);
                     const importFrom = getImportFrom(node.enum);
 
-                    // FIXME(loris): No program node can ever be in this stack.
                     const enumNode = linkables.get([...stack.getPath(), node.enum])?.type;
                     const isScalar =
                         enumNode && isNode(enumNode, 'enumTypeNode')

+ 1 - 0
packages/renderers-js/src/getRenderMapVisitor.ts

@@ -114,6 +114,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
             nameApi,
             nonScalarEnums,
             parentName,
+            stack,
         });
     const typeManifestVisitor = getTypeManifestVisitor();
     const resolvedInstructionInputVisitor = getResolvedInstructionInputsVisitor();

+ 2 - 2
packages/renderers-js/src/getTypeManifestVisitor.ts

@@ -41,9 +41,10 @@ export function getTypeManifestVisitor(input: {
     nameApi: NameApi;
     nonScalarEnums: CamelCaseString[];
     parentName?: { loose: string; strict: string };
+    stack?: NodeStack;
 }) {
     const { nameApi, linkables, nonScalarEnums, customAccountData, customInstructionData, getImportFrom } = input;
-    const stack = new NodeStack();
+    const stack = input.stack ?? new NodeStack();
     let parentName = input.parentName ?? null;
 
     return pipe(
@@ -355,7 +356,6 @@ export function getTypeManifestVisitor(input: {
                     const enumFunction = nameApi.discriminatedUnionFunction(node.enum.name);
                     const importFrom = getImportFrom(node.enum);
 
-                    // FIXME(loris): No program node can ever be in this stack.
                     const enumNode = linkables.get([...stack.getPath(), node.enum])?.type;
                     const isScalar =
                         enumNode && isNode(enumNode, 'enumTypeNode')

+ 98 - 0
packages/renderers-js/test/links/definedType.test.ts

@@ -1,11 +1,17 @@
 import {
     definedTypeLinkNode,
     definedTypeNode,
+    enumEmptyVariantTypeNode,
+    enumTupleVariantTypeNode,
+    enumTypeNode,
+    enumValueNode,
     fixedSizeTypeNode,
+    numberTypeNode,
     programNode,
     stringTypeNode,
     structFieldTypeNode,
     structTypeNode,
+    tupleTypeNode,
 } from '@codama/nodes';
 import { visit } from '@codama/visitors-core';
 import { test } from 'vitest';
@@ -91,3 +97,95 @@ test('it can override the import of a linked type', async () => {
         '../../hooked': ['Symbol', 'SymbolArgs', 'getSymbolEncoder', 'getSymbolDecoder'],
     });
 });
+
+test('it knows if an enum value is a scalar enum using link nodes', async () => {
+    // Given a program with a scalar enum linked in a default value.
+    const node = programNode({
+        definedTypes: [
+            definedTypeNode({
+                name: 'person',
+                type: structTypeNode([
+                    structFieldTypeNode({
+                        defaultValue: enumValueNode('direction', 'up'),
+                        name: 'movement',
+                        type: definedTypeLinkNode('direction'),
+                    }),
+                ]),
+            }),
+            definedTypeNode({
+                name: 'direction',
+                type: enumTypeNode([
+                    enumEmptyVariantTypeNode('up'),
+                    enumEmptyVariantTypeNode('right'),
+                    enumEmptyVariantTypeNode('down'),
+                    enumEmptyVariantTypeNode('left'),
+                ]),
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the direction enum to be exported as a scalar enum.
+    await renderMapContains(renderMap, 'types/person.ts', [
+        'movement: value.movement ?? Direction.Up',
+        'export type Person = { movement: Direction }',
+        'export type PersonArgs = { movement?: DirectionArgs }',
+        'getDirectionEncoder()',
+        'getDirectionDecoder()',
+    ]);
+
+    // And we expect the following imports.
+    await renderMapContainsImports(renderMap, 'types/person.ts', {
+        '.': ['Direction', 'DirectionArgs', 'getDirectionEncoder', 'getDirectionDecoder'],
+    });
+});
+
+test('it knows if an enum value is a data enum using link nodes', async () => {
+    // Given a program with a data enum linked in a default value.
+    const node = programNode({
+        definedTypes: [
+            definedTypeNode({
+                name: 'person',
+                type: structTypeNode([
+                    structFieldTypeNode({
+                        defaultValue: enumValueNode('action', 'stop'),
+                        name: 'nextAction',
+                        type: definedTypeLinkNode('action'),
+                    }),
+                ]),
+            }),
+            definedTypeNode({
+                name: 'action',
+                type: enumTypeNode([
+                    enumEmptyVariantTypeNode('stop'),
+                    enumEmptyVariantTypeNode('turnRight'),
+                    enumEmptyVariantTypeNode('turnLeft'),
+                    enumTupleVariantTypeNode('moveForward', tupleTypeNode([numberTypeNode('u8')])),
+                ]),
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the action enum to be exported as a data enum.
+    await renderMapContains(renderMap, 'types/person.ts', [
+        "nextAction: value.nextAction ?? action('Stop')",
+        'export type Person = { nextAction: Action }',
+        'export type PersonArgs = { nextAction?: ActionArgs }',
+        'getActionEncoder()',
+        'getActionDecoder()',
+    ]);
+
+    // And we expect the following imports.
+    await renderMapContainsImports(renderMap, 'types/person.ts', {
+        '.': ['Action', 'ActionArgs', 'getActionEncoder', 'getActionDecoder'],
+    });
+});