aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-doc/src/solidity_doc_generator.ts
blob: d2fb5b08397e365f0359359a19c27a00675633d9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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':
                docSection.methods.push(_genMethodDoc(abiDefinition as MethodAbi, compiledContract.devdoc));
                break;
            case 'fallback':
                docSection.methods.push(_genFallbackDoc(abiDefinition as FallbackAbi, 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 _genFallbackDoc(abiDefinition: FallbackAbi, devdocIfExists: DevdocOutput | undefined): SolidityMethod {
    const methodSignature = `${name}()`;
    const comment = _devdocMethodDetailsIfExist(methodSignature, devdocIfExists);

    const returnComment =
        _.isUndefined(devdocIfExists) || _.isUndefined(devdocIfExists.methods[methodSignature])
            ? undefined
            : devdocIfExists.methods[methodSignature].return;

    const methodDoc: SolidityMethod = {
        isConstructor: false,
        name: '',
        callPath: '',
        parameters: [],
        returnType: { name: 'void', typeDocType: TypeDocTypes.Intrinsic },
        returnComment,
        isConstant: true,
        isPayable: abiDefinition.payable,
        isFallback: true,
        comment,
    };
    return methodDoc;
}

function _genMethodDoc(abiDefinition: MethodAbi, devdocIfExists: DevdocOutput | undefined): SolidityMethod {
    const { parameters, methodSignature } = _genMethodParamsDoc(name, abiDefinition.inputs, devdocIfExists);
    const comment = _devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
    const returnType = _genMethodReturnTypeDoc(abiDefinition.outputs, methodSignature, devdocIfExists);
    const returnComment =
        _.isUndefined(devdocIfExists) || _.isUndefined(devdocIfExists.methods[methodSignature])
            ? undefined
            : devdocIfExists.methods[methodSignature].return;

    const methodDoc: SolidityMethod = {
        isConstructor: false,
        name: abiDefinition.name,
        callPath: '',
        parameters,
        returnType,
        returnComment,
        isConstant: abiDefinition.constant,
        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: '<No 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: 'void',
        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;
}