diff --git a/README.md b/README.md
index 1c70e0f4..296dd73c 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,8 @@ Options:
-hi, --hideInterfaces hide interfaces (default: false)
-ha, --hideAbstracts hide abstract contracts (default: false)
-hn, --hideFilename hide relative path and file name (default: false)
+ -s, --squash squash inherited contracts to the base contract(s) (default: false)
+ -hsc, --hideSourceContract hide the source contract when using squash (default: false)
-h, --help display help for command
```
diff --git a/examples/CErc20-hide.svg b/examples/CErc20-hide.svg
new file mode 100644
index 00000000..364252aa
--- /dev/null
+++ b/examples/CErc20-hide.svg
@@ -0,0 +1,234 @@
+
+
+
+
+
diff --git a/examples/CErc20.svg b/examples/CErc20.svg
new file mode 100644
index 00000000..7fe7db9d
--- /dev/null
+++ b/examples/CErc20.svg
@@ -0,0 +1,873 @@
+
+
+
+
+
diff --git a/examples/README.md b/examples/README.md
index 228653b4..3fd13074 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -39,62 +39,74 @@ sol2uml -n bsc 0xB07c1C479b2Fdeb9f9B2d02300C13b328BF86d65

Generated from version [4.7.3 contracts](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/v4.7.3/contracts)
-## MakerDAO's SAI Token
+## Uniswap V3 Router
-
-Generated from GitHub commit [84c682eeb4e27264503370ef5aafcb9ee3217acb](https://github.com/makerdao/sai/tree/84c682eeb4e27264503370ef5aafcb9ee3217acb/src) of makerdao/sai/src
+* -hp hide private and internal variables and functions
+* -hi hide interfaces
+* -hl hide libraries
+* -he hide enums
-## Tether
-
-
+
Generated from running
-
```
-sol2uml 0xdAC17F958D2ee523a2206206994597C13D831ec7
+sol2uml -hp -hi -hl -hs -he 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
```
-This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code
+This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45#code
-## 0x
-
-Generated from running
+## Uniswap V3 Router Squashed
+
+Same as the previous diagram but the inherited contracts are squashed into a single class diagram with the `-s, --squash` option.
+
+The last stereotype is the contract the variable or function is implemented in. For example, `unwrapWETH9` is implemented in the `PeripheryPaymentsWithFeeExtended` contract.
+
+
+Generated from running
```
-sol2uml 0x4F833a24e1f95D70F028921e27040Ca56E09AB0b
+sol2uml -s -hp -hi -hl -hs -he 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
```
-This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0x4F833a24e1f95D70F028921e27040Ca56E09AB0b#code
+## Uniswap V3 Router Squashed No Source
-## Compound Finance's cDAI
+Adding the `-hsc, --hideSourceContract` option to the previous diagram removes the stereotype with the source contract the variable or function was implemented in.
-
-Generated from running
+
+Generated from running
```
-sol2uml 0xf5dce57282a584d2746faf1593d3121fcac444dc
+sol2uml -s -hsc -hp -hi -hl -hs -he 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
```
-This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0xf5dce57282a584d2746faf1593d3121fcac444dc#code
-
-## Chainlink
+## Tether
-
+
Generated from running
-```bash
-sol2uml 0x79fEbF6B9F76853EDBcBc913e6aAE8232cFB9De9
+```
+sol2uml 0xdAC17F958D2ee523a2206206994597C13D831ec7
```
-This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0x79fEbF6B9F76853EDBcBc913e6aAE8232cFB9De9#code
+This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code
-## Augur
+## Compound Finance's cDAI
-
+
Generated from running
-
-```bash
-sol2uml 0x7F27B0598949DbF9e539BBD217f15BF3F5E97999
```
+sol2uml 0xf5dce57282a584d2746faf1593d3121fcac444dc
+```
+
+This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0xf5dce57282a584d2746faf1593d3121fcac444dc#code
+
+## Compound Finance's cDAI Hide
-This uses the verified Solidity code loaded to Etherscan https://etherscan.io/address/0x7F27B0598949DbF9e539BBD217f15BF3F5E97999#code
+Same as the previous except enums, stucts and interfaces are hidden.
+Also, only classes linked to the base `CErc20` contract are included.
+
+
+Generated from running
+```
+sol2uml -b CErc20 -he -hs -hi 0xf5dce57282a584d2746faf1593d3121fcac444dc
+```
diff --git a/examples/uniswap-router-squash-no-source.svg b/examples/uniswap-router-squash-no-source.svg
new file mode 100644
index 00000000..54285b7c
--- /dev/null
+++ b/examples/uniswap-router-squash-no-source.svg
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/examples/uniswap-router-squash.svg b/examples/uniswap-router-squash.svg
new file mode 100644
index 00000000..2faaaed1
--- /dev/null
+++ b/examples/uniswap-router-squash.svg
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/examples/uniswap-router.svg b/examples/uniswap-router.svg
new file mode 100644
index 00000000..c3715e58
--- /dev/null
+++ b/examples/uniswap-router.svg
@@ -0,0 +1,362 @@
+
+
+
+
+
diff --git a/lib/converterClass2Dot.d.ts b/lib/converterClass2Dot.d.ts
index ed12da33..6574c136 100644
--- a/lib/converterClass2Dot.d.ts
+++ b/lib/converterClass2Dot.d.ts
@@ -12,5 +12,6 @@ export interface ClassOptions {
hidePrivates?: boolean;
hideAbstracts?: boolean;
hideFilename?: boolean;
+ hideSourceContract?: boolean;
}
export declare const convertClass2Dot: (umlClass: UmlClass, options?: ClassOptions) => string;
diff --git a/lib/converterClass2Dot.js b/lib/converterClass2Dot.js
index b25b0868..dc2c1cef 100644
--- a/lib/converterClass2Dot.js
+++ b/lib/converterClass2Dot.js
@@ -71,7 +71,8 @@ const dotAttributeVisibilities = (umlClass, options) => {
if (umlClass.stereotype === umlClass_1.ClassStereotype.Struct ||
umlClass.stereotype === umlClass_1.ClassStereotype.Enum ||
umlClass.stereotype === umlClass_1.ClassStereotype.Constant) {
- return dotString + dotAttributes(umlClass.attributes, undefined, false);
+ return (dotString +
+ dotAttributes(umlClass.attributes, options, undefined, false));
}
// For each visibility group
for (const vizGroup of ['Private', 'Internal', 'External', 'Public']) {
@@ -100,11 +101,11 @@ const dotAttributeVisibilities = (umlClass, options) => {
attributes.push(attribute);
}
}
- dotString += dotAttributes(attributes, vizGroup);
+ dotString += dotAttributes(attributes, options, vizGroup);
}
return dotString;
};
-const dotAttributes = (attributes, vizGroup, indent = true) => {
+const dotAttributes = (attributes, options, vizGroup, indent = true) => {
if (!attributes || attributes.length === 0) {
return '';
}
@@ -112,7 +113,10 @@ const dotAttributes = (attributes, vizGroup, indent = true) => {
let dotString = vizGroup ? vizGroup + ':\\l' : '';
// for each attribute
attributes.forEach((attribute) => {
- dotString += `${indentString}${attribute.name}: ${attribute.type}\\l`;
+ const sourceContract = attribute.sourceContract && !options.hideSourceContract
+ ? ` \\<\\<${attribute.sourceContract}\\>\\>`
+ : '';
+ dotString += `${indentString}${attribute.name}: ${attribute.type}${sourceContract}\\l`;
});
return dotString;
};
@@ -179,6 +183,8 @@ const dotOperators = (umlClass, vizGroup, operators, options) => {
if (options.hideModifiers === false && operator.modifiers?.length > 0) {
dotString += ` \\<\\<${operator.modifiers.join(', ')}\\>\\>`;
}
+ if (operator.sourceContract && !options.hideSourceContract)
+ dotString += ` \\<\\<${operator.sourceContract}\\>\\>`;
dotString += '\\l';
}
return dotString;
diff --git a/lib/filterClasses.d.ts b/lib/filterClasses.d.ts
index 82053863..47ad4d5b 100644
--- a/lib/filterClasses.d.ts
+++ b/lib/filterClasses.d.ts
@@ -1,5 +1,7 @@
import { WeightedDiGraph } from 'js-graph-algorithms';
import { UmlClass } from './umlClass';
+import { ClassOptions } from './converterClass2Dot';
+export declare const filterHiddenClasses: (umlClasses: UmlClass[], options: ClassOptions) => UmlClass[];
export declare const classesConnectedToBaseContracts: (umlClasses: UmlClass[], baseContractNames: string[], depth?: number) => UmlClass[];
export declare const classesConnectedToBaseContract: (umlClasses: UmlClass[], baseContractName: string, weightedDirectedGraph: WeightedDiGraph, depth?: number) => {
[contractName: string]: UmlClass;
diff --git a/lib/filterClasses.js b/lib/filterClasses.js
index e8e37ae0..620f9bf4 100644
--- a/lib/filterClasses.js
+++ b/lib/filterClasses.js
@@ -1,8 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-exports.topologicalSortClasses = exports.classesConnectedToBaseContract = exports.classesConnectedToBaseContracts = void 0;
+exports.topologicalSortClasses = exports.classesConnectedToBaseContract = exports.classesConnectedToBaseContracts = exports.filterHiddenClasses = void 0;
const js_graph_algorithms_1 = require("js-graph-algorithms");
+const umlClass_1 = require("./umlClass");
const associations_1 = require("./associations");
+const filterHiddenClasses = (umlClasses, options) => {
+ return umlClasses.filter((u) => (u.stereotype === umlClass_1.ClassStereotype.Enum && !options.hideEnums) ||
+ (u.stereotype === umlClass_1.ClassStereotype.Struct && !options.hideStructs) ||
+ (u.stereotype === umlClass_1.ClassStereotype.Abstract &&
+ !options.hideAbstracts) ||
+ (u.stereotype === umlClass_1.ClassStereotype.Interface &&
+ !options.hideInterfaces) ||
+ (u.stereotype === umlClass_1.ClassStereotype.Constant &&
+ !options.hideConstants) ||
+ (u.stereotype === umlClass_1.ClassStereotype.Library &&
+ !options.hideLibraries) ||
+ u.stereotype === umlClass_1.ClassStereotype.None ||
+ u.stereotype === umlClass_1.ClassStereotype.Contract);
+};
+exports.filterHiddenClasses = filterHiddenClasses;
const classesConnectedToBaseContracts = (umlClasses, baseContractNames, depth) => {
let filteredUmlClasses = {};
const weightedDirectedGraph = loadWeightedDirectedGraph(umlClasses);
@@ -35,7 +51,9 @@ const classesConnectedToBaseContract = (umlClasses, baseContractName, weightedDi
};
exports.classesConnectedToBaseContract = classesConnectedToBaseContract;
function loadWeightedDirectedGraph(umlClasses) {
- const weightedDirectedGraph = new js_graph_algorithms_1.WeightedDiGraph(umlClasses.length); // the number vertices in the graph
+ const weightedDirectedGraph = new js_graph_algorithms_1.WeightedDiGraph(
+ // the number vertices in the graph
+ umlClass_1.UmlClass.idCounter + 1);
for (const sourceUmlClass of umlClasses) {
for (const association of Object.values(sourceUmlClass.associations)) {
// Find the first UML Class that matches the target class name
@@ -43,6 +61,8 @@ function loadWeightedDirectedGraph(umlClasses) {
if (!targetUmlClass) {
continue;
}
+ const isTarget = umlClasses.find((u) => u.id === targetUmlClass.id);
+ console.log(`isTarget ${isTarget} Adding edge from ${sourceUmlClass.name} with id ${sourceUmlClass.id} to ${targetUmlClass.name} with id ${targetUmlClass.id} and type ${targetUmlClass.stereotype}`);
weightedDirectedGraph.addEdge(new js_graph_algorithms_1.Edge(sourceUmlClass.id, targetUmlClass.id, 1));
}
}
diff --git a/lib/sol2uml.js b/lib/sol2uml.js
index caf8da3e..61a57c2e 100755
--- a/lib/sol2uml.js
+++ b/lib/sol2uml.js
@@ -11,6 +11,7 @@ const converterStorage2Dot_1 = require("./converterStorage2Dot");
const regEx_1 = require("./utils/regEx");
const writerFiles_1 = require("./writerFiles");
const path_1 = require("path");
+const squashClasses_1 = require("./squashClasses");
const program = new commander_1.Command();
const version = (0, path_1.basename)(__dirname) === 'lib'
? require('../package.json').version // used when run from compile js in /lib
@@ -69,6 +70,8 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
.option('-hi, --hideInterfaces', 'hide interfaces', false)
.option('-ha, --hideAbstracts', 'hide abstract contracts', false)
.option('-hn, --hideFilename', 'hide relative path and file name', false)
+ .option('-s, --squash', 'squash inherited contracts to the base contract(s)', false)
+ .option('-hsc, --hideSourceContract', 'hide the source contract when using squash', false)
.action(async (fileFolderAddress, options, command) => {
try {
const combinedOptions = {
@@ -76,12 +79,23 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
...options,
};
let { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
- let filteredUmlClasses = umlClasses;
- if (options.baseContractNames) {
- const baseContractNames = options.baseContractNames.split(',');
- filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(umlClasses, baseContractNames, options.depth);
+ if (options.squash &&
+ // Must specify base contract(s) or parse from Etherscan to get contractName
+ !(options.baseContractNames || contractName)) {
+ throw Error('Must specify base contract(s) when using the squash option against local Solidity files.');
+ }
+ // Filter out any class stereotypes that are to be hidden
+ let filteredUmlClasses = (0, filterClasses_1.filterHiddenClasses)(umlClasses, options);
+ const baseContractNames = options.baseContractNames?.split(',');
+ if (baseContractNames) {
+ // Find all the classes connected to the base classes
+ filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(filteredUmlClasses, baseContractNames, options.depth);
contractName = baseContractNames[0];
}
+ // squash contracts
+ if (options.squash) {
+ filteredUmlClasses = (0, squashClasses_1.squashUmlClasses)(filteredUmlClasses, baseContractNames || [contractName]);
+ }
const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName || 'classDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
debug(`Finished generating UML`);
diff --git a/lib/squashClasses.d.ts b/lib/squashClasses.d.ts
new file mode 100644
index 00000000..4180ceae
--- /dev/null
+++ b/lib/squashClasses.d.ts
@@ -0,0 +1,2 @@
+import { UmlClass } from './umlClass';
+export declare const squashUmlClasses: (umlClasses: UmlClass[], squashContractNames: string[]) => UmlClass[];
diff --git a/lib/squashClasses.js b/lib/squashClasses.js
new file mode 100644
index 00000000..dffcf5e1
--- /dev/null
+++ b/lib/squashClasses.js
@@ -0,0 +1,143 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.squashUmlClasses = void 0;
+const umlClass_1 = require("./umlClass");
+const crypto = __importStar(require("crypto"));
+const debug = require('debug')('sol2uml');
+const squashUmlClasses = (umlClasses, squashContractNames) => {
+ let removedClassIds = [];
+ for (const squashContractName of squashContractNames) {
+ // Find the base UML Class to squash
+ let baseIndex = umlClasses.findIndex(({ name }) => {
+ return name === squashContractName;
+ });
+ if (baseIndex === undefined) {
+ throw Error(`Failed to find contract with name "${squashContractName}" to squash`);
+ }
+ const baseClass = umlClasses[baseIndex];
+ let squashedClass = new umlClass_1.UmlClass({
+ name: baseClass.name,
+ absolutePath: baseClass.absolutePath,
+ relativePath: baseClass.relativePath,
+ });
+ squashedClass.id = baseClass.id;
+ const result = recursiveSquash(squashedClass, [], baseClass, umlClasses, 1);
+ removedClassIds = removedClassIds.concat(result.removedClassIds);
+ // Remove overridden functions from squashed class
+ squashedClass.operators = reduceOperators(squashedClass.operators);
+ umlClasses[baseIndex] = squashedClass;
+ }
+ // filter the list of classes that will be rendered
+ return umlClasses.filter((u) =>
+ // remove any squashed inherited contracts
+ !removedClassIds.includes(u.id) ||
+ // Include all base contracts
+ squashContractNames.includes(u.name));
+};
+exports.squashUmlClasses = squashUmlClasses;
+const recursiveSquash = (squashedClass, inheritedContractNames, baseClass, umlClasses, startPosition) => {
+ let currentPosition = startPosition;
+ const removedClassIds = [];
+ // For each association from the baseClass
+ for (const [targetClassName, association] of Object.entries(baseClass.associations)) {
+ // if inheritance and (Abstract or Contract)
+ // Libraries and Interfaces will be copied
+ if (association.realization) {
+ // Find the target UML Class
+ const inheritedContract = umlClasses.find(({ name }) => {
+ return name === targetClassName;
+ });
+ if (!inheritedContract) {
+ debug(`Warning: failed to find inherited contract with name ${targetClassName}`);
+ continue;
+ }
+ // Is the associated class a contract or abstract contract?
+ if (inheritedContract?.stereotype === umlClass_1.ClassStereotype.Library) {
+ squashedClass.addAssociation(association);
+ }
+ else {
+ // has the contract already been added to the inheritance tree?
+ const alreadyInherited = inheritedContractNames.includes(inheritedContract.name);
+ // Do not add inherited contract if it has already been added to the inheritance tree
+ if (!alreadyInherited) {
+ inheritedContractNames.push(inheritedContract.name);
+ const squashResult = recursiveSquash(squashedClass, inheritedContractNames, inheritedContract, umlClasses, currentPosition++);
+ // Add to list of removed class ids
+ removedClassIds.push(...squashResult.removedClassIds, inheritedContract.id);
+ }
+ }
+ }
+ else {
+ // Copy association but will not duplicate it
+ squashedClass.addAssociation(association);
+ }
+ }
+ // Copy class properties from the baseClass to the squashedClass
+ baseClass.constants.forEach((c) => squashedClass.constants.push({ ...c, sourceContract: baseClass.name }));
+ baseClass.attributes.forEach((a) => squashedClass.attributes.push({ ...a, sourceContract: baseClass.name }));
+ baseClass.enums.forEach((e) => squashedClass.enums.push(e));
+ baseClass.structs.forEach((s) => squashedClass.structs.push(s));
+ baseClass.imports.forEach((i) => squashedClass.imports.push(i));
+ // copy the functions
+ baseClass.operators.forEach((f) => squashedClass.operators.push({
+ ...f,
+ hash: hash(f),
+ inheritancePosition: currentPosition,
+ sourceContract: baseClass.name,
+ }));
+ return {
+ currentPosition,
+ removedClassIds,
+ };
+};
+const hash = (operator) => {
+ const hash = crypto.createHash('sha256');
+ let data = operator.name ?? 'fallback';
+ operator.parameters?.forEach((p) => {
+ data += ',' + p.type;
+ });
+ operator.returnParameters?.forEach((p) => {
+ data += ',' + p.type;
+ });
+ return hash.update(data).digest('hex');
+};
+const reduceOperators = (operators) => {
+ const hashes = new Set(operators.map((o) => o.hash));
+ const operatorsWithNoHash = operators.filter((o) => !o.hash);
+ const newOperators = [];
+ for (const hash of hashes) {
+ const operator = operators
+ .filter((o) => o.hash === hash)
+ // sort operators by inheritance position. smaller to highest
+ .sort((o) => o.inheritancePosition)
+ // get last operator in the array
+ .slice(-1)[0];
+ newOperators.push(operator);
+ }
+ newOperators.push(...operatorsWithNoHash);
+ return newOperators;
+};
+//# sourceMappingURL=squashClasses.js.map
\ No newline at end of file
diff --git a/lib/umlClass.d.ts b/lib/umlClass.d.ts
index afc84193..8861772c 100644
--- a/lib/umlClass.d.ts
+++ b/lib/umlClass.d.ts
@@ -44,6 +44,7 @@ export interface Attribute {
type?: string;
attributeType?: AttributeType;
compiled?: boolean;
+ sourceContract?: string;
}
export interface Parameter {
name?: string;
@@ -55,6 +56,9 @@ export interface Operator extends Attribute {
returnParameters?: Parameter[];
isPayable?: boolean;
modifiers?: string[];
+ hash?: string;
+ inheritancePosition?: number;
+ sourceContract?: string;
}
export declare enum ReferenceType {
Memory = 0,
@@ -63,12 +67,12 @@ export declare enum ReferenceType {
export interface Association {
referenceType: ReferenceType;
targetUmlClassName: string;
- targetUmlClassStereotype?: ClassStereotype;
realization?: boolean;
}
export interface Constants {
name: string;
value: number;
+ sourceContract?: string;
}
export interface ClassProperties {
name: string;
diff --git a/lib/umlClass.js b/lib/umlClass.js
index f67346e9..56a7ef8b 100644
--- a/lib/umlClass.js
+++ b/lib/umlClass.js
@@ -45,6 +45,7 @@ var ReferenceType;
})(ReferenceType = exports.ReferenceType || (exports.ReferenceType = {}));
class UmlClass {
constructor(properties) {
+ this.imports = [];
this.constants = [];
this.attributes = [];
this.operators = [];
@@ -83,9 +84,7 @@ class UmlClass {
* Does not include any grand parent associations. That has to be done recursively.
*/
getParentContracts() {
- return Object.values(this.associations).filter((association) => association.realization &&
- association.targetUmlClassStereotype !==
- ClassStereotype.Interface);
+ return Object.values(this.associations).filter((association) => association.realization);
}
}
exports.UmlClass = UmlClass;
diff --git a/package.json b/package.json
index f1ea822e..9a08e94b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sol2uml",
- "version": "2.2.6",
+ "version": "2.3.0",
"description": "Solidity contract visualisation tool.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
diff --git a/src/contracts/squash.sol b/src/contracts/squash.sol
new file mode 100644
index 00000000..67bfb3be
--- /dev/null
+++ b/src/contracts/squash.sol
@@ -0,0 +1,77 @@
+pragma solidity ^0.8.16;
+
+contract CommonContract {
+ function commonFunction() public {}
+ function commonOverride() public virtual returns (address) {}
+}
+
+contract GrandParentLeft {
+ uint256 integer1 = 1;
+ uint256 constant ConstInteger1 = 1;
+
+ function basePublicFunctionNoParams() public virtual {}
+ function parentPublicFunctionNoParams() public virtual {}
+ function grandParentPublicFunctionNoParams() public virtual {}
+
+ function basePublicFunctionIntParam(uint256 value) public virtual {}
+
+ function overrideFunction(uint256 value) public virtual {}
+ function overrideGrantParentFunctionReturn(uint256 value, address account) public virtual returns (bool) {}
+
+ function grandParentPrivateFunction() private {}
+ function grandParentInternalFunction() internal {}
+}
+
+contract ParentLeft is GrandParentLeft, CommonContract {
+ uint256 integer2 = 2;
+ uint256 constant ConstInteger2 = 2;
+
+ function basePublicFunctionNoParams() public virtual override {}
+ function parentPublicFunctionNoParams() public virtual override {}
+
+ function basePublicFunctionIntParam(uint256 value) public virtual override {}
+
+ function overrideFunction(uint256 value, address account) public virtual {}
+ function overrideParentFunctionReturn(uint256 value, address account) public virtual returns (bool, bool) {}
+
+ function parentPrivateFunction() private {}
+
+ function callInternalGrandParent() external {
+ grandParentInternalFunction();
+ }
+}
+
+contract GrandParentRight is CommonContract {
+ uint256 integer4 = 3;
+ uint256 constant ConstInteger4 = 3;
+
+ function commonOverride() public virtual override returns (address) {}
+}
+
+contract ParentRight is GrandParentRight {
+ uint256 integer5 = 4;
+ uint256 constant ConstInteger5 = 4;
+}
+
+contract Squash is ParentLeft, ParentRight {
+
+ bool baseBool = true;
+ bool constant BaseConstantBool = true;
+
+ uint256 baseInteger = 5;
+ uint256 integer3 = 5;
+ uint256 integer6 = 5;
+
+ uint256 constant ConstInteger3 = 5;
+ uint256 constant ConstInteger6 = 5;
+
+ function commonOverride() public override (CommonContract, GrandParentRight) returns (address) {}
+
+ function basePublicFunctionNoParams() public override {}
+ function basePublicFunctionIntParam(uint256 value) public virtual override {}
+
+ function overrideFunction(uint256 value, address account) public override {}
+ function overrideFunction(uint256 value, address account, bool flag) public {}
+
+ function basePrivateFunction() private {}
+}
diff --git a/src/ts/__tests__/fileParser.test.ts b/src/ts/__tests__/fileParser.test.ts
index 8a944753..db5e95fb 100644
--- a/src/ts/__tests__/fileParser.test.ts
+++ b/src/ts/__tests__/fileParser.test.ts
@@ -6,7 +6,7 @@ describe('Parser', () => {
const files = await getSolidityFilesFromFolderOrFile(
'./src/contracts'
)
- expect(files).toHaveLength(27)
+ expect(files).toHaveLength(28)
})
test('get Solidity files from folder with no sol files', async () => {
diff --git a/src/ts/converterClass2Dot.ts b/src/ts/converterClass2Dot.ts
index 816b08b7..c2fff9fd 100644
--- a/src/ts/converterClass2Dot.ts
+++ b/src/ts/converterClass2Dot.ts
@@ -23,6 +23,7 @@ export interface ClassOptions {
hidePrivates?: boolean
hideAbstracts?: boolean
hideFilename?: boolean
+ hideSourceContract?: boolean
}
export const convertClass2Dot = (
@@ -105,7 +106,7 @@ const dotClassTitle = (
const dotAttributeVisibilities = (
umlClass: UmlClass,
- options: { hidePrivates?: boolean }
+ options: { hidePrivates?: boolean; hideSourceContract?: boolean }
): string => {
if (umlClass.attributes.length === 0) return ''
@@ -116,7 +117,10 @@ const dotAttributeVisibilities = (
umlClass.stereotype === ClassStereotype.Enum ||
umlClass.stereotype === ClassStereotype.Constant
) {
- return dotString + dotAttributes(umlClass.attributes, undefined, false)
+ return (
+ dotString +
+ dotAttributes(umlClass.attributes, options, undefined, false)
+ )
}
// For each visibility group
@@ -154,7 +158,7 @@ const dotAttributeVisibilities = (
}
}
- dotString += dotAttributes(attributes, vizGroup)
+ dotString += dotAttributes(attributes, options, vizGroup)
}
return dotString
@@ -162,6 +166,7 @@ const dotAttributeVisibilities = (
const dotAttributes = (
attributes: Attribute[],
+ options: { hideSourceContract?: boolean },
vizGroup?: string,
indent = true
): string => {
@@ -174,7 +179,11 @@ const dotAttributes = (
// for each attribute
attributes.forEach((attribute) => {
- dotString += `${indentString}${attribute.name}: ${attribute.type}\\l`
+ const sourceContract =
+ attribute.sourceContract && !options.hideSourceContract
+ ? ` \\<\\<${attribute.sourceContract}\\>\\>`
+ : ''
+ dotString += `${indentString}${attribute.name}: ${attribute.type}${sourceContract}\\l`
})
return dotString
@@ -240,6 +249,7 @@ const dotOperators = (
options: {
hideModifiers?: boolean
hideEvents?: boolean
+ hideSourceContract?: boolean
}
): string => {
// Skip if there are no operators
@@ -283,6 +293,9 @@ const dotOperators = (
dotString += ` \\<\\<${operator.modifiers.join(', ')}\\>\\>`
}
+ if (operator.sourceContract && !options.hideSourceContract)
+ dotString += ` \\<\\<${operator.sourceContract}\\>\\>`
+
dotString += '\\l'
}
diff --git a/src/ts/filterClasses.ts b/src/ts/filterClasses.ts
index 03170ae5..2456b310 100644
--- a/src/ts/filterClasses.ts
+++ b/src/ts/filterClasses.ts
@@ -5,8 +5,30 @@ import {
TopologicalSort,
WeightedDiGraph,
} from 'js-graph-algorithms'
-import { UmlClass } from './umlClass'
+import { ClassStereotype, UmlClass } from './umlClass'
import { findAssociatedClass } from './associations'
+import { ClassOptions } from './converterClass2Dot'
+
+export const filterHiddenClasses = (
+ umlClasses: UmlClass[],
+ options: ClassOptions
+): UmlClass[] => {
+ return umlClasses.filter(
+ (u) =>
+ (u.stereotype === ClassStereotype.Enum && !options.hideEnums) ||
+ (u.stereotype === ClassStereotype.Struct && !options.hideStructs) ||
+ (u.stereotype === ClassStereotype.Abstract &&
+ !options.hideAbstracts) ||
+ (u.stereotype === ClassStereotype.Interface &&
+ !options.hideInterfaces) ||
+ (u.stereotype === ClassStereotype.Constant &&
+ !options.hideConstants) ||
+ (u.stereotype === ClassStereotype.Library &&
+ !options.hideLibraries) ||
+ u.stereotype === ClassStereotype.None ||
+ u.stereotype === ClassStereotype.Contract
+ )
+}
export const classesConnectedToBaseContracts = (
umlClasses: UmlClass[],
@@ -63,7 +85,10 @@ export const classesConnectedToBaseContract = (
}
function loadWeightedDirectedGraph(umlClasses: UmlClass[]): WeightedDiGraph {
- const weightedDirectedGraph = new WeightedDiGraph(umlClasses.length) // the number vertices in the graph
+ const weightedDirectedGraph = new WeightedDiGraph(
+ // the number vertices in the graph
+ UmlClass.idCounter + 1
+ )
for (const sourceUmlClass of umlClasses) {
for (const association of Object.values(sourceUmlClass.associations)) {
@@ -77,7 +102,10 @@ function loadWeightedDirectedGraph(umlClasses: UmlClass[]): WeightedDiGraph {
if (!targetUmlClass) {
continue
}
-
+ const isTarget = umlClasses.find((u) => u.id === targetUmlClass.id)
+ console.log(
+ `isTarget ${isTarget} Adding edge from ${sourceUmlClass.name} with id ${sourceUmlClass.id} to ${targetUmlClass.name} with id ${targetUmlClass.id} and type ${targetUmlClass.stereotype}`
+ )
weightedDirectedGraph.addEdge(
new Edge(sourceUmlClass.id, targetUmlClass.id, 1)
)
diff --git a/src/ts/sol2uml.ts b/src/ts/sol2uml.ts
index 2dc6b2d1..40cfb7cf 100644
--- a/src/ts/sol2uml.ts
+++ b/src/ts/sol2uml.ts
@@ -3,7 +3,10 @@
import { convertUmlClasses2Dot } from './converterClasses2Dot'
import { parserUmlClasses } from './parserGeneral'
import { EtherscanParser, networks } from './parserEtherscan'
-import { classesConnectedToBaseContracts } from './filterClasses'
+import {
+ classesConnectedToBaseContracts,
+ filterHiddenClasses,
+} from './filterClasses'
import { Command, Option } from 'commander'
import {
addStorageValues,
@@ -13,6 +16,7 @@ import { convertStorages2Dot } from './converterStorage2Dot'
import { isAddress } from './utils/regEx'
import { writeOutputFiles, writeSolidity } from './writerFiles'
import { basename } from 'path'
+import { squashUmlClasses } from './squashClasses'
const program = new Command()
const version =
@@ -133,6 +137,16 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
.option('-hi, --hideInterfaces', 'hide interfaces', false)
.option('-ha, --hideAbstracts', 'hide abstract contracts', false)
.option('-hn, --hideFilename', 'hide relative path and file name', false)
+ .option(
+ '-s, --squash',
+ 'squash inherited contracts to the base contract(s)',
+ false
+ )
+ .option(
+ '-hsc, --hideSourceContract',
+ 'hide the source contract when using squash',
+ false
+ )
.action(async (fileFolderAddress, options, command) => {
try {
const combinedOptions = {
@@ -145,17 +159,38 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
combinedOptions
)
- let filteredUmlClasses = umlClasses
- if (options.baseContractNames) {
- const baseContractNames = options.baseContractNames.split(',')
+ if (
+ options.squash &&
+ // Must specify base contract(s) or parse from Etherscan to get contractName
+ !(options.baseContractNames || contractName)
+ ) {
+ throw Error(
+ 'Must specify base contract(s) when using the squash option against local Solidity files.'
+ )
+ }
+
+ // Filter out any class stereotypes that are to be hidden
+ let filteredUmlClasses = filterHiddenClasses(umlClasses, options)
+
+ const baseContractNames = options.baseContractNames?.split(',')
+ if (baseContractNames) {
+ // Find all the classes connected to the base classes
filteredUmlClasses = classesConnectedToBaseContracts(
- umlClasses,
+ filteredUmlClasses,
baseContractNames,
options.depth
)
contractName = baseContractNames[0]
}
+ // squash contracts
+ if (options.squash) {
+ filteredUmlClasses = squashUmlClasses(
+ filteredUmlClasses,
+ baseContractNames || [contractName]
+ )
+ }
+
const dotString = convertUmlClasses2Dot(
filteredUmlClasses,
combinedOptions.clusterFolders,
diff --git a/src/ts/squashClasses.ts b/src/ts/squashClasses.ts
new file mode 100644
index 00000000..6fdad5cf
--- /dev/null
+++ b/src/ts/squashClasses.ts
@@ -0,0 +1,173 @@
+import { ClassStereotype, Operator, UmlClass } from './umlClass'
+import * as crypto from 'crypto'
+
+const debug = require('debug')('sol2uml')
+
+export const squashUmlClasses = (
+ umlClasses: UmlClass[],
+ squashContractNames: string[]
+): UmlClass[] => {
+ let removedClassIds: number[] = []
+ for (const squashContractName of squashContractNames) {
+ // Find the base UML Class to squash
+ let baseIndex = umlClasses.findIndex(({ name }) => {
+ return name === squashContractName
+ })
+ if (baseIndex === undefined) {
+ throw Error(
+ `Failed to find contract with name "${squashContractName}" to squash`
+ )
+ }
+ const baseClass = umlClasses[baseIndex]
+
+ let squashedClass = new UmlClass({
+ name: baseClass.name,
+ absolutePath: baseClass.absolutePath,
+ relativePath: baseClass.relativePath,
+ })
+ squashedClass.id = baseClass.id
+ const result = recursiveSquash(
+ squashedClass,
+ [],
+ baseClass,
+ umlClasses,
+ 1
+ )
+ removedClassIds = removedClassIds.concat(result.removedClassIds)
+
+ // Remove overridden functions from squashed class
+ squashedClass.operators = reduceOperators(squashedClass.operators)
+
+ umlClasses[baseIndex] = squashedClass
+ }
+
+ // filter the list of classes that will be rendered
+ return umlClasses.filter(
+ (u) =>
+ // remove any squashed inherited contracts
+ !removedClassIds.includes(u.id) ||
+ // Include all base contracts
+ squashContractNames.includes(u.name)
+ )
+}
+
+const recursiveSquash = (
+ squashedClass: UmlClass,
+ inheritedContractNames: string[],
+ baseClass: UmlClass,
+ umlClasses: UmlClass[],
+ startPosition: number
+): { currentPosition: number; removedClassIds: number[] } => {
+ let currentPosition = startPosition
+ const removedClassIds: number[] = []
+
+ // For each association from the baseClass
+ for (const [targetClassName, association] of Object.entries(
+ baseClass.associations
+ )) {
+ // if inheritance and (Abstract or Contract)
+ // Libraries and Interfaces will be copied
+ if (association.realization) {
+ // Find the target UML Class
+ const inheritedContract = umlClasses.find(({ name }) => {
+ return name === targetClassName
+ })
+ if (!inheritedContract) {
+ debug(
+ `Warning: failed to find inherited contract with name ${targetClassName}`
+ )
+ continue
+ }
+
+ // Is the associated class a contract or abstract contract?
+ if (inheritedContract?.stereotype === ClassStereotype.Library) {
+ squashedClass.addAssociation(association)
+ } else {
+ // has the contract already been added to the inheritance tree?
+ const alreadyInherited = inheritedContractNames.includes(
+ inheritedContract.name
+ )
+ // Do not add inherited contract if it has already been added to the inheritance tree
+ if (!alreadyInherited) {
+ inheritedContractNames.push(inheritedContract.name)
+ const squashResult = recursiveSquash(
+ squashedClass,
+ inheritedContractNames,
+ inheritedContract,
+ umlClasses,
+ currentPosition++
+ )
+ // Add to list of removed class ids
+ removedClassIds.push(
+ ...squashResult.removedClassIds,
+ inheritedContract.id
+ )
+ }
+ }
+ } else {
+ // Copy association but will not duplicate it
+ squashedClass.addAssociation(association)
+ }
+ }
+
+ // Copy class properties from the baseClass to the squashedClass
+ baseClass.constants.forEach((c) =>
+ squashedClass.constants.push({ ...c, sourceContract: baseClass.name })
+ )
+ baseClass.attributes.forEach((a) =>
+ squashedClass.attributes.push({ ...a, sourceContract: baseClass.name })
+ )
+ baseClass.enums.forEach((e) => squashedClass.enums.push(e))
+ baseClass.structs.forEach((s) => squashedClass.structs.push(s))
+
+ baseClass.imports.forEach((i) => squashedClass.imports.push(i))
+
+ // copy the functions
+ baseClass.operators.forEach((f) =>
+ squashedClass.operators.push({
+ ...f,
+ hash: hash(f),
+ inheritancePosition: currentPosition,
+ sourceContract: baseClass.name,
+ })
+ )
+
+ return {
+ currentPosition,
+ removedClassIds,
+ }
+}
+
+const hash = (operator: Operator): string => {
+ const hash = crypto.createHash('sha256')
+
+ let data = operator.name ?? 'fallback'
+ operator.parameters?.forEach((p) => {
+ data += ',' + p.type
+ })
+ operator.returnParameters?.forEach((p) => {
+ data += ',' + p.type
+ })
+
+ return hash.update(data).digest('hex')
+}
+
+const reduceOperators = (operators: Operator[]): Operator[] => {
+ const hashes = new Set(operators.map((o) => o.hash))
+
+ const operatorsWithNoHash = operators.filter((o) => !o.hash)
+
+ const newOperators: Operator[] = []
+ for (const hash of hashes) {
+ const operator = operators
+ .filter((o) => o.hash === hash)
+ // sort operators by inheritance position. smaller to highest
+ .sort((o) => o.inheritancePosition)
+ // get last operator in the array
+ .slice(-1)[0]
+ newOperators.push(operator)
+ }
+ newOperators.push(...operatorsWithNoHash)
+
+ return newOperators
+}
diff --git a/src/ts/umlClass.ts b/src/ts/umlClass.ts
index 9b61a750..e6b04bd3 100644
--- a/src/ts/umlClass.ts
+++ b/src/ts/umlClass.ts
@@ -50,6 +50,8 @@ export interface Attribute {
type?: string
attributeType?: AttributeType
compiled?: boolean // true for constants and immutables
+ // Used for squashed classes
+ sourceContract?: string
}
export interface Parameter {
@@ -64,6 +66,10 @@ export interface Operator extends Attribute {
returnParameters?: Parameter[]
isPayable?: boolean
modifiers?: string[]
+ // Used by squashed classes
+ hash?: string
+ inheritancePosition?: number
+ sourceContract?: string
}
export enum ReferenceType {
@@ -74,13 +80,14 @@ export enum ReferenceType {
export interface Association {
referenceType: ReferenceType
targetUmlClassName: string
- targetUmlClassStereotype?: ClassStereotype
realization?: boolean
}
export interface Constants {
name: string
value: number
+ // Used for squashed classes
+ sourceContract?: string
}
export interface ClassProperties {
@@ -104,7 +111,7 @@ export class UmlClass implements ClassProperties {
name: string
absolutePath: string
relativePath: string
- imports: Import[]
+ imports: Import[] = []
stereotype?: ClassStereotype
constants: Constants[] = []
@@ -159,10 +166,7 @@ export class UmlClass implements ClassProperties {
*/
getParentContracts(): Association[] {
return Object.values(this.associations).filter(
- (association) =>
- association.realization &&
- association.targetUmlClassStereotype !==
- ClassStereotype.Interface
+ (association) => association.realization
)
}
}