sol2uml
Version:
Solidity contract visualisation tool.
733 lines • 31.7 kB
JavaScript
;
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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.renameFile = void 0;
exports.convertAST2UmlClasses = convertAST2UmlClasses;
const path = __importStar(require("path"));
const path_1 = require("path");
const umlClass_1 = require("./umlClass");
const typeGuards_1 = require("./typeGuards");
const debug = require('debug')('sol2uml');
let umlClasses;
/**
* Convert solidity parser output of type `ASTNode` to UML classes of type `UMLClass`
* @param node output of Solidity parser of type `ASTNode`
* @param relativePath relative path from the working directory to the Solidity source file
* @param remappings used to rename relative paths
* @param filesystem flag if Solidity source code was parsed from the filesystem or Etherscan
* @return umlClasses array of UML class definitions of type `UmlClass`
*/
function convertAST2UmlClasses(node, relativePath, remappings, filesystem = false) {
const imports = [];
umlClasses = [];
if (node.type === 'SourceUnit') {
node.children.forEach((childNode) => {
if (childNode.type === 'ContractDefinition') {
const umlClass = new umlClass_1.UmlClass({
name: childNode.name,
absolutePath: filesystem
? path.resolve(relativePath) // resolve the absolute path
: relativePath, // from Etherscan so don't resolve
relativePath,
});
umlClasses.push(umlClass);
parseContractDefinition(childNode, umlClass);
debug(`Added contract ${childNode.name}`);
}
else if (childNode.type === 'StructDefinition') {
debug(`Adding file level struct ${childNode.name}`);
const umlClass = new umlClass_1.UmlClass({
name: childNode.name,
stereotype: umlClass_1.ClassStereotype.Struct,
absolutePath: filesystem
? path.resolve(relativePath) // resolve the absolute path
: relativePath, // from Etherscan so don't resolve
relativePath,
});
parseStructDefinition(childNode, umlClass);
debug(`Added struct ${umlClass.name}`);
umlClasses.push(umlClass);
}
else if (childNode.type === 'EnumDefinition') {
debug(`Adding file level enum ${childNode.name}`);
const umlClass = new umlClass_1.UmlClass({
name: childNode.name,
stereotype: umlClass_1.ClassStereotype.Enum,
absolutePath: filesystem
? path.resolve(relativePath) // resolve the absolute path
: relativePath, // from Etherscan so don't resolve
relativePath,
});
debug(`Added enum ${umlClass.name}`);
parseEnumDefinition(childNode, umlClass);
umlClasses.push(umlClass);
}
else if (childNode.type === 'ImportDirective') {
const codeFolder = path.dirname(relativePath);
if (filesystem) {
// resolve the imported file from the folder sol2uml was run against
try {
const importPath = require.resolve(childNode.path, {
paths: [codeFolder],
});
const newImport = {
absolutePath: importPath,
classNames: childNode.symbolAliases
? childNode.symbolAliases.map((alias) => {
return {
className: alias[0],
alias: alias[1],
};
})
: [],
};
debug(`Added filesystem import ${newImport.absolutePath} with class names ${newImport.classNames.map((i) => i.className)}`);
imports.push(newImport);
}
catch {
debug(`Failed to resolve import ${childNode.path} from file ${relativePath}`);
}
}
else {
// this has come from Etherscan
const remappedFile = (0, exports.renameFile)(childNode.path, remappings);
const importPath = remappedFile[0] === '.'
? // Use Linux paths, not Windows paths, to resolve Etherscan files
path_1.posix.join(codeFolder.toString(), remappedFile)
: remappedFile;
debug(`codeFolder ${codeFolder} childNode.path ${childNode.path} remapped to ${remappedFile}`);
const newImport = {
absolutePath: importPath,
classNames: childNode.symbolAliases
? childNode.symbolAliases.map((alias) => {
return {
className: alias[0],
alias: alias[1],
};
})
: [],
};
debug(`Added Etherscan import ${newImport.absolutePath} with:`);
newImport.classNames.forEach((className) => {
debug(`\t alias ${className.className}, name ${className.className}`);
});
imports.push(newImport);
}
}
else if (childNode.type === 'FileLevelConstant') {
debug(`Adding file level constant ${childNode.name}`);
const [type, attributeType] = parseTypeName(childNode.typeName);
const umlClass = new umlClass_1.UmlClass({
name: childNode.name,
stereotype: umlClass_1.ClassStereotype.Constant,
absolutePath: filesystem
? path.resolve(relativePath) // resolve the absolute path
: relativePath, // from Etherscan so don't resolve
relativePath,
attributes: [
{
name: childNode.name,
type,
attributeType,
},
],
});
if (childNode?.initialValue?.type === 'NumberLiteral') {
umlClass.constants.push({
name: childNode.name,
value: parseInt(childNode.initialValue.number),
});
}
// TODO handle expressions. eg N_COINS * 2
umlClasses.push(umlClass);
}
else if (childNode.type !== 'PragmaDirective') {
debug(`node type "${childNode.type}" not parsed in ${relativePath}`);
}
});
}
else {
throw new Error(`AST node not of type SourceUnit`);
}
if (umlClasses.length > 0) {
umlClasses.forEach((umlClass) => {
umlClass.imports = imports;
});
}
else {
const importUmlClass = new umlClass_1.UmlClass({
name: 'Import',
stereotype: umlClass_1.ClassStereotype.Import,
absolutePath: filesystem
? path.resolve(relativePath) // resolve the absolute path
: relativePath, // from Etherscan so don't resolve
relativePath,
});
importUmlClass.imports = imports;
umlClasses = [importUmlClass];
}
return umlClasses;
}
/**
* Parse struct definition for UML attributes and associations.
* @param node defined in ASTNode as `StructDefinition`
* @param umlClass that has struct attributes and associations added. This parameter is mutated.
*/
function parseStructDefinition(node, umlClass) {
node.members.forEach((member) => {
const [type, attributeType] = parseTypeName(member.typeName);
umlClass.attributes.push({
name: member.name,
type,
attributeType,
});
});
// Recursively parse struct members for associations
addAssociations(node.members, umlClass);
}
/**
* Parse enum definition for UML attributes and associations.
* @param node defined in ASTNode as `EnumDefinition`
* @param umlClass that has enum attributes and associations added. This parameter is mutated.
*/
function parseEnumDefinition(node, umlClass) {
let index = 0;
node.members.forEach((member) => {
umlClass.attributes.push({
name: member.name,
type: (index++).toString(),
});
});
// Recursively parse struct members for associations
addAssociations(node.members, umlClass);
}
/**
* Parse contract definition for UML attributes, operations and associations.
* @param node defined in ASTNode as `ContractDefinition`
* @param umlClass that has attributes, operations and associations added. This parameter is mutated.
*/
function parseContractDefinition(node, umlClass) {
umlClass.stereotype = parseContractKind(node.kind);
// For each base contract
node.baseContracts.forEach((baseClass) => {
// Add a realization association
umlClass.addAssociation({
referenceType: umlClass_1.ReferenceType.Storage,
targetUmlClassName: baseClass.baseName.namePath,
realization: true,
});
});
// For each sub node
node.subNodes.forEach((subNode) => {
if ((0, typeGuards_1.isStateVariableDeclaration)(subNode)) {
subNode.variables.forEach((variable) => {
const [type, attributeType] = parseTypeName(variable.typeName);
const valueStore = variable.isDeclaredConst || variable.isImmutable;
umlClass.attributes.push({
visibility: parseVisibility(variable.visibility),
name: variable.name,
type,
attributeType,
compiled: valueStore,
});
// Is the variable a constant that could be used in declaring fixed sized arrays
if (variable.isDeclaredConst) {
if (variable?.expression?.type === 'NumberLiteral') {
umlClass.constants.push({
name: variable.name,
value: parseInt(variable.expression.number),
});
}
// TODO handle expressions. eg N_COINS * 2
}
});
// Recursively parse variables for associations
addAssociations(subNode.variables, umlClass);
}
else if ((0, typeGuards_1.isUsingForDeclaration)(subNode)) {
// Add association to library contract
umlClass.addAssociation({
referenceType: umlClass_1.ReferenceType.Memory,
targetUmlClassName: subNode.libraryName,
});
}
else if ((0, typeGuards_1.isFunctionDefinition)(subNode)) {
if (subNode.isConstructor) {
umlClass.operators.push({
name: 'constructor',
stereotype: umlClass_1.OperatorStereotype.None,
parameters: parseParameters(subNode.parameters),
});
}
// If a fallback function
else if (subNode.name === '') {
umlClass.operators.push({
name: '',
stereotype: umlClass_1.OperatorStereotype.Fallback,
parameters: parseParameters(subNode.parameters),
stateMutability: subNode.stateMutability,
});
}
else {
let stereotype = umlClass_1.OperatorStereotype.None;
if (subNode.body === null) {
stereotype = umlClass_1.OperatorStereotype.Abstract;
}
else if (subNode.stateMutability === 'payable') {
stereotype = umlClass_1.OperatorStereotype.Payable;
}
umlClass.operators.push({
visibility: parseVisibility(subNode.visibility),
name: subNode.name,
stereotype,
parameters: parseParameters(subNode.parameters),
returnParameters: parseParameters(subNode.returnParameters),
modifiers: subNode.modifiers.map((m) => m.name),
});
}
// Recursively parse function parameters for associations
addAssociations(subNode.parameters, umlClass);
if (subNode.returnParameters) {
addAssociations(subNode.returnParameters, umlClass);
}
// If no body to the function, it must be either an Interface or Abstract
if (subNode.body === null) {
if (umlClass.stereotype !== umlClass_1.ClassStereotype.Interface) {
// If not Interface, it must be Abstract
umlClass.stereotype = umlClass_1.ClassStereotype.Abstract;
}
}
else {
// Recursively parse function statements for associations
addAssociations(subNode.body.statements, umlClass);
}
}
else if ((0, typeGuards_1.isModifierDefinition)(subNode)) {
umlClass.operators.push({
stereotype: umlClass_1.OperatorStereotype.Modifier,
name: subNode.name,
parameters: parseParameters(subNode.parameters),
});
if (subNode.body && subNode.body.statements) {
// Recursively parse modifier statements for associations
addAssociations(subNode.body.statements, umlClass);
}
}
else if ((0, typeGuards_1.isEventDefinition)(subNode)) {
umlClass.operators.push({
stereotype: umlClass_1.OperatorStereotype.Event,
name: subNode.name,
parameters: parseParameters(subNode.parameters),
});
// Recursively parse event parameters for associations
addAssociations(subNode.parameters, umlClass);
}
else if ((0, typeGuards_1.isStructDefinition)(subNode)) {
const structClass = new umlClass_1.UmlClass({
name: subNode.name,
absolutePath: umlClass.absolutePath,
relativePath: umlClass.relativePath,
parentId: umlClass.id,
stereotype: umlClass_1.ClassStereotype.Struct,
});
parseStructDefinition(subNode, structClass);
umlClasses.push(structClass);
// list as contract level struct
umlClass.structs.push(structClass.id);
}
else if ((0, typeGuards_1.isEnumDefinition)(subNode)) {
const enumClass = new umlClass_1.UmlClass({
name: subNode.name,
absolutePath: umlClass.absolutePath,
relativePath: umlClass.relativePath,
parentId: umlClass.id,
stereotype: umlClass_1.ClassStereotype.Enum,
});
parseEnumDefinition(subNode, enumClass);
umlClasses.push(enumClass);
// list as contract level enum
umlClass.enums.push(enumClass.id);
}
});
}
/**
* Recursively parse a list of ASTNodes for UML associations
* @param nodes array of parser output of type ASTNode
* @param umlClass that has associations added of type `Association`. This parameter is mutated.
*/
function addAssociations(nodes, umlClass) {
if (!nodes || !Array.isArray(nodes)) {
debug('Warning - can not recursively parse AST nodes for associations. Invalid nodes array');
return;
}
for (const node of nodes) {
// Some variables can be null. eg var (lad,,,) = tub.cups(cup);
if (node === null) {
break;
}
// If state variable then mark as a Storage reference, else Memory
const referenceType = node.isStateVar
? umlClass_1.ReferenceType.Storage
: umlClass_1.ReferenceType.Memory;
// Recursively parse sub nodes that can has variable declarations
switch (node.type) {
case 'VariableDeclaration':
if (!node.typeName) {
break;
}
if (node.typeName.type === 'UserDefinedTypeName') {
// Library references can have a Library dot variable notation. eg Set.Data
// Structs and enums can also be under a library or contract
const { umlClassName, structOrEnum } = parseClassName(node.typeName.namePath);
umlClass.addAssociation({
referenceType,
targetUmlClassName: umlClassName,
});
if (structOrEnum) {
umlClass.addAssociation({
referenceType,
parentUmlClassName: umlClassName,
targetUmlClassName: structOrEnum,
});
}
}
else if (node.typeName.type === 'Mapping') {
addAssociations([node.typeName.keyType], umlClass);
addAssociations([
{
...node.typeName.valueType,
isStateVar: node.isStateVar,
},
], umlClass);
// Array of user defined types
}
else if (node.typeName.type == 'ArrayTypeName') {
if (node.typeName.baseTypeName.type ===
'UserDefinedTypeName') {
const { umlClassName } = parseClassName(node.typeName.baseTypeName.namePath);
umlClass.addAssociation({
referenceType,
targetUmlClassName: umlClassName,
});
}
else if (node.typeName.length?.type === 'Identifier') {
const { umlClassName } = parseClassName(node.typeName.length.name);
umlClass.addAssociation({
referenceType,
targetUmlClassName: umlClassName,
});
}
}
break;
case 'UserDefinedTypeName':
umlClass.addAssociation({
referenceType: referenceType,
targetUmlClassName: node.namePath,
});
break;
case 'Block':
addAssociations(node.statements, umlClass);
break;
case 'StateVariableDeclaration':
case 'VariableDeclarationStatement':
addAssociations(node.variables, umlClass);
parseExpression(node.initialValue, umlClass);
break;
case 'EmitStatement':
addAssociations(node.eventCall.arguments, umlClass);
parseExpression(node.eventCall.expression, umlClass);
break;
case 'FunctionCall':
addAssociations(node.arguments, umlClass);
parseExpression(node.expression, umlClass);
break;
case 'ForStatement':
if ('statements' in node.body) {
addAssociations(node.body.statements, umlClass);
}
parseExpression(node.conditionExpression, umlClass);
parseExpression(node.loopExpression.expression, umlClass);
break;
case 'WhileStatement':
if ('statements' in node.body) {
addAssociations(node.body.statements, umlClass);
}
break;
case 'DoWhileStatement':
if ('statements' in node.body) {
addAssociations(node.body.statements, umlClass);
}
parseExpression(node.condition, umlClass);
break;
case 'ReturnStatement':
case 'ExpressionStatement':
parseExpression(node.expression, umlClass);
break;
case 'IfStatement':
if (node.trueBody) {
if ('statements' in node.trueBody) {
addAssociations(node.trueBody.statements, umlClass);
}
if ('expression' in node.trueBody) {
parseExpression(node.trueBody.expression, umlClass);
}
}
if (node.falseBody) {
if ('statements' in node.falseBody) {
addAssociations(node.falseBody.statements, umlClass);
}
if ('expression' in node.falseBody) {
parseExpression(node.falseBody.expression, umlClass);
}
}
parseExpression(node.condition, umlClass);
break;
default:
break;
}
}
}
/**
* Recursively parse an expression to add UML associations to other contracts, constants, enums or structs.
* @param expression defined in ASTNode as `Expression`
* @param umlClass that has associations added of type `Association`. This parameter is mutated.
*/
function parseExpression(expression, umlClass) {
if (!expression || !expression.type) {
return;
}
if (expression.type === 'BinaryOperation') {
parseExpression(expression.left, umlClass);
parseExpression(expression.right, umlClass);
}
else if (expression.type === 'FunctionCall') {
if (expression.expression.type === 'MemberAccess' &&
expression.expression.expression?.type === 'Identifier') {
// Pattern: ClassName.functionName(args) — explicit library/contract call
const memberName = expression.expression.memberName;
umlClass.addAssociation({
referenceType: umlClass_1.ReferenceType.Memory,
targetUmlClassName: expression.expression.expression.name,
functionsCalled: [memberName],
});
umlClass.memberAccessCalls.add(memberName);
}
else {
// Track member access calls inside other FunctionCall patterns (e.g. x.functionName())
if (expression.expression.type === 'MemberAccess') {
umlClass.memberAccessCalls.add(expression.expression.memberName);
}
parseExpression(expression.expression, umlClass);
}
expression.arguments.forEach((arg) => {
parseExpression(arg, umlClass);
});
}
else if (expression.type === 'IndexAccess') {
parseExpression(expression.base, umlClass);
parseExpression(expression.index, umlClass);
}
else if (expression.type === 'TupleExpression') {
expression.components.forEach((component) => {
parseExpression(component, umlClass);
});
}
else if (expression.type === 'MemberAccess') {
parseExpression(expression.expression, umlClass);
}
else if (expression.type === 'Conditional') {
addAssociations([expression.trueExpression], umlClass);
addAssociations([expression.falseExpression], umlClass);
}
else if (expression.type === 'Identifier') {
umlClass.addAssociation({
referenceType: umlClass_1.ReferenceType.Memory,
targetUmlClassName: expression.name,
});
}
else if (expression.type === 'NewExpression') {
addAssociations([expression.typeName], umlClass);
}
else if (expression.type === 'UnaryOperation' &&
expression.subExpression) {
parseExpression(expression.subExpression, umlClass);
}
}
/**
* Convert user defined type to class name and struct or enum.
* eg `Set.Data` has class `Set` and struct or enum of `Data`
* @param rawClassName can be `TypeName` properties `namePath`, `length.name` or `baseTypeName.namePath` as defined in the ASTNode
* @return object with `umlClassName` and `structOrEnum` of type string
*/
function parseClassName(rawClassName) {
if (!rawClassName ||
typeof rawClassName !== 'string' ||
rawClassName.length === 0) {
return {
umlClassName: '',
structOrEnum: rawClassName,
};
}
// Split the name on dot
const splitUmlClassName = rawClassName.split('.');
return {
umlClassName: splitUmlClassName[0],
structOrEnum: splitUmlClassName[1],
};
}
/**
* Converts the contract visibility to attribute or operator visibility of type `Visibility`
* @param params defined in ASTNode as VariableDeclaration.visibility, FunctionDefinition.visibility or FunctionTypeName.visibility
* @return visibility `Visibility` enum used by the `visibility` property on UML `Attribute` or `Operator`
*/
function parseVisibility(visibility) {
switch (visibility) {
case 'default':
return umlClass_1.Visibility.Public;
case 'public':
return umlClass_1.Visibility.Public;
case 'external':
return umlClass_1.Visibility.External;
case 'internal':
return umlClass_1.Visibility.Internal;
case 'private':
return umlClass_1.Visibility.Private;
default:
throw Error(`Invalid visibility ${visibility}. Was not public, external, internal or private`);
}
}
/**
* Recursively converts contract variables to UMLClass attribute types.
* Types can be standard Solidity types, arrays, mappings or user defined types like structs and enums.
* @param typeName defined in ASTNode as `TypeName`
* @return attributeTypes array of type string and `AttributeType`
*/
function parseTypeName(typeName) {
switch (typeName.type) {
case 'ElementaryTypeName':
return [typeName.name, umlClass_1.AttributeType.Elementary];
case 'UserDefinedTypeName':
return [typeName.namePath, umlClass_1.AttributeType.UserDefined];
case 'FunctionTypeName':
// TODO add params and return type
return [typeName.type + '\\(\\)', umlClass_1.AttributeType.Function];
case 'ArrayTypeName': {
const [arrayElementType] = parseTypeName(typeName.baseTypeName);
let length = '';
if (Number.isInteger(typeName.length)) {
length = typeName.length.toString();
}
else if (typeName.length?.type === 'NumberLiteral') {
length = typeName.length.number;
}
else if (typeName.length?.type === 'Identifier') {
length = typeName.length.name;
}
// TODO does not currently handle Expression types like BinaryOperation
return [arrayElementType + '[' + length + ']', umlClass_1.AttributeType.Array];
}
case 'Mapping': {
const key = typeName.keyType?.name ||
typeName.keyType?.namePath;
const [valueType] = parseTypeName(typeName.valueType);
return [
'mapping\\(' + key + '=\\>' + valueType + '\\)',
umlClass_1.AttributeType.Mapping,
];
}
default:
throw Error(`Invalid typeName ${typeName}`);
}
}
/**
* Converts the contract params to `Operator` properties `parameters` or `returnParameters`
* @param params defined in ASTNode as `VariableDeclaration`
* @return parameters or `returnParameters` of the `Operator` of type `Parameter`
*/
function parseParameters(params) {
if (!params || !params) {
return [];
}
const parameters = [];
for (const param of params) {
const [type] = parseTypeName(param.typeName);
parameters.push({
name: param.name,
type,
});
}
return parameters;
}
/**
* Converts the contract `kind` to `UMLClass` stereotype
* @param kind defined in ASTNode as ContractDefinition.kind
* @return stereotype of the `UMLClass` with type `ClassStereotype
*/
function parseContractKind(kind) {
switch (kind) {
case 'contract':
return umlClass_1.ClassStereotype.Contract;
case 'interface':
return umlClass_1.ClassStereotype.Interface;
case 'library':
return umlClass_1.ClassStereotype.Library;
case 'abstract':
return umlClass_1.ClassStereotype.Abstract;
default:
throw Error(`Invalid kind ${kind}`);
}
}
/**
* Used to rename import file names. For example
* @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
* to
* lib/openzeppelin-contracts/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
* @param fileName file name in the Solidity code
* @param mappings an array of remappings from Etherscan's settings
*/
const renameFile = (fileName, mappings) => {
let renamedFile = fileName;
for (const mapping of mappings) {
if (renamedFile.match(mapping.from)) {
const beforeFileName = renamedFile;
renamedFile = renamedFile.replace(mapping.from, mapping.to);
debug(`remapping ${beforeFileName} to ${renamedFile}`);
break;
}
}
return renamedFile;
};
exports.renameFile = renameFile;
//# sourceMappingURL=converterAST2Classes.js.map