Skip to content

Commit e55bcd7

Browse files
authored
Use program names when unwrapping link nodes (#314)
1 parent 7563224 commit e55bcd7

8 files changed

+322
-21
lines changed

.changeset/giant-items-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/visitors': minor
3+
---
4+
5+
Use program names when unwrapping link nodes

packages/visitors/src/getDefinedTypeHistogramVisitor.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import { CamelCaseString } from '@codama/nodes';
2-
import { extendVisitor, interceptVisitor, mergeVisitor, pipe, visit, Visitor } from '@codama/visitors-core';
2+
import {
3+
extendVisitor,
4+
findProgramNodeFromPath,
5+
interceptVisitor,
6+
mergeVisitor,
7+
NodeStack,
8+
pipe,
9+
recordNodeStackVisitor,
10+
visit,
11+
Visitor,
12+
} from '@codama/visitors-core';
13+
14+
type DefinedTypeHistogramKey = CamelCaseString | `${CamelCaseString}.${CamelCaseString}`;
315

416
export type DefinedTypeHistogram = {
5-
[key: CamelCaseString]: {
17+
[key: DefinedTypeHistogramKey]: {
618
directlyAsInstructionArgs: number;
719
inAccounts: number;
820
inDefinedTypes: number;
@@ -33,6 +45,7 @@ function mergeHistograms(histograms: DefinedTypeHistogram[]): DefinedTypeHistogr
3345
}
3446

3547
export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram> {
48+
const stack = new NodeStack();
3649
let mode: 'account' | 'definedType' | 'instruction' | null = null;
3750
let stackLevel = 0;
3851

@@ -67,8 +80,10 @@ export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram>
6780
},
6881

6982
visitDefinedTypeLink(node) {
83+
const program = findProgramNodeFromPath(stack.getPath());
84+
const key = program ? `${program.name}.${node.name}` : node.name;
7085
return {
71-
[node.name]: {
86+
[key]: {
7287
directlyAsInstructionArgs: Number(mode === 'instruction' && stackLevel <= 1),
7388
inAccounts: Number(mode === 'account'),
7489
inDefinedTypes: Number(mode === 'definedType'),
@@ -88,5 +103,6 @@ export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram>
88103
return mergeHistograms([...dataHistograms, ...extraHistograms, ...subHistograms]);
89104
},
90105
}),
106+
v => recordNodeStackVisitor(v, stack),
91107
);
92108
}

packages/visitors/src/unwrapDefinedTypesVisitor.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assertIsNodeFilter, camelCase, CamelCaseString, programNode } from '@codama/nodes';
22
import {
33
extendVisitor,
4+
findProgramNodeFromPath,
45
getLastNodeFromPath,
56
LinkableDictionary,
67
NodeStack,
@@ -14,16 +15,25 @@ import {
1415
export function unwrapDefinedTypesVisitor(typesToInline: string[] | '*' = '*') {
1516
const linkables = new LinkableDictionary();
1617
const stack = new NodeStack();
17-
const typesToInlineMainCased = typesToInline === '*' ? '*' : typesToInline.map(camelCase);
18-
const shouldInline = (definedType: CamelCaseString): boolean =>
19-
typesToInlineMainCased === '*' || typesToInlineMainCased.includes(definedType);
18+
const typesToInlineCamelCased = (typesToInline === '*' ? [] : typesToInline).map(fullPath => {
19+
if (!fullPath.includes('.')) return camelCase(fullPath);
20+
const [programName, typeName] = fullPath.split('.');
21+
return `${camelCase(programName)}.${camelCase(typeName)}`;
22+
});
23+
const shouldInline = (typeName: CamelCaseString, programName: CamelCaseString | undefined): boolean => {
24+
if (typesToInline === '*') return true;
25+
const fullPath = `${programName}.${typeName}`;
26+
if (!!programName && typesToInlineCamelCased.includes(fullPath)) return true;
27+
return typesToInlineCamelCased.includes(typeName);
28+
};
2029

2130
return pipe(
2231
nonNullableIdentityVisitor(),
2332
v =>
2433
extendVisitor(v, {
2534
visitDefinedTypeLink(linkType, { self }) {
26-
if (!shouldInline(linkType.name)) {
35+
const programName = linkType.program?.name ?? findProgramNodeFromPath(stack.getPath())?.name;
36+
if (!shouldInline(linkType.name, programName)) {
2737
return linkType;
2838
}
2939
const definedTypePath = linkables.getPathOrThrow(stack.getPath('definedTypeLinkNode'));
@@ -42,7 +52,7 @@ export function unwrapDefinedTypesVisitor(typesToInline: string[] | '*' = '*') {
4252
.map(account => visit(account, self))
4353
.filter(assertIsNodeFilter('accountNode')),
4454
definedTypes: program.definedTypes
45-
.filter(definedType => !shouldInline(definedType.name))
55+
.filter(definedType => !shouldInline(definedType.name, program.name))
4656
.map(type => visit(type, self))
4757
.filter(assertIsNodeFilter('definedTypeNode')),
4858
instructions: program.instructions

packages/visitors/src/unwrapInstructionArgsDefinedTypesVisitor.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
import { assertIsNode, CamelCaseString, getAllDefinedTypes, isNode } from '@codama/nodes';
2-
import { rootNodeVisitor, visit } from '@codama/visitors-core';
1+
import { assertIsNode, CamelCaseString, definedTypeLinkNode, isNode } from '@codama/nodes';
2+
import { getRecordLinkablesVisitor, LinkableDictionary, rootNodeVisitor, visit } from '@codama/visitors-core';
33

44
import { getDefinedTypeHistogramVisitor } from './getDefinedTypeHistogramVisitor';
55
import { unwrapDefinedTypesVisitor } from './unwrapDefinedTypesVisitor';
66

77
export function unwrapInstructionArgsDefinedTypesVisitor() {
88
return rootNodeVisitor(root => {
99
const histogram = visit(root, getDefinedTypeHistogramVisitor());
10-
const allDefinedTypes = getAllDefinedTypes(root);
10+
const linkables = new LinkableDictionary();
11+
visit(root, getRecordLinkablesVisitor(linkables));
1112

12-
const definedTypesToInline: string[] = Object.keys(histogram)
13+
const definedTypesToInline = (Object.keys(histogram) as CamelCaseString[])
1314
// Get all defined types used exactly once as an instruction argument.
14-
.filter(
15-
name =>
16-
(histogram[name as CamelCaseString].total ?? 0) === 1 &&
17-
(histogram[name as CamelCaseString].directlyAsInstructionArgs ?? 0) === 1,
18-
)
15+
.filter(key => (histogram[key].total ?? 0) === 1 && (histogram[key].directlyAsInstructionArgs ?? 0) === 1)
1916
// Filter out enums which are better defined as external types.
20-
.filter(name => {
21-
const found = allDefinedTypes.find(type => type.name === name);
17+
.filter(key => {
18+
const names = key.split('.');
19+
const link = names.length == 2 ? definedTypeLinkNode(names[1], names[0]) : definedTypeLinkNode(key);
20+
const found = linkables.get([link]);
2221
return found && !isNode(found.type, 'enumTypeNode');
2322
});
2423

packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
enumTypeNode,
66
instructionArgumentNode,
77
instructionNode,
8+
numberTypeNode,
89
programNode,
10+
rootNode,
911
structFieldTypeNode,
1012
structTypeNode,
1113
} from '@codama/nodes';
@@ -65,14 +67,14 @@ test('it counts the amount of times defined types are used within the tree', ()
6567

6668
// Then we expect the following histogram.
6769
expect(histogram).toEqual({
68-
myEnum: {
70+
'customProgram.myEnum': {
6971
directlyAsInstructionArgs: 0,
7072
inAccounts: 1,
7173
inDefinedTypes: 0,
7274
inInstructionArgs: 0,
7375
total: 1,
7476
},
75-
myStruct: {
77+
'customProgram.myStruct': {
7678
directlyAsInstructionArgs: 1,
7779
inAccounts: 1,
7880
inDefinedTypes: 0,
@@ -81,3 +83,47 @@ test('it counts the amount of times defined types are used within the tree', ()
8183
},
8284
});
8385
});
86+
87+
test('it counts links from different programs separately', () => {
88+
// Given a program node with a defined type used in another type.
89+
const programA = programNode({
90+
definedTypes: [
91+
definedTypeNode({ name: 'myType', type: numberTypeNode('u8') }),
92+
definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
93+
],
94+
name: 'programA',
95+
publicKey: '1111',
96+
});
97+
98+
// And another program with a defined type sharing the same name.
99+
const programB = programNode({
100+
definedTypes: [
101+
definedTypeNode({ name: 'myType', type: numberTypeNode('u16') }),
102+
definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
103+
],
104+
name: 'programB',
105+
publicKey: '2222',
106+
});
107+
108+
// When we unwrap the defined type from programA.
109+
const node = rootNode(programA, [programB]);
110+
const histogram = visit(node, getDefinedTypeHistogramVisitor());
111+
112+
// Then we expect programA to have been modified but not programB.
113+
expect(histogram).toStrictEqual({
114+
'programA.myType': {
115+
directlyAsInstructionArgs: 0,
116+
inAccounts: 0,
117+
inDefinedTypes: 1,
118+
inInstructionArgs: 0,
119+
total: 1,
120+
},
121+
'programB.myType': {
122+
directlyAsInstructionArgs: 0,
123+
inAccounts: 0,
124+
inDefinedTypes: 1,
125+
inInstructionArgs: 0,
126+
total: 1,
127+
},
128+
});
129+
});

packages/visitors/test/unwrapDefinedTypesVisitor.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,42 @@ test('it follows linked nodes using the correct paths', () => {
7272
definedTypeNode({ name: 'typeA', type: numberTypeNode('u64') }),
7373
);
7474
});
75+
76+
test('it does not unwrap types from the wrong programs', () => {
77+
// Given a program node with a defined type used in another type.
78+
const programA = programNode({
79+
definedTypes: [
80+
definedTypeNode({ name: 'myType', type: numberTypeNode('u8') }),
81+
definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
82+
],
83+
name: 'programA',
84+
publicKey: '1111',
85+
});
86+
87+
// And another program with a defined type sharing the same name.
88+
const programB = programNode({
89+
definedTypes: [
90+
definedTypeNode({ name: 'myType', type: numberTypeNode('u16') }),
91+
definedTypeNode({ name: 'myCopyType', type: definedTypeLinkNode('myType') }),
92+
],
93+
name: 'programB',
94+
publicKey: '2222',
95+
});
96+
97+
// When we unwrap the defined type from programA.
98+
const node = rootNode(programA, [programB]);
99+
const result = visit(node, unwrapDefinedTypesVisitor(['programA.myType']));
100+
101+
// Then we expect programA to have been modified but not programB.
102+
assertIsNode(result, 'rootNode');
103+
expect(result).toStrictEqual(
104+
rootNode(
105+
programNode({
106+
definedTypes: [definedTypeNode({ name: 'myCopyType', type: numberTypeNode('u8') })],
107+
name: 'programA',
108+
publicKey: '1111',
109+
}),
110+
[programB],
111+
),
112+
);
113+
});

0 commit comments

Comments
 (0)