Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# report outputs
output/

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*


# Runtime data
pids
*.pid
Expand All @@ -15,7 +19,7 @@ pids
lib-cov

# Coverage directory used by tools like istanbul
coverage
/coverage

# nyc test coverage
.nyc_output
Expand All @@ -29,14 +33,15 @@ bower_components
# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
# Compiled binary addons (https://nodejs.org/api/addons.html)
/build
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
# TypeScript v1 declaration files
typings/

# Optional npm cache directory
Expand All @@ -56,4 +61,25 @@ typings/

# dotenv environment variables file
.env

.env.local
.env.development.local
.env.test.local
.env.production.local

# next.js build output
.next

#other
.lck


# OS generated files #
######################
.DS_Store
.DS_Store?
.idea
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
64 changes: 51 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,60 @@
# Solidity Function Profiler

A command line tool that generates a human-consumable report listing a contract's functions. This is useful during manual code review to understand what functions are made public, use which modifiers, and so on.
A command line tool that generates a human readable report with contract information. This is useful during manual code review to understand what functions are made public, use which modifiers, and so on.

This version ditches the now deprecated [ConsenSys Solidity Parser](https://github.com/ConsenSys/solidity-parser) in favor of [solidity-parser-antlr](https://github.com/federicobond/solidity-parser-antlr).

Usage Example:

```
$ npm install
...
$ node index.js ~/contracts/mytoken.sol
.--------------------------------------------------------------------------------------------------------.
| ~/contracts/mytoken.sol |
|--------------------------------------------------------------------------------------------------------|
| Contract | Function | Visibility | Constant | Returns | Modifiers |
|---------------|-------------------------------|------------|----------|-----------|--------------------|
| MyToken | () | public | false | | payable |
| MyToken | initTokenHolder(address,uint) | public | false | | onlyOwner |
| MyToken | balance(address) | public | true | uint | |
| MyToken | transferAll(address,address) | external | false | | onlyTokenHolder |
| MyToken | kill() | internal | false | | |
'--------------------------------------------------------------------------------------------------------'

$ chmod +x cli.js

$ ./cli.js ~/contracts/mytoken.sol

You can also give a directory as an argument using the --dir flag, this will generate a report on all files ending in .sol the directory or its subdirectories.

$ ./cli.js --dir ~/contracts

```
This will produce a `.md` file on the `output` folder, for every contract scanned. e.g:

`~/output/myToken.sol.md` will have:

```

.------------------------------------------.
| pragma solidity 0.10.99 contract MyToken |
|------------------------------------------|
| ~/contracts/mytoken.sol |
'------------------------------------------'
.-----------------------------.
| imports |
|-----------------------------|
| ../import/FancyContract.sol |
'-----------------------------'
.----------------------------------------------------------------------.
| functions |
|----------------------------------------------------------------------|
| name | visibility | return | modifiers |
|------------------------------------|------------|--------|-----------|
| constructor(_param) payable | public | | |
| otherFunction(_param, _otherParam) | external | bool | onlyOwner |
| fancyPants(_pants) | external | | onlyOwner |
'----------------------------------------------------------------------'
.--------------.
| modifiers |
|--------------|
| onlyOwner() |
'--------------'
.-------------------------------------.
| events |
|-------------------------------------|
| MySpecialEvent(_param) |
'-------------------------------------'

```

multiple dir feature based on [this repo](https://github.com/maurelian/sol-function-profiler)
32 changes: 32 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#! /usr/bin/env node

const fs = require('fs')
const path = require('path')
const profiler = require('./index.js')
const argv = require('yargs').argv
const utils = require('./utils')

// recreate reporting output folder
const dirname = 'output'
utils.recreateFolder(dirname)

if (argv.dir) {
// is a directory?
profiler.generateReportForDir(argv.dir)
} else {
// is a single contract?
let arg_index = argv._.length - 1
let filepath = argv._[arg_index] // argv puts the final arguments in an array named "_"

if (!filepath) {
console.log('usage: --dir <dir> or path to .sol file')
process.exit(1)
}
let contract
try {
contract = fs.readFileSync(filepath, 'utf8')
} catch (e) {
throw e
}
profiler.generateReport(filepath, contract)
}
200 changes: 128 additions & 72 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,143 @@
const parser = require("solidity-parser");
const asciiTable = require('ascii-table');
const fs = require('fs')
const path = require('path')
const asciiTable = require('ascii-table')
const parser = require('solidity-parser-antlr')
const utils = require('./utils.js')
const report = require('./parsers')

if(process.argv.length < 3) {
console.log("Error: Missing argument for sol file to scan");
process.exit(1);
}
let contract
let parsedContract

var target = process.argv[2],
contract = parser.parseFile(target);
function generateReportForDir (dir) {
const files = utils.getAllFiles(dir)
files
.filter(filepath => filepath.split('.').pop() === 'sol')
.forEach(filepath => {
try {
contract = fs.readFileSync(filepath, 'utf8')
generateReport(filepath, contract)
} catch (e) {
console.log(`Error reading contract ${contract} :`, e)
}
})
}

generateReport(target, contract);
function generateReport (filepath, contract) {
// write output to file
let filename = path.basename(filepath)
let reportName = `./output/${filename}.md`
let writeStream = fs.createWriteStream(reportName)

function generateReport(target, contract) {
var table = new asciiTable(target);
table.setHeading('Contract', 'Function', 'Visibility', 'Constant', 'Returns', 'Modifiers');
try {
parsedContract = parser.parse(contract)
} catch (e) {
if (e instanceof parser.ParserError) {
console.log(e.errors)
}
}

contract.body.forEach(function(contract) {
if(contract.type == 'ContractStatement') {
contract.body.forEach(function(part) {
if(part.type == "FunctionDeclaration" && part.is_abstract == false) {
var func = parseFunctionPart(contract, part);
table.addRow(func.contract, func.function, func.visibility, func.constant, func.returns, func.modifiers);
let functionRows = []
let eventRows = []
let modifierRows = []
let importRows = []
let contractInfo = ''
for (const node of parsedContract.children) {
const type = node.type
switch (type) {
// Contract solidity version
case 'PragmaDirective':
const contractPragma = node.name
const contractPragmaVersion = node.value
contractInfo += `pragma ${contractPragma} ${contractPragmaVersion}`
break
// Contract name and type
case 'ContractDefinition':
const contractName = node.name
const contractType = node.kind
contractInfo += ` ${contractType} ${contractName}`
for (const subNode of node.subNodes) {
switch (subNode.type) {
// Contract Functions
case 'FunctionDefinition':
functionRows.push(report.parseFunction(subNode))
break
// Contract Events
case 'EventDefinition':
eventRows.push(report.parseEvent(subNode))
break
// Contract Modifiers
case 'ModifierDefinition':
modifierRows.push(report.parseModifier(subNode))
break
}
}
})
break
// Contract Imports
case 'ImportDirective':
importRows.push(report.parseImport(node))
break
}
}

let generalInfoTable = asciiTable.factory({
heading: [contractInfo],
rows: [filepath]
})

let functionTable = asciiTable.factory({
title: 'functions',
heading: ['name', 'visibility', 'return', 'modifiers'],
rows: functionRows
})

let eventTable = asciiTable.factory({
heading: ['events'],
rows: eventRows
})

let modifierTable = asciiTable.factory({
heading: ['modifiers'],
rows: modifierRows
})
console.log(table.toString());
}

function parseFunctionPart(contract, part) {
var contractName = contract.name,
funcName = part.name || "",
params = [];

if(part.params) {
part.params.forEach(function(param) {
params.push(param.literal.literal);
});
funcName += "(" + params.join(',') + ")"
} else {
funcName += "()"
let importTable = asciiTable.factory({
heading: ['imports'],
rows: importRows
})

// very fancy markdown ;)
writeStream.write('```' + '\r\n')

writeStream.write(generalInfoTable.toString() + '\r\n')

if (importRows.length > 0) {
writeStream.write(importTable.toString() + '\r\n')
}

// Default is public
var visibility = "public"
isConstant = false,
returns = [],
custom = [];

if(part.modifiers) {
part.modifiers.forEach(function(mod) {
switch(mod.name) {
case "public":
break;
case "private":
visibility = "private";
break;
case "internal":
visibility = "internal";
break;
case "external":
visibility = "external";
break;
case "constant":
isConstant = true;
break;
case "returns":
mod.params.forEach(function(param) {
returns.push(param.name);
});
break;
default:
custom.push(mod.name);
}
});
if (functionRows.length > 0) {
writeStream.write(functionTable.toString() + '\r\n')
}

if (modifierRows.length > 0) {
writeStream.write(modifierTable.toString() + '\r\n')
}

return {
contract: contractName,
function: funcName,
visibility: visibility,
constant: isConstant,
returns: returns,
modifiers: custom
if (eventRows.length > 0) {
writeStream.write(eventTable.toString() + '\r\n')
}

writeStream.write('```' + '\r\n')

// the finish event is emitted when all data has been flushed from the stream
writeStream.on('finish', function () {
console.log(`written ${reportName}`)
})

// close the stream
writeStream.end()
}

module.exports = {
generateReport,
generateReportForDir
}
Loading