aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-doc/src
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/src
parent6deb027bdf4e57f8918fd2413f0fdc55311508d3 (diff)
parentf1ecb8c5cb28a0a7ca6f7ad2ff11194091df62a4 (diff)
downloaddexon-0x-contracts-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.gz
dexon-0x-contracts-343b922ec11a6108caaf3095e59be0e56d45ee4a.tar.zst
dexon-0x-contracts-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/src')
-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
3 files changed, 542 insertions, 0 deletions
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