aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-doc/src/solidity_doc_generator.ts
blob: 240fbbd7562992b03c7b2de6e0d311f74523d080 (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
import * as _ from 'lodash';

import {
    ConstructorAbi,
    DataItem,
    DevdocOutput,
    EventAbi,
    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
 * the types that are used as input to documentation generation tools.
 * @param contractsToCompile 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(
    contractsToCompile: string[],
    contractsDir: string,
): Promise<DocAgnosticFormat> {
    const doc: DocAgnosticFormat = {};

    const compilerOptions = _makeCompilerOptions(contractsToCompile, contractsDir);
    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');
                }
                doc[contractName] = _genDocSection(compiledContract);
            }
        }
    }

    return doc;
}

function _makeCompilerOptions(contractsToCompile: string[], contractsDir: string): CompilerOptions {
    const compilerOptions: CompilerOptions = {
        contractsDir,
        contracts: '*',
        compilerSettings: {
            outputSelection: {
                ['*']: {
                    ['*']: ['abi', 'devdoc'],
                },
            },
        },
    };

    const shouldOverrideCatchAllContractsConfig = !_.isUndefined(contractsToCompile);
    if (shouldOverrideCatchAllContractsConfig) {
        compilerOptions.contracts = contractsToCompile;
    }

    return compilerOptions;
}

function _genDocSection(compiledContract: StandardContractOutput): 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(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, compiledContract.devdoc));
                break;
            default:
                throw new Error(`unknown and unsupported AbiDefinition type '${abiDefinition.type}'`);
        }
    }

    return docSection;
}

function _genConstructorDoc(abiDefinition: ConstructorAbi, devdocIfExists: DevdocOutput | undefined): SolidityMethod {
    const { parameters, methodSignature } = _genMethodParamsDoc(
        '', // TODO: update depending on how constructors are keyed in devdoc
        abiDefinition.inputs,
        devdocIfExists,
    );

    let comment;
    // TODO: use methodSignature as the key to abiEntry.devdoc.methods, and
    // from that object extract the "details" (comment) property
    comment = `something from devdoc, using ${methodSignature} to find it`;

    const constructorDoc: SolidityMethod = {
        isConstructor: true,
        name: '', // sad we have to specify this
        callPath: '', // TODO: wtf is this?
        parameters,
        returnType: { name: '', typeDocType: TypeDocTypes.Intrinsic }, // sad we have to specify this
        isConstant: false, // constructors are non-const by their nature, right?
        isPayable: abiDefinition.payable,
        comment,
    };

    return constructorDoc;
}

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);

    let comment;
    // TODO: use methodSignature as the key to abiEntry.devdoc.methods, and
    // from that object extract the "details" (comment) property
    comment = 'something from devdoc';

    const returnType =
        abiDefinition.type === 'fallback'
            ? { name: '', typeDocType: TypeDocTypes.Intrinsic }
            : _genMethodReturnTypeDoc(abiDefinition.outputs, methodSignature, devdocIfExists);

    const isConstant = abiDefinition.type === 'fallback' ? true : abiDefinition.constant;
    // TODO: determine whether fallback functions are always constant, as coded just above

    const methodDoc: SolidityMethod = {
        isConstructor: false,
        name,
        callPath: '', // TODO: wtf is this?
        parameters,
        returnType,
        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: DataItem[], 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: false, // TODO: wtf is this?
            name,
            type,
        };

        eventArgsDoc.push(eventArgDoc);
    }
    return eventArgsDoc;
}

/**
 * Extract documentation for each method paramater from @param params.
 * TODO: Then, use @param name, along with the types of the method
 * parameters, to form a method signature.  That signature is the key to
 * the method documentation held in @param devdocIfExists.
 */
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: '', // TODO: get from devdoc. see comment below.
            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);
    }
    // TODO: use methodSignature as the key to abiEntry.devdoc.methods, and
    // from that object extract the "details" (comment) property
    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.name, typeDocType: TypeDocTypes.Intrinsic });
        }
    } else if (outputs.length === 1) {
        methodReturnTypeDoc.typeDocType = TypeDocTypes.Intrinsic;
        methodReturnTypeDoc.name = outputs[0].name;
    }
    return methodReturnTypeDoc;
}