diff --git a/CODEOWNERS b/CODEOWNERS index 3cf75fb2df..94af97d9d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -25,6 +25,7 @@ packages/monorepo-scripts/ @fabioberger packages/order-utils/ @fabioberger @LogvinovLeon packages/sol-compiler/ @LogvinovLeon packages/sol-cov/ @LogvinovLeon +packages/sol-meta/ @recmo packages/sol-resolver/ @LogvinovLeon packages/subproviders/ @fabioberger @dekz packages/verdaccio/ @albrow diff --git a/packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol b/packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol index 176e283511..0989dda59e 100644 --- a/packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol +++ b/packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol @@ -17,6 +17,7 @@ */ pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; import "../../utils/ReentrancyGuard/ReentrancyGuard.sol"; @@ -239,18 +240,18 @@ contract MixinSignatureValidator is view returns (bool isValid) { - bytes memory calldata = abi.encodeWithSelector( + bytes memory callData = abi.encodeWithSelector( IWallet(walletAddress).isValidSignature.selector, hash, signature ); assembly { - let cdStart := add(calldata, 32) + let cdStart := add(callData, 32) let success := staticcall( gas, // forward all gas walletAddress, // address of Wallet contract cdStart, // pointer to start of input - mload(calldata), // length of input + mload(callData), // length of input cdStart, // write output over input 32 // output size is 32 bytes ) @@ -288,19 +289,19 @@ contract MixinSignatureValidator is view returns (bool isValid) { - bytes memory calldata = abi.encodeWithSelector( + bytes memory callData = abi.encodeWithSelector( IValidator(signerAddress).isValidSignature.selector, hash, signerAddress, signature ); assembly { - let cdStart := add(calldata, 32) + let cdStart := add(callData, 32) let success := staticcall( gas, // forward all gas validatorAddress, // address of Validator contract cdStart, // pointer to start of input - mload(calldata), // length of input + mload(callData), // length of input cdStart, // write output over input 32 // output size is 32 bytes ) diff --git a/packages/contracts/contracts/protocol/Exchange/MixinTransactions.sol b/packages/contracts/contracts/protocol/Exchange/MixinTransactions.sol index 3a76ca2020..e73eb91ae9 100644 --- a/packages/contracts/contracts/protocol/Exchange/MixinTransactions.sol +++ b/packages/contracts/contracts/protocol/Exchange/MixinTransactions.sol @@ -16,6 +16,7 @@ */ pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; import "./libs/LibExchangeErrors.sol"; import "./mixins/MSignatureValidator.sol"; diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibFillResults.sol b/packages/contracts/contracts/protocol/Exchange/libs/LibFillResults.sol index 659ae9a699..00c6268e28 100644 --- a/packages/contracts/contracts/protocol/Exchange/libs/LibFillResults.sol +++ b/packages/contracts/contracts/protocol/Exchange/libs/LibFillResults.sol @@ -17,6 +17,7 @@ */ pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; import "../../../utils/SafeMath/SafeMath.sol"; diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibOrder.sol b/packages/contracts/contracts/protocol/Exchange/libs/LibOrder.sol index 0fe7c2161f..58e0e9b3b1 100644 --- a/packages/contracts/contracts/protocol/Exchange/libs/LibOrder.sol +++ b/packages/contracts/contracts/protocol/Exchange/libs/LibOrder.sol @@ -17,6 +17,7 @@ */ pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; import "./LibEIP712.sol"; @@ -45,13 +46,13 @@ contract LibOrder is // A valid order remains fillable until it is expired, fully filled, or cancelled. // An order's state is unaffected by external factors, like account balances. enum OrderStatus { - INVALID, // Default value - INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount - INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount - FILLABLE, // Order is fillable - EXPIRED, // Order has already expired - FULLY_FILLED, // Order is fully filled - CANCELLED // Order has been cancelled + INVALID, // 0 Default value + INVALID_MAKER_ASSET_AMOUNT, // 1 Order does not have a valid maker asset amount + INVALID_TAKER_ASSET_AMOUNT, // 2 Order does not have a valid taker asset amount + FILLABLE, // 3 Order is fillable + EXPIRED, // 4 Order has already expired + FULLY_FILLED, // 5 Order is fully filled + CANCELLED // 6 Order has been cancelled } // solhint-disable max-line-length diff --git a/packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol b/packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol index 99dd47a785..86561f83c3 100644 --- a/packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol +++ b/packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol @@ -92,53 +92,53 @@ contract ReentrantERC20Token is LibOrder.Order[] memory orders; uint256[] memory takerAssetFillAmounts; bytes[] memory signatures; - bytes memory calldata; + bytes memory cdata; - // Create calldata for function that corresponds to currentFunctionId + // Create cdata for function that corresponds to currentFunctionId if (currentFunctionId == uint8(ExchangeFunction.FILL_ORDER)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.fillOrder.selector, order, 0, signature ); } else if (currentFunctionId == uint8(ExchangeFunction.FILL_OR_KILL_ORDER)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.fillOrKillOrder.selector, order, 0, signature ); } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_FILL_ORDERS)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.batchFillOrders.selector, orders, takerAssetFillAmounts, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_FILL_OR_KILL_ORDERS)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.batchFillOrKillOrders.selector, orders, takerAssetFillAmounts, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.MARKET_BUY_ORDERS)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.marketBuyOrders.selector, orders, 0, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.MARKET_SELL_ORDERS)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.marketSellOrders.selector, orders, 0, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.MATCH_ORDERS)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.matchOrders.selector, order, order, @@ -146,22 +146,22 @@ contract ReentrantERC20Token is signature ); } else if (currentFunctionId == uint8(ExchangeFunction.CANCEL_ORDER)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.cancelOrder.selector, order ); } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_CANCEL_ORDERS)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.batchCancelOrders.selector, orders ); } else if (currentFunctionId == uint8(ExchangeFunction.CANCEL_ORDERS_UP_TO)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.cancelOrdersUpTo.selector, 0 ); } else if (currentFunctionId == uint8(ExchangeFunction.SET_SIGNATURE_VALIDATOR_APPROVAL)) { - calldata = abi.encodeWithSelector( + cdata = abi.encodeWithSelector( EXCHANGE.setSignatureValidatorApproval.selector, address(0), false @@ -169,7 +169,7 @@ contract ReentrantERC20Token is } // Call Exchange function, swallow error - address(EXCHANGE).call(calldata); + address(EXCHANGE).call(cdata); // Revert reason is 100 bytes bytes memory returnData = new bytes(100); @@ -185,4 +185,4 @@ contract ReentrantERC20Token is // Transfer will return true if function failed for any other reason return true; } -} \ No newline at end of file +} diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 8ee7fa4a91..bb366542a2 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -82,6 +82,10 @@ export class Compiler { private readonly _artifactsDir: string; private readonly _solcVersionIfExists: string | undefined; private readonly _specifiedContracts: string[] | TYPE_ALL_FILES_IDENTIFIER; + public static async getSolcAsync(solcVersion: string): Promise { + const solcInstance = (await this._getSolcAsync(solcVersion)).solcInstance; + return solcInstance; + } private static async _getSolcAsync( solcVersion: string, ): Promise<{ solcInstance: solc.SolcInstance; fullSolcVersion: string }> { diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts index e55cdf6ec9..dcb56b96d3 100644 --- a/packages/sol-cov/src/ast_visitor.ts +++ b/packages/sol-cov/src/ast_visitor.ts @@ -79,9 +79,6 @@ export class ASTVisitor { public WhileStatement(ast: Parser.WhileStatement): void { this._visitStatement(ast); } - public SimpleStatement(ast: Parser.SimpleStatement): void { - this._visitStatement(ast); - } public ThrowStatement(ast: Parser.ThrowStatement): void { this._visitStatement(ast); } @@ -159,7 +156,7 @@ export class ASTVisitor { } const loc = this._getExpressionRange(ast); this._fnMap[this._entryId++] = { - name: ast.name, + name: ast.name || '', line: loc.start.line, loc, }; diff --git a/packages/sol-cov/src/get_source_range_snippet.ts b/packages/sol-cov/src/get_source_range_snippet.ts index bea17beae9..b1529e45d5 100644 --- a/packages/sol-cov/src/get_source_range_snippet.ts +++ b/packages/sol-cov/src/get_source_range_snippet.ts @@ -105,9 +105,6 @@ class ASTInfoVisitor { public WhileStatement(ast: Parser.WhileStatement): void { this._visitStatement(ast); } - public SimpleStatement(ast: Parser.SimpleStatement): void { - this._visitStatement(ast); - } public ThrowStatement(ast: Parser.ThrowStatement): void { this._visitStatement(ast); } diff --git a/packages/sol-meta/README.md b/packages/sol-meta/README.md new file mode 100644 index 0000000000..3414cbfa18 --- /dev/null +++ b/packages/sol-meta/README.md @@ -0,0 +1,388 @@ +# 0x/sol-meta + +Sol-meta is a Solidity to Solidity compiler to automatically generate testing +contracts. It does two things: exposing internal logic and stubbing/scripting +of abstract functions. + +It works by generating a new contract that inherits the one under test. The +advantage of this approach is that it does not modify the code under test. The +downside is that we can not directly test contract members marked `private`. + +## Usage + +### Command line interface + +``` +Usage: sol-meta [options] + +Options: + --version Show version number [boolean] + --help Show help [boolean] + --config path to config file [string] + --remapping path remappings for import statements [array] + --includes search paths for imported source files [array] + --output directory to output too [string] + --test run all generated mock contracts through solc [boolean] +``` + +### Configuration file + +Example: + +```json +{ + "options": { + "sources": ["../contracts/contracts/protocol/**/*.sol", "../contracts/contracts/utils/**/*.sol"] + }, + "constructors": { + "LibConstants": ["\"ZRXASSETSTRING\""] + }, + "scripted": { + "hashZeroExTransaction": [ + { + "inputs": { + "salt": 32123, + "signerAddress": "0x0123123213" + }, + "revert": "Test failed. Should never be called." + }, + { + "inputs": { + "salt": 123 + }, + "outputs": ["0x0123456"] + } + ] + }, + "contracts": { + "LibEIP712": {}, + "MixinAssetProxyDispatcher": {}, + "MixinExchangeCore": {}, + "MixinMatchOrders": { + "constructors": { + "LibConstants": ["\"Different string\""] + } + }, + "MixinSignatureValidator": {}, + "MixinTransactions": {} + } +} +``` + +## Testing stategies + +The two main ways sol-meta helps with testing is by exposing internals and +stubbing abstract functions. Exposing makes it possible to access otherwise +unaccesible variables and functoins from a test. Stubbing implements missing +functions for you so you can test a partial contract such as a mixin in +isolation. + +### Exposing + +Given an implemented contract it generates a contract that exposes all internal +functions with public wrappers. + +#### Exposed Functions + +All functions marked `internal` receive a public wrapper. The name of +the wrapper is `functionNamePublic`. The wrapped function signature is +identical to the original. + +Private functions can not be exposed. There is currently no way to test them +directly due to the inheritance approach. + +Example generated wrapper: + +```solidity + function safeAddPublic(uint256 a, uint256 b) + public pure + returns (uint256 result) + { + return safeAdd(a, bb); + } +``` + +#### Exposed Modifiers + +For every modifier a testing function is generated that allows you to test if +the modifier executes or not. The name of the tester function is +`modifierNameTest` and it's arguments are the arguments of the modifier. When +the modifier allows execution, it will simply return `true`. + +Example generated tester: + +```solidity + function onlyAuthorizedTest(address user) + public + onlyAuthorized(user) + returns (bool executed) + { + return true; + } +``` + +#### Exposed Events + +For every event, a function is created that will trigger it. The name of this +function is `EventNameEmit` and the arguments are the log event arguments. + +Example generated emitter: + +```solidity + function TransferEmit(address from, address to, uint256 value) + public + { + emit Transfer(from, to, value); + } +``` + +#### Exposed Variables + +All contract variables are given getters and setters to allow the tester to +manipulate state variables directly. The getter is named `variableNameGet`, +takes no arguments and returns the current value. The setter is named +`variableNameSet` and takes an instance of the variable type as argument. + +Currently no support for maps is implemented. + +Example generated getter and setter: + +```solidity + function totalSupplyGet() + public view + returns (uint256) + { + return totalSupply; + } + + function totalSupplySet(uint256 value) + public + { + totalSupply = value; + } +``` + +### Scripting Functions + +Any abstract function can be scripted. For this, an entry in the configuration +file is required listing inputs and outputs the stubbed function should have. +In place of an output a revert reason can be given to make the stub throw. The +list of inputs can be partial to act as a filter. + +Example configuration: + +```json + "scripted": { + "hashZeroExTransaction": [ + { + "inputs": { + "salt": 123 + }, + "outputs": ["0x0123456"] + }, + { + "inputs": { + "salt": 32123, + "signerAddress": "0x0123123213" + }, + "revert": "Test failed: invalid signer" + } + ] + }, +``` + +Example generate stub for above configuration: + +```solidity + function hashZeroExTransaction( + uint256 salt, + address signerAddress, + bytes memory data + ) + internal pure + returns (bytes32 result) + { + if (salt == 123) + return 0x0123456; + + if (salt == 32123 && signerAddress == 0x0123123213) + require(false, "Test failed: invalid signer"); + + require(false, "Unscripted input for hashZeroExTransaction"); + } +``` + +#### Stubbed Functions + +For any abstract function that is not scripted, a stub is generated. Depending +on whether the function is `pure` and has return values, the behaviour is +slightly different. + +For non-pure functions, the stub will log the call with all the arguments and +give the next from a sequence of responses. The responses are programmed using +a separate function. When no response is scheduled, it will revert with +`Unprogrammed input for `. + +Due to usage of logs and storage, this can not be used to implement `pure` +functions. Pure functions will always revert with the reason +`Abstract function called`. Different behaviour can be +achieved with a scripted function. + +There is currently an unsolved issue in the stubber where it will not detect a +public variable as implementing an abstract function. This happens for example +when `IERC20Token` requires a function `totalSupply() public returns (uint256)` +and contract contains a state variable `public uint256 totalSupply;`. + +Example stub: + +```solidity + event isValidSignatureCalled(uint256 counter, bytes32 hash, address signerAddress, bytes signature); + + struct _isValidSignature_Result { + bool _enabled; // Should always be true + bool _reverts; // When set, revert + bool isValid; // Return value + } + + function isValidSignatureMock(uint256 _counter, _isValidSignature_Result _value) + public + { + // Schedule result number `_counter`. + } + + function isValidSignature(bytes32 hash, address signerAddress, bytes memory signature) + public view + returns (bool isValid) + { + // Log the inputs and return the next scheduled result. + // ... + } +``` + +#### Stubbed Actions + +Example: + +```solidity + event _preSign_log(uint256 counter, bytes32 hash, address signerAddress, bytes signature); + + function preSign(bytes32 hash, address signerAddress, bytes signature) + external + { + emit _preSign_log(_preSign_counter, hash, signerAddress, signature); + _preSign_counter++; + } +``` + +TODO: We need some sort of result programmability here anyway because we could +revert. + +#### Stubbed Pure Functions + +Example: + +```solidity + function hashZeroExTransaction(uint256 salt, address signerAddress, bytes memory data) + internal pure + returns (bytes32 result) + { + require(false, "Abstract function hashZeroExTransaction called"); + } +``` + +### Constructors + +A constructor is created that will call all parent constructors +that require arguments. The constructor arguments need to be supplied by config +file. + +Example, the following config file: + +```json + "constructors": { + "LibConstants": ["\"ZRXASSETSTRING\""] + }, +``` + +will allow sol-meta to construct contracts inheriting from `LibConstants`: + +```solidity + constructor() public + LibConstants("ZRXASSETSTRING") + { } +``` + +Note: Currently `sol-meta` assumes that all parent constructors need to be +called. If you have a contract `A is B` and `B is C` where `B` calls the +constructor of `C`, then `sol-meta` incorrectly assumes that `C` still needs to +be called. + +### Non Abstract Forcer + +Solidity by default does not produce any error or warning when a contract is +unintentionally abstract. Instead, output for that contract is silently ignored. +To force Solidity to error out in this case, a special contract is created that +will fail to compile if the original contract is abstract: + +```solidity +contract MixinExchangeCoreMockNonAbstractForcer { + constructor() public { + new MixinExchangeCoreMock; + } +} +``` + +## Contributing + +We welcome improvements and fixes from the wider community! To report bugs +within this package, please create an issue in this repository. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting +started. + +### Install dependencies + +If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them: + +```bash +yarn config set workspaces-experimental true +``` + +Then install dependencies + +```bash +yarn install +``` + +### Build + +To build this package and all other monorepo packages that it depends on, run +the following from the monorepo root directory: + +```bash +PKG=@0xproject/sol-compiler yarn build +``` + +Or continuously rebuild on change: + +```bash +PKG=@0xproject/sol-compiler yarn watch +``` + +### Clean + +```bash +yarn clean +``` + +### Lint + +```bash +yarn lint +``` + +### Run Tests + +```bash +yarn test +``` diff --git a/packages/sol-meta/bin/sol-meta.js b/packages/sol-meta/bin/sol-meta.js new file mode 100755 index 0000000000..0e5b69af0e --- /dev/null +++ b/packages/sol-meta/bin/sol-meta.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/src/cli.js') diff --git a/packages/sol-meta/package.json b/packages/sol-meta/package.json new file mode 100644 index 0000000000..7149ae7557 --- /dev/null +++ b/packages/sol-meta/package.json @@ -0,0 +1,87 @@ +{ + "name": "@0x/sol-meta", + "version": "1.0.0", + "engines": { + "node": ">=6.12" + }, + "description": "Solidity to Solidty compiler", + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", + "scripts": { + "build": "yarn pre_build && tsc", + "build:ci": "yarn build", + "build:watch": "tsc -w", + "pre_build": "run-s update_contract_fixtures", + "update_contract_fixtures": "copyfiles 'test/fixtures/contracts/**/*' ./lib", + "test": "yarn run_mocha", + "rebuild_and_test": "run-s build test", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/index.js --exit", + "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", + "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", + "clean": "shx rm -rf lib generated_docs", + "migrate": "npm run build; node lib/src/cli.js migrate", + "lint": "tslint --project .", + "test:circleci": "yarn test:coverage", + "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" + }, + "config": { + "postpublish": { + "assets": [] + } + }, + "bin": { + "sol-meta": "bin/sol-meta.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x-monorepo.git" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/0xProject/0x-monorepo/issues" + }, + "homepage": "https://github.com/0xProject/0x-monorepo/packages/sol-meta/README.md", + "devDependencies": { + "@0x/dev-utils": "^1.0.12", + "@0x/tslint-config": "^1.0.8", + "@types/mkdirp": "^0.5.2", + "@types/require-from-string": "^1.2.0", + "@types/semver": "^5.5.0", + "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-bignumber": "^2.0.2", + "copyfiles": "^2.0.0", + "dirty-chai": "^2.0.1", + "glob": "^7.1.3", + "make-promises-safe": "^1.1.0", + "mocha": "^4.1.0", + "npm-run-all": "^4.1.2", + "nyc": "^11.0.1", + "shx": "^0.2.2", + "tslint": "5.11.0", + "typescript": "3.0.1" + }, + "dependencies": { + "@0x/assert": "^1.0.13", + "@0x/json-schemas": "^1.0.7", + "@0x/sol-compiler": "^1.1.9", + "@0x/sol-resolver": "^1.0.14", + "@0x/types": "^1.1.4", + "@0x/typescript-typings": "^3.0.2", + "@0x/utils": "^2.0.2", + "@types/yargs": "^11.0.0", + "chalk": "^2.3.0", + "glob": "^7.1.3", + "lodash": "^4.17.5", + "mkdirp": "^0.5.1", + "require-from-string": "^2.0.1", + "semver": "5.5.0", + "solc": "0.4.24", + "solidity-parser-antlr": "^0.3.2", + "source-map-support": "^0.5.0", + "yargs": "^10.0.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/sol-meta/src/cli.ts b/packages/sol-meta/src/cli.ts new file mode 100644 index 0000000000..6a3ab636de --- /dev/null +++ b/packages/sol-meta/src/cli.ts @@ -0,0 +1,138 @@ +import 'source-map-support/register'; + +import { logUtils } from '@0x/utils'; +import * as _ from 'lodash'; +import * as pathUtils from 'path'; +import * as process from 'process'; +import * as yargs from 'yargs'; + +import { ContractMockOptions, mockContract, mockContractName } from './contract_mocker'; +import { compile } from './solc_wrapper'; +import { readSources, SourceReaderOptions } from './source_reader'; +import { unparse } from './unparser'; +import * as utils from './utils'; +import { visit } from './visitor'; + +(async () => { + // Parse command line arguments and handle help + const argv = yargs + .usage('Usage: $0 [options]') + .help() + .option('config', { + type: 'string', + description: 'path to config file', + }) + .option('remapping', { + type: 'string', + array: true, + description: 'path remappings for import statements', + }) + .option('includes', { + type: 'string', + array: true, + description: 'search paths for imported source files', + }) + .option('output', { + type: 'string', + description: 'directory to output too', + }) + .options('shouldTest', { + type: 'boolean', + description: 'run all generated mock contracts through solc', + }).argv; + + // TODO(recmo): Refactor to use a typed CommandLineOptions object + let options: Partial = {}; + const defaults: ContractMockOptions = { + constructors: {}, + scripted: {}, + }; + let contracts: { [contractName: string]: ContractMockOptions } = {}; + const outputPath = `${process.cwd()}/out`; + + // Handle config file + if (!_.isUndefined(argv.config)) { + const config = JSON.parse(await utils.readFileAsync(argv.config)); + if (config.options) { + options = { ...options, ...config.options }; + } + if (config.constructors) { + defaults.constructors = { ...defaults.constructors, ...config.constructors }; + } + if (config.scripted) { + defaults.scripted = { ...defaults.scripted, ...config.scripted }; + } + if (config.contracts) { + contracts = { ...contracts, ...config.contracts }; + } + } + + // Handle command line arguments + if (!_.isUndefined(argv.remapping)) { + options.remapping = _.reduce( + _.map(argv.remapping as string[], (str: string) => str.split('=', 2)), + (remappingsAccumulator, [from, to]) => ({ ...remappingsAccumulator, [from]: to }), + {}, + ); + } + if (!_.isUndefined(argv.includes)) { + options.includes = argv.includes; + } + + // Sources can be passed as --sources or as default argument `argv._` + options.sources = [...options.sources, ...argv._]; + + // Parse all sources + const sources = await readSources(options.sources, options); + + for (const contractName of _.keys(contracts)) { + logUtils.log(`\n${contractName}`); + const spec = { + constructors: { + ...defaults.constructors, + ...contracts[contractName].constructors, + }, + scripted: { + ...defaults.scripted, + ...contracts[contractName].scripted, + }, + }; + + // Find path + const contractPath = _.find(_.keys(sources), path => contractName in sources[path].contracts); + if (_.isUndefined(contractPath)) { + throw new Error(`Could not find contract ${contractName}.`); + } + + // Create mock contract + const mock = mockContract(sources, contractPath, contractName, spec); + + // Optionally test + if (!_.isUndefined(argv.test)) { + await compile(sources, mock); + } + + // Make import paths relative + mock.children = _.map(mock.children, node => + visit(node, { + ImportDirective: importDirective => ({ + ...importDirective, + path: pathUtils.relative(outputPath, importDirective.path), + }), + SourceMember: n => n, + }), + ); + + // Write file + await utils.writeFileAsync(`${outputPath}/${mockContractName(contractName)}.sol`, unparse(mock)); + } + + // Exit successfully + process.exit(0); + + // Catch errors, log and exit with failure +})().catch(err => { + // tslint:disable-next-line:no-console + console.error(err); + process.exit(1); +}); diff --git a/packages/sol-meta/src/constructor.ts b/packages/sol-meta/src/constructor.ts new file mode 100644 index 0000000000..0be50d0c36 --- /dev/null +++ b/packages/sol-meta/src/constructor.ts @@ -0,0 +1,88 @@ +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import * as utils from './utils'; + +export interface ConstructorArguments { + [contractName: string]: utils.Literal[]; +} + +/** + * Creates a constructor function AST node with a given parameter list + */ +export function makeConstructor(consArgs: ConstructorArguments): S.FunctionDefinition { + // TODO(recmo): Only include actually used constructors. + return { + type: S.NodeType.FunctionDefinition, + name: null, + parameters: { + type: S.NodeType.ParameterList, + parameters: [], + }, + returnParameters: null, + visibility: S.Visibility.Public, + stateMutability: S.StateMutability.Default, + modifiers: _.map(_.keys(consArgs), name => ({ + type: S.NodeType.ModifierInvocation, + name, + arguments: _.map(consArgs[name], utils.literal), + })), + isConstructor: true, + body: { + type: S.NodeType.Block, + statements: [], + }, + }; +} + +/** + * Solidity by itself does not give an error when a contract is unintentionally + * abstract. We can force Solidity to produce an error and point us to the + * abstract function by trying to runtime instantiate the contract. This function + * produces a small contract that tries to instantiate the given contract. + * + * @param contractName Name of the contract to force not-abstract. + */ +export function nonAbstractForcer(contractName: string): S.ContractDefinition { + // contract SomeContractNonAbstractForcer { + // constructor() { + // new SomeContract(); + // } + // } + return { + type: S.NodeType.ContractDefinition, + kind: S.ContractKind.Contract, + name: `${contractName}NonAbstractForcer`, + baseContracts: [], + subNodes: [ + { + type: S.NodeType.FunctionDefinition, + name: null, + parameters: { + type: S.NodeType.ParameterList, + parameters: [], + }, + returnParameters: null, + visibility: S.Visibility.Public, + stateMutability: S.StateMutability.Default, + modifiers: [], + isConstructor: true, + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ExpressionStatement, + expression: { + type: S.NodeType.NewExpression, + typeName: { + type: S.NodeType.UserDefinedTypeName, + namePath: contractName, + }, + }, + }, + ], + }, + }, + ], + }; +} diff --git a/packages/sol-meta/src/contract_mocker.ts b/packages/sol-meta/src/contract_mocker.ts new file mode 100644 index 0000000000..a0717d2983 --- /dev/null +++ b/packages/sol-meta/src/contract_mocker.ts @@ -0,0 +1,130 @@ +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import { makeConstructor, nonAbstractForcer } from './constructor'; +import { exposeNode } from './exposer'; +import { flattenContract } from './flattener'; +import { FunctionScript, scriptFunction } from './scripter'; +import { SourceCollection } from './source_reader'; +import { stubFunction } from './stubber'; +import * as utils from './utils'; + +export const mockContractName = (contractName: string) => `${contractName}Mock`; + +export interface ContractMockOptions { + constructors: { + [parentName: string]: utils.Literal[]; + }; + scripted: { + [functionName: string]: FunctionScript[]; + }; +} + +/** + * Mock a contract by implementing all abstract functions. + * @param sources The collection of relevant sources. + * @param path Absolute path to the contract source. + * @param contractName Name of the contract in the source. + * @param options Mocking options such as constructors and scripted behaviour. + */ +export function mockContract( + sources: SourceCollection, + path: string, + contractName: string, + options: ContractMockOptions, +): S.SourceUnit { + const sourceInfo = sources[path]; + + // Flatten target contract, this collapses the inheritane hierarchy + if (!(contractName in sourceInfo.contracts)) { + throw new Error(`Could not find contract ${contractName} in "${path}".`); + } + const [flat, parents] = flattenContract(sourceInfo.contracts[contractName], name => { + if (!(name in sourceInfo.scope)) { + throw new Error(`Contract ${name} not in scope of ${path}.`); + } + return sourceInfo.scope[name]; + }); + + // Find all constructors that require arguments + // TODO(recmo): Ignore constructors already called from other constructors + const constructors = _.map( + _.filter(parents, ({ subNodes }) => + subNodes.some( + node => + node.type === S.NodeType.FunctionDefinition && + node.isConstructor && + node.parameters.parameters.length > 0, + ), + ), + ({ name }) => name, + ); + + // Check that we have arguments for all constructors that require them + constructors.forEach(name => { + if (!(name in options.constructors)) { + throw new Error(`No arguments for constructor ${name} in ${contractName}.`); + } + }); + + // Find all abstract functions + // TODO(recmo): Public member variables generate implicit getter functions that can satisfy an abstract. + const abstracts = flat.subNodes.filter( + node => node.type === S.NodeType.FunctionDefinition && node.body === null, + ) as S.FunctionDefinition[]; + + // Separated in scripted and mocked functions + const scripted = abstracts.filter(({ name }) => name && name in options.scripted); + const mocked = abstracts.filter(({ name }) => name && !(name in options.scripted)); + + // Create mock contract + const mock: S.ContractDefinition = { + type: S.NodeType.ContractDefinition, + kind: S.ContractKind.Contract, + name: mockContractName(contractName), + + // Inherit from source contract + baseContracts: [ + { + type: S.NodeType.InheritanceSpecifier, + baseName: utils.userType(contractName), + }, + ], + subNodes: [ + // Call parent constructors + makeConstructor(utils.objectFilter(options.constructors, key => constructors.includes(key))), + + // Expose things marked `internal` + ...utils.flatMap(flat.subNodes, exposeNode), + + // Compile-time specified scripted functions + ..._.map(scripted, func => scriptFunction(func, options.scripted[func.name || ''])), + + // Mock all remaining abstract functions + ...utils.flatMap(mocked, stubFunction), + ], + }; + + // Create source file + return { + type: S.NodeType.SourceUnit, + children: [ + // Copy over pragmas from the source file + ...sources[path].parsed.children.filter(({ type }) => type === S.NodeType.PragmaDirective), + + // Add an import to include the source file + // TODO(recmo): Make relative + { + type: S.NodeType.ImportDirective, + path, + symbolAliases: null, + }, + + // Include our mock contract + mock, + + // Add a utility contract to force an abstract mock contract into a compile error. + nonAbstractForcer(mockContractName(contractName)), + ], + }; +} diff --git a/packages/sol-meta/src/exposer.ts b/packages/sol-meta/src/exposer.ts new file mode 100644 index 0000000000..d7b0898e4d --- /dev/null +++ b/packages/sol-meta/src/exposer.ts @@ -0,0 +1,234 @@ +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import { argumentExpressions, identifier, nameParameters } from './utils'; +import { visit, Visitor } from './visitor'; + +const getterName = (name: string) => `${name}Get`; +const setterName = (name: string) => `${name}Set`; +const publicName = (name: string) => `${name}Public`; +const logEmitterName = (name: string) => `${name}Emit`; +const modifierTestName = (name: string) => `${name}Test`; + +// Creates a public getter for a state variable +const getter = (stateVar: S.StateVariableDeclaration): S.FunctionDefinition => { + const [{ name, typeName }] = stateVar.variables; + return { + type: S.NodeType.FunctionDefinition, + name: getterName(name), + visibility: S.Visibility.Public, + isConstructor: false, + stateMutability: S.StateMutability.View, + parameters: { + type: S.NodeType.ParameterList, + parameters: [], + }, + returnParameters: { + type: S.NodeType.ParameterList, + parameters: [ + { + type: S.NodeType.Parameter, + typeName, + name: null, + storageLocation: S.StorageLocation.Default, + }, + ], + }, + modifiers: [], + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ReturnStatement, + expression: identifier(name), + }, + ], + }, + }; +}; + +// Creates a public getter for a state variable +const setter = (stateVar: S.StateVariableDeclaration): S.FunctionDefinition => { + const [{ name, typeName }] = stateVar.variables; + return { + type: S.NodeType.FunctionDefinition, + name: setterName(name), + visibility: S.Visibility.Public, + isConstructor: false, + stateMutability: S.StateMutability.View, + parameters: { + type: S.NodeType.ParameterList, + parameters: [ + { + type: S.NodeType.Parameter, + typeName, + name: 'setterNewValue', + storageLocation: S.StorageLocation.Default, + }, + ], + }, + returnParameters: null, + modifiers: [], + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ExpressionStatement, + expression: { + type: S.NodeType.BinaryOperation, + operator: '=', + left: identifier(name), + right: identifier('setterNewValue'), + }, + }, + ], + }, + }; +}; + +// Creates a public wrapper for a function +const wrapFunction = (func: S.FunctionDefinition): S.FunctionDefinition => { + if (func.name === null) { + throw new Error('Anonymous function.'); + } + const params = nameParameters(func.parameters); + const call: S.FunctionCall = { + type: S.NodeType.FunctionCall, + expression: identifier(func.name), + arguments: argumentExpressions(params), + names: [], + }; + return { + ...func, + name: publicName(func.name), + visibility: S.Visibility.Public, + parameters: params, + modifiers: [], + body: { + type: S.NodeType.Block, + statements: [ + func.returnParameters + ? { + type: S.NodeType.ReturnStatement, + expression: call, + } + : { + type: S.NodeType.ExpressionStatement, + expression: call, + }, + ], + }, + }; +}; + +// Creates a public function that triggers a log event +const emitEvent = (event: S.EventDefinition): S.FunctionDefinition => { + const { name, parameters } = event; + return { + type: S.NodeType.FunctionDefinition, + name: logEmitterName(name), + visibility: S.Visibility.Public, + isConstructor: false, + stateMutability: S.StateMutability.Default, + parameters: { + type: S.NodeType.ParameterList, + parameters: _.map(parameters.parameters, param => ({ + ...param, + isIndexed: false, + })), + }, + returnParameters: null, + modifiers: [], + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.EmitStatement, + eventCall: { + type: S.NodeType.FunctionCall, + expression: identifier(name), + arguments: argumentExpressions(parameters), + names: [], + }, + }, + ], + }, + }; +}; + +// Creates a public function that has modifier +const testModifier = (modifier: S.ModifierDefinition): S.FunctionDefinition => { + const { name, parameters } = modifier; + return { + type: S.NodeType.FunctionDefinition, + name: modifierTestName(name), + visibility: S.Visibility.Public, + isConstructor: false, + stateMutability: S.StateMutability.Default, + parameters: Array.isArray(parameters) + ? { + type: S.NodeType.ParameterList, + parameters: [], + } + : parameters, + returnParameters: { + type: S.NodeType.ParameterList, + parameters: [ + { + type: S.NodeType.Parameter, + typeName: { + type: S.NodeType.ElementaryTypeName, + name: 'bool', + }, + name: 'executed', + storageLocation: S.StorageLocation.Default, + }, + ], + }, + modifiers: [ + { + type: S.NodeType.ModifierInvocation, + name, + arguments: Array.isArray(parameters) ? [] : argumentExpressions(parameters), + }, + ], + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ReturnStatement, + expression: { + type: S.NodeType.BooleanLiteral, + value: true, + }, + }, + ], + }, + }; +}; + +const visitor: Visitor = { + StateVariableDeclaration: (node: S.StateVariableDeclaration) => { + const [vardecl] = node.variables; + if (vardecl.visibility !== 'internal') { + return []; + } + if (vardecl.isDeclaredConst) { + return [getter(node)]; + } + return [getter(node), setter(node)]; + // TODO(recmo): handle mappings: The keys become additional + // function arguments to the getter and setter. + }, + + EventDefinition: (node: S.EventDefinition) => [emitEvent(node)], + + ModifierDefinition: (node: S.ModifierDefinition) => [testModifier(node)], + + FunctionDefinition: (func: S.FunctionDefinition) => + func.visibility === S.Visibility.Internal ? [wrapFunction(func)] : [], + + ASTNode: (node: S.ASTNode) => [], +}; + +export const exposeNode = (node: S.ContractMember): S.ContractMember[] => visit(node, visitor); diff --git a/packages/sol-meta/src/flattener.ts b/packages/sol-meta/src/flattener.ts new file mode 100644 index 0000000000..d26c2b62d6 --- /dev/null +++ b/packages/sol-meta/src/flattener.ts @@ -0,0 +1,80 @@ +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import { linearize } from './linearization'; + +// See: https://solidity.readthedocs.io/en/v0.4.25/contracts.html#inheritance + +// Merge a base contract into a derived contract +function merge(base: S.ContractMember[], derived: S.ContractMember[]): S.ContractMember[] { + // Extracts functions by name from contract members + const functions = (contract: S.ContractMember[]) => + (contract.filter( + node => node.type === S.NodeType.FunctionDefinition && !node.isConstructor, + ) as S.FunctionDefinition[]).reduce<{ [name: string]: S.FunctionDefinition }>( + (a, func: S.FunctionDefinition) => ({ ...a, [func.name as string]: func }), + {}, + ); + + const modifiers = (contract: S.ContractMember[]) => + (contract.filter(({ type }) => type === S.NodeType.ModifierDefinition) as S.ModifierDefinition[]).reduce<{ + [name: string]: S.ModifierDefinition; + }>((a, mod: S.ModifierDefinition) => ({ ...a, [mod.name]: mod }), {}); + + const others = (contract: S.ContractMember[]): S.ContractMember[] => + contract.filter(({ type }) => type !== S.NodeType.ModifierDefinition && type !== S.NodeType.FunctionDefinition); + + // Solidity method lookup is as if all functions are virtual. The most + // derived version is always called. We can implement this by overriding + // the base implementation. + const mergedFunctions = { + ...functions(base), + ...functions(derived), + }; + + // The same applies to modifiers + const mergedModifiers = { + ...modifiers(base), + ...modifiers(derived), + }; + + // TODO(recmo): Merge constructors + // TODO(recmo): Implement rules that enforce type signature and visibility etc. + // to be preserved when overriding. + // TODO(recmo): Check other objects than functions. + // TODO(recmo): Sort members by type + // TODO(recmo): Less derived functions remain available through `super` + // and `SomeBase.functionName(...)`. + return [...others(base), ...others(derived), ...Object.values(mergedModifiers), ...Object.values(mergedFunctions)]; +} + +/** + * Inline the inheritance hierarchy of a contract + * @param contract Contract to flatten + * @param resolve Function to lookup contract definition by name + */ +export function flattenContract( + contract: S.ContractDefinition, + resolve: (name: string) => S.ContractDefinition, +): [S.ContractDefinition, S.ContractDefinition[]] { + // Close over resolve to create a parents function + const parents = (child: S.ContractDefinition) => + _.map(child.baseContracts, ({ baseName: { namePath } }) => resolve(namePath)); + + // Linearize the contract inheritance tree from least to most derived + const linear = linearize(contract, parents); + + // Merge contract members according to linearization + const members: S.ContractMember[] = linear.reduce((a: S.ContractMember[], { subNodes }) => merge(a, subNodes), []); + + // Concatenate contract bodies + return [ + { + ...contract, + baseContracts: [], + + subNodes: members, + }, + linear, + ]; +} diff --git a/packages/sol-meta/src/index.ts b/packages/sol-meta/src/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sol-meta/src/linearization.ts b/packages/sol-meta/src/linearization.ts new file mode 100644 index 0000000000..4f18176e76 --- /dev/null +++ b/packages/sol-meta/src/linearization.ts @@ -0,0 +1,75 @@ +import * as _ from 'lodash'; + +// Implements C3 Linearization as used in Solidity +// see: https://www.python.org/download/releases/2.3/mro/ + +/** + * Produce an array such that each input array is a subarray of the result. + * In other words, if the inputs specify lists sorted according to some partial + * order produce a sorted list of all elements compatible. + * + * NOTE: merge(a.reverse(), b.reversed(), ...) = merge(a, b, ...).reversed() + * (or at least equal up to the implied partial order) + * + * @param ilists List of input lists. + */ +export function merge(...ilists: T[][]): T[] { + // Only consider non-empty lists + const lists = ilists.filter(x => x.length > 0); + if (lists.length === 0) { + return []; + } + + // The first item of each list are heads, the remainders are tails + const heads = _.map(lists, ([head, ...tail]) => head); + const tails = _.map(lists, ([head, ...tail]) => tail); + + // A good head is one that does not occur in any tail. + const goodHead = heads.find(head => tails.every(tail => !tail.includes(head))); + + // If there is no valid head the hierarchy can not be linearized. + if (goodHead === undefined) { + throw new Error('Hierarchy can not be linearized.'); + } + + // Remove the good head from the lists + const newLists = _.map(lists, list => list.filter(elem => elem !== goodHead)); + + // Prepend head to the linearization and repeat + return [goodHead, ...merge(...newLists)]; +} + +/** + * Given a final element and a parents function, compute the C3 linearization + * of all ancestors of the final element. + * + * NOTE: Solidity has its ancestors in reverse order (base to most derived) + * compared to the Python C3 Linearization algorithm (most derived to + * base). This version uses the Solidity convention. + * + * NOTE: The nature of the algorithm makes it so that some cases where a + * linearization does exists, it is not found. The way merge picks + * an arbitrary solution adds additional constraints wich can lead + * to conflicts later on. It would be better if instead of recursion, + * a single large merge was done with all the constraints. But then + * it would no longer be C3 linearization. + * + * @param final The element to compute the linearization for. + * @param parents A function mapping the ordered list of parents for a given element. + */ +export function linearize(final: T, parents: (_: T) => T[]): T[] { + // TODO(recmo): Memoization + // TODO(recmo): Throw on cycles (instead of stack overflowing) + const p = parents(final); + const recurse = _.map(p, a => linearize(a, parents)); + return [...merge(p, ...recurse), final]; +} + +/** + * Asynchronous version of linearize. See linearize. + */ +export async function linearizeAsync(final: T, parentsAsync: (_: T) => Promise): Promise { + const p = await parentsAsync(final); + const recurse = await Promise.all(_.map(p, async a => linearizeAsync(a, parentsAsync))); + return [...merge(p, ...recurse), final]; +} diff --git a/packages/sol-meta/src/parser.ts b/packages/sol-meta/src/parser.ts new file mode 100644 index 0000000000..2313dff381 --- /dev/null +++ b/packages/sol-meta/src/parser.ts @@ -0,0 +1,6 @@ +import * as parser from 'solidity-parser-antlr'; + +export const parse = (source: string): parser.SourceUnit => parser.parse(source, {}); + +// TODO(recmo): Replace [] with empty parameter list on modifiers +// TODO(recmo): Push these fixes to upstream and remove them here. diff --git a/packages/sol-meta/src/scripter.ts b/packages/sol-meta/src/scripter.ts new file mode 100644 index 0000000000..3fa0d53f57 --- /dev/null +++ b/packages/sol-meta/src/scripter.ts @@ -0,0 +1,103 @@ +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import * as utils from './utils'; + +export interface FunctionScriptReturn { + inputs: { [argName: string]: string }; + outputs: [string]; +} + +export interface FunctionScriptRevert { + inputs: { [argName: string]: string }; + revert: string; +} + +export type FunctionScript = FunctionScriptReturn | FunctionScriptRevert; + +export const isFunctionScriptReturn = (fs: FunctionScript): fs is FunctionScriptReturn => 'outputs' in fs; + +export const isFunctionScriptRevert = (fs: FunctionScript): fs is FunctionScriptRevert => 'revert' in fs; + +export const errorUnscriptedInput = (funcName: string) => `Unscripted input for ${funcName}`; + +export const guard = (clauses: { [argName: string]: string }, statements: S.Statement[]): S.IfStatement => ({ + type: S.NodeType.IfStatement, + condition: Object.keys(clauses).reduce( + (cond, argName) => ({ + type: S.NodeType.BinaryOperation, + operator: '&&', + left: cond, + right: { + type: S.NodeType.BinaryOperation, + operator: '==', + left: utils.identifier(argName), + right: utils.literal(clauses[argName]), + }, + }), + utils.litTrue, + ), + trueBody: { + type: S.NodeType.Block, + statements, + }, + falseBody: null, +}); + +export const scriptStatementReturn = (s: FunctionScriptReturn): S.IfStatement => + guard(s.inputs, [ + { + type: S.NodeType.ReturnStatement, + expression: { + type: S.NodeType.TupleExpression, + isArray: false, + components: _.map(s.outputs, utils.literal), + }, + }, + ]); + +export const scriptStatementRevert = (s: FunctionScriptRevert): S.IfStatement => + guard(s.inputs, [ + { + type: S.NodeType.ExpressionStatement, + expression: { + type: S.NodeType.FunctionCall, + expression: utils.identifier('require'), + arguments: [utils.litFalse, utils.litString(s.revert)], + }, + }, + ] as S.ExpressionStatement[]); + +export const scriptStatement = (s: FunctionScript): S.IfStatement => + isFunctionScriptReturn(s) ? scriptStatementReturn(s) : scriptStatementRevert(s); + +export const scriptFunction = (func: S.FunctionDefinition, script: FunctionScript[]): S.FunctionDefinition => { + if (func.isConstructor || func.name === null) { + throw new Error(`Function can not be scripted because it is a constructor.`); + } + + const catchAllScript = [ + ...script, + { + inputs: {}, + revert: errorUnscriptedInput(func.name), + }, + ]; + + const params = utils.nameParameters(func.parameters); + const returns = utils.nameParameters(func.returnParameters as S.ParameterList, '_ret'); + return { + type: S.NodeType.FunctionDefinition, + name: func.name, + parameters: params, + returnParameters: returns, + visibility: func.visibility, + stateMutability: func.stateMutability, + modifiers: [], + isConstructor: false, + body: { + type: S.NodeType.Block, + statements: _.map(catchAllScript, scriptStatement), + }, + }; +}; diff --git a/packages/sol-meta/src/solc_wrapper.ts b/packages/sol-meta/src/solc_wrapper.ts new file mode 100644 index 0000000000..04714090ed --- /dev/null +++ b/packages/sol-meta/src/solc_wrapper.ts @@ -0,0 +1,55 @@ +import * as _ from 'lodash'; +import { ImportContents, StandardOutput } from 'solc'; +import * as S from 'solidity-parser-antlr'; + +import { Compiler as Solc } from '@0x/sol-compiler'; + +import { SourceCollection } from './source_reader'; +import { unparse } from './unparser'; +import * as utils from './utils'; + +export const compile = async (sources: SourceCollection, ast: S.SourceUnit) => { + // Extract required version from pragma of ast + const version = + _.map(utils.pragmaNodes(ast).filter(({ name }) => name === 'solidity'), ({ value }) => value)[0] || 'latest'; + + // Get Solidity compiler + const compiler = await Solc.getSolcAsync(version); + + // Solidity standard JSON input + // TODO(recmo): Typescript typings + // See: https://solidity.readthedocs.io/en/develop/using-the-compiler.html#compiler-input-and-output-json-description + const input = { + language: 'Solidity', + sources: { + ...utils.objectMap(sources, ({ source: content }) => ({ content })), + TARGET_: { content: unparse(ast) }, + }, + settings: { + remappings: { + // TODO(recmo) + }, + }, + outputSelection: { + TARGET_: { + '*': [], + }, + }, + }; + + // All imports should be accounted for in sources, throw an error in the callback + const findImportsCallback = (importPath: string): ImportContents => { + throw new Error(`Could not find ${importPath}.`); + }; + + // Run the compiler + const result: StandardOutput = JSON.parse( + compiler.compileStandardWrapper(JSON.stringify(input), findImportsCallback), + ); + + // Throw errors + const errors = result.errors.filter(({ severity }) => severity === 'error'); + if (errors.length > 0) { + throw new Error(_.map(errors, ({ formattedMessage }) => formattedMessage).join('\n')); + } +}; diff --git a/packages/sol-meta/src/source_reader.ts b/packages/sol-meta/src/source_reader.ts new file mode 100644 index 0000000000..25f31ed010 --- /dev/null +++ b/packages/sol-meta/src/source_reader.ts @@ -0,0 +1,171 @@ +import { Deferred } from '@0x/utils'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as pathUtils from 'path'; +import * as S from 'solidity-parser-antlr'; + +import * as utils from './utils'; + +export interface SourceReaderOptions { + remapping: { [prefix: string]: string }; + includes: string[]; + sources: string[]; +} + +export interface SourceInfo { + source: string; + parsed: S.SourceUnit; + contracts: { + [name: string]: S.ContractDefinition; + }; + scope: { + [name: string]: S.ContractDefinition; + }; +} + +// TODO: We are missing the remapping and import statements. This information is necessary +// to fully interpret the ImportDirectives. We could solve this by including the +// SourceReaderOptions as part of the SourceCollection, but then we'd need to +// put the path mapping in a separate variable. +export interface SourceCollection { + [path: string]: SourceInfo; +} + +// Helper class to read contract files. You can use the function `readContracts` instead. +export class ContractReader { + public static readonly defaultOptions: SourceReaderOptions = { + remapping: {}, + includes: [], + sources: [], + }; + + private readonly _opts: SourceReaderOptions; + + private readonly _result: { + [path: string]: Promise; + }; + + constructor(opts: Partial) { + this._opts = { ...ContractReader.defaultOptions, ...opts }; + this._result = {}; + + // Unglob the source and include paths + const unglob = (paths: string[]) => + utils.flatMap(paths, pattern => (glob.hasMagic(pattern) ? glob.sync(pattern) : [pattern])); + this._opts.sources = unglob(this._opts.sources); + this._opts.includes = unglob(this._opts.includes); + + // Utility to create absolute paths + const cwd = process.cwd(); + const makeAbsolute = (path: string) => (pathUtils.isAbsolute(path) ? path : pathUtils.join(cwd, path)); + + // Make all paths absolute + this._opts.sources = _.map(this._opts.sources, makeAbsolute); + this._opts.includes = _.map(this._opts.includes, makeAbsolute); + this._opts.remapping = utils.objectMap(this._opts.remapping, makeAbsolute); + } + + public async processSourcesAsync(): Promise { + // Read all contract sources and imports + await Promise.all(_.map(this._opts.sources, async path => this._readSourceAsync(path))); + + // Resolve the result + return utils.objectPromiseAsync(this._result); + } + + // Takes an import path and returns the absolute file path + private async _resolveAsync(sourcePath: string, importPath: string): Promise { + // Try relative path + if (importPath.startsWith('./') || importPath.startsWith('../')) { + const abs = pathUtils.join(pathUtils.dirname(sourcePath), importPath); + if (await utils.existsAsync(abs)) { + return Promise.resolve(abs); + } else { + throw new Error(`Import ${importPath} from ${sourcePath} could not be found.`); + } + } + + // Try remappings + for (const prefix in this._opts.remapping) { + if (this._opts.remapping.hasOwnProperty(prefix)) { + if (importPath.startsWith(prefix)) { + const replacement = this._opts.remapping[prefix]; + const abs = pathUtils.normalize(importPath.replace(prefix, replacement)); + if (await utils.existsAsync(abs)) { + return Promise.resolve(abs); + } else { + throw new Error(`Import ${importPath} from ${sourcePath} could not be found.`); + } + } + } + } + + // Try global include directories + for (const include of this._opts.includes) { + const abs = pathUtils.join(include, importPath); + if (await utils.existsAsync(abs)) { + return Promise.resolve(abs); + } + } + throw new Error(`Import ${importPath} from ${sourcePath} could not be found.`); + } + + private async _readSourceAsync(absolutePath: string): Promise { + // Try existing promises + if (absolutePath in this._result) { + return this._result[absolutePath]; + } + + // Create promise here so it will act as a mutex. + // When we recursively re-enter this function below, the deferred + // promise will already be there and we will not repeat the work. + const deffered = new Deferred(); + this._result[absolutePath] = deffered.promise; + + // Read and save in cache + const source = await utils.readFileAsync(absolutePath); + const parsed = S.parse(source, {}); + + // Resolve import statments paths + const imports = parsed.children.filter( + ({ type }) => type === S.NodeType.ImportDirective, + ) as S.ImportDirective[]; + const importPaths = await Promise.all( + _.map(imports, async ({ path }) => this._resolveAsync(absolutePath, path)), + ); + + // Recursively parse imported sources + // Note: This will deadlock on cyclical imports. (We will end up awaiting our own promise.) + // TODO(recmo): Throw an error instead. + const importInfo = await Promise.all(_.map(importPaths, async path => this._readSourceAsync(path))); + + // Compute global scope include imports + // TODO(recmo): Support `SomeContract as SomeAlias` in import directives. + let scope: { [name: string]: S.ContractDefinition } = {}; + importInfo.forEach(({ scope: importedContracts }) => { + scope = { ...scope, ...importedContracts }; + }); + + // Add local contracts + const contracts: { [name: string]: S.ContractDefinition } = {}; + utils.contracts(parsed).forEach(contract => { + contracts[contract.name] = contract; + scope[contract.name] = contract; + }); + + // Resolve deferred promise and return resolved promise + deffered.resolve({ + source, + parsed, + contracts, + scope, + }); + return deffered.promise; + } +} + +// TODO(remco): Deduplicate with sol-compiler +export const readSources = async ( + sources: string[], + options?: Partial, +): Promise => new ContractReader({ sources, ...(options || {}) }).processSourcesAsync(); diff --git a/packages/sol-meta/src/stubber.ts b/packages/sol-meta/src/stubber.ts new file mode 100644 index 0000000000..58071b91c8 --- /dev/null +++ b/packages/sol-meta/src/stubber.ts @@ -0,0 +1,375 @@ +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import * as utils from './utils'; +// Note(recmo): False positive, see https://github.com/palantir/tslint/issues/3418 +// tslint:disable-next-line:no-duplicate-imports +import { argumentExpressions, identifier, nameParameters } from './utils'; + +// TODO(recmo): both Actions and Functions can throw in addition to returning. Throwing should take arbitrary data. + +export const mockContractName = (contractName: string) => `${contractName}Mock`; +export const programmerName = (funcName: string) => `${funcName}Mock`; +export const logEventName = (funcName: string) => `${funcName}Called`; +export const errorNoMock = (funcName: string) => `Abstract function ${funcName} called`; +export const errorUnprogrammedInput = (funcName: string) => `Unprogrammed input for ${funcName}`; + +const call = (func: S.Expression, ...args: S.Expression[]): S.FunctionCall => ({ + type: S.NodeType.FunctionCall, + expression: func, + arguments: args, + names: [], +}); + +const emit = (name: string, ...args: S.Expression[]): S.EmitStatement => ({ + type: S.NodeType.EmitStatement, + eventCall: call(identifier(name), ...args), +}); + +const makeCounter = (name: string): S.StateVariableDeclaration => ({ + type: S.NodeType.StateVariableDeclaration, + variables: [ + { + type: S.NodeType.VariableDeclaration, + typeName: utils.types.uint256, + name, + expression: utils.litNumber(0), + visibility: S.Visibility.Internal, + isStateVar: true, + isDeclaredConst: false, + isIndexed: false, + }, + ], + initialValue: utils.litNumber(0), +}); + +const makeEvent = (name: string, parameters: S.ParameterList): S.EventDefinition => ({ + type: S.NodeType.EventDefinition, + name, + parameters, + isAnonymous: false, +}); + +const makeCountedEvent = (name: string, parameters: S.ParameterList) => + makeEvent(name, { + ...parameters, + parameters: [ + { + type: S.NodeType.Parameter, + name: 'counter', + typeName: utils.types.uint256, + storageLocation: S.StorageLocation.Default, + }, + ..._.map(parameters.parameters, param => ({ + ...param, + storageLocation: S.StorageLocation.Default, + })), + ], + }); + +const makeIncrement = (name: string): S.ExpressionStatement => ({ + type: S.NodeType.ExpressionStatement, + expression: { + type: S.NodeType.UnaryOperation, + operator: '++', + subExpression: identifier(name), + isPrefix: false, + }, +}); + +const makeAction = ( + name: string, + visibility: S.Visibility, + stateMutability: S.StateMutability, + counterName: string, + eventName: string, + parameters: S.ParameterList, +): S.FunctionDefinition => ({ + type: S.NodeType.FunctionDefinition, + name, + parameters, + returnParameters: null, + body: { + type: S.NodeType.Block, + statements: [ + emit(eventName, identifier(counterName), ...argumentExpressions(parameters)), + makeIncrement(counterName), + ], + }, + visibility, + modifiers: [], + isConstructor: false, + stateMutability, +}); + +const variableDeclaration = (name: string, typeName: S.Type): S.VariableDeclaration => ({ + type: S.NodeType.VariableDeclaration, + name, + typeName, + storageLocation: S.StorageLocation.Default, + isStateVar: false, + isIndexed: false, +}); + +const makeResultType = (name: string, fields: S.ParameterList): S.StructDefinition => ({ + type: S.NodeType.StructDefinition, + name, + members: [ + variableDeclaration('_enabled', utils.types.bool), + variableDeclaration('_reverts', utils.types.bool), + ..._.map(fields.parameters, ({ name: memberName, typeName }) => + variableDeclaration(memberName as string, typeName), + ), + ], +}); + +const userType = (name: string): S.UserDefinedTypeName => ({ + type: S.NodeType.UserDefinedTypeName, + namePath: name, +}); + +const mapping = (keyType: S.Type, valueType: S.Type): S.Type => ({ + type: S.NodeType.Mapping, + keyType, + valueType, +}); + +const makeStorageVariable = (name: string, type: S.Type): S.StateVariableDeclaration => ({ + type: S.NodeType.StateVariableDeclaration, + variables: [variableDeclaration(name, type)], + initialValue: null, +}); + +const makeSetter = (name: string, resultTypeName: string, resultMapName: string): S.FunctionDefinition => ({ + type: S.NodeType.FunctionDefinition, + name, + parameters: { + type: S.NodeType.ParameterList, + parameters: [ + { + type: S.NodeType.Parameter, + name: '_counter', + typeName: utils.types.uint256, + storageLocation: S.StorageLocation.Default, + isStateVar: false, + isIndexed: false, + }, + { + type: S.NodeType.Parameter, + name: '_value', + typeName: userType(resultTypeName), + storageLocation: S.StorageLocation.Default, + isStateVar: false, + isIndexed: false, + }, + ], + }, + returnParameters: null, + visibility: S.Visibility.Public, + modifiers: [], + isConstructor: false, + stateMutability: S.StateMutability.Default, + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ExpressionStatement, + expression: { + type: S.NodeType.BinaryOperation, + operator: '=', + left: { + type: S.NodeType.IndexAccess, + base: identifier(resultMapName), + index: identifier('_counter'), + }, + right: identifier('_value'), + }, + }, + ], + }, +}); + +const makeFunction = ( + name: string, + visibility: S.Visibility, + stateMutability: S.StateMutability, + counterName: string, + eventName: string, + resultTypeName: string, + resultMapName: string, + parameters: S.ParameterList, + returnParameters: S.ParameterList, +): S.FunctionDefinition => ({ + type: S.NodeType.FunctionDefinition, + name, + parameters, + returnParameters, + visibility, + stateMutability, + modifiers: [], + isConstructor: false, + body: { + type: S.NodeType.Block, + statements: [ + emit(eventName, identifier(counterName), ...argumentExpressions(parameters)), + { + type: S.NodeType.VariableDeclarationStatement, + variables: [ + { + ...variableDeclaration('_result', userType(resultTypeName)), + storageLocation: S.StorageLocation.Storage, + }, + ], + initialValue: { + type: S.NodeType.IndexAccess, + base: identifier(resultMapName), + index: identifier(counterName), + }, + }, + makeIncrement(counterName), + { + type: S.NodeType.IfStatement, + condition: { + type: S.NodeType.MemberAccess, + expression: identifier('_result'), + memberName: '_enabled', + }, + falseBody: null, + trueBody: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ExpressionStatement, + expression: call(identifier('require'), { + type: S.NodeType.UnaryOperation, + operator: '!', + isPrefix: true, + subExpression: { + type: S.NodeType.MemberAccess, + expression: identifier('_result'), + memberName: '_reverts', + }, + }), + }, + { + type: S.NodeType.ReturnStatement, + expression: { + type: S.NodeType.TupleExpression, + isArray: false, + components: _.map( + returnParameters.parameters, + ({ name: memberName }): S.MemberAccess => ({ + type: S.NodeType.MemberAccess, + expression: identifier('_result'), + memberName: memberName as string, + }), + ), + }, + }, + ], + }, + }, + { + type: S.NodeType.ExpressionStatement, + expression: call(identifier('require'), utils.litFalse, utils.litString(errorUnprogrammedInput(name))), + }, + ], + }, +}); + +export const stubThrow = (func: S.FunctionDefinition): S.ContractMember[] => { + if (func.isConstructor || func.name === null) { + throw new Error(`Function can not be mocked because it is a constructor.`); + } + return [ + { + ...func, + body: { + type: S.NodeType.Block, + statements: [ + { + type: S.NodeType.ExpressionStatement, + expression: call( + identifier('require'), + utils.litFalse, + utils.litString(errorNoMock(func.name)), + ), + }, + ], + }, + }, + ]; +}; + +export const stubAction = (func: S.FunctionDefinition): S.ContractMember[] => { + if (func.isConstructor || func.name === null) { + throw new Error(`Function can not be mocked because it is a constructor.`); + } + if (func.stateMutability === S.StateMutability.Pure) { + // Pure actions don't make sense, but check anyway + throw new Error(`Function ${func.name} can not be mocked because it is pure.`); + } + + const counterName = `_${func.name}_counter`; + const eventName = `_${func.name}_log`; + const params = nameParameters(func.parameters); + return [ + makeCounter(counterName), + makeCountedEvent(eventName, params), + makeAction(func.name, func.visibility, func.stateMutability, counterName, eventName, params), + ]; +}; + +export const stubFunctionRuntime = (func: S.FunctionDefinition): S.ContractMember[] => { + if (func.isConstructor || func.name === null) { + throw new Error(`Constructors can not be stubbed.`); + } + if (func.stateMutability === S.StateMutability.Pure) { + throw new Error(`Function ${func.name} can not be stubbed because it is pure.`); + } + + const counterName = `_${func.name}_counter`; + const resultTypeName = `_${func.name}_Result`; + const resultMapName = `_${func.name}_results`; + const eventName = logEventName(func.name); + const setterName = programmerName(func.name); + const params = nameParameters(func.parameters); + const returns = nameParameters(func.returnParameters as S.ParameterList, '_ret'); + return [ + makeCounter(counterName), + makeCountedEvent(eventName, params), + makeResultType(resultTypeName, returns), + makeStorageVariable(resultMapName, mapping(utils.types.uint256, userType(resultTypeName))), + makeSetter(setterName, resultTypeName, resultMapName), + makeFunction( + func.name, + func.visibility, + func.stateMutability, + counterName, + eventName, + resultTypeName, + resultMapName, + params, + returns, + ), + ]; +}; + +const isDeclaration = (func: S.FunctionDefinition) => func.body === null; + +const hasReturns = (func: S.FunctionDefinition) => func.returnParameters !== null; + +export const stubFunction = (func: S.FunctionDefinition): S.ContractMember[] => { + if (!isDeclaration(func)) { + throw new Error(`Can only stub abstract functions.`); + } + if (func.stateMutability === S.StateMutability.Pure) { + return stubThrow(func); + } else { + if (hasReturns(func)) { + return stubFunctionRuntime(func); + } else { + return stubAction(func); + } + } +}; diff --git a/packages/sol-meta/src/unparser.ts b/packages/sol-meta/src/unparser.ts new file mode 100644 index 0000000000..32b37c964a --- /dev/null +++ b/packages/sol-meta/src/unparser.ts @@ -0,0 +1,192 @@ +// TODO(recmo): instead use https://github.com/prettier-solidity/prettier-plugin-solidity/blob/master/src/printer.js + +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +import { visit, Visitor } from './visitor'; + +const stresc = (s: string) => `\"${s}\"`; +const indent = (s: string) => `\t${s.replace(/\n/g, '\n\t')}`; +const block = (s: string) => `{\n${indent(s)}\n}`; +const unparen = (s: string) => s.replace(/^\((.*)\)$/, '$1'); + +const visitor: Visitor = { + // Source level + + SourceUnit: ({ children }) => _.map(children, unparse).join('\n'), + + PragmaDirective: ({ name, value }) => `pragma ${name} ${value};`, + + ImportDirective: ({ path, symbolAliases }) => + `import ` + + (symbolAliases + ? `{${_.map(symbolAliases, ([from, to]) => from + (to ? ` as ${to}` : '')).join(', ')}} from ` + : '') + + `${stresc(path)};`, + + ContractDefinition: ({ name, kind, baseContracts, subNodes }) => + `${kind} ${name} ${baseContracts.length > 0 ? 'is ' : ''}` + + _.map(baseContracts, unparse).join(', ') + + block('\n' + _.map(subNodes, unparse).join('\n\n')), + + InheritanceSpecifier: ({ baseName: { namePath } }) => namePath, + + // Contract level + + UsingForDeclaration: ({ typeName, libraryName }) => `using ${libraryName} for ${unparse(typeName)};`, + + StateVariableDeclaration: ({ variables }) => _.map(variables, unparse).join(', ') + ';', + + StructDefinition: ({ name, members }) => `struct ${name} ${block(_.map(members, unparse).join(';\n') + ';')}`, + + EnumDefinition: ({ name, members }) => `enum ${name} ${block(_.map(members, unparse).join(',\n'))}`, + + EnumValue: ({ name }) => name, + + EventDefinition: ({ name, parameters }) => `event ${name}${unparse(parameters)};`, + + ModifierDefinition: ({ name, parameters, body }) => + `modifier ${name}${Array.isArray(parameters) ? '' : unparse(parameters)} ${unparse(body)}`, + // Note: when there is no parameter block, instead of an ASTNode there is a [] + + FunctionDefinition: ({ + visibility, + name, + parameters, + body, + modifiers, + isConstructor, + stateMutability, + returnParameters, + }) => + (isConstructor ? 'constructor' : `function ${name}`) + + unparse(parameters) + + '\n' + + indent( + (visibility && visibility !== 'default' ? visibility + ' ' : '') + + (stateMutability || '') + + _.map(modifiers, unparse).join('\n') + + (returnParameters ? `\nreturns ${unparse(returnParameters)}` : ''), + ) + + '\n' + + (body ? unparse(body) : ';'), + + ParameterList: ({ parameters }) => `(${_.map(parameters, unparse).join(', ')})`, + + Parameter: ({ typeName, name, storageLocation }) => `${unparse(typeName)} ${storageLocation || ''} ${name || ''}`, + + ModifierInvocation: ({ name, arguments: args }) => `${name}(${_.map(args, unparse).join(', ')})`, + + // Statements + + Block: ({ statements }) => block(_.map(statements, unparse).join('\n')), + + VariableDeclarationStatement: ({ variables, initialValue }) => + _.map(variables, unparse).join(' ') + (initialValue ? ` = ${unparse(initialValue)};` : ';'), + + ExpressionStatement: ({ expression }) => `${unparen(unparse(expression))};`, + + EmitStatement: ({ eventCall }) => `emit ${unparen(unparse(eventCall))};`, + + ReturnStatement: ({ expression }) => `return ${expression ? unparse(expression) : ''};`, + + BreakStatement: ({}) => `break;`, + + ContinueStatement: ({}) => `continue;`, + + ThrowStatement: ({}) => `throw;`, + + IfStatement: ({ condition, trueBody, falseBody }) => + `if (${unparse(condition)})\n${unparse(trueBody)}` + (falseBody ? `else\n${unparse(falseBody)}` : ''), + + ForStatement: ({ initExpression: i, conditionExpression: c, loopExpression: l, body }) => + `for (${unparse(i).replace(';', '')}; ${unparse(c)}; ${unparse(l).replace(';', '')}) ${unparse(body)}`, + + InlineAssemblyStatement: ( + { language, body }, // TODO(recmo): use language + ) => `assembly ${unparse(body)}`, + + // Types + + ElementaryTypeName: ({ name }) => name, + + UserDefinedTypeName: ({ namePath }) => namePath, + + ArrayTypeName: ({ baseTypeName, length }) => `${unparse(baseTypeName)}[${length ? unparse(length) : ''}]`, + + Mapping: ({ keyType, valueType }) => `mapping (${unparse(keyType)} => ${unparse(valueType)})`, + + // Expressions + + Identifier: ({ name }) => name, + + BooleanLiteral: ({ value: isTrue }) => (isTrue ? 'true' : 'false'), + + NumberLiteral: ( + { number: value, subdenomination }, // TODO(recmo): subdenomination + ) => value, + + StringLiteral: ({ value }) => stresc(value), + + FunctionCall: ( + { expression, arguments: args, names }, // TODO(recmo): names + ) => `(${unparse(expression)}(${_.map(args, unparse).join(', ')}))`, + + Conditional: ({ condition, trueExpression, falseExpression }) => + `(${unparse(condition)} ? ${unparse(trueExpression)} : ${unparse(falseExpression)})`, + + UnaryOperation: ({ operator, subExpression, isPrefix }) => + `(${isPrefix ? operator : ''}${unparse(subExpression)}${isPrefix ? '' : operator})`, + + BinaryOperation: ({ operator, left, right }) => `(${unparse(left)} ${operator} ${unparse(right)})`, + + MemberAccess: ({ expression, memberName }) => `(${unparse(expression)}.${memberName})`, + + IndexAccess: ({ base, index }) => `(${unparse(base)}[${unparse(index)}])`, + + ElementaryTypeNameExpression: ({ typeName }) => `(${unparse(typeName)})`, + + VariableDeclaration: ({ typeName, name, visibility, isDeclaredConst, isIndexed, expression, storageLocation }) => + `${unparse(typeName)} ` + + (isIndexed ? 'indexed ' : '') + + (storageLocation ? storageLocation + ' ' : '') + + (visibility && visibility !== 'default' ? visibility + ' ' : '') + + (isDeclaredConst ? 'constant ' : '') + + `${name}` + + (expression ? ` = ${unparse(expression)}` : ''), + + NewExpression: ({ typeName }) => `(new ${unparse(typeName)})`, + + TupleExpression: ({ isArray, components }) => + isArray ? `[${_.map(components, unparse).join(', ')}]` : `(${_.map(components, unparse).join(', ')})`, + + // Assembly + + AssemblyBlock: ({ operations }) => block(_.map(operations, unparse).join('\n')), + + AssemblyAssignment: ({ names, expression }) => `${_.map(names, unparse).join(', ')} := ${unparse(expression)}`, + + AssemblyLocalDefinition: ({ names, expression }) => + `let ${_.map(names, unparse).join(', ')} := ${unparse(expression)}`, + + AssemblyCall: ({ functionName, arguments: args }) => + args.length === 0 ? functionName : `${functionName}(${_.map(args, unparse).join(', ')})`, + + AssemblyIf: ({ condition, body }) => `if ${unparse(condition)} ${unparse(body)}`, + + AssemblyFor: ({ pre, condition, post, body }) => `for ${_.map([pre, condition, post, body], unparse).join(' ')}`, + + AssemblySwitch: ({ expression, cases }) => `switch ${unparse(expression)}\n${_.map(cases, unparse).join('\n')}`, + + AssemblyCase: ({ value, block: asmBlock }) => `case ${unparse(value)} ${unparse(asmBlock)}`, + + DecimalNumber: ({ value }) => value, + + HexNumber: ({ value }) => value, + + ASTNode: node => { + throw new Error(`Unsupported node type ${node.type}`); + }, +}; + +export const unparse = (ast: S.ASTNode) => visit(ast, visitor); diff --git a/packages/sol-meta/src/utils.ts b/packages/sol-meta/src/utils.ts new file mode 100644 index 0000000000..0976559205 --- /dev/null +++ b/packages/sol-meta/src/utils.ts @@ -0,0 +1,138 @@ +import * as fs from 'fs'; +import * as _ from 'lodash'; +import * as S from 'solidity-parser-antlr'; + +// TODO(recmo): Replace with Array.flatMap https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap +export const flatMap = (a: A[], f: ((a: A) => B[])): B[] => ([] as B[]).concat(..._.map(a, f)); + +// TODO(recmo): Use Map instead? +export const objectZip = (keys: string[], values: T[]): { [key: string]: T } => + keys.reduce<{ [key: string]: T }>( + (others, key, index) => ({ + ...others, + [key]: values[index], + }), + {}, + ); + +// TODO(recmo): Is the order in Object.keys and Object.values equal? +export const objectMap = (obj: { [key: string]: A }, f: (v: A) => B): { [key: string]: B } => + objectZip(Object.keys(obj), _.map(_.values(obj), f)); + +export const objectPromiseAsync = async (obj: { [key: string]: Promise }): Promise<{ [key: string]: T }> => + objectZip(Object.keys(obj), await Promise.all(Object.values(obj))); + +export const objectFilter = (obj: { [key: string]: A }, f: (key: string, v: A) => boolean): { [key: string]: A } => + Object.keys(obj) + .filter(key => f(key, obj[key])) + .reduce<{ [key: string]: A }>((a, key) => ({ ...a, [key]: obj[key] }), {}); + +export const existsAsync = async (path: string): Promise => + new Promise((resolve, reject) => + fs.access(path, fs.constants.R_OK, error => (error ? resolve(false) : resolve(true))), + ); + +export const readFileAsync = async (path: string): Promise => + new Promise((resolve, reject) => + fs.readFile(path, 'utf-8', (error, contents) => (error ? reject(error) : resolve(contents))), + ); + +export const writeFileAsync = async (path: string, contents: string): Promise => + new Promise((resolve, reject) => + fs.writeFile(path, contents, 'utf-8', error => (error ? reject(error) : resolve())), + ); + +export const isPragmaDirective = (node: S.ASTNode): node is S.PragmaDirective => + node.type === S.NodeType.PragmaDirective; + +export const isImportDirective = (node: S.ASTNode): node is S.ImportDirective => + node.type === S.NodeType.ImportDirective; + +export const isContractDefinition = (node: S.ASTNode): node is S.ContractDefinition => + node.type === S.NodeType.ContractDefinition; + +export const pragmaNodes = (ast: S.SourceUnit): S.PragmaDirective[] => ast.children.filter(isPragmaDirective); + +export const importNodes = (ast: S.SourceUnit): S.ImportDirective[] => ast.children.filter(isImportDirective); + +export const contracts = (ast: S.SourceUnit): S.ContractDefinition[] => ast.children.filter(isContractDefinition); + +export const identifier = (name: string): S.Identifier => ({ + type: S.NodeType.Identifier, + name, +}); + +export const elementaryType = (name: string): S.ElementaryTypeName => ({ + type: S.NodeType.ElementaryTypeName, + name, +}); + +export const userType = (name: string): S.UserDefinedTypeName => ({ + type: S.NodeType.UserDefinedTypeName, + namePath: name, +}); + +export const types = { + bool: elementaryType('bool'), + uint256: elementaryType('uint256'), +}; + +export const litBool = (value: boolean): S.BooleanLiteral => ({ + type: S.NodeType.BooleanLiteral, + value, +}); + +export const litFalse = litBool(false); + +export const litTrue = litBool(true); + +export const isNumber = (value: string | number): value is number => typeof value === 'number'; + +const hexadecimalBase = 16; + +export const litNumber = (value: string | number): S.NumberLiteral => ({ + type: S.NodeType.NumberLiteral, + number: isNumber(value) ? `0x${value.toString(hexadecimalBase)}` : value, + subdenomination: null, +}); + +export const litString = (value: string): S.StringLiteral => ({ + type: S.NodeType.StringLiteral, + value, +}); + +export type Literal = string | number; + +export const literal = (value: Literal): S.Expression => { + if (isNumber(value)) { + return litNumber(value); + } + if (value.startsWith('"')) { + return litString(value.slice(1, -1)); + } + if (value === 'true') { + return litTrue; + } + if (value === 'false') { + return litFalse; + } + return litNumber(value); +}; + +export const nameParameters = (params: S.ParameterList, prefix: string = '_arg'): S.ParameterList => ({ + ...params, + parameters: _.map(params.parameters, (param, i) => ({ + ...param, + name: param.name || `${prefix}${i}`, + })), +}); + +export const argumentExpressions = (params: S.ParameterList): S.Expression[] => + _.map(params.parameters, ({ name }) => { + // TODO(recmo): rewrite using throw expressions or do notation + if (name !== null) { + return identifier(name); + } else { + throw new Error('Anonymous parameter'); + } + }); diff --git a/packages/sol-meta/src/visitor.ts b/packages/sol-meta/src/visitor.ts new file mode 100644 index 0000000000..38f01489c0 --- /dev/null +++ b/packages/sol-meta/src/visitor.ts @@ -0,0 +1,128 @@ +import * as S from 'solidity-parser-antlr'; + +export interface Visitor extends S.Visitor { + SourceMember?: (node: S.SourceMember) => T; + ContractMember?: (node: S.ContractMember) => T; + Statement?: (node: S.Statement) => T; + Expression?: (node: S.Expression) => T; + Type?: (node: S.Type) => T; + AssemblyStatement?: (node: S.AssemblyStatement) => T; + AssemblyExpression?: (node: S.AssemblyExpression) => T; + ASTNode?: (node: S.ASTNode) => T; +} + +export const isSourceMember = (node: S.ASTNode): node is S.SourceMember => + [S.NodeType.PragmaDirective, S.NodeType.ImportDirective, S.NodeType.ContractDefinition].includes(node.type); + +export const isContractMember = (node: S.ASTNode): node is S.ContractMember => + [ + S.NodeType.UsingForDeclaration, + S.NodeType.StateVariableDeclaration, + S.NodeType.StructDefinition, + S.NodeType.EnumDefinition, + S.NodeType.EventDefinition, + S.NodeType.ModifierDefinition, + S.NodeType.FunctionDefinition, + ].includes(node.type); + +export const isStatement = (node: S.ASTNode): node is S.Statement => + [ + S.NodeType.Block, + S.NodeType.VariableDeclarationStatement, + S.NodeType.ExpressionStatement, + S.NodeType.EmitStatement, + S.NodeType.ReturnStatement, + S.NodeType.BreakStatement, + S.NodeType.ContinueStatement, + S.NodeType.ThrowStatement, + S.NodeType.IfStatement, + S.NodeType.ForStatement, + S.NodeType.InlineAssemblyStatement, + ].includes(node.type); + +export const isExpression = (node: S.ASTNode): node is S.Expression => + [ + S.NodeType.BooleanLiteral, + S.NodeType.NumberLiteral, + S.NodeType.StringLiteral, + S.NodeType.Identifier, + S.NodeType.FunctionCall, + S.NodeType.Conditional, + S.NodeType.UnaryOperation, + S.NodeType.BinaryOperation, + S.NodeType.MemberAccess, + S.NodeType.IndexAccess, + S.NodeType.ElementaryTypeNameExpression, + S.NodeType.VariableDeclaration, + S.NodeType.NewExpression, + S.NodeType.TupleExpression, + S.NodeType.IndexAccess, + S.NodeType.MemberAccess, + ].includes(node); + +export const isType = (node: S.ASTNode): node is S.Type => + [ + S.NodeType.ElementaryTypeName, + S.NodeType.UserDefinedTypeName, + S.NodeType.Mapping, + S.NodeType.ArrayTypeName, + S.NodeType.FunctionTypeName, + ].includes(node); + +export const isAssemblyStatement = (node: S.ASTNode): node is S.AssemblyStatement => + [ + S.NodeType.AssemblyCall, // Note: also an expression! + S.NodeType.AssemblyAssignment, + S.NodeType.AssemblyLocalDefinition, + S.NodeType.AssemblyIf, + S.NodeType.AssemblyFor, + S.NodeType.AssemblySwitch, + S.NodeType.AssemblyCase, + ].includes(node); + +export const isAssemblyExpression = (node: S.ASTNode): node is S.AssemblyExpression => + [ + S.NodeType.AssemblyCall, // Note: also a statement! + S.NodeType.DecimalNumber, + S.NodeType.HexNumber, + ].includes(node); + +/** + * Dispatches based on node type. + * @param node The node to dispatch on. + * @param visitor A structure containing handlers for different node types. + */ +export function visit(node: S.ASTNode, visitor: Visitor): T { + // Try to dispatch on the exact type + const indexed = visitor as { [type: string]: (node: S.ASTNode) => T }; + if (indexed[node.type]) { + return indexed[node.type](node); + } + + // Try to dispatch on classes of nodes, picking the first match + if (isSourceMember(node) && visitor.SourceMember) { + return visitor.SourceMember(node); + } + if (isContractMember(node) && visitor.ContractMember) { + return visitor.ContractMember(node); + } + if (isStatement(node) && visitor.Statement) { + return visitor.Statement(node); + } + if (isExpression(node) && visitor.Expression) { + return visitor.Expression(node); + } + if (isType(node) && visitor.Type) { + return visitor.Type(node); + } + if (isAssemblyStatement(node) && visitor.AssemblyStatement) { + return visitor.AssemblyStatement(node); + } + if (isAssemblyExpression(node) && visitor.AssemblyExpression) { + return visitor.AssemblyExpression(node); + } + if (visitor.ASTNode) { + return visitor.ASTNode(node); + } + throw new Error(`No matching visitor found for ${node.type}.`); +} diff --git a/packages/sol-meta/test.iface.out.sol b/packages/sol-meta/test.iface.out.sol new file mode 100644 index 0000000000..f1cf9e000f --- /dev/null +++ b/packages/sol-meta/test.iface.out.sol @@ -0,0 +1,42 @@ +pragma experimental ABIEncoderV2; + +contract MAssetProxyDispatcherMock { + + uint256 internal _dispatchTransferFrom_counter = 0; + + event _dispatchTransferFrom_log(uint256 counter, bytes assetData, address from, address to, uint256 amount); + + function dispatchTransferFrom(bytes memory assetData, address from, address to, uint256 amount) + internal + { + emit _dispatchTransferFrom_log(_dispatchTransferFrom_counter, assetData, from, to, amount); + _dispatchTransferFrom_counter++; + } + + uint256 internal _someFunction_counter = 0; + + event _someFunction_log(uint256 counter, uint256 a, uint256 b); + + struct _someFunction_Result { + uint256 _ret0; + bytes _ret1; + } + + mapping (uint256 => _someFunction_Result) _someFunction_results; + + function _someFunction_set(uint256 _counter, _someFunction_Result _value) + public + { + (_someFunction_results[_counter]) = _value; + } + + function someFunction(uint256 a, uint256 b) + public + returns (uint256 _ret0, bytes _ret1) + { + emit _someFunction_log(_someFunction_counter, a, b); + _someFunction_Result storage result = (_someFunction_results[_someFunction_counter]); + _someFunction_counter++; + return ((result._ret0), (result._ret1)); + } +} diff --git a/packages/sol-meta/test.sol b/packages/sol-meta/test.sol new file mode 100644 index 0000000000..80add9bbaa --- /dev/null +++ b/packages/sol-meta/test.sol @@ -0,0 +1,33 @@ +contract MAssetProxyDispatcher { + + address public owner; + + modifier onlyOwner() { + require(((msg.sender) == owner), "ONLY_CONTRACT_OWNER"); + _; + } + + function transferOwnership(address newOwner) + public onlyOwner() + { + if ((newOwner != ((address)(0)))) + { + owner = newOwner; + } + } + + constructor() + public + { + owner = (msg.sender); + } + + function dispatchTransferFrom(bytes memory assetData, address from, address to, uint256 amount) + internal + ; + + function someFunction(uint256 a, uint256 b) + internal + returns (uint256 , bytes ) + ; +} diff --git a/packages/sol-meta/test/index.ts b/packages/sol-meta/test/index.ts new file mode 100644 index 0000000000..7a5fe6e10c --- /dev/null +++ b/packages/sol-meta/test/index.ts @@ -0,0 +1,88 @@ +import * as chai from 'chai'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import 'mocha'; +import * as pathUtils from 'path'; +import * as S from 'solidity-parser-antlr'; + +import { mockContract } from '../src/contract_mocker'; +import { parse } from '../src/parser'; +import { compile } from '../src/solc_wrapper'; +import { readSources, SourceCollection } from '../src/source_reader'; +import { unparse } from '../src/unparser'; + +const expect = chai.expect; + +const findContracts = (searchPath: string) => + glob.sync(searchPath).map(file => ({ + name: pathUtils.basename(file, '.sol'), + source: fs.readFileSync(file, 'utf8'), + })); + +const contracts = findContracts('../contracts/contracts/**/*.sol'); + +describe('Parser', () => { + it('should have test contracts', () => { + const MINIMUM_CONTRACTS_FOR_TESTS = 10; + expect(contracts).to.have.lengthOf.above(MINIMUM_CONTRACTS_FOR_TESTS); + }); + + contracts.forEach(({ name, source }) => + it(`should parse ${name}`, () => { + parse(source); + }), + ); +}); + +describe('Unparser', () => { + contracts.forEach(({ name, source }) => + it(`should unparse ${name}`, () => { + const ast = parse(source); + const src = unparse(ast); + parse(src); + // Ideally, we would test the following: + // expect(parse(src)).to.deep.equal(ast); + // But this fails on on expressiong like `2 * 3 + 1` which get rewritten + // to `((2 * 2) + 1)`. This prevents the ASTs from being identicall in + // syntax, even though they should be identical in meaning. + }), + ); +}); + +describe('Mocker', () => { + const sourcePath = '../contracts/contracts/protocol/Exchange/'; + const toMock = ['Exchange', 'MixinExchangeCore', 'MixinSignatureValidator', 'MixinWrapperFunctions']; + const path = (name: string) => `${sourcePath}/${name}.sol`; + let sources: SourceCollection; + const mocks: { [name: string]: S.SourceUnit } = {}; + + it('should read sources', async () => { + sources = await readSources(_.map(toMock, path)); + _.map(toMock, name => expect(_.keys(sources).some(absPath => absPath.endsWith(`${name}.sol`)))); + }); + _.map(toMock, name => + it(`should generate mocks for ${name}`, () => { + mocks[name] = mockContract( + sources, + _.keys(sources).find(absPath => absPath.endsWith(`${name}.sol`)) || '', + name, + { + constructors: { + LibConstants: ['"ZRXASSETSTRING"'], + Exchange: ['"ZRXASSETSTRING"'], + }, + scripted: {}, + }, + ); + }), + ); + // Note(recmo): These tests are slow + const MAX_TIME_MILLISECONDS = 60000; + describe.skip('Compiling', () => + _.map(toMock, name => + it(`should compile mock for ${name}`, async () => { + await compile(sources, mocks[name]); + }).timeout(MAX_TIME_MILLISECONDS), + )); +}); diff --git a/packages/sol-meta/tsconfig.json b/packages/sol-meta/tsconfig.json new file mode 100644 index 0000000000..d67a5f3684 --- /dev/null +++ b/packages/sol-meta/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "." + }, + "include": ["./src/**/*", "./test/**/*", "../typescript-typings/types/solidity-parser-antlr/*"] +} diff --git a/packages/sol-meta/tslint.json b/packages/sol-meta/tslint.json new file mode 100644 index 0000000000..dd9053357e --- /dev/null +++ b/packages/sol-meta/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["@0x/tslint-config"] +} diff --git a/packages/sol-meta/typedoc-tsconfig.json b/packages/sol-meta/typedoc-tsconfig.json new file mode 100644 index 0000000000..22897c1315 --- /dev/null +++ b/packages/sol-meta/typedoc-tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../typedoc-tsconfig", + "compilerOptions": { + "outDir": "lib", + "strictFunctionTypes": false + }, + "include": ["./src/**/*", "./test/**/*"] +} diff --git a/packages/typescript-typings/types/solc/index.d.ts b/packages/typescript-typings/types/solc/index.d.ts index f4c05cd7c9..eb206c5f81 100644 --- a/packages/typescript-typings/types/solc/index.d.ts +++ b/packages/typescript-typings/types/solc/index.d.ts @@ -90,6 +90,7 @@ declare module 'solc' { settings: CompilerSettings; } export interface SolcInstance { + version(): string; compile( sources: InputSources, optimizerEnabled: number, diff --git a/packages/typescript-typings/types/solidity-parser-antlr/index.d.ts b/packages/typescript-typings/types/solidity-parser-antlr/index.d.ts new file mode 100644 index 0000000000..39333eadd4 --- /dev/null +++ b/packages/typescript-typings/types/solidity-parser-antlr/index.d.ts @@ -0,0 +1,808 @@ +// Type definitions for solidity-parser-antlr 0.3.2 +// Project: https://github.com/federicobond/solidity-parser-antlr +// Definitions by: Leonid Logvinov +// Alex Browne +// Remco Bloemen +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.1 + +// See: https://solidity.readthedocs.io/en/develop/miscellaneous.html#language-grammar + +// TODO: Parameter and VariableDeclaration are the same node + +declare module 'solidity-parser-antlr' { + export interface LineColumn { + line: number; + column: number; + } + export interface Location { + start: LineColumn; + end: LineColumn; + } + export const enum NodeType { + SourceUnit = 'SourceUnit', + PragmaDirective = 'PragmaDirective', + PragmaName = 'PragmaName', + PragmaValue = 'PragmaValue', + Version = 'Version', + VersionOperator = 'VersionOperator', + VersionConstraint = 'VersionConstraint', + ImportDeclaration = 'ImportDeclaration', + ImportDirective = 'ImportDirective', + ContractDefinition = 'ContractDefinition', + InheritanceSpecifier = 'InheritanceSpecifier', + ContractPart = 'ContractPart', + StateVariableDeclaration = 'StateVariableDeclaration', + UsingForDeclaration = 'UsingForDeclaration', + StructDefinition = 'StructDefinition', + ModifierDefinition = 'ModifierDefinition', + ModifierInvocation = 'ModifierInvocation', + FunctionDefinition = 'FunctionDefinition', + ReturnParameters = 'ReturnParameters', + ModifierList = 'ModifierList', + EventDefinition = 'EventDefinition', + EnumValue = 'EnumValue', + EnumDefinition = 'EnumDefinition', + ParameterList = 'ParameterList', + Parameter = 'Parameter', + EventParameterList = 'EventParameterList', + EventParameter = 'EventParameter', + FunctionTypeParameterList = 'FunctionTypeParameterList', + FunctionTypeParameter = 'FunctionTypeParameter', + VariableDeclaration = 'VariableDeclaration', + TypeName = 'TypeName', + UserDefinedTypeName = 'UserDefinedTypeName', + Mapping = 'Mapping', + ArrayTypeName = 'ArrayTypeName', + FunctionTypeName = 'FunctionTypeName', + StorageLocation = 'StorageLocation', + StateMutability = 'StateMutability', + Block = 'Block', + ExpressionStatement = 'ExpressionStatement', + IfStatement = 'IfStatement', + WhileStatement = 'WhileStatement', + ForStatement = 'ForStatement', + InlineAssemblyStatement = 'InlineAssemblyStatement', + DoWhileStatement = 'DoWhileStatement', + ContinueStatement = 'ContinueStatement', + BreakStatement = 'BreakStatement', + ReturnStatement = 'ReturnStatement', + ThrowStatement = 'ThrowStatement', + EmitStatement = 'EmitStatement', + VariableDeclarationStatement = 'VariableDeclarationStatement', + IdentifierList = 'IdentifierList', + ElementaryTypeName = 'ElementaryTypeName', + PrimaryExpression = 'PrimaryExpression', + ExpressionList = 'ExpressionList', + NameValueList = 'NameValueList', + NameValue = 'NameValue', + FunctionCall = 'FunctionCall', + AssemblyBlock = 'AssemblyBlock', + AssemblyItem = 'AssemblyItem', + AssemblyExpression = 'AssemblyExpression', + AssemblyCall = 'AssemblyCall', + AssemblyLocalDefinition = 'AssemblyLocalDefinition', + AssemblyAssignment = 'AssemblyAssignment', + AssemblyIdentifierOrList = 'AssemblyIdentifierOrList', + AssemblyIdentifierList = 'AssemblyIdentifierList', + AssemblyStackAssignment = 'AssemblyStackAssignment', + LabelDefinition = 'LabelDefinition', + AssemblySwitch = 'AssemblySwitch', + AssemblyCase = 'AssemblyCase', + AssemblyFunctionDefinition = 'AssemblyFunctionDefinition', + AssemblyFunctionReturns = 'AssemblyFunctionReturns', + AssemblyFor = 'AssemblyFor', + AssemblyIf = 'AssemblyIf', + AssemblyLiteral = 'AssemblyLiteral', + SubAssembly = 'SubAssembly', + TupleExpression = 'TupleExpression', + ElementaryTypeNameExpression = 'ElementaryTypeNameExpression', + BooleanLiteral = 'BooleanLiteral', + NumberLiteral = 'NumberLiteral', + StringLiteral = 'StringLiteral', + Identifier = 'Identifier', + UnaryOperation = 'UnaryOperation', + BinaryOperation = 'BinaryOperation', + Conditional = 'Conditional', + IndexAccess = 'IndexAccess', + MemberAccess = 'MemberAccess', + NewExpression = 'NewExpression', + DecimalNumber = 'DecimalNumber', + HexNumber = 'HexNumber', + } + export const enum ContractKind { + Contract = 'contract', + Interface = 'interface', + Library = 'library', + } + export const enum Visibility { + Default = 'default', + Public = 'public', + Internal = 'internal', + Private = 'private', + } + export const enum StateMutability { + Default = null, + View = 'view', + Pure = 'pure', + } + export const enum StorageLocation { + Default = null, + Memory = 'memory', + Storage = 'storage', + Calldata = 'calldata', + } + export type UnOp = '++' | '--' | '-' | '+' | '!'; + export type BinOp = + | '+' + | '-' + | '*' + | '/' + | '**' + | '%' + | '<<' + | '>>' + | '&&' + | '||' + | '&' + | '|' + | '^' + | '<' + | '>' + | '<=' + | '>=' + | '==' + | '!=' + | '=' + | '|=' + | '^=' + | '&=' + | '<<=' + | '>>=' + | '+=' + | '-=' + | '*=' + | '/=' + | '%='; + + export interface BaseASTNode { + type: NodeType; + range?: [number, number]; + loc?: Location; + } + + export interface SourceUnit extends BaseASTNode { + type: NodeType.SourceUnit; + children: SourceMember[]; + } + export interface PragmaDirective extends BaseASTNode { + type: NodeType.PragmaDirective; + name: string; + value: string; + } + /* + export interface PragmaName extends BaseASTNode { + type: NodeType.PragmaName; + } + export interface PragmaValue extends BaseASTNode { + type: NodeType.PragmaValue; + } + export interface Version extends BaseASTNode { + type: NodeType.Version; + } + export interface VersionOperator extends BaseASTNode { + type: NodeType.VersionOperator; + } + export interface VersionConstraint extends BaseASTNode { + type: NodeType.VersionConstraint; + } + export interface ImportDeclaration extends BaseASTNode { + type: NodeType.ImportDeclaration; + } + */ + export interface ImportDirective extends BaseASTNode { + type: NodeType.ImportDirective; + path: string; + symbolAliases: [string, string][] | null; + } + export interface ContractDefinition extends BaseASTNode { + type: NodeType.ContractDefinition; + name: string; + kind: ContractKind; + baseContracts: InheritanceSpecifier[]; + subNodes: ContractMember[]; + } + export interface InheritanceSpecifier extends BaseASTNode { + type: NodeType.InheritanceSpecifier; + baseName: UserDefinedTypeName; + } + /* + export interface ContractPart extends BaseASTNode { + type: NodeType.ContractPart; + } + */ + export interface UsingForDeclaration extends BaseASTNode { + type: NodeType.UsingForDeclaration; + typeName: Type; + libraryName: string; + } + export interface StateVariableDeclaration extends BaseASTNode { + type: NodeType.StateVariableDeclaration; + variables: VariableDeclaration[]; + initialValue: Expression | null; // TODO check if exists + } + export interface StructDefinition extends BaseASTNode { + type: NodeType.StructDefinition; + name: string; + members: VariableDeclaration[]; + } + export interface EnumDefinition extends BaseASTNode { + type: NodeType.EnumDefinition; + name: string; + members: EnumValue[]; + } + export interface EnumValue extends BaseASTNode { + type: NodeType.EnumValue; + name: string; + } + export interface EventDefinition extends BaseASTNode { + type: NodeType.EventDefinition; + name: string; + parameters: ParameterList; + isAnonymous: boolean; + } + export interface ModifierDefinition extends BaseASTNode { + type: NodeType.ModifierDefinition; + name: string; + parameters: ParameterList | []; + body: Block; + } + export interface FunctionDefinition extends BaseASTNode { + type: NodeType.FunctionDefinition; + name: string | null; // Null for constructors + parameters: ParameterList; + returnParameters: ParameterList | null; + body: Block | null; + visibility: Visibility; + modifiers: ModifierInvocation[]; + isConstructor: boolean; + stateMutability: StateMutability; + } + export interface ModifierInvocation extends BaseASTNode { + type: NodeType.ModifierInvocation; + name: string; + arguments: Expression[]; + } + export interface ParameterList extends BaseASTNode { + type: NodeType.ParameterList; + parameters: Parameter[]; + } + export interface Parameter extends BaseASTNode { + type: NodeType.Parameter; + name: string | null; + typeName: Type; + visibility?: Visibility; + storageLocation?: StorageLocation | null; + expression?: Expression; + isStateVar?: boolean; + isDeclaredConst?: boolean; + isIndexed?: boolean; + } + /* + export interface EventParameterList extends BaseASTNode { + type: NodeType.EventParameterList; + } + export interface EventParameter extends BaseASTNode { + type: NodeType.EventParameter; + } + export interface FunctionTypeParameterList extends BaseASTNode { + type: NodeType.FunctionTypeParameterList; + } + export interface FunctionTypeParameter extends BaseASTNode { + type: NodeType.FunctionTypeParameter; + } + */ + export interface Block extends BaseASTNode { + type: NodeType.Block; + statements: Statement[]; + } + export interface VariableDeclaration extends BaseASTNode { + type: NodeType.VariableDeclaration; + name: string; + visibility?: Visibility; + storageLocation?: StorageLocation; + typeName: Type; + expression?: Expression; + isStateVar: boolean; + isDeclaredConst?: boolean; + isIndexed: boolean; + } + /* + export interface TypeName extends BaseASTNode { + type: NodeType.TypeName; + } + export interface StorageLocation extends BaseASTNode { + type: NodeType.StorageLocation; + } + export interface StateMutability extends BaseASTNode { + type: NodeType.StateMutability; + } + */ + export interface ExpressionStatement extends BaseASTNode { + type: NodeType.ExpressionStatement; + expression: Expression; + } + export interface IfStatement extends BaseASTNode { + type: NodeType.IfStatement; + condition: Expression; + trueBody: Statement; + falseBody: Statement | null; + } + export interface WhileStatement extends BaseASTNode { + type: NodeType.WhileStatement; + // TODO + } + export interface ForStatement extends BaseASTNode { + type: NodeType.ForStatement; + initExpression: Expression; + conditionExpression: Expression; + loopExpression: Expression; + body: Statement; + } + export interface InlineAssemblyStatement extends BaseASTNode { + type: NodeType.InlineAssemblyStatement; + language: string; + body: AssemblyBlock; + } + export interface DoWhileStatement extends BaseASTNode { + type: NodeType.DoWhileStatement; + // TODO + } + export interface ContinueStatement extends BaseASTNode { + type: NodeType.ContinueStatement; + } + export interface BreakStatement extends BaseASTNode { + type: NodeType.BreakStatement; + } + export interface ReturnStatement extends BaseASTNode { + type: NodeType.ReturnStatement; + expression: Expression | null; + } + export interface ThrowStatement extends BaseASTNode { + type: NodeType.ThrowStatement; + } + export interface EmitStatement extends BaseASTNode { + type: NodeType.EmitStatement; + eventCall: FunctionCall; + } + export interface VariableDeclarationStatement extends BaseASTNode { + type: NodeType.VariableDeclarationStatement; + variables: VariableDeclaration[]; + initialValue: Expression; + } + export interface NewExpression extends BaseASTNode { + type: NodeType.NewExpression; + typeName: Type; + } + + // Types + export interface ElementaryTypeName extends BaseASTNode { + type: NodeType.ElementaryTypeName; + name: string; + } + export interface UserDefinedTypeName extends BaseASTNode { + type: NodeType.UserDefinedTypeName; + namePath: string; + } + export interface Mapping extends BaseASTNode { + type: NodeType.Mapping; + keyType: Type; + valueType: Type; + } + export interface ArrayTypeName extends BaseASTNode { + type: NodeType.Mapping; + baseTypeName: Type; + length: Expression | null; + } + export interface FunctionTypeName extends BaseASTNode { + type: NodeType.FunctionTypeName; + // TODO + } + /* + export interface PrimaryExpression extends BaseASTNode { + type: NodeType.PrimaryExpression; + } + export interface ExpressionList extends BaseASTNode { + type: NodeType.ExpressionList; + } + */ + export interface NameValueList extends BaseASTNode { + type: NodeType.NameValueList; + // TODO + } + export interface NameValue extends BaseASTNode { + type: NodeType.NameValue; + // TODO + } + + // Expressions + export interface Identifier extends BaseASTNode { + type: NodeType.Identifier; + name: string; + } + export interface BooleanLiteral extends BaseASTNode { + type: NodeType.BooleanLiteral; + value: boolean; + } + export interface NumberLiteral extends BaseASTNode { + type: NodeType.NumberLiteral; + number: string; + subdenomination: any; // TODO + } + export interface StringLiteral extends BaseASTNode { + type: NodeType.StringLiteral; + value: string; + } + export interface FunctionCall extends BaseASTNode { + type: NodeType.FunctionCall; + expression: Expression; + arguments: Expression[]; + names: []; + } + export interface TupleExpression extends BaseASTNode { + type: NodeType.TupleExpression; + components: Expression[]; + isArray: boolean; + } + export interface ElementaryTypeNameExpression extends BaseASTNode { + type: NodeType.ElementaryTypeNameExpression; + typeName: Type; + } + export interface UnaryOperation extends BaseASTNode { + type: NodeType.UnaryOperation; + operator: UnOp; + isPrefix: boolean; + subExpression: Expression; + } + export interface BinaryOperation extends BaseASTNode { + type: NodeType.BinaryOperation; + operator: BinOp; + left: Expression; + right: Expression; + } + export interface Conditional extends BaseASTNode { + type: NodeType.Conditional; + condition: Expression; + trueExpression: Expression; + falseExpression: Expression; + } + export interface IndexAccess extends BaseASTNode { + type: NodeType.IndexAccess; + base: Expression; + index: Expression; + } + export interface MemberAccess extends BaseASTNode { + type: NodeType.MemberAccess; + expression: Expression; + memberName: string; + } + + export interface AssemblyBlock extends BaseASTNode { + type: NodeType.AssemblyBlock; + operations: AssemblyStatement[]; + } + /* + export interface AssemblyItem extends BaseASTNode { + type: NodeType.AssemblyItem; + } + export interface AssemblyExpression extends BaseASTNode { + type: NodeType.AssemblyExpression; + } + */ + export interface AssemblyCall extends BaseASTNode { + type: NodeType.AssemblyCall; + functionName: string; + arguments: AssemblyExpression[]; + } + export interface AssemblyLocalDefinition extends BaseASTNode { + type: NodeType.AssemblyLocalDefinition; + names: Identifier[]; + expression: AssemblyExpression; + } + export interface AssemblyAssignment extends BaseASTNode { + type: NodeType.AssemblyAssignment; + names: Identifier[]; + expression: AssemblyExpression; + } + /* + export interface AssemblyIdentifierOrList extends BaseASTNode { + type: NodeType.AssemblyIdentifierOrList; + } + export interface AssemblyIdentifierList extends BaseASTNode { + type: NodeType.AssemblyIdentifierList; + } + export interface AssemblyStackAssignment extends BaseASTNode { + type: NodeType.AssemblyStackAssignment; + } + */ + export interface LabelDefinition extends BaseASTNode { + type: NodeType.LabelDefinition; + // TODO + } + export interface AssemblySwitch extends BaseASTNode { + type: NodeType.AssemblySwitch; + expression: Expression; + cases: AssemblyCase[]; + } + export interface AssemblyCase extends BaseASTNode { + type: NodeType.AssemblyCase; + value: Expression; + block: AssemblyBlock; + } + export interface AssemblyFunctionDefinition extends BaseASTNode { + type: NodeType.AssemblyFunctionDefinition; + // TODO + } + export interface AssemblyFunctionReturns extends BaseASTNode { + type: NodeType.AssemblyFunctionReturns; + // TODO + } + export interface AssemblyFor extends BaseASTNode { + type: NodeType.AssemblyFor; + pre: AssemblyBlock | AssemblyExpression; + condition: AssemblyExpression; + post: AssemblyBlock | AssemblyExpression; + body: AssemblyBlock; + } + export interface AssemblyIf extends BaseASTNode { + type: NodeType.AssemblyIf; + condition: AssemblyExpression; + body: AssemblyBlock; + } + export interface DecimalNumber extends BaseASTNode { + type: NodeType.AssemblyIf; + value: string; + } + export interface HexNumber extends BaseASTNode { + type: NodeType.AssemblyIf; + value: string; + } + + /* + export interface AssemblyLiteral extends BaseASTNode { + type: NodeType.AssemblyLiteral; + } + export interface SubAssembly extends BaseASTNode { + type: NodeType.SubAssembly; + } + */ + export type SourceMember = PragmaDirective | ImportDirective | ContractDefinition; + export type ContractMember = + | UsingForDeclaration + | StateVariableDeclaration + | StructDefinition + | EnumDefinition + | EventDefinition + | ModifierDefinition + | FunctionDefinition; + export type Statement = + | Block + | VariableDeclarationStatement + | ExpressionStatement + | EmitStatement + | ReturnStatement + | BreakStatement + | ContinueStatement + | ThrowStatement + | IfStatement + | ForStatement + | InlineAssemblyStatement; + export type Expression = + | BooleanLiteral + | NumberLiteral + | StringLiteral + | Identifier + | FunctionCall + | Conditional + | UnaryOperation + | BinaryOperation + | MemberAccess + | IndexAccess + | ElementaryTypeNameExpression + | VariableDeclaration // TODO: Is this really an expression? + | NewExpression + | TupleExpression + | IndexAccess + | MemberAccess; + export type Type = ElementaryTypeName | UserDefinedTypeName | Mapping | ArrayTypeName | FunctionTypeName; + export type AssemblyStatement = + | AssemblyCall + | AssemblyAssignment + | AssemblyLocalDefinition + | AssemblyIf + | AssemblyFor + | AssemblySwitch + | AssemblyCase; + export type AssemblyExpression = AssemblyCall | DecimalNumber | HexNumber; + export type ASTNode = + | SourceUnit + | PragmaDirective + | PragmaName + | PragmaValue + | Version + | VersionOperator + | VersionConstraint + | ImportDeclaration + | ImportDirective + | ContractDefinition + | InheritanceSpecifier + | ContractPart + | StateVariableDeclaration + | UsingForDeclaration + | StructDefinition + | ModifierDefinition + | ModifierInvocation + | FunctionDefinition + | ReturnParameters + | ModifierList + | EventDefinition + | EnumValue + | EnumDefinition + | ParameterList + | Parameter + | EventParameterList + | EventParameter + | FunctionTypeParameterList + | FunctionTypeParameter + | VariableDeclaration + | TypeName + | UserDefinedTypeName + | Mapping + | FunctionTypeName + | StorageLocation + | StateMutability + | Block + | Statement + | ExpressionStatement + | IfStatement + | WhileStatement + | ForStatement + | InlineAssemblyStatement + | DoWhileStatement + | ContinueStatement + | BreakStatement + | ReturnStatement + | ThrowStatement + | EmitStatement + | VariableDeclarationStatement + | IdentifierList + | ElementaryTypeName + | Expression + | PrimaryExpression + | ExpressionList + | NameValueList + | NameValue + | FunctionCall + | AssemblyBlock + | AssemblyItem + | AssemblyCall + | AssemblyLocalDefinition + | AssemblyAssignment + | AssemblyIdentifierOrList + | AssemblyIdentifierList + | AssemblyStackAssignment + | LabelDefinition + | AssemblySwitch + | AssemblyCase + | AssemblyFunctionDefinition + | AssemblyFunctionReturns + | AssemblyFor + | AssemblyIf + | AssemblyLiteral + | SubAssembly + | TupleExpression + | ElementaryTypeNameExpression + | BooleanLiteral + | NumberLiteral + | Identifier + | BinaryOperation + | Conditional; + export interface Visitor { + SourceUnit?: (node: SourceUnit) => T; + PragmaDirective?: (node: PragmaDirective) => T; + PragmaName?: (node: PragmaName) => T; + PragmaValue?: (node: PragmaValue) => T; + Version?: (node: Version) => T; + VersionOperator?: (node: VersionOperator) => T; + VersionConstraint?: (node: VersionConstraint) => T; + ImportDeclaration?: (node: ImportDeclaration) => T; + ImportDirective?: (node: ImportDirective) => T; + ContractDefinition?: (node: ContractDefinition) => T; + InheritanceSpecifier?: (node: InheritanceSpecifier) => T; + ContractPart?: (node: ContractPart) => T; + StateVariableDeclaration?: (node: StateVariableDeclaration) => T; + UsingForDeclaration?: (node: UsingForDeclaration) => T; + StructDefinition?: (node: StructDefinition) => T; + ModifierDefinition?: (node: ModifierDefinition) => T; + ModifierInvocation?: (node: ModifierInvocation) => T; + FunctionDefinition?: (node: FunctionDefinition) => T; + ReturnParameters?: (node: ReturnParameters) => T; + ModifierList?: (node: ModifierList) => T; + EventDefinition?: (node: EventDefinition) => T; + EnumValue?: (node: EnumValue) => T; + EnumDefinition?: (node: EnumDefinition) => T; + ParameterList?: (node: ParameterList) => T; + Parameter?: (node: Parameter) => T; + EventParameterList?: (node: EventParameterList) => T; + EventParameter?: (node: EventParameter) => T; + FunctionTypeParameterList?: (node: FunctionTypeParameterList) => T; + FunctionTypeParameter?: (node: FunctionTypeParameter) => T; + VariableDeclaration?: (node: VariableDeclaration) => T; + TypeName?: (node: TypeName) => T; + UserDefinedTypeName?: (node: UserDefinedTypeName) => T; + Mapping?: (node: Mapping) => T; + ArrayTypeName?: (node: ArrayTypeName) => T; + FunctionTypeName?: (node: FunctionTypeName) => T; + StorageLocation?: (node: StorageLocation) => T; + StateMutability?: (node: StateMutability) => T; + Block?: (node: Block) => T; + ExpressionStatement?: (node: ExpressionStatement) => T; + IfStatement?: (node: IfStatement) => T; + WhileStatement?: (node: WhileStatement) => T; + ForStatement?: (node: ForStatement) => T; + InlineAssemblyStatement?: (node: InlineAssemblyStatement) => T; + DoWhileStatement?: (node: DoWhileStatement) => T; + ContinueStatement?: (node: ContinueStatement) => T; + BreakStatement?: (node: BreakStatement) => T; + ReturnStatement?: (node: ReturnStatement) => T; + ThrowStatement?: (node: ThrowStatement) => T; + EmitStatement?: (node: EmitStatement) => T; + VariableDeclarationStatement?: (node: VariableDeclarationStatement) => T; + IdentifierList?: (node: IdentifierList) => T; + ElementaryTypeName?: (node: ElementaryTypeName) => T; + PrimaryExpression?: (node: PrimaryExpression) => T; + ExpressionList?: (node: ExpressionList) => T; + NameValueList?: (node: NameValueList) => T; + NameValue?: (node: NameValue) => T; + FunctionCall?: (node: FunctionCall) => T; + AssemblyBlock?: (node: AssemblyBlock) => T; + AssemblyItem?: (node: AssemblyItem) => T; + AssemblyExpression?: (node: AssemblyExpression) => T; + AssemblyCall?: (node: AssemblyCall) => T; + AssemblyLocalDefinition?: (node: AssemblyLocalDefinition) => T; + AssemblyAssignment?: (node: AssemblyAssignment) => T; + AssemblyIdentifierOrList?: (node: AssemblyIdentifierOrList) => T; + AssemblyIdentifierList?: (node: AssemblyIdentifierList) => T; + AssemblyStackAssignment?: (node: AssemblyStackAssignment) => T; + LabelDefinition?: (node: LabelDefinition) => T; + AssemblySwitch?: (node: AssemblySwitch) => T; + AssemblyCase?: (node: AssemblyCase) => T; + AssemblyFunctionDefinition?: (node: AssemblyFunctionDefinition) => T; + AssemblyFunctionReturns?: (node: AssemblyFunctionReturns) => T; + AssemblyFor?: (node: AssemblyFor) => T; + AssemblyIf?: (node: AssemblyIf) => T; + AssemblyLiteral?: (node: AssemblyLiteral) => T; + SubAssembly?: (node: SubAssembly) => T; + TupleExpression?: (node: TupleExpression) => T; + ElementaryTypeNameExpression?: (node: ElementaryTypeNameExpression) => T; + BooleanLiteral?: (node: BooleanLiteral) => T; + NumberLiteral?: (node: NumberLiteral) => T; + StringLiteral?: (node: StringLiteral) => T; + Identifier?: (node: Identifier) => T; + UnaryOperation?: (node: UnaryOperation) => T; + BinaryOperation?: (node: BinaryOperation) => T; + Conditional?: (node: Conditional) => T; + MemberAccess?: (node: MemberAccess) => T; + IndexAccess?: (node: IndexAccess) => T; + NewExpression?: (node: NewExpression) => T; + DecimalNumber?: (node: DecimalNumber) => T; + HexNumber?: (node: HexNumber) => T; + } + export interface ParserOpts { + tolerant?: boolean; + range?: boolean; + loc?: boolean; + } + export function parse(sourceCode: string, parserOpts: ParserOpts): SourceUnit; + export function visit(ast: ASTNode, visitor: Visitor): void; +} diff --git a/packages/utils/src/deferred.ts b/packages/utils/src/deferred.ts new file mode 100644 index 0000000000..bd9a54ad2e --- /dev/null +++ b/packages/utils/src/deferred.ts @@ -0,0 +1,20 @@ +export class Deferred { + public promise: Promise; + public reject: (reason: any) => void; + public resolve: (value: T) => void; + constructor() { + // Hack(recmo): Define reject and resolve here so TS does not complain + // about them not being defined in the constructor. The + // promise we create will overwrite them. + this.reject = () => { + throw new Error('Unimplemented reject.'); + }; + this.resolve = () => { + throw new Error('Unimplemented resolve.'); + }; + this.promise = new Promise((resolve, reject) => { + this.reject = reject; + this.resolve = resolve; + }); + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 082aff6bbe..aa0034fbb6 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -10,4 +10,5 @@ export { NULL_BYTES } from './constants'; export { errorUtils } from './error_utils'; export { fetchAsync } from './fetch_async'; export { signTypedDataUtils } from './sign_typed_data_utils'; +export { Deferred } from './deferred'; export import AbiEncoder = require('./abi_encoder'); diff --git a/yarn.lock b/yarn.lock index a7a55a7b3d..e2c5ca2bf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -472,6 +472,27 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@0x/json-schemas@^1.0.7": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@0x/json-schemas/-/json-schemas-2.1.3.tgz#07cf30f19c61c11c843f5ef9a1e653defb3f12c7" + integrity sha512-nBIPpbikfgiT1CoNnOxcc9wV7rutCaUg1tnFxccTVGGazVu6AOXeWGa/LqbOLOcAfX4cQoFpW3yJdNo1lDNzLw== + dependencies: + "@0x/typescript-typings" "^3.0.5" + "@types/node" "*" + jsonschema "^1.2.0" + lodash.values "^4.3.0" + +"@0x/typescript-typings@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@0x/typescript-typings/-/typescript-typings-3.0.5.tgz#77fe522315ae21b795dfa6c181148f25f0b98c49" + integrity sha512-aMG5WJEAcfisdfw13RPdeYEyLv/7IValxWLcqiLqRcleU91P1GWPppTSamPdtxM7rIxURDOaUHvxOqibZmze6g== + dependencies: + "@types/bn.js" "^4.11.0" + "@types/react" "*" + bignumber.js "~4.1.0" + ethereum-types "^1.1.3" + popper.js "1.14.3" + "@0xproject/npm-cli-login@^0.0.11": version "0.0.11" resolved "https://registry.yarnpkg.com/@0xproject/npm-cli-login/-/npm-cli-login-0.0.11.tgz#3f1ec06112ce62aad300ff0575358f68aeecde2e" @@ -5829,6 +5850,14 @@ ethereum-common@^0.0.18: version "0.0.18" resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" +ethereum-types@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-1.1.3.tgz#0e069874d91e5845a8ac0d3af732b8f9e6a213af" + integrity sha512-wGMAdw1hby/aixUCWEkp6rDJUZpRtiMfasOha/2nQq+2aFk5xOkLrk1TlTn9Dr4ybcLftGPjKs4aimNARcMLQA== + dependencies: + "@types/node" "*" + bignumber.js "~4.1.0" + ethereumjs-abi@0.6.5, ethereumjs-abi@^0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241" @@ -13996,6 +14025,11 @@ solidity-parser-antlr@^0.2.12: version "0.2.12" resolved "https://registry.yarnpkg.com/solidity-parser-antlr/-/solidity-parser-antlr-0.2.12.tgz#1154f183d5cdda2c7677549ee584dbdb7fb2269c" +solidity-parser-antlr@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/solidity-parser-antlr/-/solidity-parser-antlr-0.3.2.tgz#1cf9d019280550a31299dc380e87a310dc4ca154" + integrity sha512-aO/lbnc14A81cQigN5sKNuwbxohPyJOq7kpLirYT/6emCw5Gjb0TJoZ3TzL1tYdIX6gjTAMlQ1UZwOcrzOAp4w== + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"