aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-doc/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/sol-doc/src')
-rw-r--r--packages/sol-doc/src/cli.ts28
-rw-r--r--packages/sol-doc/src/index.ts1
-rw-r--r--packages/sol-doc/src/solidity_doc_generator.ts306
3 files changed, 335 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..eb0f00bf6
--- /dev/null
+++ b/packages/sol-doc/src/cli.ts
@@ -0,0 +1,28 @@
+import 'source-map-support/register';
+import * as yargs from 'yargs';
+
+import { logUtils } from '@0xproject/utils';
+
+import { generateSolDocAsync } from './solidity_doc_generator';
+
+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;
+ const doc = await generateSolDocAsync(argv.contractsDir, argv.contracts);
+ 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..03f3c9de6
--- /dev/null
+++ b/packages/sol-doc/src/index.ts
@@ -0,0 +1 @@
+export { generateSolDocAsync } from './solidity_doc_generator';
diff --git a/packages/sol-doc/src/solidity_doc_generator.ts b/packages/sol-doc/src/solidity_doc_generator.ts
new file mode 100644
index 000000000..5ddf001a6
--- /dev/null
+++ b/packages/sol-doc/src/solidity_doc_generator.ts
@@ -0,0 +1,306 @@
+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;
+}