sol2uml 2.5.24 → 2.5.25
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/README.md +2 -0
- package/lib/converterAST2Classes.js +18 -1
- package/lib/converterClass2Dot.d.ts +2 -0
- package/lib/converterClass2Dot.js +8 -4
- package/lib/converterClasses2Dot.js +33 -0
- package/lib/parserEtherscan.js +23 -8
- package/lib/sol2uml.js +3 -1
- package/lib/squashClasses.js +6 -0
- package/lib/umlClass.d.ts +2 -0
- package/lib/umlClass.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,6 +111,7 @@ Options:
|
|
|
111
111
|
-c, --clusterFolders cluster contracts into source folders (default: false)
|
|
112
112
|
-hv, --hideVariables hide variables from contracts, interfaces, structs and enums (default: false)
|
|
113
113
|
-hf, --hideFunctions hide functions from contracts, interfaces and libraries (default: false)
|
|
114
|
+
-hy, --hideTypes hide types of variables, function arguments and return types (default: false)
|
|
114
115
|
-hp, --hidePrivates hide private and internal attributes and operators (default: false)
|
|
115
116
|
-hm, --hideModifiers hide modifier functions from contracts (default: false)
|
|
116
117
|
-ht, --hideEvents hide events from contracts, interfaces and libraries (default: false)
|
|
@@ -122,6 +123,7 @@ Options:
|
|
|
122
123
|
-hi, --hideInterfaces hide interfaces (default: false)
|
|
123
124
|
-ha, --hideAbstracts hide abstract contracts (default: false)
|
|
124
125
|
-hn, --hideFilename hide relative path and file name (default: false)
|
|
126
|
+
-hd, --hideDepFunctions hide function names on dependency arrows (default: false)
|
|
125
127
|
-s, --squash squash inherited contracts to the base contract(s) (default: false)
|
|
126
128
|
-hsc, --hideSourceContract hide the source contract when using squash (default: false)
|
|
127
129
|
-h, --help display help for command
|
|
@@ -533,7 +533,24 @@ function parseExpression(expression, umlClass) {
|
|
|
533
533
|
parseExpression(expression.right, umlClass);
|
|
534
534
|
}
|
|
535
535
|
else if (expression.type === 'FunctionCall') {
|
|
536
|
-
|
|
536
|
+
if (expression.expression.type === 'MemberAccess' &&
|
|
537
|
+
expression.expression.expression?.type === 'Identifier') {
|
|
538
|
+
// Pattern: ClassName.functionName(args) — explicit library/contract call
|
|
539
|
+
const memberName = expression.expression.memberName;
|
|
540
|
+
umlClass.addAssociation({
|
|
541
|
+
referenceType: umlClass_1.ReferenceType.Memory,
|
|
542
|
+
targetUmlClassName: expression.expression.expression.name,
|
|
543
|
+
functionsCalled: [memberName],
|
|
544
|
+
});
|
|
545
|
+
umlClass.memberAccessCalls.add(memberName);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// Track member access calls inside other FunctionCall patterns (e.g. x.functionName())
|
|
549
|
+
if (expression.expression.type === 'MemberAccess') {
|
|
550
|
+
umlClass.memberAccessCalls.add(expression.expression.memberName);
|
|
551
|
+
}
|
|
552
|
+
parseExpression(expression.expression, umlClass);
|
|
553
|
+
}
|
|
537
554
|
expression.arguments.forEach((arg) => {
|
|
538
555
|
parseExpression(arg, umlClass);
|
|
539
556
|
});
|
|
@@ -4,6 +4,7 @@ export interface ClassOptions {
|
|
|
4
4
|
hideContracts?: boolean;
|
|
5
5
|
hideVariables?: boolean;
|
|
6
6
|
hideFunctions?: boolean;
|
|
7
|
+
hideTypes?: boolean;
|
|
7
8
|
hideModifiers?: boolean;
|
|
8
9
|
hideEvents?: boolean;
|
|
9
10
|
hideStructs?: boolean;
|
|
@@ -14,6 +15,7 @@ export interface ClassOptions {
|
|
|
14
15
|
hideAbstracts?: boolean;
|
|
15
16
|
hideFilename?: boolean;
|
|
16
17
|
hideSourceContract?: boolean;
|
|
18
|
+
hideDepFunctions?: boolean;
|
|
17
19
|
backColor?: string;
|
|
18
20
|
shapeColor?: string;
|
|
19
21
|
fillColor?: string;
|
|
@@ -116,7 +116,8 @@ const dotAttributes = (attributes, options, vizGroup, indent = true) => {
|
|
|
116
116
|
const sourceContract = attribute.sourceContract && !options.hideSourceContract
|
|
117
117
|
? ` \\<\\<${attribute.sourceContract}\\>\\>`
|
|
118
118
|
: '';
|
|
119
|
-
|
|
119
|
+
const type = options.hideTypes ? '' : `: ${attribute.type}`;
|
|
120
|
+
dotString += `${indentString}${attribute.name}${type}${sourceContract}\\l`;
|
|
120
121
|
});
|
|
121
122
|
return dotString;
|
|
122
123
|
};
|
|
@@ -176,8 +177,8 @@ const dotOperators = (umlClass, vizGroup, operators, options) => {
|
|
|
176
177
|
dotString += dotOperatorStereotype(umlClass, operator.stereotype);
|
|
177
178
|
}
|
|
178
179
|
dotString += operator.name;
|
|
179
|
-
dotString += dotParameters(operator.parameters);
|
|
180
|
-
if (operator.returnParameters?.length > 0) {
|
|
180
|
+
dotString += dotParameters(operator.parameters, false, options.hideTypes);
|
|
181
|
+
if (operator.returnParameters?.length > 0 && !options.hideTypes) {
|
|
181
182
|
dotString += ': ' + dotParameters(operator.returnParameters, true);
|
|
182
183
|
}
|
|
183
184
|
if (options.hideModifiers === false && operator.modifiers?.length > 0) {
|
|
@@ -214,7 +215,10 @@ const dotOperatorStereotype = (umlClass, operatorStereotype) => {
|
|
|
214
215
|
}
|
|
215
216
|
return dotString + ' ';
|
|
216
217
|
};
|
|
217
|
-
const dotParameters = (parameters, returnParams = false) => {
|
|
218
|
+
const dotParameters = (parameters, returnParams = false, hideTypes = false) => {
|
|
219
|
+
if (hideTypes && !returnParams) {
|
|
220
|
+
return '()';
|
|
221
|
+
}
|
|
218
222
|
if (parameters.length == 1 && !parameters[0].name) {
|
|
219
223
|
if (returnParams) {
|
|
220
224
|
return parameters[0].type;
|
|
@@ -139,6 +139,39 @@ function addAssociationToDot(sourceUmlClass, targetUmlClass, association, classO
|
|
|
139
139
|
dotString += 'weight=3, ';
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
// Add function name labels on dependency arrows (not on realization/inheritance)
|
|
143
|
+
if (!association.realization && !classOptions.hideDepFunctions) {
|
|
144
|
+
const functionNames = getAssociationFunctionNames(sourceUmlClass, targetUmlClass, association);
|
|
145
|
+
if (functionNames.length > 0) {
|
|
146
|
+
const label = functionNames.map((fn) => fn + '\\l').join('');
|
|
147
|
+
dotString += `label="${label}", fontsize=10, `;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
142
150
|
return dotString + ']';
|
|
143
151
|
}
|
|
152
|
+
function getAssociationFunctionNames(sourceUmlClass, targetUmlClass, association) {
|
|
153
|
+
const names = new Set();
|
|
154
|
+
// Add explicitly captured function names
|
|
155
|
+
if (association.functionsCalled) {
|
|
156
|
+
for (const fn of association.functionsCalled) {
|
|
157
|
+
names.add(fn);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Cross-reference: if no explicit calls, check if source class's member
|
|
161
|
+
// access calls match any of the target class's operator names.
|
|
162
|
+
// Applies to libraries (using...for) and interfaces (e.g. IERC20(token).transfer())
|
|
163
|
+
if (names.size === 0 &&
|
|
164
|
+
(targetUmlClass.stereotype === umlClass_1.ClassStereotype.Library ||
|
|
165
|
+
targetUmlClass.stereotype === umlClass_1.ClassStereotype.Interface) &&
|
|
166
|
+
sourceUmlClass.memberAccessCalls?.size > 0 &&
|
|
167
|
+
targetUmlClass.operators?.length > 0) {
|
|
168
|
+
const targetOperatorNames = new Set(targetUmlClass.operators.map((op) => op.name));
|
|
169
|
+
for (const callName of sourceUmlClass.memberAccessCalls) {
|
|
170
|
+
if (targetOperatorNames.has(callName)) {
|
|
171
|
+
names.add(callName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [...names].sort();
|
|
176
|
+
}
|
|
144
177
|
//# sourceMappingURL=converterClasses2Dot.js.map
|
package/lib/parserEtherscan.js
CHANGED
|
@@ -192,14 +192,29 @@ class EtherscanParser {
|
|
|
192
192
|
const description = `get verified source code for address ${contractAddress} from Etherscan API.`;
|
|
193
193
|
try {
|
|
194
194
|
debug(`About to get Solidity source code for ${contractAddress} from ${this.url}`);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
let response;
|
|
196
|
+
const maxRetries = 3;
|
|
197
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
198
|
+
response = await axios_1.default.get(this.url, {
|
|
199
|
+
params: {
|
|
200
|
+
module: 'contract',
|
|
201
|
+
action: 'getsourcecode',
|
|
202
|
+
address: contractAddress,
|
|
203
|
+
apikey: this.apiKey,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
// Retry on rate limit errors
|
|
207
|
+
if (!Array.isArray(response?.data?.result) &&
|
|
208
|
+
typeof response?.data?.result === 'string' &&
|
|
209
|
+
response.data.result.includes('rate limit') &&
|
|
210
|
+
attempt < maxRetries) {
|
|
211
|
+
const delay = attempt * 2000;
|
|
212
|
+
debug(`Rate limited on attempt ${attempt}. Retrying in ${delay}ms...`);
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
203
218
|
if (!Array.isArray(response?.data?.result)) {
|
|
204
219
|
throw new Error(`Failed to ${description}. No result array in HTTP data: ${JSON.stringify(response?.data)}`);
|
|
205
220
|
}
|
package/lib/sol2uml.js
CHANGED
|
@@ -34,7 +34,7 @@ Can also flatten or compare verified source files on Etherscan-like explorers.`)
|
|
|
34
34
|
parserEtherscan_1.networks.join(', '))
|
|
35
35
|
.default('ethereum')
|
|
36
36
|
.env('ETH_NETWORK'))
|
|
37
|
-
.addOption(new commander_1.Option('-e, --explorerUrl <url>', 'Override the `network` option with a custom blockchain explorer API URL. eg Polygon
|
|
37
|
+
.addOption(new commander_1.Option('-e, --explorerUrl <url>', 'Override the `network` option with a custom blockchain explorer API URL. eg Polygon Amoy testnet https://api-amoy.polygonscan.com/api').env('EXPLORER_URL'))
|
|
38
38
|
.addOption(new commander_1.Option('-k, --apiKey <key>', 'Blockchain explorer API key.').env('SCAN_API_KEY'))
|
|
39
39
|
.option('-bc, --backColor <color>', 'Canvas background color. "none" will use a transparent canvas.', 'white')
|
|
40
40
|
.option('-sc, --shapeColor <color>', 'Basic drawing color for graphics, not text', 'black')
|
|
@@ -61,6 +61,7 @@ program
|
|
|
61
61
|
.option('-c, --clusterFolders', 'cluster contracts into source folders', false)
|
|
62
62
|
.option('-hv, --hideVariables', 'hide variables from contracts, interfaces, structs and enums', false)
|
|
63
63
|
.option('-hf, --hideFunctions', 'hide functions from contracts, interfaces and libraries', false)
|
|
64
|
+
.option('-hy, --hideTypes', 'hide types of variables, function arguments and return types', false)
|
|
64
65
|
.option('-hp, --hidePrivates', 'hide private and internal attributes and operators', false)
|
|
65
66
|
.option('-hm, --hideModifiers', 'hide modifier functions from contracts', false)
|
|
66
67
|
.option('-ht, --hideEvents', 'hide events from contracts, interfaces and libraries', false)
|
|
@@ -72,6 +73,7 @@ program
|
|
|
72
73
|
.option('-hi, --hideInterfaces', 'hide interfaces', false)
|
|
73
74
|
.option('-ha, --hideAbstracts', 'hide abstract contracts', false)
|
|
74
75
|
.option('-hn, --hideFilename', 'hide relative path and file name', false)
|
|
76
|
+
.option('-hd, --hideDepFunctions', 'hide function names on dependency arrows', false)
|
|
75
77
|
.option('-s, --squash', 'squash inherited contracts to the base contract(s)', false)
|
|
76
78
|
.option('-hsc, --hideSourceContract', 'hide the source contract when using squash', false)
|
|
77
79
|
.action(async (fileFolderAddress, options, command) => {
|
package/lib/squashClasses.js
CHANGED
|
@@ -117,6 +117,12 @@ const recursiveSquash = (squashedClass, inheritedContractNames, baseClass, umlCl
|
|
|
117
117
|
baseClass.enums.forEach((e) => squashedClass.enums.push(e));
|
|
118
118
|
baseClass.structs.forEach((s) => squashedClass.structs.push(s));
|
|
119
119
|
baseClass.imports.forEach((i) => squashedClass.imports.push(i));
|
|
120
|
+
// Merge memberAccessCalls for cross-referencing function names on dependency arrows
|
|
121
|
+
if (baseClass.memberAccessCalls?.size > 0) {
|
|
122
|
+
for (const call of baseClass.memberAccessCalls) {
|
|
123
|
+
squashedClass.memberAccessCalls.add(call);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
120
126
|
// copy the functions
|
|
121
127
|
baseClass.operators.forEach((f) => squashedClass.operators.push({
|
|
122
128
|
...f,
|
package/lib/umlClass.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export interface Association {
|
|
|
69
69
|
parentUmlClassName?: string;
|
|
70
70
|
targetUmlClassName: string;
|
|
71
71
|
realization?: boolean;
|
|
72
|
+
functionsCalled?: string[];
|
|
72
73
|
}
|
|
73
74
|
export interface Constants {
|
|
74
75
|
name: string;
|
|
@@ -108,6 +109,7 @@ export declare class UmlClass implements ClassProperties {
|
|
|
108
109
|
associations: {
|
|
109
110
|
[name: string]: Association;
|
|
110
111
|
};
|
|
112
|
+
memberAccessCalls: Set<string>;
|
|
111
113
|
constructor(properties: ClassProperties);
|
|
112
114
|
addAssociation(association: Association): void;
|
|
113
115
|
/**
|
package/lib/umlClass.js
CHANGED
|
@@ -52,6 +52,8 @@ class UmlClass {
|
|
|
52
52
|
this.enums = [];
|
|
53
53
|
this.structs = [];
|
|
54
54
|
this.associations = {};
|
|
55
|
+
// Tracks all member access function call names for cross-referencing with using...for libraries
|
|
56
|
+
this.memberAccessCalls = new Set();
|
|
55
57
|
if (!properties || !properties.name) {
|
|
56
58
|
throw TypeError(`Failed to instantiate UML Class with no name property`);
|
|
57
59
|
}
|
|
@@ -73,6 +75,20 @@ class UmlClass {
|
|
|
73
75
|
if (association.referenceType === ReferenceType.Storage) {
|
|
74
76
|
this.associations[association.targetUmlClassName].referenceType = ReferenceType.Storage;
|
|
75
77
|
}
|
|
78
|
+
// Merge functionsCalled arrays with deduplication
|
|
79
|
+
if (association.functionsCalled?.length) {
|
|
80
|
+
const existing = this.associations[association.targetUmlClassName];
|
|
81
|
+
if (!existing.functionsCalled) {
|
|
82
|
+
existing.functionsCalled = [...association.functionsCalled];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const fn of association.functionsCalled) {
|
|
86
|
+
if (!existing.functionsCalled.includes(fn)) {
|
|
87
|
+
existing.functionsCalled.push(fn);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
94
|
/**
|