Parcourir la source

Use program names when unwrapping link nodes (#314)

Loris Leiva il y a 1 an
Parent
commit
e55bcd7ee7

+ 5 - 0
.changeset/giant-items-sniff.md

@@ -0,0 +1,5 @@
+---
+'@codama/visitors': minor
+---
+
+Use program names when unwrapping link nodes

+ 19 - 3
packages/visitors/src/getDefinedTypeHistogramVisitor.ts

@@ -1,8 +1,20 @@
 import { CamelCaseString } from '@codama/nodes';
-import { extendVisitor, interceptVisitor, mergeVisitor, pipe, visit, Visitor } from '@codama/visitors-core';
+import {
+    extendVisitor,
+    findProgramNodeFromPath,
+    interceptVisitor,
+    mergeVisitor,
+    NodeStack,
+    pipe,
+    recordNodeStackVisitor,
+    visit,
+    Visitor,
+} from '@codama/visitors-core';
+
+type DefinedTypeHistogramKey = CamelCaseString | `${CamelCaseString}.${CamelCaseString}`;
 
 export type DefinedTypeHistogram = {
-    [key: CamelCaseString]: {
+    [key: DefinedTypeHistogramKey]: {
         directlyAsInstructionArgs: number;
         inAccounts: number;
         inDefinedTypes: number;
@@ -33,6 +45,7 @@ function mergeHistograms(histograms: DefinedTypeHistogram[]): DefinedTypeHistogr
 }
 
 export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram> {
+    const stack = new NodeStack();
     let mode: 'account' | 'definedType' | 'instruction' | null = null;
     let stackLevel = 0;
 
@@ -67,8 +80,10 @@ export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram>
                 },
 
                 visitDefinedTypeLink(node) {
+                    const program = findProgramNodeFromPath(stack.getPath());
+                    const key = program ? `${program.name}.${node.name}` : node.name;
                     return {
-                        [node.name]: {
+                        [key]: {
                             directlyAsInstructionArgs: Number(mode === 'instruction' && stackLevel <= 1),
                             inAccounts: Number(mode === 'account'),
                             inDefinedTypes: Number(mode === 'definedType'),
@@ -88,5 +103,6 @@ export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram>
                     return mergeHistograms([...dataHistograms, ...extraHistograms, ...subHistograms]);
                 },
             }),
+        v => recordNodeStackVisitor(v, stack),
     );
 }

+ 15 - 5
packages/visitors/src/unwrapDefinedTypesVisitor.ts

@@ -1,6 +1,7 @@
 import { assertIsNodeFilter, camelCase, CamelCaseString, programNode } from '@codama/nodes';
 import {
     extendVisitor,
+    findProgramNodeFromPath,
     getLastNodeFromPath,
     LinkableDictionary,
     NodeStack,
@@ -14,16 +15,25 @@ import {
 export function unwrapDefinedTypesVisitor(typesToInline: string[] | '*' = '*') {
     const linkables = new LinkableDictionary();
     const stack = new NodeStack();
-    const typesToInlineMainCased = typesToInline === '*' ? '*' : typesToInline.map(camelCase);
-    const shouldInline = (definedType: CamelCaseString): boolean =>
-        typesToInlineMainCased === '*' || typesToInlineMainCased.includes(definedType);
+    const typesToInlineCamelCased = (typesToInline === '*' ? [] : typesToInline).map(fullPath => {
+        if (!fullPath.includes('.')) return camelCase(fullPath);
+        const [programName, typeName] = fullPath.split('.');
+        return `${camelCase(programName)}.${camelCase(typeName)}`;
+    });
+    const shouldInline = (typeName: CamelCaseString, programName: CamelCaseString | undefined): boolean => {
+        if (typesToInline === '*') return true;
+        const fullPath = `${programName}.${typeName}`;
+        if (!!programName && typesToInlineCamelCased.includes(fullPath)) return true;
+        return typesToInlineCamelCased.includes(typeName);
+    };
 
     return pipe(
         nonNullableIdentityVisitor(),
         v =>
             extendVisitor(v, {
                 visitDefinedTypeLink(linkType, { self }) {
-                    if (!shouldInline(linkType.name)) {
+                    const programName = linkType.program?.name ?? findProgramNodeFromPath(stack.getPath())?.name;
+                    if (!shouldInline(linkType.name, programName)) {
                         return linkType;
                     }
                     const definedTypePath = linkables.getPathOrThrow(stack.getPath('definedTypeLinkNode'));
@@ -42,7 +52,7 @@ export function unwrapDefinedTypesVisitor(typesToInline: string[] | '*' = '*') {
                             .map(account => visit(account, self))
                             .filter(assertIsNodeFilter('accountNode')),
                         definedTypes: program.definedTypes
-                            .filter(definedType => !shouldInline(definedType.name))
+                            .filter(definedType => !shouldInline(definedType.name, program.name))
                             .map(type => visit(type, self))
                             .filter(assertIsNodeFilter('definedTypeNode')),
                         instructions: program.instructions

+ 10 - 11
packages/visitors/src/unwrapInstructionArgsDefinedTypesVisitor.ts

@@ -1,5 +1,5 @@
-import { assertIsNode, CamelCaseString, getAllDefinedTypes, isNode } from '@codama/nodes';
-import { rootNodeVisitor, visit } from '@codama/visitors-core';
+import { assertIsNode, CamelCaseString, definedTypeLinkNode, isNode } from '@codama/nodes';
+import { getRecordLinkablesVisitor, LinkableDictionary, rootNodeVisitor, visit } from '@codama/visitors-core';
 
 import { getDefinedTypeHistogramVisitor } from './getDefinedTypeHistogramVisitor';
 import { unwrapDefinedTypesVisitor } from './unwrapDefinedTypesVisitor';
@@ -7,18 +7,17 @@ import { unwrapDefinedTypesVisitor } from './unwrapDefinedTypesVisitor';
 export function unwrapInstructionArgsDefinedTypesVisitor() {
     return rootNodeVisitor(root => {
         const histogram = visit(root, getDefinedTypeHistogramVisitor());
-        const allDefinedTypes = getAllDefinedTypes(root);
+        const linkables = new LinkableDictionary();
+        visit(root, getRecordLinkablesVisitor(linkables));
 
-        const definedTypesToInline: string[] = Object.keys(histogram)
+        const definedTypesToInline = (Object.keys(histogram) as CamelCaseString[])
             // Get all defined types used exactly once as an instruction argument.
-            .filter(
-                name =>
-                    (histogram[name as CamelCaseString].total ?? 0) === 1 &&
-                    (histogram[name as CamelCaseString].directlyAsInstructionArgs ?? 0) === 1,
-            )
+            .filter(key => (histogram[key].total ?? 0) === 1 && (histogram[key].directlyAsInstructionArgs ?? 0) === 1)
             // Filter out enums which are better defined as external types.
-            .filter(name => {
-                const found = allDefinedTypes.find(type => type.name === name);
+            .filter(key => {
+                const names = key.split('.');
+                const link = names.length == 2 ? definedTypeLinkNode(names[1], names[0]) : definedTypeLinkNode(key);
+                const found = linkables.get([link]);
                 return found && !isNode(found.type, 'enumTypeNode');
             });
 

+ 48 - 2
packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts

@@ -5,7 +5,9 @@ import {
     enumTypeNode,
     instructionArgumentNode,
     instructionNode,
+    numberTypeNode,
     programNode,
+    rootNode,
     structFieldTypeNode,
     structTypeNode,
 } from '@codama/nodes';
@@ -65,14 +67,14 @@ test('it counts the amount of times defined types are used within the tree', ()
 
     // Then we expect the following histogram.
     expect(histogram).toEqual({
-        myEnum: {
+        'customProgram.myEnum': {
             directlyAsInstructionArgs: 0,
             inAccounts: 1,
             inDefinedTypes: 0,
             inInstructionArgs: 0,
             total: 1,
         },
-        myStruct: {
+        'customProgram.myStruct': {
             directlyAsInstructionArgs: 1,
             inAccounts: 1,
             inDefinedTypes: 0,
@@ -81,3 +83,47 @@ test('it counts the amount of times defined types are used within the tree', ()
         },
     });
 });
+
+test('it counts links from different programs separately', () => {
+    // Given a program node with a defined type used in another type.
+    const programA = programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'myType', type: numberTypeNode('u8') }),
+            definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
+        ],
+        name: 'programA',
+        publicKey: '1111',
+    });
+
+    // And another program with a defined type sharing the same name.
+    const programB = programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'myType', type: numberTypeNode('u16') }),
+            definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
+        ],
+        name: 'programB',
+        publicKey: '2222',
+    });
+
+    // When we unwrap the defined type from programA.
+    const node = rootNode(programA, [programB]);
+    const histogram = visit(node, getDefinedTypeHistogramVisitor());
+
+    // Then we expect programA to have been modified but not programB.
+    expect(histogram).toStrictEqual({
+        'programA.myType': {
+            directlyAsInstructionArgs: 0,
+            inAccounts: 0,
+            inDefinedTypes: 1,
+            inInstructionArgs: 0,
+            total: 1,
+        },
+        'programB.myType': {
+            directlyAsInstructionArgs: 0,
+            inAccounts: 0,
+            inDefinedTypes: 1,
+            inInstructionArgs: 0,
+            total: 1,
+        },
+    });
+});

+ 39 - 0
packages/visitors/test/unwrapDefinedTypesVisitor.test.ts

@@ -72,3 +72,42 @@ test('it follows linked nodes using the correct paths', () => {
         definedTypeNode({ name: 'typeA', type: numberTypeNode('u64') }),
     );
 });
+
+test('it does not unwrap types from the wrong programs', () => {
+    // Given a program node with a defined type used in another type.
+    const programA = programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'myType', type: numberTypeNode('u8') }),
+            definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
+        ],
+        name: 'programA',
+        publicKey: '1111',
+    });
+
+    // And another program with a defined type sharing the same name.
+    const programB = programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'myType', type: numberTypeNode('u16') }),
+            definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
+        ],
+        name: 'programB',
+        publicKey: '2222',
+    });
+
+    // When we unwrap the defined type from programA.
+    const node = rootNode(programA, [programB]);
+    const result = visit(node, unwrapDefinedTypesVisitor(['programA.myType']));
+
+    // Then we expect programA to have been modified but not programB.
+    assertIsNode(result, 'rootNode');
+    expect(result).toStrictEqual(
+        rootNode(
+            programNode({
+                definedTypes: [definedTypeNode({ name: 'myCopyType', type: numberTypeNode('u8') })],
+                name: 'programA',
+                publicKey: '1111',
+            }),
+            [programB],
+        ),
+    );
+});

+ 183 - 0
packages/visitors/test/unwrapInstructionArgsDefinedTypesVisitor.test.ts

@@ -0,0 +1,183 @@
+import {
+    arrayTypeNode,
+    definedTypeLinkNode,
+    definedTypeNode,
+    fixedCountNode,
+    instructionArgumentNode,
+    instructionNode,
+    numberTypeNode,
+    programNode,
+    rootNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@codama/nodes';
+import { visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { unwrapInstructionArgsDefinedTypesVisitor } from '../src';
+
+test('it unwraps defined type link nodes used as instruction arguments', () => {
+    // Given a program with a type used only once as a direct instruction argument.
+    const node = rootNode(
+        programNode({
+            definedTypes: [
+                definedTypeNode({
+                    name: 'typeA',
+                    type: structTypeNode([structFieldTypeNode({ name: 'foo', type: numberTypeNode('u8') })]),
+                }),
+                definedTypeNode({
+                    name: 'typeB',
+                    type: structTypeNode([structFieldTypeNode({ name: 'bar', type: numberTypeNode('u8') })]),
+                }),
+            ],
+            instructions: [
+                instructionNode({
+                    arguments: [instructionArgumentNode({ name: 'argA', type: definedTypeLinkNode('typeA') })],
+                    name: 'myInstruction',
+                }),
+            ],
+            name: 'MyProgram',
+            publicKey: '1111',
+        }),
+    );
+
+    // When the defined type link nodes are unwrapped.
+    const result = visit(node, unwrapInstructionArgsDefinedTypesVisitor());
+
+    // Then we expect the following node.
+    expect(result).toStrictEqual(
+        rootNode(
+            programNode({
+                definedTypes: [
+                    definedTypeNode({
+                        name: 'typeB',
+                        type: structTypeNode([structFieldTypeNode({ name: 'bar', type: numberTypeNode('u8') })]),
+                    }),
+                ],
+                instructions: [
+                    instructionNode({
+                        arguments: [
+                            instructionArgumentNode({
+                                name: 'argA',
+                                type: structTypeNode([
+                                    structFieldTypeNode({ name: 'foo', type: numberTypeNode('u8') }),
+                                ]),
+                            }),
+                        ],
+                        name: 'myInstruction',
+                    }),
+                ],
+                name: 'MyProgram',
+                publicKey: '1111',
+            }),
+        ),
+    );
+});
+
+test('it does not unwrap defined type link nodes that are used in more than one place.', () => {
+    // Given a link node used in an instruction argument and in another place.
+    const node = rootNode(
+        programNode({
+            definedTypes: [
+                definedTypeNode({
+                    name: 'myType',
+                    type: structTypeNode([structFieldTypeNode({ name: 'foo', type: numberTypeNode('u8') })]),
+                }),
+                definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
+            ],
+            instructions: [
+                instructionNode({
+                    arguments: [instructionArgumentNode({ name: 'myArg', type: definedTypeLinkNode('myType') })],
+                    name: 'myInstruction',
+                }),
+            ],
+            name: 'MyProgram',
+            publicKey: '1111',
+        }),
+    );
+
+    // When we try to unwrap defined type link nodes for instruction arguments.
+    const result = visit(node, unwrapInstructionArgsDefinedTypesVisitor());
+
+    // Then we expect the same node.
+    expect(result).toStrictEqual(node);
+});
+
+test('it only unwraps defined type link nodes if they are direct instruction arguments', () => {
+    // Given a link node used in an instruction argument but not as a direct argument.
+    const node = rootNode(
+        programNode({
+            definedTypes: [
+                definedTypeNode({
+                    name: 'myType',
+                    type: structTypeNode([structFieldTypeNode({ name: 'foo', type: numberTypeNode('u8') })]),
+                }),
+            ],
+            instructions: [
+                instructionNode({
+                    arguments: [
+                        instructionArgumentNode({
+                            name: 'myArg',
+                            type: arrayTypeNode(definedTypeLinkNode('myType'), fixedCountNode(3)),
+                        }),
+                    ],
+                    name: 'myInstruction',
+                }),
+            ],
+            name: 'MyProgram',
+            publicKey: '1111',
+        }),
+    );
+
+    // When we try to unwrap defined type link nodes for instruction arguments.
+    const result = visit(node, unwrapInstructionArgsDefinedTypesVisitor());
+
+    // Then we expect the same node.
+    expect(result).toStrictEqual(node);
+});
+
+test('it does not unwrap defined type link nodes from other programs', () => {
+    // Given a program that defines the
+    const programA = programNode({
+        definedTypes: [definedTypeNode({ name: 'myType', type: numberTypeNode('u8') })],
+        instructions: [
+            instructionNode({
+                arguments: [instructionArgumentNode({ name: 'myArg', type: definedTypeLinkNode('myType') })],
+                name: 'myInstruction',
+            }),
+        ],
+        name: 'programA',
+        publicKey: '1111',
+    });
+
+    // And another program with a defined type sharing the same name.
+    const programB = programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'myType', type: numberTypeNode('u16') }),
+            definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
+        ],
+        name: 'programB',
+        publicKey: '2222',
+    });
+
+    // When we unwrap defined type link nodes for instruction arguments for both of them.
+    const node = rootNode(programA, [programB]);
+    const result = visit(node, unwrapInstructionArgsDefinedTypesVisitor());
+
+    // Then we expect program A to have been modified but not program B.
+    expect(result).toStrictEqual(
+        rootNode(
+            programNode({
+                instructions: [
+                    instructionNode({
+                        arguments: [instructionArgumentNode({ name: 'myArg', type: numberTypeNode('u8') })],
+                        name: 'myInstruction',
+                    }),
+                ],
+                name: 'programA',
+                publicKey: '1111',
+            }),
+            [programB],
+        ),
+    );
+});

+ 3 - 0
vitest.workspace.ts

@@ -0,0 +1,3 @@
+import { defineWorkspace } from 'vitest/config';
+
+export default defineWorkspace(['./packages/internals/vitest.config.node.mts']);