aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-doc
diff options
context:
space:
mode:
authorBrandon Millman <brandon.millman@gmail.com>2018-10-03 07:13:16 +0800
committerBrandon Millman <brandon.millman@gmail.com>2018-10-03 07:13:16 +0800
commit343b922ec11a6108caaf3095e59be0e56d45ee4a (patch)
treead38a124853c4cd153f5a290a0dc461447f8c799 /packages/sol-doc
parent6deb027bdf4e57f8918fd2413f0fdc55311508d3 (diff)
parentf1ecb8c5cb28a0a7ca6f7ad2ff11194091df62a4 (diff)
downloaddexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar
dexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.gz
dexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.bz2
dexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.lz
dexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.xz
dexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.zst
dexon-sol-tools-343b922ec11a6108caaf3095e59be0e56d45ee4a.zip
Merge branch 'development' into feature/asset-buyer/improve-asset-buyer-manager
* development: (178 commits) Change cache key back to repo from repo-built Change the lint command back Merge build & install Remove deps cache all together Cache all nested node_modules directories Explicitly specify yarn cache folder Ignore linter issues Fix linter issue Separate deps and built caches Build tslint rules before running linter Cache yarn cache directory without node modules Run linter before prettier as it fails more often Add yarn cache path Split CI install and build steps Move bundle-size out of static tests and don't wait for a build with static tests Introduce a build:ci command that doesn't build webpack bundles Measure only one bundle size as they're the same Fix linter errors Fix no_website CI builds Check bundle size on CI ...
Diffstat (limited to 'packages/sol-doc')
-rw-r--r--packages/sol-doc/CHANGELOG.json30
-rw-r--r--packages/sol-doc/CHANGELOG.md18
-rwxr-xr-xpackages/sol-doc/bin/sol-doc.js2
-rw-r--r--packages/sol-doc/coverage/.gitkeep0
-rw-r--r--packages/sol-doc/package.json51
-rw-r--r--packages/sol-doc/src/cli.ts41
-rw-r--r--packages/sol-doc/src/index.ts1
-rw-r--r--packages/sol-doc/src/sol_doc.ts500
-rw-r--r--packages/sol-doc/test/fixtures/contracts/MultipleReturnValues.sol7
-rw-r--r--packages/sol-doc/test/fixtures/contracts/NatspecEverything.sol40
-rw-r--r--packages/sol-doc/test/fixtures/contracts/StructParamAndReturn.sol18
-rw-r--r--packages/sol-doc/test/fixtures/contracts/TokenTransferProxy.sol115
-rw-r--r--packages/sol-doc/test/fixtures/contracts/TokenTransferProxyNoDevdoc.sol100
-rw-r--r--packages/sol-doc/test/solidity_doc_generator_test.ts273
-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
17 files changed, 1220 insertions, 0 deletions
diff --git a/packages/sol-doc/CHANGELOG.json b/packages/sol-doc/CHANGELOG.json
new file mode 100644
index 000000000..9d3f4bcde
--- /dev/null
+++ b/packages/sol-doc/CHANGELOG.json
@@ -0,0 +1,30 @@
+[
+ {
+ "timestamp": 1538475601,
+ "version": "1.0.2",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ },
+ {
+ "timestamp": 1538157789,
+ "version": "1.0.1",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ },
+ {
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note":
+ "Utility to generate documentation for Solidity smart contracts, outputting a format compliant with @0xproject/types.DocAgnosticFormat",
+ "pr": 1004
+ }
+ ]
+ }
+]
diff --git a/packages/sol-doc/CHANGELOG.md b/packages/sol-doc/CHANGELOG.md
new file mode 100644
index 000000000..9f16fb108
--- /dev/null
+++ b/packages/sol-doc/CHANGELOG.md
@@ -0,0 +1,18 @@
+<!--
+changelogUtils.file is auto-generated using the monorepo-scripts package. Don't edit directly.
+Edit the package's CHANGELOG.json file only.
+-->
+
+CHANGELOG
+
+## v1.0.2 - _October 2, 2018_
+
+ * Dependencies updated
+
+## v1.0.1 - _September 28, 2018_
+
+ * Dependencies updated
+
+## v1.0.0 - _Invalid date_
+
+ * Utility to generate documentation for Solidity smart contracts, outputting a format compliant with @0xproject/types.DocAgnosticFormat (#1004)
diff --git a/packages/sol-doc/bin/sol-doc.js b/packages/sol-doc/bin/sol-doc.js
new file mode 100755
index 000000000..35c9ae735
--- /dev/null
+++ b/packages/sol-doc/bin/sol-doc.js
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('../lib/src/cli.js');
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..0b0c7c963
--- /dev/null
+++ b/packages/sol-doc/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@0xproject/sol-doc",
+ "version": "1.0.1",
+ "description": "Solidity documentation generator",
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.js",
+ "scripts": {
+ "build": "tsc",
+ "build:ci": "yarn build",
+ "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 6000 --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",
+ "generate-v1-protocol-docs": "(cd ../contracts/src/1.0.0; node ../../../../node_modules/.bin/sol-doc --contracts-dir . --contracts Exchange/Exchange_v1.sol TokenRegistry/TokenRegistry.sol TokenTransferProxy/TokenTransferProxy_v1.sol) > v1.0.0.json",
+ "generate-v2-protocol-docs": "(cd ../contracts/src/2.0.0; node ../../../../node_modules/.bin/sol-doc --contracts-dir . --contracts Exchange/Exchange.sol AssetProxy/ERC20Proxy.sol AssetProxy/ERC721Proxy.sol OrderValidator/OrderValidator.sol Forwarder/Forwarder.sol AssetProxyOwner/AssetProxyOwner.sol) > v2.0.0.json",
+ "deploy-v2-protocol-docs": "aws --profile 0xproject s3 cp --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json v2.0.0.json s3://staging-doc-jsons/contracts/",
+ "deploy-v1-protocol-docs": "aws --profile 0xproject s3 cp --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json v1.0.0.json s3://staging-doc-jsons/contracts/"
+ },
+ "bin": {
+ "sol-doc": "bin/sol-doc.js"
+ },
+ "repository": "https://github.com/0xProject/0x-monorepo.git",
+ "author": "F. Eugene Aumson",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@0xproject/sol-compiler": "^1.1.6",
+ "@0xproject/types": "^1.1.2",
+ "@0xproject/utils": "^2.0.0",
+ "ethereum-types": "^1.0.9",
+ "ethereumjs-util": "^5.1.1",
+ "lodash": "^4.17.10",
+ "yargs": "^12.0.2"
+ },
+ "devDependencies": {
+ "@0xproject/tslint-config": "^1.0.7",
+ "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/cli.ts b/packages/sol-doc/src/cli.ts
new file mode 100644
index 000000000..a1847e868
--- /dev/null
+++ b/packages/sol-doc/src/cli.ts
@@ -0,0 +1,41 @@
+import 'source-map-support/register';
+import * as yargs from 'yargs';
+
+import { logUtils } from '@0xproject/utils';
+
+import { SolDoc } from './sol_doc';
+
+const JSON_TAB_WIDTH = 4;
+
+(async () => {
+ const argv = yargs
+ .option('contracts-dir', {
+ type: 'string',
+ description: 'path of contracts directory to compile',
+ })
+ .option('contracts', {
+ type: 'string',
+ description: 'comma separated list of contracts to compile',
+ })
+ .demandOption('contracts-dir')
+ .array('contracts')
+ .help().argv;
+ // Unfortunately, the only way to currently retrieve the declared structs within Solidity contracts
+ // is to tease them out of the params/return values included in the ABI. These structures do
+ // not include the structs actual name, so we need a mapping to assign the proper name to a
+ // struct. If the name is not in this mapping, the structs name will default to the param/return value
+ // name (which mostly coincide).
+ const customTypeHashToName: { [hash: string]: string } = {
+ '52d4a768701076c7bac06e386e430883975eb398732eccba797fd09dd064a60e': 'Order',
+ '46f7e8c4d144d11a72ce5338458ea37b933500d7a65e740cbca6d16e350eaa48': 'FillResults',
+ c22239cf0d29df1e6cf1be54f21692a8c0b3a48b9367540d4ffff4608b331ce9: 'OrderInfo',
+ c21e9ff31a30941c22e1cb43752114bb467c34dea58947f98966c9030fc8e4a9: 'TraderInfo',
+ '6de3264a1040e027d4bdd29c71e963028238ac4ef060541078a7aced44a4d46f': 'MatchedFillResults',
+ };
+ const solDoc = new SolDoc();
+ const doc = await solDoc.generateSolDocAsync(argv.contractsDir, argv.contracts, customTypeHashToName);
+ process.stdout.write(JSON.stringify(doc, null, JSON_TAB_WIDTH));
+})().catch(err => {
+ logUtils.warn(err);
+ process.exit(1);
+});
diff --git a/packages/sol-doc/src/index.ts b/packages/sol-doc/src/index.ts
new file mode 100644
index 000000000..521668cc8
--- /dev/null
+++ b/packages/sol-doc/src/index.ts
@@ -0,0 +1 @@
+export { SolDoc } from './sol_doc';
diff --git a/packages/sol-doc/src/sol_doc.ts b/packages/sol-doc/src/sol_doc.ts
new file mode 100644
index 000000000..138882c92
--- /dev/null
+++ b/packages/sol-doc/src/sol_doc.ts
@@ -0,0 +1,500 @@
+import * as path from 'path';
+
+import {
+ AbiDefinition,
+ ConstructorAbi,
+ DataItem,
+ DevdocOutput,
+ EventAbi,
+ EventParameter,
+ FallbackAbi,
+ MethodAbi,
+ StandardContractOutput,
+} from 'ethereum-types';
+import ethUtil = require('ethereumjs-util');
+import * as _ from 'lodash';
+
+import { Compiler, CompilerOptions } from '@0xproject/sol-compiler';
+import {
+ CustomType,
+ CustomTypeChild,
+ DocAgnosticFormat,
+ DocSection,
+ Event,
+ EventArg,
+ ObjectMap,
+ Parameter,
+ SolidityMethod,
+ Type,
+ TypeDocTypes,
+} from '@0xproject/types';
+
+export class SolDoc {
+ private _customTypeHashToName: ObjectMap<string> | undefined;
+ private static _genEventDoc(abiDefinition: EventAbi): Event {
+ const eventDoc: Event = {
+ name: abiDefinition.name,
+ eventArgs: SolDoc._genEventArgsDoc(abiDefinition.inputs),
+ };
+ return eventDoc;
+ }
+ private static _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;
+ }
+ private static _genFallbackDoc(
+ abiDefinition: FallbackAbi,
+ devdocIfExists: DevdocOutput | undefined,
+ ): SolidityMethod {
+ const methodSignature = `()`;
+ const comment = SolDoc._devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
+
+ const returnComment =
+ _.isUndefined(devdocIfExists) || _.isUndefined(devdocIfExists.methods[methodSignature])
+ ? undefined
+ : devdocIfExists.methods[methodSignature].return;
+
+ const methodDoc: SolidityMethod = {
+ isConstructor: false,
+ name: 'fallback',
+ callPath: '',
+ parameters: [],
+ returnType: { name: 'void', typeDocType: TypeDocTypes.Intrinsic },
+ returnComment,
+ isConstant: true,
+ isPayable: abiDefinition.payable,
+ isFallback: true,
+ comment: _.isEmpty(comment)
+ ? 'The fallback function. It is executed on a call to the contract if none of the other functions match the given public identifier (or if no data was supplied at all).'
+ : comment,
+ };
+ return methodDoc;
+ }
+ private static _genEventArgsDoc(args: EventParameter[]): 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: arg.indexed,
+ name,
+ type,
+ };
+
+ eventArgsDoc.push(eventArgDoc);
+ }
+ return eventArgsDoc;
+ }
+ private static _dedupStructs(customTypes: CustomType[]): CustomType[] {
+ const uniqueCustomTypes: CustomType[] = [];
+ const seenTypes: { [hash: string]: boolean } = {};
+ _.each(customTypes, customType => {
+ const hash = SolDoc._generateCustomTypeHash(customType);
+ if (!seenTypes[hash]) {
+ uniqueCustomTypes.push(customType);
+ seenTypes[hash] = true;
+ }
+ });
+ return uniqueCustomTypes;
+ }
+ private static _capitalize(text: string): string {
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
+ }
+ private static _generateCustomTypeHash(customType: CustomType): string {
+ const customTypeWithoutName = _.cloneDeep(customType);
+ delete customTypeWithoutName.name;
+ const customTypeWithoutNameStr = JSON.stringify(customTypeWithoutName);
+ const hash = ethUtil.sha256(customTypeWithoutNameStr).toString('hex');
+ return hash;
+ }
+ private static _makeCompilerOptions(contractsDir: string, contractsToCompile?: string[]): CompilerOptions {
+ const compilerOptions: CompilerOptions = {
+ contractsDir,
+ contracts: '*',
+ compilerSettings: {
+ outputSelection: {
+ ['*']: {
+ ['*']: ['abi', 'devdoc'],
+ },
+ },
+ },
+ };
+
+ const shouldOverrideCatchAllContractsConfig =
+ !_.isUndefined(contractsToCompile) && contractsToCompile.length > 0;
+ if (shouldOverrideCatchAllContractsConfig) {
+ compilerOptions.contracts = contractsToCompile;
+ }
+
+ return compilerOptions;
+ }
+ /**
+ * Invoke the Solidity compiler and transform its ABI and devdoc outputs into a
+ * JSON format easily consumed by documentation rendering tools.
+ * @param contractsToDocument 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.
+ */
+ public async generateSolDocAsync(
+ contractsDir: string,
+ contractsToDocument?: string[],
+ customTypeHashToName?: ObjectMap<string>,
+ ): Promise<DocAgnosticFormat> {
+ this._customTypeHashToName = customTypeHashToName;
+ const docWithDependencies: DocAgnosticFormat = {};
+ const compilerOptions = SolDoc._makeCompilerOptions(contractsDir, contractsToDocument);
+ const compiler = new Compiler(compilerOptions);
+ const compilerOutputs = await compiler.getCompilerOutputsAsync();
+ let structs: CustomType[] = [];
+ 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');
+ }
+ docWithDependencies[contractName] = this._genDocSection(compiledContract, contractName);
+ structs = [...structs, ...this._extractStructs(compiledContract)];
+ }
+ }
+ }
+ structs = SolDoc._dedupStructs(structs);
+ structs = this._overwriteStructNames(structs);
+
+ let doc: DocAgnosticFormat = {};
+ if (_.isUndefined(contractsToDocument) || contractsToDocument.length === 0) {
+ doc = docWithDependencies;
+ } else {
+ for (const contractToDocument of contractsToDocument) {
+ const contractBasename = path.basename(contractToDocument);
+ const contractName =
+ contractBasename.lastIndexOf('.sol') === -1
+ ? contractBasename
+ : contractBasename.substring(0, contractBasename.lastIndexOf('.sol'));
+ doc[contractName] = docWithDependencies[contractName];
+ }
+ }
+
+ if (structs.length > 0) {
+ doc.structs = {
+ comment: '',
+ constructors: [],
+ methods: [],
+ properties: [],
+ types: structs,
+ functions: [],
+ events: [],
+ };
+ }
+
+ delete this._customTypeHashToName; // Clean up instance state
+ return doc;
+ }
+ private _getCustomTypeFromDataItem(inputOrOutput: DataItem): CustomType {
+ const customType: CustomType = {
+ name: _.capitalize(inputOrOutput.name),
+ kindString: 'Interface',
+ children: [],
+ };
+ _.each(inputOrOutput.components, (component: DataItem) => {
+ const childType = this._getTypeFromDataItem(component);
+ const customTypeChild = {
+ name: component.name,
+ type: childType,
+ };
+ // (fabio): Not sure why this type casting is necessary. Seems TS doesn't
+ // deduce that `customType.children` cannot be undefined anymore after being
+ // set to `[]` above.
+ (customType.children as CustomTypeChild[]).push(customTypeChild);
+ });
+ return customType;
+ }
+ private _getNameFromDataItemIfExists(dataItem: DataItem): string | undefined {
+ if (_.isUndefined(dataItem.components)) {
+ return undefined;
+ }
+ const customType = this._getCustomTypeFromDataItem(dataItem);
+ const hash = SolDoc._generateCustomTypeHash(customType);
+ if (_.isUndefined(this._customTypeHashToName) || _.isUndefined(this._customTypeHashToName[hash])) {
+ return undefined;
+ }
+ return this._customTypeHashToName[hash];
+ }
+ private _getTypeFromDataItem(dataItem: DataItem): Type {
+ const typeDocType = !_.isUndefined(dataItem.components) ? TypeDocTypes.Reference : TypeDocTypes.Intrinsic;
+ let typeName: string;
+ if (typeDocType === TypeDocTypes.Reference) {
+ const nameIfExists = this._getNameFromDataItemIfExists(dataItem);
+ typeName = _.isUndefined(nameIfExists) ? SolDoc._capitalize(dataItem.name) : nameIfExists;
+ } else {
+ typeName = dataItem.type;
+ }
+
+ const isArrayType = _.endsWith(dataItem.type, '[]');
+ let type: Type;
+ if (isArrayType) {
+ // tslint:disable-next-line:custom-no-magic-numbers
+ typeName = typeDocType === TypeDocTypes.Intrinsic ? typeName.slice(0, -2) : typeName;
+ type = {
+ elementType: { name: typeName, typeDocType },
+ typeDocType: TypeDocTypes.Array,
+ name: '',
+ };
+ } else {
+ type = { name: typeName, typeDocType };
+ }
+ return type;
+ }
+ private _overwriteStructNames(customTypes: CustomType[]): CustomType[] {
+ if (_.isUndefined(this._customTypeHashToName)) {
+ return customTypes;
+ }
+ const localCustomTypes = _.cloneDeep(customTypes);
+ _.each(localCustomTypes, (customType, i) => {
+ const hash = SolDoc._generateCustomTypeHash(customType);
+ if (!_.isUndefined(this._customTypeHashToName) && !_.isUndefined(this._customTypeHashToName[hash])) {
+ localCustomTypes[i].name = this._customTypeHashToName[hash];
+ }
+ });
+ return localCustomTypes;
+ }
+ private _extractStructs(compiledContract: StandardContractOutput): CustomType[] {
+ let customTypes: CustomType[] = [];
+ for (const abiDefinition of compiledContract.abi) {
+ let types: CustomType[] = [];
+ switch (abiDefinition.type) {
+ case 'constructor': {
+ types = this._getStructsAsCustomTypes(abiDefinition);
+ break;
+ }
+ case 'function': {
+ types = this._getStructsAsCustomTypes(abiDefinition);
+ break;
+ }
+ case 'event':
+ case 'fallback':
+ // No types exist
+ break;
+ default:
+ throw new Error(
+ `unknown and unsupported AbiDefinition type '${(abiDefinition as AbiDefinition).type}'`, // tslint:disable-line:no-unnecessary-type-assertion
+ );
+ }
+ customTypes = [...customTypes, ...types];
+ }
+ return customTypes;
+ }
+ private _genDocSection(compiledContract: StandardContractOutput, contractName: string): 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(
+ this._genConstructorDoc(contractName, abiDefinition, compiledContract.devdoc),
+ );
+ break;
+ case 'event':
+ (docSection.events as Event[]).push(SolDoc._genEventDoc(abiDefinition));
+ // note that we're not sending devdoc to this._genEventDoc().
+ // that's because the type of the events array doesn't have any fields for documentation!
+ break;
+ case 'function':
+ docSection.methods.push(this._genMethodDoc(abiDefinition, compiledContract.devdoc));
+ break;
+ case 'fallback':
+ docSection.methods.push(SolDoc._genFallbackDoc(abiDefinition, compiledContract.devdoc));
+ break;
+ default:
+ throw new Error(
+ `unknown and unsupported AbiDefinition type '${(abiDefinition as AbiDefinition).type}'`, // tslint:disable-line:no-unnecessary-type-assertion
+ );
+ }
+ }
+
+ return docSection;
+ }
+ private _genConstructorDoc(
+ contractName: string,
+ abiDefinition: ConstructorAbi,
+ devdocIfExists: DevdocOutput | undefined,
+ ): SolidityMethod {
+ const { parameters, methodSignature } = this._genMethodParamsDoc('', abiDefinition.inputs, devdocIfExists);
+
+ const comment = SolDoc._devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
+
+ const constructorDoc: SolidityMethod = {
+ isConstructor: true,
+ name: contractName,
+ callPath: '',
+ parameters,
+ returnType: { name: contractName, typeDocType: TypeDocTypes.Reference }, // sad we have to specify this
+ isConstant: false,
+ isPayable: abiDefinition.payable,
+ comment,
+ };
+
+ return constructorDoc;
+ }
+ private _genMethodDoc(abiDefinition: MethodAbi, devdocIfExists: DevdocOutput | undefined): SolidityMethod {
+ const name = abiDefinition.name;
+ const { parameters, methodSignature } = this._genMethodParamsDoc(name, abiDefinition.inputs, devdocIfExists);
+ const devDocComment = SolDoc._devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
+ const returnType = this._genMethodReturnTypeDoc(abiDefinition.outputs);
+ const returnComment =
+ _.isUndefined(devdocIfExists) || _.isUndefined(devdocIfExists.methods[methodSignature])
+ ? undefined
+ : devdocIfExists.methods[methodSignature].return;
+
+ const hasNoNamedParameters = _.isUndefined(_.find(parameters, p => !_.isEmpty(p.name)));
+ const isGeneratedGetter = hasNoNamedParameters;
+ const comment =
+ _.isEmpty(devDocComment) && isGeneratedGetter
+ ? `This is an auto-generated accessor method of the '${name}' contract instance variable.`
+ : devDocComment;
+ const methodDoc: SolidityMethod = {
+ isConstructor: false,
+ name,
+ callPath: '',
+ parameters,
+ returnType,
+ returnComment,
+ isConstant: abiDefinition.constant,
+ isPayable: abiDefinition.payable,
+ comment,
+ };
+ return methodDoc;
+ }
+ /**
+ * Extract documentation for each method parameter from @param params.
+ */
+ private _genMethodParamsDoc(
+ name: string,
+ abiParams: DataItem[],
+ devdocIfExists: DevdocOutput | undefined,
+ ): { parameters: Parameter[]; methodSignature: string } {
+ const parameters: Parameter[] = [];
+ for (const abiParam of abiParams) {
+ const type = this._getTypeFromDataItem(abiParam);
+
+ const parameter: Parameter = {
+ name: abiParam.name,
+ comment: '<No comment>',
+ isOptional: false, // Unsupported in Solidity, until resolution of https://github.com/ethereum/solidity/issues/232
+ type,
+ };
+ parameters.push(parameter);
+ }
+
+ const methodSignature = `${name}(${abiParams
+ .map(abiParam => {
+ if (!_.startsWith(abiParam.type, 'tuple')) {
+ return abiParam.type;
+ } else {
+ // Need to expand tuples:
+ // E.g: fillOrder(tuple,uint256,bytes) -> fillOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)
+ const isArray = _.endsWith(abiParam.type, '[]');
+ const expandedTypes = _.map(abiParam.components, c => c.type);
+ const type = `(${expandedTypes.join(',')})${isArray ? '[]' : ''}`;
+ return type;
+ }
+ })
+ .join(',')})`;
+
+ 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 };
+ }
+ private _genMethodReturnTypeDoc(outputs: DataItem[]): Type {
+ let type: Type;
+ if (outputs.length > 1) {
+ type = {
+ name: '',
+ typeDocType: TypeDocTypes.Tuple,
+ tupleElements: [],
+ };
+ for (const output of outputs) {
+ const tupleType = this._getTypeFromDataItem(output);
+ (type.tupleElements as Type[]).push(tupleType);
+ }
+ return type;
+ } else if (outputs.length === 1) {
+ const output = outputs[0];
+ type = this._getTypeFromDataItem(output);
+ } else {
+ type = {
+ name: 'void',
+ typeDocType: TypeDocTypes.Intrinsic,
+ };
+ }
+ return type;
+ }
+ private _getStructsAsCustomTypes(abiDefinition: AbiDefinition): CustomType[] {
+ const customTypes: CustomType[] = [];
+ // We cast to `any` here because we do not know yet if this type of abiDefinition contains
+ // an `input` key
+ if (!_.isUndefined((abiDefinition as any).inputs)) {
+ const methodOrConstructorAbi = abiDefinition as MethodAbi | ConstructorAbi;
+ _.each(methodOrConstructorAbi.inputs, input => {
+ if (!_.isUndefined(input.components)) {
+ const customType = this._getCustomTypeFromDataItem(input);
+ customTypes.push(customType);
+ }
+ });
+ }
+ if (!_.isUndefined((abiDefinition as any).outputs)) {
+ const methodAbi = abiDefinition as MethodAbi; // tslint:disable-line:no-unnecessary-type-assertion
+ _.each(methodAbi.outputs, output => {
+ if (!_.isUndefined(output.components)) {
+ const customType = this._getCustomTypeFromDataItem(output);
+ customTypes.push(customType);
+ }
+ });
+ }
+ return customTypes;
+ }
+}
+// tslint:disable:max-file-line-count
diff --git a/packages/sol-doc/test/fixtures/contracts/MultipleReturnValues.sol b/packages/sol-doc/test/fixtures/contracts/MultipleReturnValues.sol
new file mode 100644
index 000000000..1e898622c
--- /dev/null
+++ b/packages/sol-doc/test/fixtures/contracts/MultipleReturnValues.sol
@@ -0,0 +1,7 @@
+pragma solidity ^0.4.24;
+
+contract MultipleReturnValues {
+ function methodWithMultipleReturnValues() public pure returns(int, int) {
+ return (0, 0);
+ }
+}
diff --git a/packages/sol-doc/test/fixtures/contracts/NatspecEverything.sol b/packages/sol-doc/test/fixtures/contracts/NatspecEverything.sol
new file mode 100644
index 000000000..c6ad3db81
--- /dev/null
+++ b/packages/sol-doc/test/fixtures/contracts/NatspecEverything.sol
@@ -0,0 +1,40 @@
+pragma solidity ^0.4.24;
+
+/// @title Contract Title
+/// @dev This is a very long documentation comment at the contract level.
+/// It actually spans multiple lines, too.
+contract NatspecEverything {
+ int d;
+
+ /// @dev Constructor @dev
+ /// @param p Constructor @param
+ constructor(int p) public { d = p; }
+
+ /// @notice publicMethod @notice
+ /// @dev publicMethod @dev
+ /// @param p publicMethod @param
+ /// @return publicMethod @return
+ function publicMethod(int p) public pure returns(int r) { return p; }
+
+ /// @dev Fallback @dev
+ function () public {}
+
+ /// @notice externalMethod @notice
+ /// @dev externalMethod @dev
+ /// @param p externalMethod @param
+ /// @return externalMethod @return
+ function externalMethod(int p) external pure returns(int r) { return p; }
+
+ /// @dev Here is a really long developer documentation comment, which spans
+ /// multiple lines, for the purposes of making sure that broken lines are
+ /// consolidated into one devdoc comment.
+ function methodWithLongDevdoc(int p) public pure returns(int) { return p; }
+
+ /// @dev AnEvent @dev
+ /// @param p on this event is an integer.
+ event AnEvent(int p);
+
+ /// @dev methodWithSolhintDirective @dev
+ // solhint-disable no-empty-blocks
+ function methodWithSolhintDirective() public pure {}
+}
diff --git a/packages/sol-doc/test/fixtures/contracts/StructParamAndReturn.sol b/packages/sol-doc/test/fixtures/contracts/StructParamAndReturn.sol
new file mode 100644
index 000000000..b9a7ccdbc
--- /dev/null
+++ b/packages/sol-doc/test/fixtures/contracts/StructParamAndReturn.sol
@@ -0,0 +1,18 @@
+pragma solidity 0.4.24;
+pragma experimental ABIEncoderV2;
+
+
+contract StructParamAndReturn {
+
+ struct Stuff {
+ address anAddress;
+ uint256 aNumber;
+ }
+
+ /// @dev DEV_COMMENT
+ /// @param stuff STUFF_COMMENT
+ /// @return RETURN_COMMENT
+ function methodWithStructParamAndReturn(Stuff stuff) public pure returns(Stuff) {
+ return stuff;
+ }
+}
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/fixtures/contracts/TokenTransferProxyNoDevdoc.sol b/packages/sol-doc/test/fixtures/contracts/TokenTransferProxyNoDevdoc.sol
new file mode 100644
index 000000000..cc45a79e9
--- /dev/null
+++ b/packages/sol-doc/test/fixtures/contracts/TokenTransferProxyNoDevdoc.sol
@@ -0,0 +1,100 @@
+/*
+
+ 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";
+
+contract TokenTransferProxyNoDevdoc is Ownable {
+
+ 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
+ */
+
+ function addAuthorizedAddress(address target)
+ public
+ onlyOwner
+ targetNotAuthorized(target)
+ {
+ authorized[target] = true;
+ authorities.push(target);
+ LogAuthorizedAddressAdded(target, msg.sender);
+ }
+
+ 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);
+ }
+
+ function transferFrom(
+ address token,
+ address from,
+ address to,
+ uint value)
+ public
+ onlyAuthorized
+ returns (bool)
+ {
+ return Token(token).transferFrom(from, to, value);
+ }
+
+ /*
+ * Public constant functions
+ */
+
+ 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..f166fb143
--- /dev/null
+++ b/packages/sol-doc/test/solidity_doc_generator_test.ts
@@ -0,0 +1,273 @@
+import * as _ from 'lodash';
+
+import * as chai from 'chai';
+import 'mocha';
+
+import { DocAgnosticFormat, Event, SolidityMethod } from '@0xproject/types';
+
+import { SolDoc } from '../src/sol_doc';
+
+import { chaiSetup } from './util/chai_setup';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const solDoc = new SolDoc();
+
+describe('#SolidityDocGenerator', () => {
+ it('should generate a doc object that matches the devdoc-free TokenTransferProxy fixture', async () => {
+ const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
+ 'TokenTransferProxyNoDevdoc',
+ ]);
+ expect(doc).to.not.be.undefined();
+
+ verifyTokenTransferProxyABIIsDocumented(doc, 'TokenTransferProxyNoDevdoc');
+ });
+ const docPromises: Array<Promise<DocAgnosticFormat>> = [
+ solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`),
+ solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, []),
+ ];
+ docPromises.forEach(docPromise => {
+ it('should generate a doc object that matches the TokenTransferProxy fixture with its dependencies', async () => {
+ const doc = await docPromise;
+ expect(doc).to.not.be.undefined();
+
+ verifyTokenTransferProxyAndDepsABIsAreDocumented(doc, 'TokenTransferProxy');
+
+ let addAuthorizedAddressMethod: SolidityMethod | undefined;
+ for (const method of doc.TokenTransferProxy.methods) {
+ if (method.name === 'addAuthorizedAddress') {
+ addAuthorizedAddressMethod = method;
+ }
+ }
+ const tokenTransferProxyAddAuthorizedAddressComment = 'Authorizes an address.';
+ expect((addAuthorizedAddressMethod as SolidityMethod).comment).to.equal(
+ tokenTransferProxyAddAuthorizedAddressComment,
+ );
+
+ const expectedParamComment = 'Address to authorize.';
+ expect((addAuthorizedAddressMethod as SolidityMethod).parameters[0].comment).to.equal(expectedParamComment);
+ });
+ });
+ it('should generate a doc object that matches the TokenTransferProxy fixture', async () => {
+ const doc: DocAgnosticFormat = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
+ 'TokenTransferProxy',
+ ]);
+ verifyTokenTransferProxyABIIsDocumented(doc, 'TokenTransferProxy');
+ });
+ describe('when processing all the permutations of devdoc stuff that we use in our contracts', () => {
+ let doc: DocAgnosticFormat;
+ before(async () => {
+ doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, ['NatspecEverything']);
+ expect(doc).to.not.be.undefined();
+ expect(doc.NatspecEverything).to.not.be.undefined();
+ });
+ it('should emit the contract @title as its comment', () => {
+ expect(doc.NatspecEverything.comment).to.equal('Contract Title');
+ });
+ describe('should emit public method documentation for', () => {
+ let methodDoc: SolidityMethod;
+ before(() => {
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ methodDoc = doc.NatspecEverything.methods.find(method => {
+ return method.name === 'publicMethod';
+ }) as SolidityMethod;
+ if (_.isUndefined(methodDoc)) {
+ throw new Error('publicMethod not found');
+ }
+ });
+ it('method name', () => {
+ expect(methodDoc.name).to.equal('publicMethod');
+ });
+ it('method comment', () => {
+ expect(methodDoc.comment).to.equal('publicMethod @dev');
+ });
+ it('parameter name', () => {
+ expect(methodDoc.parameters[0].name).to.equal('p');
+ });
+ it('parameter comment', () => {
+ expect(methodDoc.parameters[0].comment).to.equal('publicMethod @param');
+ });
+ it('return type', () => {
+ expect(methodDoc.returnType.name).to.equal('int256');
+ });
+ it('return comment', () => {
+ expect(methodDoc.returnComment).to.equal('publicMethod @return');
+ });
+ });
+ describe('should emit external method documentation for', () => {
+ let methodDoc: SolidityMethod;
+ before(() => {
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ methodDoc = doc.NatspecEverything.methods.find(method => {
+ return method.name === 'externalMethod';
+ }) as SolidityMethod;
+ if (_.isUndefined(methodDoc)) {
+ throw new Error('externalMethod not found');
+ }
+ });
+ it('method name', () => {
+ expect(methodDoc.name).to.equal('externalMethod');
+ });
+ it('method comment', () => {
+ expect(methodDoc.comment).to.equal('externalMethod @dev');
+ });
+ it('parameter name', () => {
+ expect(methodDoc.parameters[0].name).to.equal('p');
+ });
+ it('parameter comment', () => {
+ expect(methodDoc.parameters[0].comment).to.equal('externalMethod @param');
+ });
+ it('return type', () => {
+ expect(methodDoc.returnType.name).to.equal('int256');
+ });
+ it('return comment', () => {
+ expect(methodDoc.returnComment).to.equal('externalMethod @return');
+ });
+ });
+ it('should not truncate a multi-line devdoc comment', () => {
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ const methodDoc: SolidityMethod = doc.NatspecEverything.methods.find(method => {
+ return method.name === 'methodWithLongDevdoc';
+ }) as SolidityMethod;
+ if (_.isUndefined(methodDoc)) {
+ throw new Error('methodWithLongDevdoc not found');
+ }
+ expect(methodDoc.comment).to.equal(
+ 'Here is a really long developer documentation comment, which spans multiple lines, for the purposes of making sure that broken lines are consolidated into one devdoc comment.',
+ );
+ });
+ describe('should emit event documentation for', () => {
+ let eventDoc: Event;
+ before(() => {
+ eventDoc = (doc.NatspecEverything.events as Event[])[0];
+ });
+ it('event name', () => {
+ expect(eventDoc.name).to.equal('AnEvent');
+ });
+ it('parameter name', () => {
+ expect(eventDoc.eventArgs[0].name).to.equal('p');
+ });
+ });
+ it('should not let solhint directives obscure natspec content', () => {
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ const methodDoc: SolidityMethod = doc.NatspecEverything.methods.find(method => {
+ return method.name === 'methodWithSolhintDirective';
+ }) as SolidityMethod;
+ if (_.isUndefined(methodDoc)) {
+ throw new Error('methodWithSolhintDirective not found');
+ }
+ expect(methodDoc.comment).to.equal('methodWithSolhintDirective @dev');
+ });
+ });
+ it('should document a method that returns multiple values', async () => {
+ const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
+ 'MultipleReturnValues',
+ ]);
+ expect(doc.MultipleReturnValues).to.not.be.undefined();
+ expect(doc.MultipleReturnValues.methods).to.not.be.undefined();
+ let methodWithMultipleReturnValues: SolidityMethod | undefined;
+ for (const method of doc.MultipleReturnValues.methods) {
+ if (method.name === 'methodWithMultipleReturnValues') {
+ methodWithMultipleReturnValues = method;
+ }
+ }
+ if (_.isUndefined(methodWithMultipleReturnValues)) {
+ throw new Error('method should not be undefined');
+ }
+ const returnType = methodWithMultipleReturnValues.returnType;
+ expect(returnType.typeDocType).to.equal('tuple');
+ if (_.isUndefined(returnType.tupleElements)) {
+ throw new Error('returnType.tupleElements should not be undefined');
+ }
+ expect(returnType.tupleElements.length).to.equal(2);
+ });
+ it('should document a method that has a struct param and return value', async () => {
+ const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
+ 'StructParamAndReturn',
+ ]);
+ expect(doc.StructParamAndReturn).to.not.be.undefined();
+ expect(doc.StructParamAndReturn.methods).to.not.be.undefined();
+ let methodWithStructParamAndReturn: SolidityMethod | undefined;
+ for (const method of doc.StructParamAndReturn.methods) {
+ if (method.name === 'methodWithStructParamAndReturn') {
+ methodWithStructParamAndReturn = method;
+ }
+ }
+ if (_.isUndefined(methodWithStructParamAndReturn)) {
+ throw new Error('method should not be undefined');
+ }
+ /**
+ * Solc maps devDoc comments to methods using a method signature. If we incorrectly
+ * generate the methodSignatures, the devDoc comments won't be correctly associated
+ * with their methods and they won't show up in the output. By checking that the comments
+ * are included for a method with structs as params/returns, we are sure that the methodSignature
+ * generation is correct for this case.
+ */
+ expect(methodWithStructParamAndReturn.comment).to.be.equal('DEV_COMMENT');
+ expect(methodWithStructParamAndReturn.returnComment).to.be.equal('RETURN_COMMENT');
+ expect(methodWithStructParamAndReturn.parameters[0].comment).to.be.equal('STUFF_COMMENT');
+ });
+ it('should document the structs included in a contract', async () => {
+ const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
+ 'StructParamAndReturn',
+ ]);
+ expect(doc.structs).to.not.be.undefined();
+ expect(doc.structs.types.length).to.be.equal(1);
+ });
+});
+
+function verifyTokenTransferProxyABIIsDocumented(doc: DocAgnosticFormat, contractName: string): void {
+ expect(doc[contractName]).to.not.be.undefined();
+ expect(doc[contractName].constructors).to.not.be.undefined();
+ const tokenTransferProxyConstructorCount = 0;
+ const tokenTransferProxyMethodCount = 8;
+ const tokenTransferProxyEventCount = 3;
+ expect(doc[contractName].constructors.length).to.equal(tokenTransferProxyConstructorCount);
+ expect(doc[contractName].methods.length).to.equal(tokenTransferProxyMethodCount);
+ const events = doc[contractName].events;
+ if (_.isUndefined(events)) {
+ throw new Error('events should never be undefined');
+ }
+ expect(events.length).to.equal(tokenTransferProxyEventCount);
+}
+
+function verifyTokenTransferProxyAndDepsABIsAreDocumented(doc: DocAgnosticFormat, contractName: string): void {
+ verifyTokenTransferProxyABIIsDocumented(doc, contractName);
+
+ expect(doc.ERC20).to.not.be.undefined();
+ expect(doc.ERC20.constructors).to.not.be.undefined();
+ expect(doc.ERC20.methods).to.not.be.undefined();
+ 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);
+
+ expect(doc.ERC20Basic).to.not.be.undefined();
+ expect(doc.ERC20Basic.constructors).to.not.be.undefined();
+ expect(doc.ERC20Basic.methods).to.not.be.undefined();
+ 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[contractName].methods) {
+ if (method.name === 'addAuthorizedAddress') {
+ addAuthorizedAddressMethod = method;
+ }
+ }
+ expect(
+ addAuthorizedAddressMethod,
+ `method addAuthorizedAddress not found in ${JSON.stringify(doc[contractName].methods)}`,
+ ).to.not.be.undefined();
+}
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"]
+}