Generate (complete) solidity docs (#2391)

* `@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>
This commit is contained in:
Lawrence Forman 2020-01-03 22:59:18 -05:00 committed by GitHub
parent 9d5724e1a0
commit b7b457b076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2758 additions and 1187 deletions

View File

@ -147,7 +147,7 @@ contract ERC20BridgeSampler is
); );
// The fillable amount is zero if the order is not fillable or if the // The fillable amount is zero if the order is not fillable or if the
// signature is invalid. // signature is invalid.
if (orderInfo.orderStatus != uint8(LibOrder.OrderStatus.FILLABLE) || if (orderInfo.orderStatus != LibOrder.OrderStatus.FILLABLE ||
!isValidSignature) { !isValidSignature) {
orderFillableTakerAssetAmounts[i] = 0; orderFillableTakerAssetAmounts[i] = 0;
} else { } else {

View File

@ -305,6 +305,8 @@ contract TestERC20BridgeSampler is
TestERC20BridgeSamplerEth2Dai public eth2Dai; TestERC20BridgeSamplerEth2Dai public eth2Dai;
TestERC20BridgeSamplerKyberNetwork public kyber; TestERC20BridgeSamplerKyberNetwork public kyber;
uint8 private constant MAX_ORDER_STATUS = uint8(LibOrder.OrderStatus.CANCELLED) + 1;
constructor() public { constructor() public {
uniswap = new TestERC20BridgeSamplerUniswapExchangeFactory(); uniswap = new TestERC20BridgeSamplerUniswapExchangeFactory();
eth2Dai = new TestERC20BridgeSamplerEth2Dai(); eth2Dai = new TestERC20BridgeSamplerEth2Dai();
@ -336,9 +338,8 @@ contract TestERC20BridgeSampler is
bytes32 orderHash = keccak256(abi.encode(order.salt)); bytes32 orderHash = keccak256(abi.encode(order.salt));
// Everything else is derived from the hash. // Everything else is derived from the hash.
orderInfo.orderHash = orderHash; orderInfo.orderHash = orderHash;
orderInfo.orderStatus = uint8(uint256(orderHash) % uint8(-1)); orderInfo.orderStatus = LibOrder.OrderStatus(uint256(orderHash) % MAX_ORDER_STATUS);
orderInfo.orderTakerAssetFilledAmount = orderInfo.orderTakerAssetFilledAmount = uint256(orderHash) % order.takerAssetAmount;
uint256(orderHash) % order.takerAssetAmount;
fillableTakerAssetAmount = fillableTakerAssetAmount =
order.takerAssetAmount - orderInfo.orderTakerAssetFilledAmount; order.takerAssetAmount - orderInfo.orderTakerAssetFilledAmount;
isValidSignature = uint256(orderHash) % 2 == 1; isValidSignature = uint256(orderHash) % 2 == 1;

View File

@ -7,9 +7,7 @@ import { ContractArtifact } from 'ethereum-types';
import * as ERC20BridgeSampler from '../generated-artifacts/ERC20BridgeSampler.json'; import * as ERC20BridgeSampler from '../generated-artifacts/ERC20BridgeSampler.json';
import * as IERC20BridgeSampler from '../generated-artifacts/IERC20BridgeSampler.json'; import * as IERC20BridgeSampler from '../generated-artifacts/IERC20BridgeSampler.json';
import * as IKyberNetwork from '../generated-artifacts/IKyberNetwork.json';
export const artifacts = { export const artifacts = {
ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact, ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact,
IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact, IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact,
IKyberNetwork: IKyberNetwork as ContractArtifact,
}; };

View File

@ -5,4 +5,3 @@
*/ */
export * from '../generated-wrappers/erc20_bridge_sampler'; export * from '../generated-wrappers/erc20_bridge_sampler';
export * from '../generated-wrappers/i_erc20_bridge_sampler'; export * from '../generated-wrappers/i_erc20_bridge_sampler';
export * from '../generated-wrappers/i_kyber_network';

View File

@ -5,7 +5,6 @@
"files": [ "files": [
"generated-artifacts/ERC20BridgeSampler.json", "generated-artifacts/ERC20BridgeSampler.json",
"generated-artifacts/IERC20BridgeSampler.json", "generated-artifacts/IERC20BridgeSampler.json",
"generated-artifacts/IKyberNetwork.json",
"test/generated-artifacts/ERC20BridgeSampler.json", "test/generated-artifacts/ERC20BridgeSampler.json",
"test/generated-artifacts/IDevUtils.json", "test/generated-artifacts/IDevUtils.json",
"test/generated-artifacts/IERC20BridgeSampler.json", "test/generated-artifacts/IERC20BridgeSampler.json",

View File

@ -29,9 +29,11 @@ contract LibEIP712ExchangeDomain {
// EIP712 Exchange Domain Version value // EIP712 Exchange Domain Version value
string constant internal _EIP712_EXCHANGE_DOMAIN_VERSION = "3.0.0"; string constant internal _EIP712_EXCHANGE_DOMAIN_VERSION = "3.0.0";
// Hash of the EIP712 Domain Separator data // solhint-disable var-name-mixedcase
// solhint-disable-next-line var-name-mixedcase /// @dev Hash of the EIP712 Domain Separator data
/// @return 0 Domain hash.
bytes32 public EIP712_EXCHANGE_DOMAIN_HASH; bytes32 public EIP712_EXCHANGE_DOMAIN_HASH;
// solhint-enable var-name-mixedcase
/// @param chainId Chain ID of the network this contract is deployed on. /// @param chainId Chain ID of the network this contract is deployed on.
/// @param verifyingContractAddressIfExists Address of the verifying contract (null if the address of this contract) /// @param verifyingContractAddressIfExists Address of the verifying contract (null if the address of this contract)

View File

@ -60,6 +60,7 @@ library LibOrder {
} }
// solhint-disable max-line-length // solhint-disable max-line-length
/// @dev Canonical order structure.
struct Order { struct Order {
address makerAddress; // Address that created the order. address makerAddress; // Address that created the order.
address takerAddress; // Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order. address takerAddress; // Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order.
@ -78,8 +79,9 @@ library LibOrder {
} }
// solhint-enable max-line-length // solhint-enable max-line-length
/// @dev Order information returned by `getOrderInfo()`.
struct OrderInfo { struct OrderInfo {
uint8 orderStatus; // Status that describes order's validity and fillability. OrderStatus orderStatus; // Status that describes order's validity and fillability.
bytes32 orderHash; // EIP712 typed data hash of the order (see LibOrder.getTypedDataHash). bytes32 orderHash; // EIP712 typed data hash of the order (see LibOrder.getTypedDataHash).
uint256 orderTakerAssetFilledAmount; // Amount of order that has already been filled. uint256 orderTakerAssetFilledAmount; // Amount of order that has already been filled.
} }

View File

@ -29,6 +29,7 @@ import "./MixinTransferSimulator.sol";
// MixinAssetProxyDispatcher, MixinExchangeCore, MixinSignatureValidator, // MixinAssetProxyDispatcher, MixinExchangeCore, MixinSignatureValidator,
// and MixinTransactions are all inherited via the other Mixins that are // and MixinTransactions are all inherited via the other Mixins that are
// used. // used.
/// @dev The 0x Exchange contract.
contract Exchange is contract Exchange is
LibEIP712ExchangeDomain, LibEIP712ExchangeDomain,
MixinMatchOrders, MixinMatchOrders,

View File

@ -62,11 +62,11 @@ contract MixinAssetProxyDispatcher is
/// @dev Gets an asset proxy. /// @dev Gets an asset proxy.
/// @param assetProxyId Id of the asset proxy. /// @param assetProxyId Id of the asset proxy.
/// @return The asset proxy registered to assetProxyId. Returns 0x0 if no proxy is registered. /// @return assetProxy The asset proxy address registered to assetProxyId. Returns 0x0 if no proxy is registered.
function getAssetProxy(bytes4 assetProxyId) function getAssetProxy(bytes4 assetProxyId)
external external
view view
returns (address) returns (address assetProxy)
{ {
return _assetProxies[assetProxyId]; return _assetProxies[assetProxyId];
} }

View File

@ -45,14 +45,21 @@ contract MixinExchangeCore is
using LibSafeMath for uint256; using LibSafeMath for uint256;
using LibBytes for bytes; using LibBytes for bytes;
// Mapping of orderHash => amount of takerAsset already bought by maker /// @dev Mapping of orderHash => amount of takerAsset already bought by maker
/// @param 0 Order hash.
/// @return 0 The amount of taker asset filled.
mapping (bytes32 => uint256) public filled; mapping (bytes32 => uint256) public filled;
// Mapping of orderHash => cancelled /// @dev Mapping of orderHash => cancelled
/// @param 0 Order hash.
/// @return 0 Whether the order was cancelled.
mapping (bytes32 => bool) public cancelled; mapping (bytes32 => bool) public cancelled;
// Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable /// @dev Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable
// Orders with specified senderAddress and with a salt less than their epoch are considered cancelled /// Orders with specified senderAddress and with a salt less than their epoch are considered cancelled
/// @param 0 Address of the order's maker.
/// @param 1 Address of the order's sender.
/// @return 0 Minimum valid order epoch.
mapping (address => mapping (address => uint256)) public orderEpoch; mapping (address => mapping (address => uint256)) public orderEpoch;
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch /// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
@ -94,7 +101,7 @@ contract MixinExchangeCore is
/// @param order Order struct containing order specifications. /// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell. /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker. /// @param signature Proof that order has been created by maker.
/// @return Amounts filled and fees paid by maker and taker. /// @return fillResults Amounts filled and fees paid by maker and taker.
function fillOrder( function fillOrder(
LibOrder.Order memory order, LibOrder.Order memory order,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
@ -125,7 +132,7 @@ contract MixinExchangeCore is
/// @dev Gets information about an order: status, hash, and amount filled. /// @dev Gets information about an order: status, hash, and amount filled.
/// @param order Order to gather information on. /// @param order Order to gather information on.
/// @return OrderInfo Information about the order and its state. /// @return orderInfo Information about the order and its state.
/// See LibOrder.OrderInfo for a complete description. /// See LibOrder.OrderInfo for a complete description.
function getOrderInfo(LibOrder.Order memory order) function getOrderInfo(LibOrder.Order memory order)
public public
@ -140,7 +147,7 @@ contract MixinExchangeCore is
// edge cases in the supporting infrastructure because they have // edge cases in the supporting infrastructure because they have
// an 'infinite' price when computed by a simple division. // an 'infinite' price when computed by a simple division.
if (order.makerAssetAmount == 0) { if (order.makerAssetAmount == 0) {
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.INVALID_MAKER_ASSET_AMOUNT); orderInfo.orderStatus = LibOrder.OrderStatus.INVALID_MAKER_ASSET_AMOUNT;
return orderInfo; return orderInfo;
} }
@ -149,35 +156,35 @@ contract MixinExchangeCore is
// Instead of distinguishing between unfilled and filled zero taker // Instead of distinguishing between unfilled and filled zero taker
// amount orders, we choose not to support them. // amount orders, we choose not to support them.
if (order.takerAssetAmount == 0) { if (order.takerAssetAmount == 0) {
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.INVALID_TAKER_ASSET_AMOUNT); orderInfo.orderStatus = LibOrder.OrderStatus.INVALID_TAKER_ASSET_AMOUNT;
return orderInfo; return orderInfo;
} }
// Validate order availability // Validate order availability
if (orderInfo.orderTakerAssetFilledAmount >= order.takerAssetAmount) { if (orderInfo.orderTakerAssetFilledAmount >= order.takerAssetAmount) {
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.FULLY_FILLED); orderInfo.orderStatus = LibOrder.OrderStatus.FULLY_FILLED;
return orderInfo; return orderInfo;
} }
// Validate order expiration // Validate order expiration
// solhint-disable-next-line not-rely-on-time // solhint-disable-next-line not-rely-on-time
if (block.timestamp >= order.expirationTimeSeconds) { if (block.timestamp >= order.expirationTimeSeconds) {
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.EXPIRED); orderInfo.orderStatus = LibOrder.OrderStatus.EXPIRED;
return orderInfo; return orderInfo;
} }
// Check if order has been cancelled // Check if order has been cancelled
if (cancelled[orderInfo.orderHash]) { if (cancelled[orderInfo.orderHash]) {
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.CANCELLED); orderInfo.orderStatus = LibOrder.OrderStatus.CANCELLED;
return orderInfo; return orderInfo;
} }
if (orderEpoch[order.makerAddress][order.senderAddress] > order.salt) { if (orderEpoch[order.makerAddress][order.senderAddress] > order.salt) {
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.CANCELLED); orderInfo.orderStatus = LibOrder.OrderStatus.CANCELLED;
return orderInfo; return orderInfo;
} }
// All other statuses are ruled out: order is Fillable // All other statuses are ruled out: order is Fillable
orderInfo.orderStatus = uint8(LibOrder.OrderStatus.FILLABLE); orderInfo.orderStatus = LibOrder.OrderStatus.FILLABLE;
return orderInfo; return orderInfo;
} }
@ -185,7 +192,7 @@ contract MixinExchangeCore is
/// @param order Order struct containing order specifications. /// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell. /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker. /// @param signature Proof that order has been created by maker.
/// @return Amounts filled and fees paid by maker and taker. /// @return fillResults Amounts filled and fees paid by maker and taker.
function _fillOrder( function _fillOrder(
LibOrder.Order memory order, LibOrder.Order memory order,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
@ -255,7 +262,7 @@ contract MixinExchangeCore is
_assertValidCancel(order, orderInfo); _assertValidCancel(order, orderInfo);
// Noop if order is already unfillable // Noop if order is already unfillable
if (orderInfo.orderStatus != uint8(LibOrder.OrderStatus.FILLABLE)) { if (orderInfo.orderStatus != LibOrder.OrderStatus.FILLABLE) {
return; return;
} }
@ -337,7 +344,7 @@ contract MixinExchangeCore is
view view
{ {
// An order can only be filled if its status is FILLABLE. // An order can only be filled if its status is FILLABLE.
if (orderInfo.orderStatus != uint8(LibOrder.OrderStatus.FILLABLE)) { if (orderInfo.orderStatus != LibOrder.OrderStatus.FILLABLE) {
LibRichErrors.rrevert(LibExchangeRichErrors.OrderStatusError( LibRichErrors.rrevert(LibExchangeRichErrors.OrderStatusError(
orderInfo.orderHash, orderInfo.orderHash,
LibOrder.OrderStatus(orderInfo.orderStatus) LibOrder.OrderStatus(orderInfo.orderStatus)

View File

@ -29,10 +29,12 @@ contract MixinProtocolFees is
IProtocolFees, IProtocolFees,
Ownable Ownable
{ {
// The protocol fee multiplier -- the owner can update this field. /// @dev The protocol fee multiplier -- the owner can update this field.
/// @return 0 Gas multplier.
uint256 public protocolFeeMultiplier; uint256 public protocolFeeMultiplier;
// The address of the registered protocolFeeCollector contract -- the owner can update this field. /// @dev The address of the registered protocolFeeCollector contract -- the owner can update this field.
/// @return 0 Contract to forward protocol fees to.
address public protocolFeeCollector; address public protocolFeeCollector;
/// @dev Allows the owner to update the protocol fee multiplier. /// @dev Allows the owner to update the protocol fee multiplier.

View File

@ -47,10 +47,16 @@ contract MixinSignatureValidator is
// bytes4(keccak256("isValidWalletSignature(bytes32,address,bytes)")) // bytes4(keccak256("isValidWalletSignature(bytes32,address,bytes)"))
bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381; bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381;
// Mapping of hash => signer => signed /// @dev Mapping of hash => signer => signed
/// @param 0 Order hash.
/// @param 1 Signer address.
/// @return 0 Whether the hash is presigned.
mapping (bytes32 => mapping (address => bool)) public preSigned; mapping (bytes32 => mapping (address => bool)) public preSigned;
// Mapping of signer => validator => approved /// @dev Mapping of signer => validator => approved
/// @param 0 Signer address.
/// @param 1 Signature validator address.
/// @return 0 Whether the validator is allowed to validate on behalf of the signer.
mapping (address => mapping (address => bool)) public allowedValidators; mapping (address => mapping (address => bool)) public allowedValidators;
/// @dev Approves a hash on-chain. /// @dev Approves a hash on-chain.

View File

@ -36,11 +36,14 @@ contract MixinTransactions is
{ {
using LibZeroExTransaction for LibZeroExTransaction.ZeroExTransaction; using LibZeroExTransaction for LibZeroExTransaction.ZeroExTransaction;
// Mapping of transaction hash => executed /// @dev Mapping of transaction hash => executed
// This prevents transactions from being executed more than once. /// This prevents transactions from being executed more than once.
/// @param 0 The transaction hash.
/// @return 0 Whether the transation was executed.
mapping (bytes32 => bool) public transactionsExecuted; mapping (bytes32 => bool) public transactionsExecuted;
// Address of current transaction signer /// @dev Address of current transaction signer.
/// @return 0 The address associated with the the current transaction.
address public currentContextAddress; address public currentContextAddress;
/// @dev Executes an Exchange method call in the context of signer. /// @dev Executes an Exchange method call in the context of signer.
@ -62,7 +65,7 @@ contract MixinTransactions is
/// @dev Executes a batch of Exchange method calls in the context of signer(s). /// @dev Executes a batch of Exchange method calls in the context of signer(s).
/// @param transactions Array of 0x transaction structures. /// @param transactions Array of 0x transaction structures.
/// @param signatures Array of proofs that transactions have been signed by signer(s). /// @param signatures Array of proofs that transactions have been signed by signer(s).
/// @return Array containing ABI encoded return data for each of the underlying Exchange function calls. /// @return returnData Array containing ABI encoded return data for each of the underlying Exchange function calls.
function batchExecuteTransactions( function batchExecuteTransactions(
LibZeroExTransaction.ZeroExTransaction[] memory transactions, LibZeroExTransaction.ZeroExTransaction[] memory transactions,
bytes[] memory signatures bytes[] memory signatures
@ -70,10 +73,10 @@ contract MixinTransactions is
public public
payable payable
disableRefundUntilEnd disableRefundUntilEnd
returns (bytes[] memory) returns (bytes[] memory returnData)
{ {
uint256 length = transactions.length; uint256 length = transactions.length;
bytes[] memory returnData = new bytes[](length); returnData = new bytes[](length);
for (uint256 i = 0; i != length; i++) { for (uint256 i = 0; i != length; i++) {
returnData[i] = _executeTransaction(transactions[i], signatures[i]); returnData[i] = _executeTransaction(transactions[i], signatures[i]);
} }
@ -117,7 +120,7 @@ contract MixinTransactions is
_setCurrentContextAddressIfRequired(signerAddress, address(0)); _setCurrentContextAddressIfRequired(signerAddress, address(0));
emit TransactionExecution(transactionHash); emit TransactionExecution(transactionHash);
return returnData; return returnData;
} }

View File

@ -36,10 +36,11 @@ contract MixinWrapperFunctions is
{ {
using LibSafeMath for uint256; using LibSafeMath for uint256;
/// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled. /// @dev Fills the input order. Reverts if exact `takerAssetFillAmount` not filled.
/// @param order Order struct containing order specifications. /// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell. /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker. /// @param signature Proof that order has been created by maker.
/// @return fillResults Amounts filled and fees paid.
function fillOrKillOrder( function fillOrKillOrder(
LibOrder.Order memory order, LibOrder.Order memory order,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
@ -62,7 +63,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders. /// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
/// @param signatures Proofs that orders have been created by makers. /// @param signatures Proofs that orders have been created by makers.
/// @return Array of amounts filled and fees paid by makers and taker. /// @return fillResults Array of amounts filled and fees paid by makers and taker.
function batchFillOrders( function batchFillOrders(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256[] memory takerAssetFillAmounts, uint256[] memory takerAssetFillAmounts,
@ -89,7 +90,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders. /// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
/// @param signatures Proofs that orders have been created by makers. /// @param signatures Proofs that orders have been created by makers.
/// @return Array of amounts filled and fees paid by makers and taker. /// @return fillResults Array of amounts filled and fees paid by makers and taker.
function batchFillOrKillOrders( function batchFillOrKillOrders(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256[] memory takerAssetFillAmounts, uint256[] memory takerAssetFillAmounts,
@ -116,7 +117,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders. /// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
/// @param signatures Proofs that orders have been created by makers. /// @param signatures Proofs that orders have been created by makers.
/// @return Array of amounts filled and fees paid by makers and taker. /// @return fillResults Array of amounts filled and fees paid by makers and taker.
function batchFillOrdersNoThrow( function batchFillOrdersNoThrow(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256[] memory takerAssetFillAmounts, uint256[] memory takerAssetFillAmounts,
@ -145,7 +146,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell. /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signatures Proofs that orders have been signed by makers. /// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker. /// @return fillResults Amounts filled and fees paid by makers and taker.
function marketSellOrdersNoThrow( function marketSellOrdersNoThrow(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
@ -186,7 +187,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param makerAssetFillAmount Desired amount of makerAsset to buy. /// @param makerAssetFillAmount Desired amount of makerAsset to buy.
/// @param signatures Proofs that orders have been signed by makers. /// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker. /// @return fillResults Amounts filled and fees paid by makers and taker.
function marketBuyOrdersNoThrow( function marketBuyOrdersNoThrow(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256 makerAssetFillAmount, uint256 makerAssetFillAmount,
@ -234,7 +235,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param takerAssetFillAmount Minimum amount of takerAsset to sell. /// @param takerAssetFillAmount Minimum amount of takerAsset to sell.
/// @param signatures Proofs that orders have been signed by makers. /// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker. /// @return fillResults Amounts filled and fees paid by makers and taker.
function marketSellOrdersFillOrKill( function marketSellOrdersFillOrKill(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
@ -259,7 +260,7 @@ contract MixinWrapperFunctions is
/// @param orders Array of order specifications. /// @param orders Array of order specifications.
/// @param makerAssetFillAmount Minimum amount of makerAsset to buy. /// @param makerAssetFillAmount Minimum amount of makerAsset to buy.
/// @param signatures Proofs that orders have been signed by makers. /// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker. /// @return fillResults Amounts filled and fees paid by makers and taker.
function marketBuyOrdersFillOrKill( function marketBuyOrdersFillOrKill(
LibOrder.Order[] memory orders, LibOrder.Order[] memory orders,
uint256 makerAssetFillAmount, uint256 makerAssetFillAmount,
@ -295,7 +296,7 @@ contract MixinWrapperFunctions is
/// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled. /// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled.
/// @param order Order struct containing order specifications. /// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell. /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker. /// @param fillResults ignature Proof that order has been created by maker.
function _fillOrKillOrder( function _fillOrKillOrder(
LibOrder.Order memory order, LibOrder.Order memory order,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
@ -324,7 +325,7 @@ contract MixinWrapperFunctions is
/// @param order Order struct containing order specifications. /// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell. /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker. /// @param signature Proof that order has been created by maker.
/// @return Amounts filled and fees paid by maker and taker. /// @return fillResults Amounts filled and fees paid by maker and taker.
function _fillOrderNoThrow( function _fillOrderNoThrow(
LibOrder.Order memory order, LibOrder.Order memory order,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,

View File

@ -30,7 +30,7 @@ import "../src/Exchange.sol";
contract TestWrapperFunctions is contract TestWrapperFunctions is
Exchange Exchange
{ {
uint8 internal constant MAX_ORDER_STATUS = uint8(LibOrder.OrderStatus.CANCELLED); LibOrder.OrderStatus internal constant MAX_ORDER_STATUS = LibOrder.OrderStatus.CANCELLED;
uint256 internal constant ALWAYS_FAILING_SALT = uint256(-1); uint256 internal constant ALWAYS_FAILING_SALT = uint256(-1);
string internal constant ALWAYS_FAILING_SALT_REVERT_REASON = "ALWAYS_FAILING_SALT"; string internal constant ALWAYS_FAILING_SALT_REVERT_REASON = "ALWAYS_FAILING_SALT";
@ -61,7 +61,7 @@ contract TestWrapperFunctions is
// Lower uint128 of `order.salt` is the `orderTakerAssetFilledAmount`. // Lower uint128 of `order.salt` is the `orderTakerAssetFilledAmount`.
orderInfo.orderTakerAssetFilledAmount = uint128(order.salt); orderInfo.orderTakerAssetFilledAmount = uint128(order.salt);
// High byte of `order.salt` is the `orderStatus`. // High byte of `order.salt` is the `orderStatus`.
orderInfo.orderStatus = uint8(order.salt >> 248) % (MAX_ORDER_STATUS + 1); orderInfo.orderStatus = LibOrder.OrderStatus(uint8(order.salt >> 248) % (uint8(MAX_ORDER_STATUS) + 1));
orderInfo.orderHash = order.getTypedDataHash(EIP712_EXCHANGE_DOMAIN_HASH); orderInfo.orderHash = order.getTypedDataHash(EIP712_EXCHANGE_DOMAIN_HASH);
} }

View File

@ -14,6 +14,7 @@
"*": { "*": {
"*": [ "*": [
"abi", "abi",
"devdoc",
"evm.bytecode.object", "evm.bytecode.object",
"evm.bytecode.sourceMap", "evm.bytecode.sourceMap",
"evm.deployedBytecode.object", "evm.deployedBytecode.object",

View File

@ -26,6 +26,7 @@ import "./interfaces/IStorageInit.sol";
import "./interfaces/IStakingProxy.sol"; import "./interfaces/IStakingProxy.sol";
/// #dev The 0x Staking contract.
contract StakingProxy is contract StakingProxy is
IStakingProxy, IStakingProxy,
MixinStorage, MixinStorage,

View File

@ -51,19 +51,23 @@ contract MixinStorage is
// tracking Pool Id, a unique identifier for each staking pool. // tracking Pool Id, a unique identifier for each staking pool.
bytes32 public lastPoolId; bytes32 public lastPoolId;
// mapping from Maker Address to Pool Id of maker /// @dev Mapping from Maker Address to pool Id of maker
/// @param 0 Maker address.
/// @return 0 The pool ID.
mapping (address => bytes32) public poolIdByMaker; mapping (address => bytes32) public poolIdByMaker;
// mapping from Pool Id to Pool // mapping from Pool Id to Pool
mapping (bytes32 => IStructs.Pool) internal _poolById; mapping (bytes32 => IStructs.Pool) internal _poolById;
// mapping from PoolId to balance of members /// @dev mapping from pool ID to reward balance of members
/// @param 0 Pool ID.
/// @return 0 The total reward balance of members in this pool.
mapping (bytes32 => uint256) public rewardsByPoolId; mapping (bytes32 => uint256) public rewardsByPoolId;
// current epoch // The current epoch.
uint256 public currentEpoch; uint256 public currentEpoch;
// current epoch start time // The current epoch start time.
uint256 public currentEpochStartTimeInSeconds; uint256 public currentEpochStartTimeInSeconds;
// mapping from Pool Id to Epoch to Reward Ratio // mapping from Pool Id to Epoch to Reward Ratio
@ -72,7 +76,9 @@ contract MixinStorage is
// mapping from Pool Id to Epoch // mapping from Pool Id to Epoch
mapping (bytes32 => uint256) internal _cumulativeRewardsByPoolLastStored; mapping (bytes32 => uint256) internal _cumulativeRewardsByPoolLastStored;
// registered 0x Exchange contracts /// @dev Registered 0x Exchange contracts, capable of paying protocol fees.
/// @param 0 The address to check.
/// @return 0 Whether the address is a registered exchange.
mapping (address => bool) public validExchanges; mapping (address => bool) public validExchanges;
/* Tweakable parameters */ /* Tweakable parameters */
@ -95,11 +101,16 @@ contract MixinStorage is
/* State for finalization */ /* State for finalization */
/// @dev Stats for each pool that generated fees with sufficient stake to earn rewards. /// @dev Stats for each pool that generated fees with sufficient stake to earn rewards.
/// See `_minimumPoolStake` in MixinParams. /// See `_minimumPoolStake` in `MixinParams`.
/// @param 0 Pool ID.
/// @param 1 Epoch number.
/// @return 0 Pool fee stats.
mapping (bytes32 => mapping (uint256 => IStructs.PoolStats)) public poolStatsByEpoch; mapping (bytes32 => mapping (uint256 => IStructs.PoolStats)) public poolStatsByEpoch;
/// @dev Aggregated stats across all pools that generated fees with sufficient stake to earn rewards. /// @dev Aggregated stats across all pools that generated fees with sufficient stake to earn rewards.
/// See `_minimumPoolStake` in MixinParams. /// See `_minimumPoolStake` in MixinParams.
/// @param 0 Epoch number.
/// @return 0 Reward computation stats.
mapping (uint256 => IStructs.AggregatedStats) public aggregatedStatsByEpoch; mapping (uint256 => IStructs.AggregatedStats) public aggregatedStatsByEpoch;
/// @dev The WETH balance of this contract that is reserved for pool reward payouts. /// @dev The WETH balance of this contract that is reserved for pool reward payouts.

View File

@ -50,9 +50,9 @@ interface IStructs {
/// @dev Encapsulates a balance for the current and next epochs. /// @dev Encapsulates a balance for the current and next epochs.
/// Note that these balances may be stale if the current epoch /// Note that these balances may be stale if the current epoch
/// is greater than `currentEpoch`. /// is greater than `currentEpoch`.
/// @param currentEpoch the current epoch /// @param currentEpoch The current epoch
/// @param currentEpochBalance balance in the current epoch. /// @param currentEpochBalance Balance in the current epoch.
/// @param nextEpochBalance balance in `currentEpoch+1`. /// @param nextEpochBalance Balance in `currentEpoch+1`.
struct StoredBalance { struct StoredBalance {
uint64 currentEpoch; uint64 currentEpoch;
uint96 currentEpochBalance; uint96 currentEpochBalance;
@ -68,7 +68,7 @@ interface IStructs {
} }
/// @dev Info used to describe a status. /// @dev Info used to describe a status.
/// @param status of the stake. /// @param status Status of the stake.
/// @param poolId Unique Id of pool. This is set when status=DELEGATED. /// @param poolId Unique Id of pool. This is set when status=DELEGATED.
struct StakeInfo { struct StakeInfo {
StakeStatus status; StakeStatus status;
@ -76,15 +76,15 @@ interface IStructs {
} }
/// @dev Struct to represent a fraction. /// @dev Struct to represent a fraction.
/// @param numerator of fraction. /// @param numerator Numerator of fraction.
/// @param denominator of fraction. /// @param denominator Denominator of fraction.
struct Fraction { struct Fraction {
uint256 numerator; uint256 numerator;
uint256 denominator; uint256 denominator;
} }
/// @dev Holds the metadata for a staking pool. /// @dev Holds the metadata for a staking pool.
/// @param operator of the pool. /// @param operator Operator of the pool.
/// @param operatorShare Fraction of the total balance owned by the operator, in ppm. /// @param operatorShare Fraction of the total balance owned by the operator, in ppm.
struct Pool { struct Pool {
address operator; address operator;

View File

@ -31,7 +31,7 @@ contract MixinStake is
/// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. /// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault.
/// Unstake to retrieve the ZRX. Stake is in the 'Active' status. /// Unstake to retrieve the ZRX. Stake is in the 'Active' status.
/// @param amount of ZRX to stake. /// @param amount Amount of ZRX to stake.
function stake(uint256 amount) function stake(uint256 amount)
external external
{ {
@ -56,7 +56,7 @@ contract MixinStake is
/// @dev Unstake. Tokens are withdrawn from the ZRX Vault and returned to /// @dev Unstake. Tokens are withdrawn from the ZRX Vault and returned to
/// the staker. Stake must be in the 'undelegated' status in both the /// the staker. Stake must be in the 'undelegated' status in both the
/// current and next epoch in order to be unstaked. /// current and next epoch in order to be unstaked.
/// @param amount of ZRX to unstake. /// @param amount Amount of ZRX to unstake.
function unstake(uint256 amount) function unstake(uint256 amount)
external external
{ {
@ -99,9 +99,9 @@ contract MixinStake is
/// @dev Moves stake between statuses: 'undelegated' or 'delegated'. /// @dev Moves stake between statuses: 'undelegated' or 'delegated'.
/// Delegated stake can also be moved between pools. /// Delegated stake can also be moved between pools.
/// This change comes into effect next epoch. /// This change comes into effect next epoch.
/// @param from status to move stake out of. /// @param from Status to move stake out of.
/// @param to status to move stake into. /// @param to Status to move stake into.
/// @param amount of stake to move. /// @param amount Amount of stake to move.
function moveStake( function moveStake(
IStructs.StakeInfo calldata from, IStructs.StakeInfo calldata from,
IStructs.StakeInfo calldata to, IStructs.StakeInfo calldata to,

View File

@ -35,7 +35,13 @@ contract Authorizable is
_; _;
} }
/// @dev Whether an adderss is authorized to call privileged functions.
/// @param 0 Address to query.
/// @return 0 Whether the address is authorized.
mapping (address => bool) public authorized; mapping (address => bool) public authorized;
/// @dev Whether an adderss is authorized to call privileged functions.
/// @param 0 Index of authorized address.
/// @return 0 Authorized address.
address[] public authorities; address[] public authorities;
/// @dev Initializes the `owner` address. /// @dev Initializes the `owner` address.

View File

@ -21,6 +21,7 @@ pragma solidity ^0.5.9;
contract LibEIP1271 { contract LibEIP1271 {
// Magic bytes returned by EIP1271 wallets on success. /// @dev Magic bytes returned by EIP1271 wallets on success.
/// @return 0 Magic bytes.
bytes4 constant public EIP1271_MAGIC_VALUE = 0x20c13b0b; bytes4 constant public EIP1271_MAGIC_VALUE = 0x20c13b0b;
} }

View File

@ -26,6 +26,8 @@ import "./LibRichErrors.sol";
contract Ownable is contract Ownable is
IOwnable IOwnable
{ {
/// @dev The owner of this contract.
/// @return 0 The owner address.
address public owner; address public owner;
constructor () constructor ()
@ -39,6 +41,8 @@ contract Ownable is
_; _;
} }
/// @dev Change the owner of this contract.
/// @param newOwner New owner address.
function transferOwnership(address newOwner) function transferOwnership(address newOwner)
public public
onlyOwner onlyOwner

View File

@ -1,4 +1,13 @@
[ [
{
"version": "4.0.3",
"changes": [
{
"note": "More tolerant of AST-only compilation targets",
"pr": 2391
}
]
},
{ {
"timestamp": 1576540892, "timestamp": 1576540892,
"version": "4.0.2", "version": "4.0.2",

View File

@ -325,20 +325,21 @@ export class Compiler {
for (const contractPath of input.contractsToCompile) { for (const contractPath of input.contractsToCompile) {
const contractName = contractPathToData[contractPath].contractName; const contractName = contractPathToData[contractPath].contractName;
if (compilerOutput.contracts[contractPath] !== undefined) {
const compiledContract = compilerOutput.contracts[contractPath][contractName]; const compiledContract = compilerOutput.contracts[contractPath][contractName];
if (compiledContract === undefined) { if (compiledContract === undefined) {
throw new Error( throw new Error(
`Contract ${contractName} not found in ${contractPath}. Please make sure your contract has the same name as it's file name`, `Contract ${contractName} not found in ${contractPath}. Please make sure your contract has the same name as it's file name`,
); );
}
if (this._shouldSaveStandardInput) {
await fsWrapper.writeFileAsync(
`${this._artifactsDir}/${contractName}.input.json`,
utils.stringifyWithFormatting(input.standardInput),
);
}
addHexPrefixToContractBytecode(compiledContract);
} }
if (this._shouldSaveStandardInput) {
await fsWrapper.writeFileAsync(
`${this._artifactsDir}/${contractName}.input.json`,
utils.stringifyWithFormatting(input.standardInput),
);
}
addHexPrefixToContractBytecode(compiledContract);
if (shouldPersist) { if (shouldPersist) {
await this._persistCompiledContractAsync( await this._persistCompiledContractAsync(

1
packages/sol-doc/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/docs

View File

@ -0,0 +1,10 @@
# Blacklist all files
.*
*
# Whitelist lib
!lib/**/*
# Blacklist tests and publish scripts
/lib/test/*
/lib/monorepo_scripts/
# Package specific ignore
/docs

View File

@ -1,4 +1,13 @@
[ [
{
"version": "3.1.0",
"changes": [
{
"note": "Rewrite the whole thing to use custom AST walker.",
"pr": 2391
}
]
},
{ {
"timestamp": 1576540892, "timestamp": 1576540892,
"version": "3.0.2", "version": "3.0.2",

View File

@ -5,6 +5,7 @@
"main": "lib/src/index.js", "main": "lib/src/index.js",
"types": "lib/src/index.d.js", "types": "lib/src/index.d.js",
"scripts": { "scripts": {
"start": "node ./lib/src/cli.js",
"build": "tsc", "build": "tsc",
"build:ci": "yarn build", "build:ci": "yarn build",
"test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 6000 --exit", "test": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 6000 --exit",
@ -13,11 +14,16 @@
"coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
"lint": "tslint --format stylish --project .", "lint": "tslint --format stylish --project .",
"fix": "tslint --fix --format stylish --project .", "fix": "tslint --fix --format stylish --project .",
"clean": "shx rm -rf lib", "clean": "shx rm -rf lib docs",
"generate-v1-protocol-docs": "(cd ../contracts/src/1.0.0; node ../../../../node_modules/.bin/sol-doc --contracts-dir . --contracts Exchange/Exchange_v1.sol TokenRegistry/TokenRegistry.sol TokenTransferProxy/TokenTransferProxy_v1.sol) > v1.0.0.json", "generate-protocol-docs": "COMMIT=`git rev-parse --short HEAD`; mkdir -p ${npm_package_config_outputDir}; yarn start `echo ${npm_package_config_sources} | sed -r 's/(\\S+?)\\b/--source \\1/g'` --root ../../ --root ../../node_modules/@0x/contracts-=contracts/ `echo ${npm_package_config_contracts} | sed -r 's/(\\w+?)\\b/--contract \\1/g'` --md ${npm_package_config_outputDir}/reference.mdx --md-url-prefix \"${npm_package_config_repoBlobRoot}/${COMMIT}\"",
"generate-v2-protocol-docs": "(cd ../contracts/src/2.0.0; node ../../../../node_modules/.bin/sol-doc --contracts-dir . --contracts Exchange/Exchange.sol AssetProxy/ERC20Proxy.sol AssetProxy/ERC721Proxy.sol OrderValidator/OrderValidator.sol Forwarder/Forwarder.sol AssetProxyOwner/AssetProxyOwner.sol) > v2.0.0.json", "s3:sync_md_docs": "aws s3 sync ${npm_package_config_outputDir} s3://docs-markdown/${npm_package_config_s3DocsPath} --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers"
"deploy-v2-protocol-docs": "aws --profile 0xproject s3 cp --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json v2.0.0.json s3://staging-doc-jsons/contracts/", },
"deploy-v1-protocol-docs": "aws --profile 0xproject s3 cp --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json v1.0.0.json s3://staging-doc-jsons/contracts/" "config": {
"outputDir": "./docs",
"repoBlobRoot": "https://github.com/0xProject/0x-monorepo/blob",
"sources": "../../contracts/exchange/contracts/src/Exchange.sol ../../contracts/exchange-forwarder/contracts/src/Forwarder.sol ../../contracts/staking/contracts/src/Staking.sol ../../contracts/coordinator/contracts/src/Coordinator.sol",
"contracts": "Exchange Forwarder Staking Coordinator",
"s3DocsPath": "@0x/contracts-docs/v3.0.0"
}, },
"bin": { "bin": {
"sol-doc": "bin/sol-doc.js" "sol-doc": "bin/sol-doc.js"
@ -31,11 +37,13 @@
"@0x/utils": "^5.1.1", "@0x/utils": "^5.1.1",
"ethereum-types": "^3.0.0", "ethereum-types": "^3.0.0",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"lodash": "^4.17.11", "glob": "^7.1.2",
"yargs": "^10.0.3" "yargs": "^10.0.3"
}, },
"devDependencies": { "devDependencies": {
"lodash": "^4.17.11",
"@0x/tslint-config": "^4.0.0", "@0x/tslint-config": "^4.0.0",
"@0x/dev-utils": "^3.0.2",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"chai": "^4.0.1", "chai": "^4.0.1",
"chai-as-promised": "^7.1.0", "chai-as-promised": "^7.1.0",

View File

@ -1,41 +1,83 @@
import { logUtils } from '@0x/utils';
import * as fs from 'fs';
import * as glob from 'glob';
import 'source-map-support/register'; import 'source-map-support/register';
import { promisify } from 'util';
import * as yargs from 'yargs'; import * as yargs from 'yargs';
import { logUtils } from '@0x/utils'; import { extractDocsAsync } from './extract_docs';
import { generateMarkdownFromDocs } from './gen_md';
import { transformDocs } from './transform_docs';
import { SolDoc } from './sol_doc'; const JSON_TAB_WIDTH = 2;
const JSON_TAB_WIDTH = 4;
(async () => { (async () => {
const argv = yargs const argv = yargs
.option('contracts-dir', { .option('source', {
type: 'string', type: 'string',
description: 'path of contracts directory to compile', array: true,
description: 'glob paths of source files to compile',
demandOption: true,
}) })
.option('contracts', { .option('contract', {
type: 'string', type: 'string',
description: 'comma separated list of contracts to compile', array: true,
description: 'generate docs for only a contract',
})
.option('complete', {
type: 'boolean',
description: 'generate docs for all contracts and private methods',
})
.option('noFlatten', {
type: 'boolean',
description: 'do not merge inherited contracts',
})
.option('json', {
type: 'string',
description: 'file to save JSON to',
})
.option('root', {
type: 'string',
array: true,
description: 'rewrite paths as relative to these directory',
})
.option('md', {
type: 'string',
description: 'file to save markdown to',
})
.option('mdUrlPrefix', {
type: 'string',
description: 'prefix for markdown links',
}) })
.demandOption('contracts-dir')
.array('contracts')
.help().argv; .help().argv;
// Unfortunately, the only way to currently retrieve the declared structs within Solidity contracts const sources = await getContractsAsync(argv.source);
// is to tease them out of the params/return values included in the ABI. These structures do if (!sources.length) {
// not include the structs actual name, so we need a mapping to assign the proper name to a throw new Error('no sources found');
// struct. If the name is not in this mapping, the structs name will default to the param/return value }
// name (which mostly coincide). const docs = transformDocs(await extractDocsAsync(sources, argv.root), {
const customTypeHashToName: { [hash: string]: string } = { onlyExposed: !argv.complete,
'52d4a768701076c7bac06e386e430883975eb398732eccba797fd09dd064a60e': 'Order', flatten: !argv.noFlatten,
'46f7e8c4d144d11a72ce5338458ea37b933500d7a65e740cbca6d16e350eaa48': 'FillResults', contracts: argv.contract,
c22239cf0d29df1e6cf1be54f21692a8c0b3a48b9367540d4ffff4608b331ce9: 'OrderInfo', });
c21e9ff31a30941c22e1cb43752114bb467c34dea58947f98966c9030fc8e4a9: 'TraderInfo', if (argv.json) {
'6de3264a1040e027d4bdd29c71e963028238ac4ef060541078a7aced44a4d46f': 'MatchedFillResults', await writeTextFileAsync(argv.json, JSON.stringify(docs, null, JSON_TAB_WIDTH));
}; }
const solDoc = new SolDoc(); if (argv.md) {
const doc = await solDoc.generateSolDocAsync(argv.contractsDir, argv.contracts, customTypeHashToName); await writeTextFileAsync(argv.md, generateMarkdownFromDocs(docs, { urlPrefix: argv.mdUrlPrefix }));
process.stdout.write(JSON.stringify(doc, null, JSON_TAB_WIDTH)); }
})().catch(err => { })().catch(err => {
logUtils.warn(err); logUtils.warn(err);
process.exit(1); process.exit(1);
}); });
async function getContractsAsync(contractsGlobs: string[]): Promise<string[]> {
let sources: string[] = [];
for (const g of contractsGlobs) {
sources = [...sources, ...(await promisify(glob)(g))];
}
return sources;
}
async function writeTextFileAsync(file: string, content: string): Promise<void> {
return promisify(fs.writeFile)(file, content, { encoding: 'utf-8' });
}

View File

@ -0,0 +1,638 @@
import { Compiler } from '@0x/sol-compiler';
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import {
ArrayTypeNameNode,
AstNode,
ContractKind,
EnumValueNode,
FunctionKind,
isArrayTypeNameNode,
isContractDefinitionNode,
isEnumDefinitionNode,
isEventDefinitionNode,
isFunctionDefinitionNode,
isMappingTypeNameNode,
isSourceUnitNode,
isStructDefinitionNode,
isUserDefinedTypeNameNode,
isVariableDeclarationNode,
MappingTypeNameNode,
ParameterListNode,
SourceUnitNode,
splitAstNodeSrc,
StateMutability,
StorageLocation,
TypeNameNode,
VariableDeclarationNode,
Visibility,
} from './sol_ast';
export { ContractKind, FunctionKind, StateMutability, StorageLocation, Visibility } from './sol_ast';
export interface DocumentedItem {
doc: string;
line: number;
file: string;
}
export interface EnumValueDocs extends DocumentedItem {
value: number;
}
export interface ParamDocs extends DocumentedItem {
type: string;
indexed: boolean;
storageLocation: StorageLocation;
order: number;
}
export interface ParamDocsMap {
[name: string]: ParamDocs;
}
export interface EnumValueDocsMap {
[name: string]: EnumValueDocs;
}
export interface MethodDocs extends DocumentedItem {
name: string;
contract: string;
stateMutability: string;
visibility: Visibility;
isAccessor: boolean;
kind: FunctionKind;
parameters: ParamDocsMap;
returns: ParamDocsMap;
}
export interface EnumDocs extends DocumentedItem {
contract: string;
values: EnumValueDocsMap;
}
export interface StructDocs extends DocumentedItem {
contract: string;
fields: ParamDocsMap;
}
export interface EventDocs extends DocumentedItem {
contract: string;
name: string;
parameters: ParamDocsMap;
}
export interface ContractDocs extends DocumentedItem {
kind: ContractKind;
inherits: string[];
methods: MethodDocs[];
events: EventDocs[];
enums: {
[typeName: string]: EnumDocs;
};
structs: {
[typeName: string]: StructDocs;
};
}
export interface SolidityDocs {
contracts: {
[typeName: string]: ContractDocs;
};
}
interface SolcOutput {
sources: { [file: string]: { id: number; ast: SourceUnitNode } };
contracts: {
[file: string]: {
[contract: string]: {
metadata: string;
};
};
};
}
interface ContractMetadata {
sources: { [file: string]: { content: string } };
settings: { remappings: string[] };
}
interface SourceData {
path: string;
content: string;
}
interface Natspec {
comment: string;
dev: string;
params: { [name: string]: string };
returns: { [name: string]: string };
}
/**
* Extract documentation, as JSON, from contract files.
*/
export async function extractDocsAsync(contractPaths: string[], roots: string[] = []): Promise<SolidityDocs> {
const outputs = await compileAsync(contractPaths);
const sourceContents = (await Promise.all(outputs.map(getSourceContentsFromCompilerOutputAsync))).map(sources =>
rewriteSourcePaths(sources, roots),
);
const docs = createEmptyDocs();
outputs.forEach((output, outputIdx) => {
for (const file of Object.keys(output.contracts)) {
const fileDocs = extractDocsFromFile(
output.sources[file].ast,
sourceContents[outputIdx][output.sources[file].id],
);
mergeDocs(docs, fileDocs);
}
});
return docs;
}
async function compileAsync(files: string[]): Promise<SolcOutput[]> {
const compiler = new Compiler({
contracts: files,
compilerSettings: {
outputSelection: {
'*': {
'*': ['metadata'],
'': ['ast'],
},
},
},
});
return (compiler.getCompilerOutputsAsync() as any) as Promise<SolcOutput[]>;
}
async function getSourceContentsFromCompilerOutputAsync(output: SolcOutput): Promise<SourceData[]> {
const sources: SourceData[] = [];
for (const [importFile, fileOutput] of Object.entries(output.contracts)) {
if (importFile in sources) {
continue;
}
for (const contractOutput of Object.values(fileOutput)) {
const metadata = JSON.parse(contractOutput.metadata || '{}') as ContractMetadata;
let filePath = importFile;
if (!path.isAbsolute(filePath)) {
const { remappings } = metadata.settings;
let longestPrefix = '';
let longestPrefixReplacement = '';
for (const remapping of remappings) {
const [from, to] = remapping.substr(1).split('=');
if (longestPrefix.length < from.length) {
if (filePath.startsWith(from)) {
longestPrefix = from;
longestPrefixReplacement = to;
}
}
}
filePath = filePath.slice(longestPrefix.length);
filePath = path.join(longestPrefixReplacement, filePath);
}
const content = await promisify(fs.readFile)(filePath, { encoding: 'utf-8' });
sources[output.sources[importFile].id] = {
path: path.relative('.', filePath),
content,
};
}
}
return sources;
}
function rewriteSourcePaths(sources: SourceData[], roots: string[]): SourceData[] {
const _roots = roots.map(root => root.split('='));
return sources.map(s => {
let longestPrefix = '';
let longestPrefixReplacement = '';
for (const [from, to] of _roots) {
if (from.length > longestPrefix.length) {
if (s.path.startsWith(from)) {
longestPrefix = from;
longestPrefixReplacement = to || '';
}
}
}
return {
...s,
path: `${longestPrefixReplacement}${s.path.substr(longestPrefix.length)}`,
};
});
}
function mergeDocs(dst: SolidityDocs, ...srcs: SolidityDocs[]): SolidityDocs {
if (srcs.length === 0) {
return dst;
}
for (const src of srcs) {
dst.contracts = {
...dst.contracts,
...src.contracts,
};
}
return dst;
}
function createEmptyDocs(): SolidityDocs {
return { contracts: {} };
}
function extractDocsFromFile(ast: SourceUnitNode, source: SourceData): SolidityDocs {
const HIDDEN_VISIBILITIES = [Visibility.Private, Visibility.Internal];
const docs = createEmptyDocs();
const visit = (node: AstNode, currentContractName?: string) => {
const { offset } = splitAstNodeSrc(node.src);
if (isSourceUnitNode(node)) {
for (const child of node.nodes) {
visit(child);
}
} else if (isContractDefinitionNode(node)) {
const natspec = getNatspecBefore(source.content, offset);
docs.contracts[node.name] = {
file: source.path,
line: getAstNodeLineNumber(node, source.content),
doc: natspec.dev || natspec.comment,
kind: node.contractKind,
inherits: node.baseContracts.map(c => normalizeType(c.baseName.typeDescriptions.typeString)),
methods: [],
events: [],
enums: {},
structs: {},
};
for (const child of node.nodes) {
visit(child, node.name);
}
} else if (!currentContractName) {
return;
} else if (isVariableDeclarationNode(node)) {
if (HIDDEN_VISIBILITIES.includes(node.visibility)) {
return;
}
if (!node.stateVariable) {
return;
}
const natspec = getNatspecBefore(source.content, offset);
docs.contracts[currentContractName].methods.push({
file: source.path,
line: getAstNodeLineNumber(node, source.content),
doc: getDocStringAround(source.content, offset),
name: node.name,
contract: currentContractName,
kind: FunctionKind.Function,
visibility: Visibility.External,
parameters: extractAcessorParameterDocs(node.typeName, natspec, source),
returns: extractAccesorReturnDocs(node.typeName, natspec, source),
stateMutability: StateMutability.View,
isAccessor: true,
});
} else if (isFunctionDefinitionNode(node)) {
const natspec = getNatspecBefore(source.content, offset);
docs.contracts[currentContractName].methods.push({
file: source.path,
line: getAstNodeLineNumber(node, source.content),
doc: natspec.dev || natspec.comment || getCommentsBefore(source.content, offset),
name: node.name,
contract: currentContractName,
kind: node.kind,
visibility: node.visibility,
parameters: extractFunctionParameterDocs(node.parameters, natspec, source),
returns: extractFunctionReturnDocs(node.returnParameters, natspec, source),
stateMutability: node.stateMutability,
isAccessor: false,
});
} else if (isStructDefinitionNode(node)) {
const natspec = getNatspecBefore(source.content, offset);
docs.contracts[currentContractName].structs[node.canonicalName] = {
contract: currentContractName,
file: source.path,
line: getAstNodeLineNumber(node, source.content),
doc: natspec.dev || natspec.comment || getCommentsBefore(source.content, offset),
fields: extractStructFieldDocs(node.members, natspec, source),
};
} else if (isEnumDefinitionNode(node)) {
const natspec = getNatspecBefore(source.content, offset);
docs.contracts[currentContractName].enums[node.canonicalName] = {
contract: currentContractName,
file: source.path,
line: getAstNodeLineNumber(node, source.content),
doc: natspec.dev || natspec.comment || getCommentsBefore(source.content, offset),
values: extractEnumValueDocs(node.members, natspec, source),
};
} else if (isEventDefinitionNode(node)) {
const natspec = getNatspecBefore(source.content, offset);
docs.contracts[currentContractName].events.push({
contract: currentContractName,
file: source.path,
line: getAstNodeLineNumber(node, source.content),
doc: natspec.dev || natspec.comment || getCommentsBefore(source.content, offset),
name: node.name,
parameters: extractFunctionParameterDocs(node.parameters, natspec, source),
});
}
};
visit(ast);
return docs;
}
function extractAcessorParameterDocs(typeNameNode: TypeNameNode, natspec: Natspec, source: SourceData): ParamDocsMap {
const params: ParamDocsMap = {};
const lineNumber = getAstNodeLineNumber(typeNameNode, source.content);
if (isMappingTypeNameNode(typeNameNode)) {
// Handle mappings.
let node = typeNameNode;
let order = 0;
do {
const paramName = `${Object.keys(params).length}`;
params[paramName] = {
file: source.path,
line: lineNumber,
doc: natspec.params[paramName] || '',
type: normalizeType(node.keyType.typeDescriptions.typeString),
indexed: false,
storageLocation: StorageLocation.Default,
order: order++,
};
node = node.valueType as MappingTypeNameNode;
} while (isMappingTypeNameNode(node));
} else if (isArrayTypeNameNode(typeNameNode)) {
// Handle arrays.
let node = typeNameNode;
let order = 0;
do {
const paramName = `${Object.keys(params).length}`;
params[paramName] = {
file: source.path,
line: lineNumber,
doc: natspec.params[paramName] || '',
type: 'uint256',
indexed: false,
storageLocation: StorageLocation.Default,
order: order++,
};
node = node.baseType as ArrayTypeNameNode;
} while (isArrayTypeNameNode(node));
}
return params;
}
function extractAccesorReturnDocs(typeNameNode: TypeNameNode, natspec: Natspec, source: SourceData): ParamDocsMap {
let type = typeNameNode.typeDescriptions.typeString;
let storageLocation = StorageLocation.Default;
if (isMappingTypeNameNode(typeNameNode)) {
// Handle mappings.
let node = typeNameNode;
while (isMappingTypeNameNode(node.valueType)) {
node = node.valueType;
}
type = node.valueType.typeDescriptions.typeString;
storageLocation = type.startsWith('struct') ? StorageLocation.Memory : StorageLocation.Default;
} else if (isArrayTypeNameNode(typeNameNode)) {
// Handle arrays.
type = typeNameNode.baseType.typeDescriptions.typeString;
storageLocation = type.startsWith('struct') ? StorageLocation.Memory : StorageLocation.Default;
} else if (isUserDefinedTypeNameNode(typeNameNode)) {
storageLocation = typeNameNode.typeDescriptions.typeString.startsWith('struct')
? StorageLocation.Memory
: StorageLocation.Default;
}
return {
'0': {
storageLocation,
type: normalizeType(type),
file: source.path,
line: getAstNodeLineNumber(typeNameNode, source.content),
doc: natspec.returns['0'] || '',
indexed: false,
order: 0,
},
};
}
function extractFunctionParameterDocs(
paramListNodes: ParameterListNode,
natspec: Natspec,
source: SourceData,
): ParamDocsMap {
const params: ParamDocsMap = {};
for (const param of paramListNodes.parameters) {
params[param.name] = {
file: source.path,
line: getAstNodeLineNumber(param, source.content),
doc: natspec.params[param.name] || '',
type: normalizeType(param.typeName.typeDescriptions.typeString),
indexed: param.indexed,
storageLocation: param.storageLocation,
order: 0,
};
}
return params;
}
function extractFunctionReturnDocs(
paramListNodes: ParameterListNode,
natspec: Natspec,
source: SourceData,
): ParamDocsMap {
const returns: ParamDocsMap = {};
let order = 0;
for (const [idx, param] of Object.entries(paramListNodes.parameters)) {
returns[param.name || idx] = {
file: source.path,
line: getAstNodeLineNumber(param, source.content),
doc: natspec.returns[param.name || idx] || '',
type: normalizeType(param.typeName.typeDescriptions.typeString),
indexed: false,
storageLocation: param.storageLocation,
order: order++,
};
}
return returns;
}
function extractStructFieldDocs(
fieldNodes: VariableDeclarationNode[],
natspec: Natspec,
source: SourceData,
): ParamDocsMap {
const fields: ParamDocsMap = {};
let order = 0;
for (const field of fieldNodes) {
const { offset } = splitAstNodeSrc(field.src);
fields[field.name] = {
file: source.path,
line: getAstNodeLineNumber(field, source.content),
doc: natspec.params[field.name] || getDocStringAround(source.content, offset),
type: normalizeType(field.typeName.typeDescriptions.typeString),
indexed: false,
storageLocation: field.storageLocation,
order: order++,
};
}
return fields;
}
function extractEnumValueDocs(valuesNodes: EnumValueNode[], natspec: Natspec, source: SourceData): EnumValueDocsMap {
const values: EnumValueDocsMap = {};
for (const value of valuesNodes) {
const { offset } = splitAstNodeSrc(value.src);
values[value.name] = {
file: source.path,
line: getAstNodeLineNumber(value, source.content),
doc: natspec.params[value.name] || getDocStringAround(source.content, offset),
value: Object.keys(values).length,
};
}
return values;
}
function offsetToLineIndex(code: string, offset: number): number {
let currentOffset = 0;
let lineIdx = 0;
while (currentOffset <= offset) {
const lineEnd = code.indexOf('\n', currentOffset);
if (lineEnd === -1) {
return lineIdx;
}
currentOffset = lineEnd + 1;
++lineIdx;
}
return lineIdx - 1;
}
function offsetToLine(code: string, offset: number): string {
let lineEnd = code.substr(offset).search(/\r?\n/);
lineEnd = lineEnd === -1 ? code.length - offset : lineEnd;
let lineStart = code.lastIndexOf('\n', offset);
lineStart = lineStart === -1 ? 0 : lineStart;
return code.substr(lineStart, offset - lineStart + lineEnd).trim();
}
function getPrevLine(code: string, offset: number): [string | undefined, number] {
const lineStart = code.lastIndexOf('\n', offset);
if (lineStart <= 0) {
return [undefined, 0];
}
const prevLineStart = code.lastIndexOf('\n', lineStart - 1);
if (prevLineStart === -1) {
return [code.substr(0, lineStart).trim(), 0];
}
return [code.substring(prevLineStart + 1, lineStart).trim(), prevLineStart + 1];
}
function getAstNodeLineNumber(node: AstNode, code: string): number {
return offsetToLineIndex(code, splitAstNodeSrc(node.src).offset) + 1;
}
function getNatspecBefore(code: string, offset: number): Natspec {
const natspec = { comment: '', dev: '', params: {}, returns: {} };
// Walk backwards through the lines until there is no longer a natspec
// comment.
let currentDirectivePayloads = [];
let currentLine: string | undefined;
let currentOffset = offset;
while (true) {
[currentLine, currentOffset] = getPrevLine(code, currentOffset);
if (currentLine === undefined) {
break;
}
const m = /^\/\/\/\s*(?:@(\w+\b)\s*)?(.*?)$/.exec(currentLine);
if (!m) {
break;
}
const directive = m[1];
let directiveParam: string | undefined;
let rest = m[2] || '';
// Parse directives that take a parameter.
if (directive === 'param' || directive === 'return') {
const m2 = /^(\w+\b)(.*)$/.exec(rest);
if (m2) {
directiveParam = m2[1];
rest = m2[2] || '';
}
}
currentDirectivePayloads.push(rest);
if (directive !== undefined) {
const fullPayload = currentDirectivePayloads
.reverse()
.map(s => s.trim())
.join(' ');
switch (directive) {
case 'dev':
natspec.dev = fullPayload;
break;
case 'param':
if (directiveParam) {
natspec.params = {
...natspec.params,
[directiveParam]: fullPayload,
};
}
break;
case 'return':
if (directiveParam) {
natspec.returns = {
...natspec.returns,
[directiveParam]: fullPayload,
};
}
break;
default:
break;
}
currentDirectivePayloads = [];
}
}
if (currentDirectivePayloads.length > 0) {
natspec.comment = currentDirectivePayloads
.reverse()
.map(s => s.trim())
.join(' ');
}
return natspec;
}
function getTrailingCommentAt(code: string, offset: number): string {
const m = /\/\/\s*(.+)\s*$/.exec(offsetToLine(code, offset));
return m ? m[1] : '';
}
function getCommentsBefore(code: string, offset: number): string {
let currentOffset = offset;
const comments = [];
do {
let prevLine;
[prevLine, currentOffset] = getPrevLine(code, currentOffset);
if (prevLine === undefined) {
break;
}
const m = /^\s*\/\/\s*(.+)\s*$/.exec(prevLine);
if (m && !m[1].startsWith('solhint')) {
comments.push(m[1].trim());
} else {
break;
}
} while (currentOffset > 0);
return comments.reverse().join(' ');
}
function getDocStringBefore(code: string, offset: number): string {
const natspec = getNatspecBefore(code, offset);
return natspec.dev || natspec.comment || getCommentsBefore(code, offset);
}
function getDocStringAround(code: string, offset: number): string {
const natspec = getNatspecBefore(code, offset);
return natspec.dev || natspec.comment || getDocStringBefore(code, offset) || getTrailingCommentAt(code, offset);
}
function normalizeType(type: string): string {
const m = /^(?:\w+ )?(.*)$/.exec(type);
if (!m) {
return type;
}
return m[1];
}
// tslint:disable-next-line: max-file-line-count

View File

@ -0,0 +1,232 @@
import { DocumentedItem, EventDocs, MethodDocs, SolidityDocs } from './extract_docs';
export interface MarkdownOpts {
urlPrefix: string;
}
/**
* Convert JSON docs to markdown.
*/
export function generateMarkdownFromDocs(docs: SolidityDocs, opts: Partial<MarkdownOpts> = {}): string {
const lines: string[] = [];
const sortedContracts = Object.keys(docs.contracts).sort();
for (const contractName of sortedContracts) {
lines.push(...generateContractsContent(contractName, docs, opts));
}
return lines.join('\n');
}
function generateContractsContent(name: string, docs: SolidityDocs, opts: Partial<MarkdownOpts>): string[] {
const contract = docs.contracts[name];
const enums = [];
const sortedEnums = Object.entries(contract.enums).sort(([a], [b]) => a.localeCompare(b));
for (const [enumName, enumDocs] of sortedEnums) {
enums.push([
`### ${toCode(enumName)}`,
enumDocs.doc,
'',
toSourceAttributionLink(enumDocs, opts),
'',
`***Members***`,
...createTableContent(
['Name', 'Value', 'Description'],
Object.entries(enumDocs.values).map(([n, d]) => [
toSourceLink(toCode(n), d, opts),
toCode(d.value),
d.doc,
]),
),
]);
}
const structSections = [];
const sortedStructs = Object.entries(contract.structs).sort(([a], [b]) => a.localeCompare(b));
for (const [structName, structDocs] of sortedStructs) {
structSections.push([
`### ${toCode(structName)}`,
structDocs.doc,
'',
toSourceAttributionLink(structDocs, opts),
'',
`***Fields***`,
...createTableContent(
['Name', 'Type', 'Description'],
Object.entries(structDocs.fields).map(([n, d]) => [
toSourceLink(toCode(n), d, opts),
toCode(d.type),
d.doc,
]),
),
]);
}
const eventSections = [];
const sortedEvents = contract.events.sort((a, b) => a.name.localeCompare(b.name));
for (const event of sortedEvents) {
eventSections.push([
`### ${toCode(event.name)}`,
event.doc,
'',
`${toCode(getEventSignature(event))}`,
'',
toSourceAttributionLink(event, opts),
'',
`***Parameters***`,
...createTableContent(
['Name', 'Type', 'Indexed', 'Description'],
Object.entries(event.parameters).map(([n, d]) => [
toSourceLink(toCode(n), d, opts),
toCode(d.type),
toCode(d.indexed),
d.doc,
]),
),
]);
}
const methodSections = [];
const sortedMethods = contract.methods.sort((a, b) => a.name.localeCompare(b.name));
for (const method of sortedMethods) {
const annotation = method.isAccessor ? ' *(generated)*' : method.kind === 'fallback' ? ' *(fallback)*' : '';
methodSections.push([
`### ${toCode(getNormalizedMethodName(method))}`,
method.doc,
'',
`${toCode(getMethodSignature(method))}${annotation}`,
'',
toSourceAttributionLink(method, opts),
'',
...(Object.keys(method.parameters).length !== 0
? [
`***Parameters***`,
...createTableContent(
['Name', 'Type', 'Description'],
Object.entries(method.parameters).map(([n, d]) => [
toSourceLink(toCode(n), d, opts),
toCode(d.type),
d.doc,
]),
),
]
: []),
...(Object.keys(method.returns).length !== 0
? [
`***Returns***`,
...createTableContent(
['Name', 'Type', 'Description'],
Object.entries(method.returns).map(([n, d]) => [
toSourceLink(toCode(n), d, opts),
toCode(d.type),
d.doc,
]),
),
]
: []),
]);
}
return [
`# ${contract.kind} ${toCode(name)}`,
contract.doc,
'',
toSourceAttributionLink(contract, opts),
'',
...(enums.length > 0 ? ['## Enums', ...joinSections(enums)] : []),
...(structSections.length > 0 ? ['## Structs', ...joinSections(structSections)] : []),
...(eventSections.length > 0 ? ['## Events', ...joinSections(eventSections)] : []),
...(methodSections.length > 0 ? ['## Methods', ...joinSections(methodSections)] : []),
];
}
interface Stringable {
toString(): string;
}
function joinSections(sections: string[][]): string[] {
if (sections.length === 0) {
return [];
}
const joined: string[] = [];
for (const s of sections) {
joined.push(...s, '---');
}
return joined.slice(0, joined.length - 1);
}
function toCode(v: Stringable | boolean): string {
if (typeof v === 'boolean') {
return `\`${v ? true : false}\``;
}
return `\`${v}\``;
}
function toSourceLink(text: string, doc: DocumentedItem, opts: Partial<MarkdownOpts>): string {
return `[${text}](${toSourceURL(doc.file, doc.line, opts.urlPrefix)})`;
}
function toSourceAttributionLink(doc: DocumentedItem, opts: Partial<MarkdownOpts>): string {
return `&nbsp; *Defined in ${toSourceLink(`${doc.file}:${doc.line}`, doc, opts)}*`;
}
function toSourceURL(file: string, line: number, prefix?: string): string {
if (file.startsWith('/')) {
return `${file}#L${line}`;
}
const _prefix = !prefix || prefix.endsWith('/') ? prefix || '' : `${prefix}/`;
return `${_prefix}${file}#L${line}`;
}
function getMethodSignature(method: MethodDocs): string {
const args = Object.entries(method.parameters).map(([_name, param]) => {
return /^\d+$/.test(_name) ? param.type : `${param.type} ${_name}`;
});
const returns = Object.entries(method.returns).map(([_name, param]) => {
return /^\d+$/.test(_name) ? param.type : `${param.type} ${_name}`;
});
const _returns = returns.length !== 0 ? `: (${returns.join(', ')})` : '';
const mutabilityPrefix = ['view', 'pure'].includes(method.stateMutability)
? 'constant '
: method.stateMutability === 'payable'
? 'payable '
: '';
return `${mutabilityPrefix}function ${getNormalizedMethodName(method)}(${args.join(', ')})${_returns}`;
}
function getNormalizedMethodName(method: MethodDocs): string {
let name = method.name;
if (method.kind === 'constructor') {
name = 'constructor';
} else if (method.kind === 'fallback') {
name = '<fallback>';
}
return name;
}
function getEventSignature(event: EventDocs): string {
const args = Object.entries(event.parameters).map(([name, param]) => {
return /^\d+$/.test(name) ? param.type : `${param.type} ${name}`;
});
return `event ${event.name}(${args.join(', ')})`;
}
function createTableContent(headers: string[], rows: Stringable[][]): string[] {
const [_headers, _rows] = filterTableEmptyColumns(headers, rows);
const lines = [
_headers.join(' | '),
_headers.map(h => h.replace(/./g, '-')).join(' | '),
..._rows.map(r => r.join(' | ')),
].map(line => `| ${line} |`);
return ['', ...lines, ''];
}
function filterTableEmptyColumns(headers: string[], rows: Stringable[][]): [string[], Stringable[][]] {
const emptyColumnIndicesByRow = rows.map(r => r.map((c, i) => i).filter(i => r[i] === ''));
const emptyColumnIndices = emptyColumnIndicesByRow.reduce((acc, row) => {
for (const i of row) {
if (!acc.includes(i)) {
acc.push(i);
}
}
return acc;
}, []);
return [
headers.filter((v, i) => !emptyColumnIndices.includes(i)),
rows.filter((v, i) => !emptyColumnIndices.includes(i)),
];
}

View File

@ -1 +1,20 @@
export { SolDoc } from './sol_doc'; export {
ContractDocs,
ContractKind,
EnumValueDocs,
EnumValueDocsMap,
EventDocs,
extractDocsAsync,
FunctionKind,
MethodDocs,
ParamDocs,
ParamDocsMap,
SolidityDocs,
StorageLocation,
StructDocs,
Visibility,
} from './extract_docs';
export { transformDocs, TransformOpts } from './transform_docs';
export { generateMarkdownFromDocs, MarkdownOpts } from './gen_md';

View File

@ -0,0 +1,231 @@
export enum AstNodeType {
SourceUnit = 'SourceUnit',
ContractDefinition = 'ContractDefinition',
FunctionDefinition = 'FunctionDefinition',
ParameterList = 'ParameterList',
VariableDeclaration = 'VariableDeclaration',
UserDefinedTypeName = 'UserDefinedTypeName',
ElementaryTypeName = 'ElementaryTypeName',
ArrayTypeName = 'ArrayTypeName',
Mapping = 'Mapping',
StructDefinition = 'StructDefinition',
EnumDefinition = 'EnumDefinition',
EnumValue = 'EnumValue',
InheritanceSpecifier = 'InheritanceSpecifier',
EventDefinition = 'EventDefinition',
}
export enum Visibility {
Internal = 'internal',
External = 'external',
Public = 'public',
Private = 'private',
}
export enum StateMutability {
Nonpayable = 'nonpayable',
Payable = 'payable',
View = 'view',
Pure = 'pure',
}
export enum FunctionKind {
Constructor = 'constructor',
Function = 'function',
Fallback = 'fallback',
}
export enum ContractKind {
Contract = 'contract',
Interface = 'interface',
Library = 'library',
}
export enum StorageLocation {
Default = 'default',
Storage = 'storage',
Memory = 'memory',
CallData = 'calldata',
}
export interface AstNode {
id: number;
nodeType: AstNodeType;
src: string;
}
export interface SourceUnitNode extends AstNode {
path: string;
nodes: AstNode[];
exportedSymbols: {
[symbol: string]: number[];
};
}
export interface ContractDefinitionNode extends AstNode {
name: string;
contractKind: ContractKind;
fullyImplemented: boolean;
linearizedBaseContracts: number[];
contractDependencies: number[];
baseContracts: InheritanceSpecifierNode[];
nodes: AstNode[];
}
export interface InheritanceSpecifierNode extends AstNode {
baseName: UserDefinedTypeNameNode;
}
export interface FunctionDefinitionNode extends AstNode {
name: string;
implemented: boolean;
scope: number;
kind: FunctionKind;
parameters: ParameterListNode;
returnParameters: ParameterListNode;
visibility: Visibility;
stateMutability: StateMutability;
}
export interface ParameterListNode extends AstNode {
parameters: VariableDeclarationNode[];
}
export interface VariableDeclarationNode extends AstNode {
name: string;
value: AstNode | null;
constant: boolean;
scope: number;
visibility: Visibility;
stateVariable: boolean;
storageLocation: StorageLocation;
indexed: boolean;
typeName: TypeNameNode;
}
export interface TypeNameNode extends AstNode {
name: string;
typeDescriptions: {
typeIdentifier: string;
typeString: string;
};
}
export interface UserDefinedTypeNameNode extends TypeNameNode {
referencedDeclaration: number;
}
export interface MappingTypeNameNode extends TypeNameNode {
keyType: ElementaryTypeNameNode;
valueType: TypeNameNode;
}
export interface ElementaryTypeNameNode extends TypeNameNode {}
export interface ArrayTypeNameNode extends TypeNameNode {
length: number | null;
baseType: TypeNameNode;
}
export interface StructDefinitionNode extends AstNode {
scope: number;
name: string;
canonicalName: string;
members: VariableDeclarationNode[];
}
export interface EnumDefinitionNode extends AstNode {
name: string;
canonicalName: string;
members: EnumValueNode[];
}
export interface EnumValueNode extends AstNode {
name: string;
}
export interface EventDefinitionNode extends AstNode {
name: string;
parameters: ParameterListNode;
}
/**
* Check if a node is a SourceUnit node.
*/
export function isSourceUnitNode(node: AstNode): node is SourceUnitNode {
return node.nodeType === AstNodeType.SourceUnit;
}
/**
* Check if a node is a ContractDefinition ode.
*/
export function isContractDefinitionNode(node: AstNode): node is ContractDefinitionNode {
return node.nodeType === AstNodeType.ContractDefinition;
}
/**
* Check if a node is a VariableDeclaration ode.
*/
export function isVariableDeclarationNode(node: AstNode): node is VariableDeclarationNode {
return node.nodeType === AstNodeType.VariableDeclaration;
}
/**
* Check if a node is a FunctionDefinition node.
*/
export function isFunctionDefinitionNode(node: AstNode): node is FunctionDefinitionNode {
return node.nodeType === AstNodeType.FunctionDefinition;
}
/**
* Check if a node is a StructDefinition ode.
*/
export function isStructDefinitionNode(node: AstNode): node is StructDefinitionNode {
return node.nodeType === AstNodeType.StructDefinition;
}
/**
* Check if a node is a EnumDefinition ode.
*/
export function isEnumDefinitionNode(node: AstNode): node is EnumDefinitionNode {
return node.nodeType === AstNodeType.EnumDefinition;
}
/**
* Check if a node is a Mapping node.
*/
export function isMappingTypeNameNode(node: AstNode): node is MappingTypeNameNode {
return node.nodeType === AstNodeType.Mapping;
}
/**
* Check if a node is a ArrayTypeName node.
*/
export function isArrayTypeNameNode(node: AstNode): node is ArrayTypeNameNode {
return node.nodeType === AstNodeType.ArrayTypeName;
}
/**
* Check if a node is a UserDefinedTypeName node.
*/
export function isUserDefinedTypeNameNode(node: AstNode): node is UserDefinedTypeNameNode {
return node.nodeType === AstNodeType.UserDefinedTypeName;
}
/**
* Check if a node is a EventDefinition node.
*/
export function isEventDefinitionNode(node: AstNode): node is EventDefinitionNode {
return node.nodeType === AstNodeType.EventDefinition;
}
/**
* Split an AST source mapping string into its parts.
*/
export function splitAstNodeSrc(src: string): { offset: number; length: number; sourceId: number } {
// tslint:disable-next-line: custom-no-magic-numbers
const [offset, length, sourceId] = src.split(':').map(s => parseInt(s, 10));
return { offset, length, sourceId };
}
// tslint:disable-next-line: max-file-line-count

View File

@ -1,505 +0,0 @@
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 '@0x/sol-compiler';
import {
CustomType,
CustomTypeChild,
DocAgnosticFormat,
DocSection,
Event,
EventArg,
ObjectMap,
Parameter,
SolidityMethod,
Type,
TypeDocTypes,
} from '@0x/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 (
devdocIfExists !== undefined &&
devdocIfExists.methods !== undefined &&
devdocIfExists.methods[methodSignature] !== undefined &&
devdocIfExists.methods[methodSignature].details !== undefined
) {
details = devdocIfExists.methods[methodSignature].details;
}
return details;
}
private static _genFallbackDoc(
abiDefinition: FallbackAbi,
devdocIfExists: DevdocOutput | undefined,
): SolidityMethod {
const methodSignature = `()`;
const comment = SolDoc._devdocMethodDetailsIfExist(methodSignature, devdocIfExists);
const returnComment =
devdocIfExists === undefined || devdocIfExists.methods[methodSignature] === undefined
? 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 = contractsToCompile !== undefined && 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 (compiledContract.abi === undefined) {
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 (contractsToDocument === undefined || 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 (dataItem.components === undefined) {
return undefined;
}
const customType = this._getCustomTypeFromDataItem(dataItem);
const hash = SolDoc._generateCustomTypeHash(customType);
if (this._customTypeHashToName === undefined || this._customTypeHashToName[hash] === undefined) {
return undefined;
}
return this._customTypeHashToName[hash];
}
private _getTypeFromDataItem(dataItem: DataItem): Type {
const typeDocType = dataItem.components !== undefined ? TypeDocTypes.Reference : TypeDocTypes.Intrinsic;
let typeName: string;
if (typeDocType === TypeDocTypes.Reference) {
const nameIfExists = this._getNameFromDataItemIfExists(dataItem);
typeName = nameIfExists === undefined ? 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 (this._customTypeHashToName === undefined) {
return customTypes;
}
const localCustomTypes = _.cloneDeep(customTypes);
_.each(localCustomTypes, (customType, i) => {
const hash = SolDoc._generateCustomTypeHash(customType);
if (this._customTypeHashToName !== undefined && this._customTypeHashToName[hash] !== undefined) {
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:
compiledContract.devdoc === undefined || compiledContract.devdoc.title === undefined
? ''
: compiledContract.devdoc.title,
constructors: [],
methods: [],
properties: [],
types: [],
functions: [],
events: [],
};
for (const abiDefinition of compiledContract.abi) {
switch (abiDefinition.type) {
case 'constructor':
docSection.constructors.push(
// tslint:disable-next-line:no-unnecessary-type-assertion
this._genConstructorDoc(contractName, abiDefinition as ConstructorAbi, compiledContract.devdoc),
);
break;
case 'event':
// tslint:disable-next-line:no-unnecessary-type-assertion
(docSection.events as Event[]).push(SolDoc._genEventDoc(abiDefinition as EventAbi));
// 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':
// tslint:disable-next-line:no-unnecessary-type-assertion
docSection.methods.push(this._genMethodDoc(abiDefinition as MethodAbi, compiledContract.devdoc));
break;
case 'fallback':
// tslint:disable-next-line:no-unnecessary-type-assertion
docSection.methods.push(
SolDoc._genFallbackDoc(abiDefinition as FallbackAbi, 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 =
devdocIfExists === undefined || devdocIfExists.methods[methodSignature] === undefined
? undefined
: devdocIfExists.methods[methodSignature].return;
const hasNoNamedParameters = _.find(parameters, p => !_.isEmpty(p.name)) === undefined;
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 (devdocIfExists !== undefined) {
const devdocMethodIfExists = devdocIfExists.methods[methodSignature];
if (devdocMethodIfExists !== undefined) {
const devdocParamsIfExist = devdocMethodIfExists.params;
if (devdocParamsIfExist !== undefined) {
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 ((abiDefinition as any).inputs !== undefined) {
const methodOrConstructorAbi = abiDefinition as MethodAbi | ConstructorAbi;
_.each(methodOrConstructorAbi.inputs, input => {
if (!input.components === undefined) {
const customType = this._getCustomTypeFromDataItem(input);
customTypes.push(customType);
}
});
}
if ((abiDefinition as any).outputs !== undefined) {
const methodAbi = abiDefinition as MethodAbi; // tslint:disable-line:no-unnecessary-type-assertion
_.each(methodAbi.outputs, output => {
if (output.components !== undefined) {
const customType = this._getCustomTypeFromDataItem(output);
customTypes.push(customType);
}
});
}
return customTypes;
}
}
// tslint:disable:max-file-line-count

View File

@ -0,0 +1,235 @@
import { ContractDocs, EventDocs, MethodDocs, ParamDocsMap, SolidityDocs, Visibility } from './extract_docs';
export interface TransformOpts {
onlyExposed: boolean;
flatten: boolean;
contracts: string[];
}
interface TypesUsage {
[type: string]: {
methods: MethodDocs[];
events: EventDocs[];
structs: string[];
};
}
/**
* Apply some nice transformations to extracted JSON docs, such as flattening
* inherited contracts and filtering out unexposed or unused types.
*/
export function transformDocs(docs: SolidityDocs, opts: Partial<TransformOpts> = {}): SolidityDocs {
const _opts = {
onlyExposed: false,
flatten: false,
contracts: undefined,
...opts,
};
const _docs = {
...docs,
contracts: { ...docs.contracts },
};
if (_opts.flatten) {
for (const [contractName] of Object.entries(docs.contracts)) {
_docs.contracts[contractName] = flattenContract(contractName, docs);
}
}
return filterTypes(_docs, _opts.contracts || Object.keys(docs.contracts), _opts.onlyExposed);
}
function flattenContract(contractName: string, docs: SolidityDocs, seen: string[] = []): ContractDocs {
seen.push(contractName);
const contract = docs.contracts[contractName];
const bases = [];
for (const ancestor of contract.inherits) {
if (!seen.includes(ancestor)) {
bases.push(flattenContract(ancestor, docs, seen));
}
}
return mergeContracts([...bases, contract]);
}
function mergeContracts(contracts: ContractDocs[]): ContractDocs {
return {
...contracts[contracts.length - 1],
methods: mergeMethods(concat(...contracts.map(c => c.methods))),
events: mergeEvents(concat(...contracts.map(c => c.events))),
};
}
function concat<T>(...arrs: T[][]): T[] {
return arrs.reduce((prev: T[], curr: T[]) => {
prev.push(...curr);
return prev;
}, []);
}
function mergeMethods(methods: MethodDocs[]): MethodDocs[] {
const ids: string[] = [];
const merged: MethodDocs[] = [];
for (const method of methods) {
if (method.visibility === Visibility.Private) {
continue;
}
const id = getMethodId(method.name, method.parameters);
if (method.kind === 'constructor') {
const constructorIndex = merged.findIndex(m => m.kind === 'constructor');
if (constructorIndex !== -1) {
merged[constructorIndex] = method;
ids[constructorIndex] = id;
continue;
}
}
const existingIdx = ids.indexOf(id);
if (existingIdx !== -1) {
merged[existingIdx] = method;
ids[existingIdx] = id;
} else {
merged.push(method);
ids.push(id);
}
}
return merged;
}
function mergeEvents(events: EventDocs[]): EventDocs[] {
const ids: string[] = [];
const merged: EventDocs[] = [];
for (const event of events) {
const selector = getMethodId(event.name, event.parameters);
const existingIdx = ids.indexOf(selector);
if (existingIdx !== -1) {
merged[existingIdx] = event;
ids[existingIdx] = selector;
} else {
merged.push(event);
ids.push(selector);
}
}
return merged;
}
function getMethodId(name: string, params: ParamDocsMap): string {
const paramsTypes = Object.values(params).map(p => p.type);
return `${name}(${paramsTypes.join(',')})`;
}
function filterTypes(docs: SolidityDocs, contracts: string[], onlyExposed: boolean = false): SolidityDocs {
const inheritedContracts = getAllInheritedContracts(contracts, docs);
const contractsWithInheritance = [...inheritedContracts, ...contracts];
const filteredDocs: SolidityDocs = {
...docs,
contracts: {},
};
const usages = getTypeUsage(docs);
for (const [contractName, contract] of Object.entries(docs.contracts)) {
if (inheritedContracts.includes(contractName) && !contracts.includes(contractName)) {
continue;
}
const filteredContract: ContractDocs = {
...contract,
methods: contract.methods.filter(m => !onlyExposed || isMethodVisible(m)),
structs: {},
enums: {},
};
for (const [typeName, doc] of Object.entries(contract.structs)) {
if (isTypeUsedByContracts(typeName, usages, contractsWithInheritance, onlyExposed)) {
filteredContract.structs[typeName] = doc;
}
}
for (const [typeName, doc] of Object.entries(contract.enums)) {
if (isTypeUsedByContracts(typeName, usages, contractsWithInheritance, onlyExposed)) {
filteredContract.enums[typeName] = doc;
}
}
if (
contracts.includes(contractName) ||
Object.keys(filteredContract.structs).length !== 0 ||
Object.keys(filteredContract.enums).length !== 0
) {
filteredDocs.contracts[contractName] = filteredContract;
}
}
return filteredDocs;
}
function getAllInheritedContracts(contracts: string[], docs: SolidityDocs): string[] {
const result: string[] = [];
for (const contract of contracts) {
for (const inherited of docs.contracts[contract].inherits) {
if (result.includes(inherited)) {
continue;
}
result.push(inherited, ...getAllInheritedContracts([inherited], docs));
}
}
return result;
}
function getTypeUsage(docs: SolidityDocs): TypesUsage {
const types: TypesUsage = {};
const addTypeUser = (type: string, user: { method?: MethodDocs; event?: EventDocs; struct?: string }) => {
if (types[type] === undefined) {
types[type] = { methods: [], events: [], structs: [] };
}
if (user.method !== undefined) {
types[type].methods.push(user.method);
}
if (user.event !== undefined) {
types[type].events.push(user.event);
}
if (user.struct !== undefined) {
types[type].structs.push(user.struct);
}
};
for (const contract of Object.values(docs.contracts)) {
for (const [typeName, doc] of Object.entries(contract.structs)) {
for (const field of Object.values(doc.fields)) {
addTypeUser(field.type, { struct: typeName });
}
}
for (const doc of contract.events) {
for (const param of Object.values(doc.parameters)) {
addTypeUser(param.type, { event: doc });
}
}
for (const doc of contract.methods) {
for (const param of Object.values(doc.parameters)) {
addTypeUser(param.type, { method: doc });
}
for (const param of Object.values(doc.returns)) {
addTypeUser(param.type, { method: doc });
}
}
}
return types;
}
function isTypeUsedByContracts(
type: string,
usages: TypesUsage,
contracts: string[],
onlyExposed: boolean = false,
): boolean {
const usage = usages[type];
if (usage === undefined) {
return false;
}
for (const struct of usage.structs) {
if (isTypeUsedByContracts(struct, usages, contracts, onlyExposed)) {
return true;
}
}
if (usage.events.some(e => contracts.includes(e.contract))) {
return true;
}
if (usage.methods.filter(m => !onlyExposed || isMethodVisible(m)).some(m => contracts.includes(m.contract))) {
return true;
}
return false;
}
function isMethodVisible(method: MethodDocs): boolean {
const VISIBLES = [Visibility.External, Visibility.Public];
return VISIBLES.includes(method.visibility);
}

View File

@ -0,0 +1,514 @@
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

View File

@ -1,7 +0,0 @@
pragma solidity ^0.4.24;
contract MultipleReturnValues {
function methodWithMultipleReturnValues() public pure returns(int, int) {
return (0, 0);
}
}

View File

@ -1,40 +0,0 @@
pragma solidity ^0.4.24;
/// @title Contract Title
/// @dev This is a very long documentation comment at the contract level.
/// It actually spans multiple lines, too.
contract NatspecEverything {
int d;
/// @dev Constructor @dev
/// @param p Constructor @param
constructor(int p) public { d = p; }
/// @notice publicMethod @notice
/// @dev publicMethod @dev
/// @param p publicMethod @param
/// @return publicMethod @return
function publicMethod(int p) public pure returns(int r) { return p; }
/// @dev Fallback @dev
function () public {}
/// @notice externalMethod @notice
/// @dev externalMethod @dev
/// @param p externalMethod @param
/// @return externalMethod @return
function externalMethod(int p) external pure returns(int r) { return p; }
/// @dev Here is a really long developer documentation comment, which spans
/// multiple lines, for the purposes of making sure that broken lines are
/// consolidated into one devdoc comment.
function methodWithLongDevdoc(int p) public pure returns(int) { return p; }
/// @dev AnEvent @dev
/// @param p on this event is an integer.
event AnEvent(int p);
/// @dev methodWithSolhintDirective @dev
// solhint-disable no-empty-blocks
function methodWithSolhintDirective() public pure {}
}

View File

@ -1,18 +0,0 @@
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
contract StructParamAndReturn {
struct Stuff {
address anAddress;
uint256 aNumber;
}
/// @dev DEV_COMMENT
/// @param stuff STUFF_COMMENT
/// @return RETURN_COMMENT
function methodWithStructParamAndReturn(Stuff stuff) public pure returns(Stuff) {
return stuff;
}
}

View File

@ -1,115 +0,0 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.4.14;
import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol";
import { ERC20 as Token } from "zeppelin-solidity/contracts/token/ERC20/ERC20.sol";
/// @title TokenTransferProxy - Transfers tokens on behalf of contracts that have been approved via decentralized governance.
/// @author Amir Bandeali - <amir@0xProject.com>, Will Warren - <will@0xProject.com>
contract TokenTransferProxy is Ownable {
/// @dev Only authorized addresses can invoke functions with this modifier.
modifier onlyAuthorized {
require(authorized[msg.sender]);
_;
}
modifier targetAuthorized(address target) {
require(authorized[target]);
_;
}
modifier targetNotAuthorized(address target) {
require(!authorized[target]);
_;
}
mapping (address => bool) public authorized;
address[] public authorities;
event LogAuthorizedAddressAdded(address indexed target, address indexed caller);
event LogAuthorizedAddressRemoved(address indexed target, address indexed caller);
/*
* Public functions
*/
/// @dev Authorizes an address.
/// @param target Address to authorize.
function addAuthorizedAddress(address target)
public
onlyOwner
targetNotAuthorized(target)
{
authorized[target] = true;
authorities.push(target);
LogAuthorizedAddressAdded(target, msg.sender);
}
/// @dev Removes authorizion of an address.
/// @param target Address to remove authorization from.
function removeAuthorizedAddress(address target)
public
onlyOwner
targetAuthorized(target)
{
delete authorized[target];
for (uint i = 0; i < authorities.length; i++) {
if (authorities[i] == target) {
authorities[i] = authorities[authorities.length - 1];
authorities.length -= 1;
break;
}
}
LogAuthorizedAddressRemoved(target, msg.sender);
}
/// @dev Calls into ERC20 Token contract, invoking transferFrom.
/// @param token Address of token to transfer.
/// @param from Address to transfer token from.
/// @param to Address to transfer token to.
/// @param value Amount of token to transfer.
/// @return Success of transfer.
function transferFrom(
address token,
address from,
address to,
uint value)
public
onlyAuthorized
returns (bool)
{
return Token(token).transferFrom(from, to, value);
}
/*
* Public constant functions
*/
/// @dev Gets all authorized addresses.
/// @return Array of authorized addresses.
function getAuthorizedAddresses()
public
constant
returns (address[])
{
return authorities;
}
}

View File

@ -1,100 +0,0 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.4.14;
import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol";
import { ERC20 as Token } from "zeppelin-solidity/contracts/token/ERC20/ERC20.sol";
contract TokenTransferProxyNoDevdoc is Ownable {
modifier onlyAuthorized {
require(authorized[msg.sender]);
_;
}
modifier targetAuthorized(address target) {
require(authorized[target]);
_;
}
modifier targetNotAuthorized(address target) {
require(!authorized[target]);
_;
}
mapping (address => bool) public authorized;
address[] public authorities;
event LogAuthorizedAddressAdded(address indexed target, address indexed caller);
event LogAuthorizedAddressRemoved(address indexed target, address indexed caller);
/*
* Public functions
*/
function addAuthorizedAddress(address target)
public
onlyOwner
targetNotAuthorized(target)
{
authorized[target] = true;
authorities.push(target);
LogAuthorizedAddressAdded(target, msg.sender);
}
function removeAuthorizedAddress(address target)
public
onlyOwner
targetAuthorized(target)
{
delete authorized[target];
for (uint i = 0; i < authorities.length; i++) {
if (authorities[i] == target) {
authorities[i] = authorities[authorities.length - 1];
authorities.length -= 1;
break;
}
}
LogAuthorizedAddressRemoved(target, msg.sender);
}
function transferFrom(
address token,
address from,
address to,
uint value)
public
onlyAuthorized
returns (bool)
{
return Token(token).transferFrom(from, to, value);
}
/*
* Public constant functions
*/
function getAuthorizedAddresses()
public
constant
returns (address[])
{
return authorities;
}
}

View File

@ -0,0 +1,114 @@
import { chaiSetup } from '@0x/dev-utils';
import { expect } from 'chai';
import * as _ from 'lodash';
import { FunctionKind, SolidityDocs } from '../src/extract_docs';
import { generateMarkdownFromDocs } from '../src/gen_md';
import { randomContract, randomWord } from './utils/random_docs';
chaiSetup.configure();
// tslint:disable: custom-no-magic-numbers
describe('generateMarkdownFromDocs()', () => {
const URL_PREFIX = randomWord();
const DOCS: SolidityDocs = {
contracts: {
..._.mapValues(
_.groupBy(
_.times(_.random(2, 8), () =>
((name: string) => ({ name, ...randomContract(name) }))(`${randomWord()}Contract`),
),
'name',
),
g => g[0],
),
},
};
let md: string;
let mdLines: string[];
function getMarkdownHeaders(level: number): string[] {
const lines = mdLines.filter(line => new RegExp(`^\\s*#{${level}}[^#]`).test(line));
// tslint:disable-next-line: no-non-null-assertion
return lines.map(line => /^\s*#+\s*(.+?)\s*$/.exec(line)![1]);
}
function getMarkdownLinks(): string[] {
const links: string[] = [];
for (const line of mdLines) {
const re = /\[[^\]]+\]\(([^)]+)\)/g;
let m: string[] | undefined | null;
do {
m = re.exec(line);
if (m) {
links.push(m[1]);
}
} while (m);
}
return links;
}
before(() => {
md = generateMarkdownFromDocs(DOCS, { urlPrefix: URL_PREFIX });
mdLines = md.split('\n');
});
it('generates entries for all contracts', () => {
const headers = getMarkdownHeaders(1);
for (const [contractName, contract] of Object.entries(DOCS.contracts)) {
expect(headers).to.include(`${contract.kind} \`${contractName}\``);
}
});
it('generates entries for all enums', () => {
const headers = getMarkdownHeaders(3);
for (const contract of Object.values(DOCS.contracts)) {
for (const enumName of Object.keys(contract.enums)) {
expect(headers).to.include(`\`${enumName}\``);
}
}
});
it('generates entries for all structs', () => {
const headers = getMarkdownHeaders(3);
for (const contract of Object.values(DOCS.contracts)) {
for (const structName of Object.keys(contract.structs)) {
expect(headers).to.include(`\`${structName}\``);
}
}
});
it('generates entries for all events', () => {
const headers = getMarkdownHeaders(3);
for (const contract of Object.values(DOCS.contracts)) {
for (const event of contract.events) {
expect(headers).to.include(`\`${event.name}\``);
}
}
});
it('generates entries for all methods', () => {
const headers = getMarkdownHeaders(3);
for (const contract of Object.values(DOCS.contracts)) {
for (const method of contract.methods) {
if (method.kind === FunctionKind.Fallback) {
expect(headers).to.include(`\`<fallback>\``);
} else if (method.kind === FunctionKind.Constructor) {
expect(headers).to.include(`\`constructor\``);
} else {
expect(headers).to.include(`\`${method.name}\``);
}
}
}
});
it('prefixes all URLs with the prefix', () => {
const urls = getMarkdownLinks();
for (const url of urls) {
expect(url.startsWith(URL_PREFIX)).to.be.true();
}
});
});
// tslint:disable: max-file-line-count

View File

@ -0,0 +1,43 @@
pragma solidity ^0.5;
pragma experimental ABIEncoderV2;
import "./InterfaceContract.sol";
import "./LibraryContract.sol";
/// @dev Documentation for `BaseContract`.
contract BaseContract {
/// @dev Documentation for `BaseContractEvent1`.
/// @param p1 Documentation for `p1`.
/// @param p2 Documentation for `p2`.
event BaseContractEvent1(address indexed p1, InterfaceContract.InterfaceStruct p2);
// Documentation for `BaseContractEvent2`.
event BaseContractEvent2(
uint256 p1,
uint256 indexed p2
);
/// @dev Documentation for `baseContractField1`.
/// @param 1 Documentation for `1`.
/// @param 0 Documentation for `0`.
/// @return 0 Documentation for `0`.
mapping (bytes32 => mapping(address => InterfaceContract.InterfaceStruct)) public baseContractField1;
/// @dev Documentation for `baseContractField2`.
/// @param 0 Documentation for `0`.
bytes32[] public baseContractField2;
/// @dev Documentation for `_baseContractField3`.
uint256 private _baseContractField3;
/// @dev Documentation for `baseContractMethod1`.
/// @param p1 Documentation for `p1`.
/// @param p2 Documentation for `p2`.
/// @return 0 Documentation for `0`.
function baseContractMethod1(bytes memory p1, bytes32 p2)
internal
returns (InterfaceContract.InterfaceStruct memory)
{}
}

View File

@ -0,0 +1,14 @@
pragma solidity ^0.5;
contract InterfaceContract {
/// @dev Documentation for `InterfaceStruct`.
/// @param structField2 Documentation for `structField2`.
struct InterfaceStruct {
address structField1; // Documentation for `structField1`.
uint256 structField2; // Stuff to ignore.
// Documentation for `structField3`.
bytes32 structField3;
}
}

View File

@ -0,0 +1,22 @@
pragma solidity ^0.5;
/// @dev Documentation for `LibraryContract`.
contract LibraryContract {
/// @dev Documentation for `LibraryContractEnum`.
/// @param EnumMember1 Documentation for `EnumMember1`.
enum LibraryContractEnum {
EnumMember1,
EnumMember2, // Documentation for `EnumMember2`.
// Documentation for `EnumMember3`.
EnumMember3,
EnumMember4
}
/// @dev Documentation for `LibraryStruct`.
/// @param structField Documentation for `structField`.
struct LibraryStruct {
mapping (bytes32 => address) structField;
}
}

View File

@ -0,0 +1,55 @@
pragma solidity ^0.5;
pragma experimental ABIEncoderV2;
import "./InterfaceContract.sol";
import "./LibraryContract.sol";
import "./BaseContract.sol";
/// @dev Documentation for `TestContract`.
contract TestContract is
BaseContract,
InterfaceContract
{
/// @dev Documentation for `testContractMethod1`.
function testContractMethod1() public {}
// Stuff to ignore.
/// @dev Documentation for `testContractMethod2`.
/// @param p2 Documentation for `p2`.
/// @param p1 Documentation for `p1`.
/// @param p3 Documentation for `p3`.
/// @return r1 Documentation for `r1`.
function testContractMethod2(
address p1,
uint256 p2,
LibraryContract.LibraryContractEnum p3
)
internal
returns (int32 r1)
{
return r1;
}
/// @dev Documentation for `testContractMethod3`.
/// @param p1 Documentation for `p1`.
/// @return r1 Documentation for `r1`.
function testContractMethod3(InterfaceContract.InterfaceStruct calldata p1)
external
returns (bytes32[][] memory r1)
{
return r1;
}
// Documentation for `testContractMethod4`.
function testContractMethod4(
LibraryContract.LibraryStruct[] storage p1,
InterfaceContract.InterfaceStruct[] memory p2,
bytes[] memory p3
)
private
returns (bytes memory r1, bytes memory r2)
{
return (r1, r2);
}
}

View File

@ -1,273 +0,0 @@
import * as _ from 'lodash';
import * as chai from 'chai';
import 'mocha';
import { DocAgnosticFormat, Event, SolidityMethod } from '@0x/types';
import { SolDoc } from '../src/sol_doc';
import { chaiSetup } from './util/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const solDoc = new SolDoc();
describe('#SolidityDocGenerator', () => {
it('should generate a doc object that matches the devdoc-free TokenTransferProxy fixture', async () => {
const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
'TokenTransferProxyNoDevdoc',
]);
expect(doc).to.not.be.undefined();
verifyTokenTransferProxyABIIsDocumented(doc, 'TokenTransferProxyNoDevdoc');
});
const docPromises: Array<Promise<DocAgnosticFormat>> = [
solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`),
solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, []),
];
docPromises.forEach(docPromise => {
it('should generate a doc object that matches the TokenTransferProxy fixture with its dependencies', async () => {
const doc = await docPromise;
expect(doc).to.not.be.undefined();
verifyTokenTransferProxyAndDepsABIsAreDocumented(doc, 'TokenTransferProxy');
let addAuthorizedAddressMethod: SolidityMethod | undefined;
for (const method of doc.TokenTransferProxy.methods) {
if (method.name === 'addAuthorizedAddress') {
addAuthorizedAddressMethod = method;
}
}
const tokenTransferProxyAddAuthorizedAddressComment = 'Authorizes an address.';
expect((addAuthorizedAddressMethod as SolidityMethod).comment).to.equal(
tokenTransferProxyAddAuthorizedAddressComment,
);
const expectedParamComment = 'Address to authorize.';
expect((addAuthorizedAddressMethod as SolidityMethod).parameters[0].comment).to.equal(expectedParamComment);
});
});
it('should generate a doc object that matches the TokenTransferProxy fixture', async () => {
const doc: DocAgnosticFormat = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
'TokenTransferProxy',
]);
verifyTokenTransferProxyABIIsDocumented(doc, 'TokenTransferProxy');
});
describe('when processing all the permutations of devdoc stuff that we use in our contracts', () => {
let doc: DocAgnosticFormat;
before(async () => {
doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, ['NatspecEverything']);
expect(doc).to.not.be.undefined();
expect(doc.NatspecEverything).to.not.be.undefined();
});
it('should emit the contract @title as its comment', () => {
expect(doc.NatspecEverything.comment).to.equal('Contract Title');
});
describe('should emit public method documentation for', () => {
let methodDoc: SolidityMethod;
before(() => {
// tslint:disable-next-line:no-unnecessary-type-assertion
methodDoc = doc.NatspecEverything.methods.find(method => {
return method.name === 'publicMethod';
}) as SolidityMethod;
if (methodDoc === undefined) {
throw new Error('publicMethod not found');
}
});
it('method name', () => {
expect(methodDoc.name).to.equal('publicMethod');
});
it('method comment', () => {
expect(methodDoc.comment).to.equal('publicMethod @dev');
});
it('parameter name', () => {
expect(methodDoc.parameters[0].name).to.equal('p');
});
it('parameter comment', () => {
expect(methodDoc.parameters[0].comment).to.equal('publicMethod @param');
});
it('return type', () => {
expect(methodDoc.returnType.name).to.equal('int256');
});
it('return comment', () => {
expect(methodDoc.returnComment).to.equal('publicMethod @return');
});
});
describe('should emit external method documentation for', () => {
let methodDoc: SolidityMethod;
before(() => {
// tslint:disable-next-line:no-unnecessary-type-assertion
methodDoc = doc.NatspecEverything.methods.find(method => {
return method.name === 'externalMethod';
}) as SolidityMethod;
if (methodDoc === undefined) {
throw new Error('externalMethod not found');
}
});
it('method name', () => {
expect(methodDoc.name).to.equal('externalMethod');
});
it('method comment', () => {
expect(methodDoc.comment).to.equal('externalMethod @dev');
});
it('parameter name', () => {
expect(methodDoc.parameters[0].name).to.equal('p');
});
it('parameter comment', () => {
expect(methodDoc.parameters[0].comment).to.equal('externalMethod @param');
});
it('return type', () => {
expect(methodDoc.returnType.name).to.equal('int256');
});
it('return comment', () => {
expect(methodDoc.returnComment).to.equal('externalMethod @return');
});
});
it('should not truncate a multi-line devdoc comment', () => {
// tslint:disable-next-line:no-unnecessary-type-assertion
const methodDoc: SolidityMethod = doc.NatspecEverything.methods.find(method => {
return method.name === 'methodWithLongDevdoc';
}) as SolidityMethod;
if (methodDoc === undefined) {
throw new Error('methodWithLongDevdoc not found');
}
expect(methodDoc.comment).to.equal(
'Here is a really long developer documentation comment, which spans multiple lines, for the purposes of making sure that broken lines are consolidated into one devdoc comment.',
);
});
describe('should emit event documentation for', () => {
let eventDoc: Event;
before(() => {
eventDoc = (doc.NatspecEverything.events as Event[])[0];
});
it('event name', () => {
expect(eventDoc.name).to.equal('AnEvent');
});
it('parameter name', () => {
expect(eventDoc.eventArgs[0].name).to.equal('p');
});
});
it('should not let solhint directives obscure natspec content', () => {
// tslint:disable-next-line:no-unnecessary-type-assertion
const methodDoc: SolidityMethod = doc.NatspecEverything.methods.find(method => {
return method.name === 'methodWithSolhintDirective';
}) as SolidityMethod;
if (methodDoc === undefined) {
throw new Error('methodWithSolhintDirective not found');
}
expect(methodDoc.comment).to.equal('methodWithSolhintDirective @dev');
});
});
it('should document a method that returns multiple values', async () => {
const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
'MultipleReturnValues',
]);
expect(doc.MultipleReturnValues).to.not.be.undefined();
expect(doc.MultipleReturnValues.methods).to.not.be.undefined();
let methodWithMultipleReturnValues: SolidityMethod | undefined;
for (const method of doc.MultipleReturnValues.methods) {
if (method.name === 'methodWithMultipleReturnValues') {
methodWithMultipleReturnValues = method;
}
}
if (methodWithMultipleReturnValues === undefined) {
throw new Error('method should not be undefined');
}
const returnType = methodWithMultipleReturnValues.returnType;
expect(returnType.typeDocType).to.equal('tuple');
if (returnType.tupleElements === undefined) {
throw new Error('returnType.tupleElements should not be undefined');
}
expect(returnType.tupleElements.length).to.equal(2);
});
it('should document a method that has a struct param and return value', async () => {
const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
'StructParamAndReturn',
]);
expect(doc.StructParamAndReturn).to.not.be.undefined();
expect(doc.StructParamAndReturn.methods).to.not.be.undefined();
let methodWithStructParamAndReturn: SolidityMethod | undefined;
for (const method of doc.StructParamAndReturn.methods) {
if (method.name === 'methodWithStructParamAndReturn') {
methodWithStructParamAndReturn = method;
}
}
if (methodWithStructParamAndReturn === undefined) {
throw new Error('method should not be undefined');
}
/**
* Solc maps devDoc comments to methods using a method signature. If we incorrectly
* generate the methodSignatures, the devDoc comments won't be correctly associated
* with their methods and they won't show up in the output. By checking that the comments
* are included for a method with structs as params/returns, we are sure that the methodSignature
* generation is correct for this case.
*/
expect(methodWithStructParamAndReturn.comment).to.be.equal('DEV_COMMENT');
expect(methodWithStructParamAndReturn.returnComment).to.be.equal('RETURN_COMMENT');
expect(methodWithStructParamAndReturn.parameters[0].comment).to.be.equal('STUFF_COMMENT');
});
it('should document the structs included in a contract', async () => {
const doc = await solDoc.generateSolDocAsync(`${__dirname}/../../test/fixtures/contracts`, [
'StructParamAndReturn',
]);
expect(doc.structs).to.not.be.undefined();
expect(doc.structs.types.length).to.be.equal(1);
});
});
function verifyTokenTransferProxyABIIsDocumented(doc: DocAgnosticFormat, contractName: string): void {
expect(doc[contractName]).to.not.be.undefined();
expect(doc[contractName].constructors).to.not.be.undefined();
const tokenTransferProxyConstructorCount = 0;
const tokenTransferProxyMethodCount = 8;
const tokenTransferProxyEventCount = 3;
expect(doc[contractName].constructors.length).to.equal(tokenTransferProxyConstructorCount);
expect(doc[contractName].methods.length).to.equal(tokenTransferProxyMethodCount);
const events = doc[contractName].events;
if (events === undefined) {
throw new Error('events should never be undefined');
}
expect(events.length).to.equal(tokenTransferProxyEventCount);
}
function verifyTokenTransferProxyAndDepsABIsAreDocumented(doc: DocAgnosticFormat, contractName: string): void {
verifyTokenTransferProxyABIIsDocumented(doc, contractName);
expect(doc.ERC20).to.not.be.undefined();
expect(doc.ERC20.constructors).to.not.be.undefined();
expect(doc.ERC20.methods).to.not.be.undefined();
const erc20ConstructorCount = 0;
const erc20MethodCount = 6;
const erc20EventCount = 2;
expect(doc.ERC20.constructors.length).to.equal(erc20ConstructorCount);
expect(doc.ERC20.methods.length).to.equal(erc20MethodCount);
if (doc.ERC20.events === undefined) {
throw new Error('events should never be undefined');
}
expect(doc.ERC20.events.length).to.equal(erc20EventCount);
expect(doc.ERC20Basic).to.not.be.undefined();
expect(doc.ERC20Basic.constructors).to.not.be.undefined();
expect(doc.ERC20Basic.methods).to.not.be.undefined();
const erc20BasicConstructorCount = 0;
const erc20BasicMethodCount = 3;
const erc20BasicEventCount = 1;
expect(doc.ERC20Basic.constructors.length).to.equal(erc20BasicConstructorCount);
expect(doc.ERC20Basic.methods.length).to.equal(erc20BasicMethodCount);
if (doc.ERC20Basic.events === undefined) {
throw new Error('events should never be undefined');
}
expect(doc.ERC20Basic.events.length).to.equal(erc20BasicEventCount);
let addAuthorizedAddressMethod: SolidityMethod | undefined;
for (const method of doc[contractName].methods) {
if (method.name === 'addAuthorizedAddress') {
addAuthorizedAddressMethod = method;
}
}
expect(
addAuthorizedAddressMethod,
`method addAuthorizedAddress not found in ${JSON.stringify(doc[contractName].methods)}`,
).to.not.be.undefined();
}

View File

@ -0,0 +1,225 @@
import { chaiSetup } from '@0x/dev-utils';
import { expect } from 'chai';
import * as _ from 'lodash';
import { ContractKind, EventDocs, FunctionKind, MethodDocs, SolidityDocs, Visibility } from '../src/extract_docs';
import { transformDocs } from '../src/transform_docs';
import {
randomContract,
randomEnum,
randomEvent,
randomMethod,
randomParameter,
randomStruct,
randomWord,
} from './utils/random_docs';
chaiSetup.configure();
// tslint:disable: custom-no-magic-numbers
describe('transformDocs()', () => {
const INTERFACE_CONTRACT = 'InterfaceContract';
const TEST_CONTRACT = 'TestContract';
const BASE_CONTRACT = 'BaseContract';
const OTHER_CONTRACT = 'OtherContract';
const LIBRARY_CONTRACT = 'LibraryContract';
const LIBRARY_EVENT = 'LibraryContract.LibraryEvent';
const INTERFACE_EVENT = 'InterfaceContract.InterfaceEvent';
const BASE_CONTRACT_EVENT = 'BaseContract.BaseContractEvent';
const LIBRARY_ENUM = 'LibraryContract.LibraryEnum';
const INTERFACE_ENUM = 'InterfaceContract.InterfaceEnum';
const BASE_CONTRACT_ENUM = 'BaseContract.BaseContractEnum';
const LIBRARY_STRUCT = 'LibraryContract.LibraryStruct';
const INTERFACE_STRUCT = 'InterfaceContract.InterfaceStruct';
const BASE_CONTRACT_STRUCT = 'BaseContract.BaseContractStruct';
const OTHER_CONTRACT_STRUCT = 'OtherContract.OtherContractStruct';
const INPUT_DOCS: SolidityDocs = {
contracts: {
[LIBRARY_CONTRACT]: _.merge(randomContract(LIBRARY_CONTRACT, { kind: ContractKind.Library }), {
events: {
[LIBRARY_EVENT]: randomEvent({ contract: LIBRARY_CONTRACT }),
},
structs: {
[LIBRARY_STRUCT]: randomStruct({ contract: LIBRARY_CONTRACT }),
},
enums: {
[LIBRARY_ENUM]: randomEnum({ contract: LIBRARY_CONTRACT }),
},
}),
[INTERFACE_CONTRACT]: _.merge(randomContract(INTERFACE_CONTRACT, { kind: ContractKind.Interface }), {
events: {
[INTERFACE_EVENT]: randomEvent({ contract: INTERFACE_CONTRACT }),
},
structs: {
[INTERFACE_STRUCT]: randomStruct({ contract: INTERFACE_CONTRACT }),
},
enums: {
[INTERFACE_ENUM]: randomEnum({ contract: INTERFACE_CONTRACT }),
},
}),
[BASE_CONTRACT]: _.merge(randomContract(BASE_CONTRACT, { kind: ContractKind.Contract }), {
events: {
[BASE_CONTRACT_EVENT]: randomEvent({ contract: BASE_CONTRACT }),
},
structs: {
[BASE_CONTRACT_STRUCT]: randomStruct({ contract: BASE_CONTRACT }),
},
enums: {
[BASE_CONTRACT_ENUM]: randomEnum({ contract: BASE_CONTRACT }),
},
}),
[TEST_CONTRACT]: _.merge(
randomContract(TEST_CONTRACT, { kind: ContractKind.Contract, inherits: [BASE_CONTRACT] }),
{
methods: [
randomMethod({
contract: TEST_CONTRACT,
visibility: Visibility.External,
parameters: {
[randomWord()]: randomParameter(0, { type: INTERFACE_ENUM }),
},
}),
randomMethod({
contract: TEST_CONTRACT,
visibility: Visibility.Private,
parameters: {
[randomWord()]: randomParameter(0, { type: LIBRARY_STRUCT }),
},
}),
],
},
),
[OTHER_CONTRACT]: _.merge(randomContract(OTHER_CONTRACT, { kind: ContractKind.Contract }), {
structs: {
[OTHER_CONTRACT_STRUCT]: randomStruct({
contract: OTHER_CONTRACT,
fields: {
[randomWord()]: randomParameter(0, { type: LIBRARY_ENUM }),
},
}),
},
methods: [
randomMethod({
contract: OTHER_CONTRACT,
visibility: Visibility.Public,
returns: {
[randomWord()]: randomParameter(0, { type: OTHER_CONTRACT_STRUCT }),
},
}),
randomMethod({
contract: OTHER_CONTRACT,
visibility: Visibility.Internal,
returns: {
[randomWord()]: randomParameter(0, { type: INTERFACE_STRUCT }),
},
}),
],
events: [
randomEvent({
contract: OTHER_CONTRACT,
parameters: {
[randomWord()]: randomParameter(0, { type: LIBRARY_STRUCT }),
},
}),
],
}),
},
};
function getMethodId(method: MethodDocs): string {
if (method.kind === FunctionKind.Constructor) {
return 'constructor';
}
return getEventId(method);
}
function getEventId(method: EventDocs | MethodDocs): string {
const paramsTypes = Object.values(method.parameters).map(p => p.type);
return `${method.name}(${paramsTypes.join(',')})`;
}
function getAllTypes(docs: SolidityDocs): string[] {
const allTypes: string[] = [];
for (const contract of Object.values(docs.contracts)) {
for (const structName of Object.keys(contract.structs)) {
allTypes.push(structName);
}
for (const enumName of Object.keys(contract.enums)) {
allTypes.push(enumName);
}
}
return allTypes;
}
it('returns all contracts with no target contracts', () => {
const docs = transformDocs(INPUT_DOCS);
expect(Object.keys(docs.contracts)).to.deep.eq([
LIBRARY_CONTRACT,
INTERFACE_CONTRACT,
BASE_CONTRACT,
TEST_CONTRACT,
OTHER_CONTRACT,
]);
});
it('returns requested AND related contracts', () => {
const contracts = [TEST_CONTRACT, OTHER_CONTRACT];
const docs = transformDocs(INPUT_DOCS, { contracts });
expect(Object.keys(docs.contracts)).to.deep.eq([LIBRARY_CONTRACT, INTERFACE_CONTRACT, ...contracts]);
});
it('returns exposed and unexposed items by default', () => {
const contracts = [TEST_CONTRACT];
const docs = transformDocs(INPUT_DOCS, { contracts });
expect(Object.keys(docs.contracts)).to.deep.eq([LIBRARY_CONTRACT, INTERFACE_CONTRACT, ...contracts]);
const allTypes = getAllTypes(docs);
// Check for an exposed type.
expect(allTypes).to.include(INTERFACE_ENUM);
// Check for an unexposed type.
expect(allTypes).to.include(LIBRARY_STRUCT);
});
it('can hide unexposed items', () => {
const contracts = [OTHER_CONTRACT];
const docs = transformDocs(INPUT_DOCS, { contracts, onlyExposed: true });
expect(Object.keys(docs.contracts)).to.deep.eq([LIBRARY_CONTRACT, ...contracts]);
const allTypes = getAllTypes(docs);
// Check for an exposed type.
expect(allTypes).to.include(LIBRARY_ENUM);
// Check for an unexposed type.
expect(allTypes).to.not.include(INTERFACE_STRUCT);
});
describe('flattening', () => {
it('merges inherited methods', () => {
const docs = transformDocs(INPUT_DOCS, { contracts: [TEST_CONTRACT], flatten: true });
const allMethods = _.uniqBy(
_.flatten(
[BASE_CONTRACT, TEST_CONTRACT].map(c =>
INPUT_DOCS.contracts[c].methods.filter(m => m.visibility !== Visibility.Private),
),
),
m => getMethodId(m),
);
const outputMethods = docs.contracts[TEST_CONTRACT].methods;
expect(outputMethods).to.length(allMethods.length);
for (const method of outputMethods) {
expect(allMethods.map(m => getMethodId(m))).to.include(getMethodId(method));
}
});
it('merges inherited events', () => {
const docs = transformDocs(INPUT_DOCS, { contracts: [TEST_CONTRACT], flatten: true });
const allEvents = _.uniqBy(
_.flatten([BASE_CONTRACT, TEST_CONTRACT].map(c => INPUT_DOCS.contracts[c].events)),
e => getEventId(e),
);
const outputEvents = docs.contracts[TEST_CONTRACT].events;
expect(outputEvents).to.length(allEvents.length);
for (const event of outputEvents) {
expect(allEvents.map(m => getEventId(m))).to.include(getEventId(event));
}
});
});
});

View File

@ -1,13 +0,0 @@
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
import ChaiBigNumber = require('chai-bignumber');
import * as dirtyChai from 'dirty-chai';
export const chaiSetup = {
configure(): void {
chai.config.includeStack = true;
chai.use(ChaiBigNumber());
chai.use(dirtyChai);
chai.use(chaiAsPromised);
},
};

View File

@ -0,0 +1,175 @@
import * as _ from 'lodash';
import {
ContractDocs,
ContractKind,
DocumentedItem,
EnumDocs,
EnumValueDocs,
EventDocs,
FunctionKind,
MethodDocs,
ParamDocs,
ParamDocsMap,
StateMutability,
StorageLocation,
StructDocs,
Visibility,
} from '../../src/extract_docs';
// tslint:disable: custom-no-magic-numbers completed-docs
const LETTERS = _.times(26, n => String.fromCharCode('a'.charCodeAt(0) + n));
export function randomWord(maxLength: number = 13): string {
return _.sampleSize(LETTERS, _.random(1, maxLength)).join('');
}
export function randomSentence(): string {
const numWords = _.random(3, 64);
return _.capitalize(
_.times(numWords, () => randomWord())
.join(' ')
.concat('.'),
);
}
export function randomDocs(): DocumentedItem {
return {
doc: randomSentence(),
line: _.random(1, 65536),
file: _.capitalize(randomWord()).concat('.sol'),
};
}
export function randomBoolean(): boolean {
return _.random(0, 1) === 1;
}
export function randomType(): string {
return _.sampleSize(['uint256', 'bytes32', 'bool', 'uint32', 'int256', 'int64', 'uint8'], 1)[0];
}
export function randomStorageLocation(): StorageLocation {
return _.sampleSize([StorageLocation.Default, StorageLocation.Memory, StorageLocation.Storage])[0];
}
export function randomContractKind(): ContractKind {
return _.sampleSize([ContractKind.Contract, ContractKind.Interface, ContractKind.Library])[0];
}
export function randomMutability(): StateMutability {
return _.sampleSize([
StateMutability.Nonpayable,
StateMutability.Payable,
StateMutability.Pure,
StateMutability.View,
])[0];
}
export function randomVisibility(): Visibility {
return _.sampleSize([Visibility.External, Visibility.Internal, Visibility.Public, Visibility.Private])[0];
}
export function randomFunctionKind(): FunctionKind {
return _.sampleSize([FunctionKind.Constructor, FunctionKind.Fallback, FunctionKind.Function])[0];
}
export function randomParameters(): ParamDocsMap {
const numParams = _.random(0, 7);
return _.zipObject(_.times(numParams, () => randomWord()), _.times(numParams, idx => randomParameter(idx)));
}
export function randomParameter(order: number, fields?: Partial<ParamDocs>): ParamDocs {
return {
...randomDocs(),
type: randomType(),
indexed: randomBoolean(),
storageLocation: randomStorageLocation(),
order,
...fields,
};
}
export function randomEvent(fields?: Partial<EventDocs>): EventDocs {
return {
...randomDocs(),
contract: `${randomWord()}Contract`,
name: `${randomWord()}Event`,
parameters: randomParameters(),
...fields,
};
}
export function randomMethod(fields?: Partial<MethodDocs>): MethodDocs {
return {
...randomDocs(),
contract: `${randomWord()}Contract`,
name: `${randomWord()}Method`,
kind: randomFunctionKind(),
isAccessor: randomBoolean(),
stateMutability: randomMutability(),
visibility: randomVisibility(),
returns: randomParameters(),
parameters: randomParameters(),
...fields,
};
}
export function randomStruct(fields?: Partial<StructDocs>): StructDocs {
return {
...randomDocs(),
contract: `${randomWord()}Contract`,
fields: randomParameters(),
...fields,
};
}
export function randomEnum(fields?: Partial<EnumDocs>): EnumDocs {
return {
...randomDocs(),
contract: `${randomWord()}Contract`,
values: _.mapValues(
_.groupBy(
_.times(_.random(1, 8), i => ({
...randomDocs(),
value: i,
name: randomWord(),
})),
'name',
),
v => (_.omit(v[0], 'name') as any) as EnumValueDocs,
),
...fields,
};
}
export function randomContract(contractName: string, fields?: Partial<ContractDocs>): ContractDocs {
return {
...randomDocs(),
kind: randomContractKind(),
inherits: [],
events: _.times(_.random(1, 4), () => randomEvent({ contract: contractName })),
methods: _.times(_.random(1, 4), () => randomMethod({ contract: contractName })),
structs: _.mapValues(
_.groupBy(
_.times(_.random(1, 4), () => ({
...randomStruct({ contract: contractName }),
name: `${randomWord()}Struct`,
})),
'name',
),
v => (_.omit(v[0], 'name') as any) as StructDocs,
),
enums: _.mapValues(
_.groupBy(
_.times(_.random(1, 4), () => ({
...randomEnum({ contract: contractName }),
name: `${randomWord()}Enum`,
})),
'name',
),
v => (_.omit(v[0], 'name') as any) as EnumDocs,
),
...fields,
};
}