aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-doc
diff options
context:
space:
mode:
Diffstat (limited to 'packages/sol-doc')
-rw-r--r--packages/sol-doc/coverage/.gitkeep0
-rw-r--r--packages/sol-doc/package.json41
-rw-r--r--packages/sol-doc/src/index.ts1
-rw-r--r--packages/sol-doc/src/solidity_doc_generator.ts284
-rw-r--r--packages/sol-doc/test/fixtures/contracts/TokenTransferProxy.sol115
-rw-r--r--packages/sol-doc/test/solidity_doc_generator_test.ts76
-rw-r--r--packages/sol-doc/test/util/chai_setup.ts13
-rw-r--r--packages/sol-doc/tsconfig.json8
-rw-r--r--packages/sol-doc/tslint.json3
9 files changed, 541 insertions, 0 deletions
diff --git a/packages/sol-doc/coverage/.gitkeep b/packages/sol-doc/coverage/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/sol-doc/coverage/.gitkeep
diff --git a/packages/sol-doc/package.json b/packages/sol-doc/package.json
new file mode 100644
index 000000000..e3bc8753c
--- /dev/null
+++ b/packages/sol-doc/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@0xproject/sol-doc",
+ "version": "1.0.0",
+ "description": "Solidity documentation generator",
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.js",
+ "scripts": {
+ "build": "tsc",
+ "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 5000 --exit",
+ "test:circleci": "yarn test:coverage",
+ "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
+ "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
+ "lint": "tslint --project . --format stylish",
+ "clean": "shx rm -rf lib"
+ },
+ "repository": "https://github.com/0xProject/0x-monorepo.git",
+ "author": "F. Eugene Aumson",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@0xproject/sol-compiler": "^1.0.5",
+ "@0xproject/types": "^1.0.1-rc.6",
+ "@0xproject/utils": "^1.0.5",
+ "ethereum-types": "^1.0.4",
+ "lodash": "^4.17.10"
+ },
+ "devDependencies": {
+ "@0xproject/tslint-config": "^1.0.6",
+ "chai": "^4.1.2",
+ "chai-as-promised": "^7.1.0",
+ "chai-bignumber": "^2.0.2",
+ "dirty-chai": "^2.0.1",
+ "make-promises-safe": "^1.1.0",
+ "mocha": "^5.2.0",
+ "shx": "^0.2.2",
+ "source-map-support": "^0.5.0",
+ "tslint": "5.11.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/sol-doc/src/index.ts b/packages/sol-doc/src/index.ts
new file mode 100644
index 000000000..03f3c9de6
--- /dev/null
+++ b/packages/sol-doc/src/index.ts
@@ -0,0 +1 @@
+export { generateSolDocAsync } from './solidity_doc_generator';
diff --git a/packages/sol-doc/src/solidity_doc_generator.ts b/packages/sol-doc/src/solidity_doc_generator.ts
new file mode 100644
index 000000000..a1ef32e79
--- /dev/null
+++ b/packages/sol-doc/src/solidity_doc_generator.ts
@@ -0,0 +1,284 @@
+import * as _ from 'lodash';
+
+import {
+ ConstructorAbi,
+ DataItem,
+ DevdocOutput,
+ EventAbi,
+ FallbackAbi,
+ MethodAbi,
+ StandardContractOutput,
+} from 'ethereum-types';
+
+import { Compiler, CompilerOptions } from '@0xproject/sol-compiler';
+import {
+ DocAgnosticFormat,
+ DocSection,
+ Event,
+ EventArg,
+ Parameter,
+ SolidityMethod,
+ Type,
+ TypeDocTypes,
+} from '@0xproject/types';
+
+/**
+ * Invoke the Solidity compiler and transform its ABI and devdoc outputs into
+ * the types that are used as input to documentation generation tools.
+ * @param contractsToCompile list of contracts for which to generate doc objects
+ * @param contractsDir the directory in which to find the `contractsToCompile` as well as their dependencies.
+ * @return doc object for use with documentation generation tools.
+ */
+export async function generateSolDocAsync(
+ contractsToCompile: string[],
+ contractsDir: string,
+): Promise<DocAgnosticFormat> {
+ const doc: DocAgnosticFormat = {};
+
+ const compilerOptions = _makeCompilerOptions(contractsToCompile, contractsDir);
+ const compiler = new Compiler(compilerOptions);
+ const compilerOutputs = await compiler.getCompilerOutputsAsync();
+ for (const compilerOutput of compilerOutputs) {
+ const contractFileNames = _.keys(compilerOutput.contracts);
+ for (const contractFileName of contractFileNames) {
+ const contractNameToOutput = compilerOutput.contracts[contractFileName];
+
+ const contractNames = _.keys(contractNameToOutput);
+ for (const contractName of contractNames) {
+ const compiledContract = contractNameToOutput[contractName];
+ if (_.isUndefined(compiledContract.abi)) {
+ throw new Error('compiled contract did not contain ABI output');
+ }
+ doc[contractName] = _genDocSection(compiledContract);
+ }
+ }
+ }
+
+ return doc;
+}
+
+function _makeCompilerOptions(contractsToCompile: string[], contractsDir: string): CompilerOptions {
+ const compilerOptions: CompilerOptions = {
+ contractsDir,
+ contracts: '*',
+ compilerSettings: {
+ outputSelection: {
+ ['*']: {
+ ['*']: ['abi', 'devdoc'],
+ },
+ },
+ },
+ };
+
+ const shouldOverrideCatchAllContractsConfig = !_.isUndefined(contractsToCompile);
+ if (shouldOverrideCatchAllContractsConfig) {
+ compilerOptions.contracts = contractsToCompile;
+ }
+
+ return compilerOptions;
+}
+
+function _genDocSection(compiledContract: StandardContractOutput): DocSection {
+ const docSection: DocSection = {
+ comment: _.isUndefined(compiledContract.devdoc) ? '' : compiledContract.devdoc.title,
+ constructors: [],
+ methods: [],
+ properties: [],
+ types: [],
+ functions: [],
+ events: [],
+ };
+
+ for (const abiDefinition of compiledContract.abi) {
+ switch (abiDefinition.type) {
+ case 'constructor':
+ docSection.constructors.push(_genConstructorDoc(abiDefinition, compiledContract.devdoc));
+ break;
+ case 'event':
+ (docSection.events as Event[]).push(_genEventDoc(abiDefinition));
+ // note that we're not sending devdoc to _genEventDoc().
+ // that's because the type of the events array doesn't have any fields for documentation!
+ break;
+ case 'function':
+ docSection.methods.push(_genMethodDoc(abiDefinition, compiledContract.devdoc));
+ break;
+ default:
+ throw new Error(`unknown and unsupported AbiDefinition type '${abiDefinition.type}'`);
+ }
+ }
+
+ return docSection;
+}
+
+function _genConstructorDoc(abiDefinition: ConstructorAbi, devdocIfExists: DevdocOutput | undefined): SolidityMethod {
+ const { parameters, methodSignature } = _genMethodParamsDoc(
+ '', // TODO: update depending on how constructors are keyed in devdoc
+ abiDefinition.inputs,
+ devdocIfExists,
+ );
+
+ const comment = _devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
+ // TODO: figure out why devdoc'd constructors don't get output by solc
+
+ const constructorDoc: SolidityMethod = {
+ isConstructor: true,
+ name: '', // sad we have to specify this
+ callPath: '', // TODO: wtf is this?
+ parameters,
+ returnType: { name: '', typeDocType: TypeDocTypes.Intrinsic }, // sad we have to specify this
+ isConstant: false, // constructors are non-const by their nature, right?
+ isPayable: abiDefinition.payable,
+ comment,
+ };
+
+ return constructorDoc;
+}
+
+function _devdocMethodDetailsIfExist(
+ methodSignature: string,
+ devdocIfExists: DevdocOutput | undefined,
+): string | undefined {
+ let details;
+ if (!_.isUndefined(devdocIfExists)) {
+ const devdocMethodsIfExist = devdocIfExists.methods;
+ if (!_.isUndefined(devdocMethodsIfExist)) {
+ const devdocMethodIfExists = devdocMethodsIfExist[methodSignature];
+ if (!_.isUndefined(devdocMethodIfExists)) {
+ const devdocMethodDetailsIfExist = devdocMethodIfExists.details;
+ if (!_.isUndefined(devdocMethodDetailsIfExist)) {
+ details = devdocMethodDetailsIfExist;
+ }
+ }
+ }
+ }
+ return details;
+}
+
+function _genMethodDoc(
+ abiDefinition: MethodAbi | FallbackAbi,
+ devdocIfExists: DevdocOutput | undefined,
+): SolidityMethod {
+ const name = abiDefinition.type === 'fallback' ? '' : abiDefinition.name;
+
+ const { parameters, methodSignature } =
+ abiDefinition.type === 'fallback'
+ ? { parameters: [], methodSignature: `${name}()` }
+ : _genMethodParamsDoc(name, abiDefinition.inputs, devdocIfExists);
+
+ const comment = _devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
+
+ const returnType =
+ abiDefinition.type === 'fallback'
+ ? { name: '', typeDocType: TypeDocTypes.Intrinsic }
+ : _genMethodReturnTypeDoc(abiDefinition.outputs, methodSignature, devdocIfExists);
+
+ const isConstant = abiDefinition.type === 'fallback' ? true : abiDefinition.constant;
+ // TODO: determine whether fallback functions are always constant, as coded just above
+
+ const methodDoc: SolidityMethod = {
+ isConstructor: false,
+ name,
+ callPath: '', // TODO: wtf is this?
+ parameters,
+ returnType,
+ isConstant,
+ isPayable: abiDefinition.payable,
+ comment,
+ };
+ return methodDoc;
+}
+
+function _genEventDoc(abiDefinition: EventAbi): Event {
+ const eventDoc: Event = {
+ name: abiDefinition.name,
+ eventArgs: _genEventArgsDoc(abiDefinition.inputs, undefined),
+ };
+ return eventDoc;
+}
+
+function _genEventArgsDoc(args: DataItem[], devdocIfExists: DevdocOutput | undefined): EventArg[] {
+ const eventArgsDoc: EventArg[] = [];
+
+ for (const arg of args) {
+ const name = arg.name;
+
+ const type: Type = {
+ name: arg.type,
+ typeDocType: TypeDocTypes.Intrinsic,
+ };
+
+ const eventArgDoc: EventArg = {
+ isIndexed: false, // TODO: wtf is this?
+ name,
+ type,
+ };
+
+ eventArgsDoc.push(eventArgDoc);
+ }
+ return eventArgsDoc;
+}
+
+/**
+ * Extract documentation for each method paramater from @param params.
+ */
+function _genMethodParamsDoc(
+ name: string,
+ abiParams: DataItem[],
+ devdocIfExists: DevdocOutput | undefined,
+): { parameters: Parameter[]; methodSignature: string } {
+ const parameters: Parameter[] = [];
+ let methodSignature = `${name}(`;
+
+ for (const abiParam of abiParams) {
+ const parameter: Parameter = {
+ name: abiParam.name,
+ comment: '',
+ isOptional: false, // Unsupported in Solidity, until resolution of https://github.com/ethereum/solidity/issues/232
+ type: { name: abiParam.type, typeDocType: TypeDocTypes.Intrinsic },
+ };
+ parameters.push(parameter);
+ methodSignature = `${methodSignature}${abiParam.type},`;
+ }
+
+ if (methodSignature.slice(-1) === ',') {
+ methodSignature = methodSignature.slice(0, -1);
+ }
+ methodSignature += ')';
+
+ if (!_.isUndefined(devdocIfExists)) {
+ const devdocMethodIfExists = devdocIfExists.methods[methodSignature];
+ if (!_.isUndefined(devdocMethodIfExists)) {
+ const devdocParamsIfExist = devdocMethodIfExists.params;
+ if (!_.isUndefined(devdocParamsIfExist)) {
+ for (const parameter of parameters) {
+ parameter.comment = devdocParamsIfExist[parameter.name];
+ }
+ }
+ }
+ }
+
+ return { parameters, methodSignature };
+}
+
+function _genMethodReturnTypeDoc(
+ outputs: DataItem[],
+ methodSignature: string,
+ devdocIfExists: DevdocOutput | undefined,
+): Type {
+ const methodReturnTypeDoc: Type = {
+ name: '',
+ typeDocType: TypeDocTypes.Intrinsic,
+ tupleElements: undefined,
+ };
+ if (outputs.length > 1) {
+ methodReturnTypeDoc.typeDocType = TypeDocTypes.Tuple;
+ methodReturnTypeDoc.tupleElements = [];
+ for (const output of outputs) {
+ methodReturnTypeDoc.tupleElements.push({ name: output.name, typeDocType: TypeDocTypes.Intrinsic });
+ }
+ } else if (outputs.length === 1) {
+ methodReturnTypeDoc.typeDocType = TypeDocTypes.Intrinsic;
+ methodReturnTypeDoc.name = outputs[0].name;
+ }
+ return methodReturnTypeDoc;
+}
diff --git a/packages/sol-doc/test/fixtures/contracts/TokenTransferProxy.sol b/packages/sol-doc/test/fixtures/contracts/TokenTransferProxy.sol
new file mode 100644
index 000000000..44570d459
--- /dev/null
+++ b/packages/sol-doc/test/fixtures/contracts/TokenTransferProxy.sol
@@ -0,0 +1,115 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity ^0.4.14;
+
+import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol";
+import { ERC20 as Token } from "zeppelin-solidity/contracts/token/ERC20/ERC20.sol";
+
+/// @title TokenTransferProxy - Transfers tokens on behalf of contracts that have been approved via decentralized governance.
+/// @author Amir Bandeali - <amir@0xProject.com>, Will Warren - <will@0xProject.com>
+contract TokenTransferProxy is Ownable {
+
+ /// @dev Only authorized addresses can invoke functions with this modifier.
+ modifier onlyAuthorized {
+ require(authorized[msg.sender]);
+ _;
+ }
+
+ modifier targetAuthorized(address target) {
+ require(authorized[target]);
+ _;
+ }
+
+ modifier targetNotAuthorized(address target) {
+ require(!authorized[target]);
+ _;
+ }
+
+ mapping (address => bool) public authorized;
+ address[] public authorities;
+
+ event LogAuthorizedAddressAdded(address indexed target, address indexed caller);
+ event LogAuthorizedAddressRemoved(address indexed target, address indexed caller);
+
+ /*
+ * Public functions
+ */
+
+ /// @dev Authorizes an address.
+ /// @param target Address to authorize.
+ function addAuthorizedAddress(address target)
+ public
+ onlyOwner
+ targetNotAuthorized(target)
+ {
+ authorized[target] = true;
+ authorities.push(target);
+ LogAuthorizedAddressAdded(target, msg.sender);
+ }
+
+ /// @dev Removes authorizion of an address.
+ /// @param target Address to remove authorization from.
+ function removeAuthorizedAddress(address target)
+ public
+ onlyOwner
+ targetAuthorized(target)
+ {
+ delete authorized[target];
+ for (uint i = 0; i < authorities.length; i++) {
+ if (authorities[i] == target) {
+ authorities[i] = authorities[authorities.length - 1];
+ authorities.length -= 1;
+ break;
+ }
+ }
+ LogAuthorizedAddressRemoved(target, msg.sender);
+ }
+
+ /// @dev Calls into ERC20 Token contract, invoking transferFrom.
+ /// @param token Address of token to transfer.
+ /// @param from Address to transfer token from.
+ /// @param to Address to transfer token to.
+ /// @param value Amount of token to transfer.
+ /// @return Success of transfer.
+ function transferFrom(
+ address token,
+ address from,
+ address to,
+ uint value)
+ public
+ onlyAuthorized
+ returns (bool)
+ {
+ return Token(token).transferFrom(from, to, value);
+ }
+
+ /*
+ * Public constant functions
+ */
+
+ /// @dev Gets all authorized addresses.
+ /// @return Array of authorized addresses.
+ function getAuthorizedAddresses()
+ public
+ constant
+ returns (address[])
+ {
+ return authorities;
+ }
+}
diff --git a/packages/sol-doc/test/solidity_doc_generator_test.ts b/packages/sol-doc/test/solidity_doc_generator_test.ts
new file mode 100644
index 000000000..7f11fcdfd
--- /dev/null
+++ b/packages/sol-doc/test/solidity_doc_generator_test.ts
@@ -0,0 +1,76 @@
+import * as _ from 'lodash';
+
+import * as chai from 'chai';
+import 'mocha';
+
+import { SolidityMethod } from '@0xproject/types';
+
+import { generateSolDocAsync } from '../src/solidity_doc_generator';
+
+import { chaiSetup } from './util/chai_setup';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('#SolidityDocGenerator', () => {
+ it('should generate a doc object that matches the TokenTransferProxy fixture', async () => {
+ const doc = await generateSolDocAsync(['TokenTransferProxy'], `${__dirname}/../../test/fixtures/contracts`);
+
+ expect(doc).to.not.be.undefined();
+
+ const tokenTransferProxyConstructorCount = 0;
+ const tokenTransferProxyMethodCount = 8;
+ const tokenTransferProxyEventCount = 3;
+ expect(doc.TokenTransferProxy.constructors.length).to.equal(tokenTransferProxyConstructorCount);
+ expect(doc.TokenTransferProxy.methods.length).to.equal(tokenTransferProxyMethodCount);
+ if (_.isUndefined(doc.TokenTransferProxy.events)) {
+ throw new Error('events should never be undefined');
+ }
+ expect(doc.TokenTransferProxy.events.length).to.equal(tokenTransferProxyEventCount);
+
+ const ownableConstructorCount = 1;
+ const ownableMethodCount = 2;
+ const ownableEventCount = 1;
+ expect(doc.Ownable.constructors.length).to.equal(ownableConstructorCount);
+ expect(doc.Ownable.methods.length).to.equal(ownableMethodCount);
+ if (_.isUndefined(doc.Ownable.events)) {
+ throw new Error('events should never be undefined');
+ }
+ expect(doc.Ownable.events.length).to.equal(ownableEventCount);
+
+ const erc20ConstructorCount = 0;
+ const erc20MethodCount = 6;
+ const erc20EventCount = 2;
+ expect(doc.ERC20.constructors.length).to.equal(erc20ConstructorCount);
+ expect(doc.ERC20.methods.length).to.equal(erc20MethodCount);
+ if (_.isUndefined(doc.ERC20.events)) {
+ throw new Error('events should never be undefined');
+ }
+ expect(doc.ERC20.events.length).to.equal(erc20EventCount);
+
+ const erc20BasicConstructorCount = 0;
+ const erc20BasicMethodCount = 3;
+ const erc20BasicEventCount = 1;
+ expect(doc.ERC20Basic.constructors.length).to.equal(erc20BasicConstructorCount);
+ expect(doc.ERC20Basic.methods.length).to.equal(erc20BasicMethodCount);
+ if (_.isUndefined(doc.ERC20Basic.events)) {
+ throw new Error('events should never be undefined');
+ }
+ expect(doc.ERC20Basic.events.length).to.equal(erc20BasicEventCount);
+
+ let addAuthorizedAddressMethod: SolidityMethod | undefined;
+ for (const method of doc.TokenTransferProxy.methods) {
+ if (method.name === 'addAuthorizedAddress') {
+ addAuthorizedAddressMethod = method;
+ }
+ }
+ expect(
+ addAuthorizedAddressMethod,
+ `method addAuthorizedAddress not found in ${JSON.stringify(doc.TokenTransferProxy.methods)}`,
+ ).to.not.be.undefined();
+ const tokenTransferProxyAddAuthorizedAddressComment = 'Authorizes an address.';
+ expect((addAuthorizedAddressMethod as SolidityMethod).comment).to.equal(
+ tokenTransferProxyAddAuthorizedAddressComment,
+ );
+ });
+});
diff --git a/packages/sol-doc/test/util/chai_setup.ts b/packages/sol-doc/test/util/chai_setup.ts
new file mode 100644
index 000000000..1a8733093
--- /dev/null
+++ b/packages/sol-doc/test/util/chai_setup.ts
@@ -0,0 +1,13 @@
+import * as chai from 'chai';
+import chaiAsPromised = require('chai-as-promised');
+import ChaiBigNumber = require('chai-bignumber');
+import * as dirtyChai from 'dirty-chai';
+
+export const chaiSetup = {
+ configure(): void {
+ chai.config.includeStack = true;
+ chai.use(ChaiBigNumber());
+ chai.use(dirtyChai);
+ chai.use(chaiAsPromised);
+ },
+};
diff --git a/packages/sol-doc/tsconfig.json b/packages/sol-doc/tsconfig.json
new file mode 100644
index 000000000..2ee711adc
--- /dev/null
+++ b/packages/sol-doc/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": "."
+ },
+ "include": ["./src/**/*", "./test/**/*"]
+}
diff --git a/packages/sol-doc/tslint.json b/packages/sol-doc/tslint.json
new file mode 100644
index 000000000..ffaefe83a
--- /dev/null
+++ b/packages/sol-doc/tslint.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["@0xproject/tslint-config"]
+}