* `@0x/sol-doc`: New doc generator. * `@0x/sol-compiler`: Be more tolerant of AST-only compilation targets. * `@0x/contracts-exchange`: Add more devdoc comments. `@0x/contracts-exchange-libs`: Add more devdoc comments. * `@0x/sol-doc`: Update package script. * `@0x/sol-doc`: Remove unused files and update package scripts to be easier to configure. * Add more devdocs to contracts. * `@0x/sol-doc`: Remove doc artifacts. * `@0x/sol-doc`: Add `.gitignore` and `.npmignore`. * `@0x/contracts-exchange`: Fix compilation errors. * Fix more broken contracts. * `@0x/contracts-erc20-bridge-sampler`: Fix failing tests. * `@0x/contracts-asset-proxy`: Remove accidentally introduced hackathion file (lol). * `@0x/sol-doc`: Prevent some inherited contracts from being included in docs unintentionally. * `@0x/sol-doc`: Rename test file. * `@0x/contracts-exchange`: Update `orderEpoch` devdoc. * `@0x/sol-doc`: Tweak event and function docs. * Update CODEOWNERS. * `@0x/sol-doc` Tweak function md generation. * `@0x/sol-doc`: add `transformDocs()` tests. * `@0x/sol-doc`: add `extract_docs` tests. * `@0x/sol-doc` Fix linter errors. * `@0x/contracts-erc20-bridge-sampler`: Fix broken `ERC20BridgeSampler.sol` compile. * `@0x/sol-doc` Fix mismatched `dev-utils` dep version. * `@0x/sol-doc`: Add `gen_md` tests. * `@0x/sol-doc`: Remove `fs.promises` calls. * `@0x/sol-doc`: Fix linter errors. * `@0x/sol-doc`: Export all relevant types and functions. Co-authored-by: Lawrence Forman <me@merklejerk.com>
515 lines
20 KiB
TypeScript
515 lines
20 KiB
TypeScript
import { chaiSetup } from '@0x/dev-utils';
|
|
import { expect } from 'chai';
|
|
import * as _ from 'lodash';
|
|
import * as path from 'path';
|
|
|
|
import { extractDocsAsync, MethodDocs, SolidityDocs, StorageLocation, Visibility } from '../src/extract_docs';
|
|
|
|
chaiSetup.configure();
|
|
|
|
// tslint:disable: custom-no-magic-numbers
|
|
describe('extractDocsAsync()', () => {
|
|
const INTERFACE_CONTRACT = 'InterfaceContract';
|
|
const TEST_CONTRACT = 'TestContract';
|
|
const BASE_CONTRACT = 'BaseContract';
|
|
const LIBRARY_CONTRACT = 'LibraryContract';
|
|
const INPUT_CONTRACTS = [TEST_CONTRACT, BASE_CONTRACT, LIBRARY_CONTRACT, INTERFACE_CONTRACT];
|
|
const INPUT_FILE_PATHS = INPUT_CONTRACTS.map(f => path.resolve(__dirname, '../../test/inputs', `${f}.sol`));
|
|
|
|
let docs: SolidityDocs;
|
|
|
|
function createDocString(itemName: string): string {
|
|
return `Documentation for \`${itemName}\`.`;
|
|
}
|
|
|
|
before(async () => {
|
|
docs = await extractDocsAsync(_.shuffle(INPUT_FILE_PATHS));
|
|
});
|
|
|
|
describe('contracts', () => {
|
|
it('extracts all contracts with docs', async () => {
|
|
const contractLines: { [name: string]: number } = {
|
|
[TEST_CONTRACT]: 10,
|
|
[BASE_CONTRACT]: 9,
|
|
[INTERFACE_CONTRACT]: 4,
|
|
[LIBRARY_CONTRACT]: 5,
|
|
};
|
|
const NO_DOCS = [INTERFACE_CONTRACT];
|
|
for (const contract of INPUT_CONTRACTS) {
|
|
const cd = docs.contracts[contract];
|
|
expect(cd).to.exist('');
|
|
if (NO_DOCS.includes(contract)) {
|
|
expect(cd.doc).to.eq('');
|
|
} else {
|
|
expect(cd.doc).to.eq(createDocString(contract));
|
|
}
|
|
expect(cd.line, `${contract}.line`).to.eq(contractLines[contract]);
|
|
}
|
|
});
|
|
|
|
it('extracts contract inheritance', async () => {
|
|
const contractInherits: { [name: string]: string[] } = {
|
|
[TEST_CONTRACT]: [BASE_CONTRACT, INTERFACE_CONTRACT],
|
|
[BASE_CONTRACT]: [],
|
|
[INTERFACE_CONTRACT]: [],
|
|
[LIBRARY_CONTRACT]: [],
|
|
};
|
|
for (const contract of INPUT_CONTRACTS) {
|
|
const cd = docs.contracts[contract];
|
|
expect(cd.inherits).to.deep.eq(contractInherits[contract]);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('methods', () => {
|
|
interface ExpectedMethodProps {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
visibility: Visibility;
|
|
params?: {
|
|
[name: string]: {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
type: string;
|
|
storage?: StorageLocation;
|
|
};
|
|
};
|
|
returns?: {
|
|
[name: string]: {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
type: string;
|
|
storage?: StorageLocation;
|
|
};
|
|
};
|
|
}
|
|
|
|
function assertMethodDocs(fullMethodName: string, props: ExpectedMethodProps): void {
|
|
const [contractName, methodName] = fullMethodName.split('.');
|
|
const m = docs.contracts[contractName].methods.find(_m => _m.name === methodName) as MethodDocs;
|
|
{
|
|
const doc = props.noDoc ? '' : createDocString(methodName);
|
|
expect(m).to.exist('');
|
|
expect(m.visibility).to.eq(props.visibility);
|
|
expect(m.contract).to.eq(contractName);
|
|
expect(m.doc).to.eq(doc);
|
|
}
|
|
const params = props.params || {};
|
|
expect(Object.keys(m.parameters), 'number of parameters').to.be.length(Object.keys(params).length);
|
|
for (const [paramName, paramDoc] of Object.entries(params)) {
|
|
const actualParam = m.parameters[paramName];
|
|
const doc = paramDoc.noDoc ? '' : createDocString(paramName);
|
|
const storage = paramDoc.storage === undefined ? StorageLocation.Default : paramDoc.storage;
|
|
expect(actualParam).to.exist('');
|
|
expect(actualParam.doc).to.eq(doc);
|
|
expect(actualParam.line).to.eq(paramDoc.line);
|
|
expect(actualParam.storageLocation).to.eq(storage);
|
|
expect(actualParam.type).to.eq(paramDoc.type);
|
|
}
|
|
const returns = props.returns || {};
|
|
expect(Object.keys(m.returns), 'number of returns').to.be.length(Object.keys(returns).length);
|
|
for (const [returnName, returnDoc] of Object.entries(returns)) {
|
|
const actualReturn = m.returns[returnName];
|
|
const doc = returnDoc.noDoc ? '' : createDocString(returnName);
|
|
const storage = returnDoc.storage === undefined ? StorageLocation.Default : returnDoc.storage;
|
|
expect(actualReturn).to.exist('');
|
|
expect(actualReturn.doc).to.eq(doc);
|
|
expect(actualReturn.line).to.eq(returnDoc.line);
|
|
expect(actualReturn.storageLocation).to.eq(storage);
|
|
expect(actualReturn.type).to.eq(returnDoc.type);
|
|
}
|
|
}
|
|
|
|
describe('`TestContract`', () => {
|
|
it('`testContractMethod1`', () => {
|
|
assertMethodDocs('TestContract.testContractMethod1', {
|
|
line: 15,
|
|
visibility: Visibility.Public,
|
|
});
|
|
});
|
|
|
|
it('`testContractMethod2`', () => {
|
|
assertMethodDocs('TestContract.testContractMethod2', {
|
|
line: 15,
|
|
visibility: Visibility.Internal,
|
|
params: {
|
|
p1: {
|
|
line: 24,
|
|
type: 'address',
|
|
},
|
|
p2: {
|
|
line: 25,
|
|
type: 'uint256',
|
|
},
|
|
p3: {
|
|
line: 26,
|
|
type: 'LibraryContract.LibraryContractEnum',
|
|
},
|
|
},
|
|
returns: {
|
|
r1: {
|
|
line: 29,
|
|
type: 'int32',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('`testContractMethod3`', () => {
|
|
assertMethodDocs('TestContract.testContractMethod3', {
|
|
line: 37,
|
|
visibility: Visibility.External,
|
|
params: {
|
|
p1: {
|
|
line: 37,
|
|
type: 'InterfaceContract.InterfaceStruct',
|
|
storage: StorageLocation.CallData,
|
|
},
|
|
},
|
|
returns: {
|
|
r1: {
|
|
line: 39,
|
|
type: 'bytes32[][]',
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('`testContractMethod4`', () => {
|
|
assertMethodDocs('TestContract.testContractMethod4', {
|
|
line: 45,
|
|
visibility: Visibility.Private,
|
|
params: {
|
|
p1: {
|
|
line: 46,
|
|
type: 'LibraryContract.LibraryStruct[]',
|
|
noDoc: true,
|
|
storage: StorageLocation.Storage,
|
|
},
|
|
p2: {
|
|
line: 47,
|
|
type: 'InterfaceContract.InterfaceStruct[]',
|
|
noDoc: true,
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
p3: {
|
|
line: 48,
|
|
type: 'bytes[]',
|
|
noDoc: true,
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
},
|
|
returns: {
|
|
r1: {
|
|
line: 51,
|
|
type: 'bytes',
|
|
noDoc: true,
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
r2: {
|
|
line: 51,
|
|
type: 'bytes',
|
|
noDoc: true,
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('`BaseContract`', () => {
|
|
it('`baseContractMethod1`', () => {
|
|
assertMethodDocs('BaseContract.baseContractMethod1', {
|
|
line: 36,
|
|
visibility: Visibility.Internal,
|
|
params: {
|
|
p1: {
|
|
line: 39,
|
|
type: 'bytes',
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
p2: {
|
|
line: 39,
|
|
type: 'bytes32',
|
|
},
|
|
},
|
|
returns: {
|
|
'0': {
|
|
line: 41,
|
|
type: 'InterfaceContract.InterfaceStruct',
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('`baseContractField1`', () => {
|
|
assertMethodDocs('BaseContract.baseContractField1', {
|
|
line: 26,
|
|
visibility: Visibility.External,
|
|
params: {
|
|
'0': {
|
|
line: 26,
|
|
type: 'bytes32',
|
|
},
|
|
'1': {
|
|
line: 26,
|
|
type: 'address',
|
|
},
|
|
},
|
|
returns: {
|
|
'0': {
|
|
line: 26,
|
|
type: 'InterfaceContract.InterfaceStruct',
|
|
storage: StorageLocation.Memory,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('`baseContractField2`', () => {
|
|
assertMethodDocs('BaseContract.baseContractField2', {
|
|
line: 30,
|
|
visibility: Visibility.External,
|
|
params: {
|
|
'0': {
|
|
line: 30,
|
|
type: 'uint256',
|
|
},
|
|
},
|
|
returns: {
|
|
'0': {
|
|
noDoc: true,
|
|
line: 30,
|
|
type: 'bytes32',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('`baseContractField3`', () => {
|
|
// This field is private so no method should exist for it.
|
|
expect(docs.contracts.TestContract.events.find(e => e.name === 'baseContractField3')).to.eq(undefined);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('events', () => {
|
|
interface ExpectedEventProps {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
params?: {
|
|
[name: string]: {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
type: string;
|
|
indexed?: boolean;
|
|
};
|
|
};
|
|
}
|
|
|
|
function assertEventDocs(fullEventName: string, props: ExpectedEventProps): void {
|
|
const [contractName, eventName] = fullEventName.split('.');
|
|
const e = docs.contracts[contractName].events.find(_e => _e.name === eventName) as MethodDocs;
|
|
{
|
|
const doc = props.noDoc ? '' : createDocString(eventName);
|
|
expect(e).to.exist('');
|
|
expect(e.contract).to.eq(contractName);
|
|
expect(e.doc).to.eq(doc);
|
|
}
|
|
const params = props.params || {};
|
|
expect(Object.keys(e.parameters), 'number of parameters').to.be.length(Object.keys(params).length);
|
|
for (const [paramName, paramDoc] of Object.entries(params)) {
|
|
const actualParam = e.parameters[paramName];
|
|
const doc = paramDoc.noDoc ? '' : createDocString(paramName);
|
|
const isIndexed = paramDoc.indexed === undefined ? false : paramDoc.indexed;
|
|
expect(actualParam).to.exist('');
|
|
expect(actualParam.doc).to.eq(doc);
|
|
expect(actualParam.line).to.eq(paramDoc.line);
|
|
expect(actualParam.indexed).to.eq(isIndexed);
|
|
expect(actualParam.type).to.eq(paramDoc.type);
|
|
}
|
|
}
|
|
|
|
describe('`BaseContract`', () => {
|
|
it('`BaseContractEvent1`', () => {
|
|
assertEventDocs('BaseContract.BaseContractEvent1', {
|
|
line: 14,
|
|
params: {
|
|
p1: {
|
|
line: 14,
|
|
type: 'address',
|
|
indexed: true,
|
|
},
|
|
p2: {
|
|
line: 14,
|
|
type: 'InterfaceContract.InterfaceStruct',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('`BaseContractEvent2`', () => {
|
|
assertEventDocs('BaseContract.BaseContractEvent2', {
|
|
line: 16,
|
|
params: {
|
|
p1: {
|
|
line: 17,
|
|
type: 'uint256',
|
|
noDoc: true,
|
|
},
|
|
p2: {
|
|
line: 18,
|
|
type: 'uint256',
|
|
indexed: true,
|
|
noDoc: true,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('enums', () => {
|
|
interface ExpectedEnumProps {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
values?: {
|
|
[name: string]: {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
value: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
function assertEnumDocs(fullEnumName: string, props: ExpectedEnumProps): void {
|
|
const [contractName, enumName] = fullEnumName.split('.');
|
|
const e = docs.contracts[contractName].enums[`${contractName}.${enumName}`];
|
|
{
|
|
const doc = props.noDoc ? '' : createDocString(enumName);
|
|
expect(e).to.exist('');
|
|
expect(e.contract).to.eq(contractName);
|
|
expect(e.doc).to.eq(doc);
|
|
}
|
|
const values = props.values || {};
|
|
expect(Object.keys(e.values), 'number of values').to.be.length(Object.keys(values).length);
|
|
for (const [valueName, valueDoc] of Object.entries(values)) {
|
|
const actualValue = e.values[valueName];
|
|
const doc = valueDoc.noDoc ? '' : createDocString(valueName);
|
|
expect(actualValue).to.exist('');
|
|
expect(actualValue.doc).to.eq(doc);
|
|
expect(actualValue.line).to.eq(valueDoc.line);
|
|
expect(actualValue.value).to.eq(valueDoc.value);
|
|
}
|
|
}
|
|
|
|
describe('`LibraryContract`', () => {
|
|
it('`LibraryContractEnum`', () => {
|
|
assertEnumDocs('LibraryContract.LibraryContractEnum', {
|
|
line: 9,
|
|
values: {
|
|
EnumMember1: {
|
|
line: 10,
|
|
value: 0,
|
|
},
|
|
EnumMember2: {
|
|
line: 11,
|
|
value: 1,
|
|
},
|
|
EnumMember3: {
|
|
line: 13,
|
|
value: 2,
|
|
},
|
|
EnumMember4: {
|
|
noDoc: true,
|
|
line: 14,
|
|
value: 3,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('structs', () => {
|
|
interface ExpectedStructProps {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
fields?: {
|
|
[name: string]: {
|
|
noDoc?: boolean;
|
|
line: number;
|
|
type: string;
|
|
order: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
function assertStructDocs(fullStructName: string, props: ExpectedStructProps): void {
|
|
const [contractName, structName] = fullStructName.split('.');
|
|
const s = docs.contracts[contractName].structs[`${contractName}.${structName}`];
|
|
{
|
|
const doc = props.noDoc ? '' : createDocString(structName);
|
|
expect(s).to.exist('');
|
|
expect(s.contract).to.eq(contractName);
|
|
expect(s.doc).to.eq(doc);
|
|
}
|
|
const fields = props.fields || {};
|
|
expect(Object.keys(s.fields), 'number of fields').to.be.length(Object.keys(fields).length);
|
|
for (const [fieldName, fieldDoc] of Object.entries(fields)) {
|
|
const actualField = s.fields[fieldName];
|
|
const doc = fieldDoc.noDoc ? '' : createDocString(fieldName);
|
|
expect(actualField).to.exist('');
|
|
expect(actualField.doc).to.eq(doc);
|
|
expect(actualField.line).to.eq(fieldDoc.line);
|
|
expect(actualField.type).to.eq(fieldDoc.type);
|
|
expect(actualField.storageLocation).to.eq(StorageLocation.Default);
|
|
expect(actualField.indexed).to.eq(false);
|
|
}
|
|
}
|
|
|
|
describe('`LibraryContract`', () => {
|
|
it('`LibraryStruct`', () => {
|
|
assertStructDocs('LibraryContract.LibraryStruct', {
|
|
line: 19,
|
|
fields: {
|
|
structField: {
|
|
line: 20,
|
|
type: 'mapping(bytes32 => address)',
|
|
order: 0,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('`InterfaceContract`', () => {
|
|
it('`InterfaceStruct`', () => {
|
|
assertStructDocs('InterfaceContract.InterfaceStruct', {
|
|
line: 9,
|
|
fields: {
|
|
structField1: {
|
|
line: 9,
|
|
type: 'address',
|
|
order: 0,
|
|
},
|
|
structField2: {
|
|
line: 10,
|
|
type: 'uint256',
|
|
order: 1,
|
|
},
|
|
structField3: {
|
|
line: 12,
|
|
type: 'bytes32',
|
|
order: 2,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
// tslint:disable: max-file-line-count
|