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:
parent
9d5724e1a0
commit
b7b457b076
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
|
||||||
};
|
};
|
||||||
|
@ -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';
|
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
1
packages/sol-doc/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/docs
|
10
packages/sol-doc/.npmignore
Normal file
10
packages/sol-doc/.npmignore
Normal 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
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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' });
|
||||||
|
}
|
||||||
|
638
packages/sol-doc/src/extract_docs.ts
Normal file
638
packages/sol-doc/src/extract_docs.ts
Normal 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
|
232
packages/sol-doc/src/gen_md.ts
Normal file
232
packages/sol-doc/src/gen_md.ts
Normal 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 ` *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)),
|
||||||
|
];
|
||||||
|
}
|
@ -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';
|
||||||
|
231
packages/sol-doc/src/sol_ast.ts
Normal file
231
packages/sol-doc/src/sol_ast.ts
Normal 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
|
@ -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
|
|
235
packages/sol-doc/src/transform_docs.ts
Normal file
235
packages/sol-doc/src/transform_docs.ts
Normal 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);
|
||||||
|
}
|
514
packages/sol-doc/test/extract_docs_test.ts
Normal file
514
packages/sol-doc/test/extract_docs_test.ts
Normal 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
|
@ -1,7 +0,0 @@
|
|||||||
pragma solidity ^0.4.24;
|
|
||||||
|
|
||||||
contract MultipleReturnValues {
|
|
||||||
function methodWithMultipleReturnValues() public pure returns(int, int) {
|
|
||||||
return (0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
114
packages/sol-doc/test/gen_md_test.ts
Normal file
114
packages/sol-doc/test/gen_md_test.ts
Normal 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
|
43
packages/sol-doc/test/inputs/BaseContract.sol
Normal file
43
packages/sol-doc/test/inputs/BaseContract.sol
Normal 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)
|
||||||
|
{}
|
||||||
|
}
|
14
packages/sol-doc/test/inputs/InterfaceContract.sol
Normal file
14
packages/sol-doc/test/inputs/InterfaceContract.sol
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
22
packages/sol-doc/test/inputs/LibraryContract.sol
Normal file
22
packages/sol-doc/test/inputs/LibraryContract.sol
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
55
packages/sol-doc/test/inputs/TestContract.sol
Normal file
55
packages/sol-doc/test/inputs/TestContract.sol
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
225
packages/sol-doc/test/transform_docs_test.ts
Normal file
225
packages/sol-doc/test/transform_docs_test.ts
Normal 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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
175
packages/sol-doc/test/utils/random_docs.ts
Normal file
175
packages/sol-doc/test/utils/random_docs.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user