diff options
Diffstat (limited to 'packages/sol-doc')
-rw-r--r-- | packages/sol-doc/CHANGELOG.json | 9 | ||||
-rw-r--r-- | packages/sol-doc/CHANGELOG.md | 4 | ||||
-rw-r--r-- | packages/sol-doc/package.json | 4 | ||||
-rw-r--r-- | packages/sol-doc/src/cli.ts | 17 | ||||
-rw-r--r-- | packages/sol-doc/src/index.ts | 2 | ||||
-rw-r--r-- | packages/sol-doc/src/sol_doc.ts | 500 | ||||
-rw-r--r-- | packages/sol-doc/src/solidity_doc_generator.ts | 306 | ||||
-rw-r--r-- | packages/sol-doc/test/fixtures/contracts/StructParamAndReturn.sol | 18 | ||||
-rw-r--r-- | packages/sol-doc/test/solidity_doc_generator_test.ts | 50 |
9 files changed, 593 insertions, 317 deletions
diff --git a/packages/sol-doc/CHANGELOG.json b/packages/sol-doc/CHANGELOG.json index 77f90ff7a..9d3f4bcde 100644 --- a/packages/sol-doc/CHANGELOG.json +++ b/packages/sol-doc/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "timestamp": 1538475601, + "version": "1.0.2", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { "timestamp": 1538157789, "version": "1.0.1", "changes": [ diff --git a/packages/sol-doc/CHANGELOG.md b/packages/sol-doc/CHANGELOG.md index 9ff92821e..9f16fb108 100644 --- a/packages/sol-doc/CHANGELOG.md +++ b/packages/sol-doc/CHANGELOG.md @@ -5,6 +5,10 @@ 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 diff --git a/packages/sol-doc/package.json b/packages/sol-doc/package.json index 2f8525ff4..0b0c7c963 100644 --- a/packages/sol-doc/package.json +++ b/packages/sol-doc/package.json @@ -6,6 +6,7 @@ "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", @@ -13,7 +14,7 @@ "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 $(cd protocol; ls -C1 */*.sol */interfaces/*.sol) ) > v2.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/" }, @@ -28,6 +29,7 @@ "@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" }, diff --git a/packages/sol-doc/src/cli.ts b/packages/sol-doc/src/cli.ts index eb0f00bf6..a1847e868 100644 --- a/packages/sol-doc/src/cli.ts +++ b/packages/sol-doc/src/cli.ts @@ -3,7 +3,7 @@ import * as yargs from 'yargs'; import { logUtils } from '@0xproject/utils'; -import { generateSolDocAsync } from './solidity_doc_generator'; +import { SolDoc } from './sol_doc'; const JSON_TAB_WIDTH = 4; @@ -20,7 +20,20 @@ const JSON_TAB_WIDTH = 4; .demandOption('contracts-dir') .array('contracts') .help().argv; - const doc = await generateSolDocAsync(argv.contractsDir, argv.contracts); + // 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); diff --git a/packages/sol-doc/src/index.ts b/packages/sol-doc/src/index.ts index 03f3c9de6..521668cc8 100644 --- a/packages/sol-doc/src/index.ts +++ b/packages/sol-doc/src/index.ts @@ -1 +1 @@ -export { generateSolDocAsync } from './solidity_doc_generator'; +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/src/solidity_doc_generator.ts b/packages/sol-doc/src/solidity_doc_generator.ts deleted file mode 100644 index 5ddf001a6..000000000 --- a/packages/sol-doc/src/solidity_doc_generator.ts +++ /dev/null @@ -1,306 +0,0 @@ -import * as path from 'path'; - -import * as _ from 'lodash'; - -import { - AbiDefinition, - ConstructorAbi, - DataItem, - DevdocOutput, - EventAbi, - EventParameter, - 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 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. - */ -export async function generateSolDocAsync( - contractsDir: string, - contractsToDocument?: string[], -): Promise<DocAgnosticFormat> { - const docWithDependencies: DocAgnosticFormat = {}; - const compilerOptions = _makeCompilerOptions(contractsDir, contractsToDocument); - 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'); - } - docWithDependencies[contractName] = _genDocSection(compiledContract, contractName); - } - } - } - - 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]; - } - } - - return doc; -} - -function _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; -} - -function _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(_genConstructorDoc(contractName, 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': - case 'fallback': - docSection.methods.push(_genMethodDoc(abiDefinition, compiledContract.devdoc)); - break; - default: - throw new Error( - `unknown and unsupported AbiDefinition type '${(abiDefinition as AbiDefinition).type}'`, - ); - } - } - - return docSection; -} - -function _genConstructorDoc( - contractName: string, - abiDefinition: ConstructorAbi, - devdocIfExists: DevdocOutput | undefined, -): SolidityMethod { - const { parameters, methodSignature } = _genMethodParamsDoc('', abiDefinition.inputs, devdocIfExists); - - const comment = _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; -} - -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 returnComment = - _.isUndefined(devdocIfExists) || _.isUndefined(devdocIfExists.methods[methodSignature]) - ? undefined - : devdocIfExists.methods[methodSignature].return; - - const isConstant = abiDefinition.type === 'fallback' ? true : abiDefinition.constant; - - const methodDoc: SolidityMethod = { - isConstructor: false, - name, - callPath: '', - parameters, - returnType, - returnComment, - 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: EventParameter[], 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: arg.indexed, - name, - type, - }; - - eventArgsDoc.push(eventArgDoc); - } - return eventArgsDoc; -} - -/** - * Extract documentation for each method parameter from @param params. - */ -function _genMethodParamsDoc( - name: string, - abiParams: DataItem[], - devdocIfExists: DevdocOutput | undefined, -): { parameters: Parameter[]; methodSignature: string } { - const parameters: Parameter[] = []; - 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); - } - - const methodSignature = `${name}(${abiParams - .map(abiParam => { - return abiParam.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 }; -} - -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.type, typeDocType: TypeDocTypes.Intrinsic }); - } - } else if (outputs.length === 1) { - methodReturnTypeDoc.typeDocType = TypeDocTypes.Intrinsic; - methodReturnTypeDoc.name = outputs[0].type; - } - return methodReturnTypeDoc; -} 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/solidity_doc_generator_test.ts b/packages/sol-doc/test/solidity_doc_generator_test.ts index c780d3d31..f166fb143 100644 --- a/packages/sol-doc/test/solidity_doc_generator_test.ts +++ b/packages/sol-doc/test/solidity_doc_generator_test.ts @@ -5,16 +5,17 @@ import 'mocha'; import { DocAgnosticFormat, Event, SolidityMethod } from '@0xproject/types'; -import { generateSolDocAsync } from '../src/solidity_doc_generator'; +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 generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [ + const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [ 'TokenTransferProxyNoDevdoc', ]); expect(doc).to.not.be.undefined(); @@ -22,8 +23,8 @@ describe('#SolidityDocGenerator', () => { verifyTokenTransferProxyABIIsDocumented(doc, 'TokenTransferProxyNoDevdoc'); }); const docPromises: Array<Promise<DocAgnosticFormat>> = [ - generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`), - generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, []), + 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 () => { @@ -48,7 +49,7 @@ describe('#SolidityDocGenerator', () => { }); }); it('should generate a doc object that matches the TokenTransferProxy fixture', async () => { - const doc: DocAgnosticFormat = await generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [ + const doc: DocAgnosticFormat = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [ 'TokenTransferProxy', ]); verifyTokenTransferProxyABIIsDocumented(doc, 'TokenTransferProxy'); @@ -56,7 +57,7 @@ describe('#SolidityDocGenerator', () => { describe('when processing all the permutations of devdoc stuff that we use in our contracts', () => { let doc: DocAgnosticFormat; before(async () => { - doc = await generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, ['NatspecEverything']); + doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, ['NatspecEverything']); expect(doc).to.not.be.undefined(); expect(doc.NatspecEverything).to.not.be.undefined(); }); @@ -159,7 +160,9 @@ describe('#SolidityDocGenerator', () => { }); }); it('should document a method that returns multiple values', async () => { - const doc = await generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, ['MultipleReturnValues']); + 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; @@ -178,6 +181,39 @@ describe('#SolidityDocGenerator', () => { } 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 { |