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 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
- parseExpression(expression.expression, umlClass);
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
- dotString += `${indentString}${attribute.name}: ${attribute.type}${sourceContract}\\l`;
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
@@ -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
- const response = await axios_1.default.get(this.url, {
196
- params: {
197
- module: 'contract',
198
- action: 'getsourcecode',
199
- address: contractAddress,
200
- apikey: this.apiKey,
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 Mumbai testnet https://api-testnet.polygonscan.com/api').env('EXPLORER_URL'))
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) => {
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol2uml",
3
- "version": "2.5.24",
3
+ "version": "2.5.25",
4
4
  "description": "Solidity contract visualisation tool.",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",