From 18b9fe525622d9e18491f76136d7697806996a09 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 16 Mar 2018 15:09:11 +0100 Subject: Enable strictNullChecks --- packages/deployer/src/cli.ts | 4 +- packages/deployer/src/commands.ts | 2 +- packages/deployer/src/compiler.ts | 210 +++++++++----------------------- packages/deployer/src/utils/compiler.ts | 118 ++++++++++++++++++ packages/deployer/src/utils/types.ts | 2 +- packages/deployer/test/deploy_test.ts | 2 +- 6 files changed, 182 insertions(+), 156 deletions(-) create mode 100644 packages/deployer/src/utils/compiler.ts (limited to 'packages/deployer') diff --git a/packages/deployer/src/cli.ts b/packages/deployer/src/cli.ts index efbf56bb0..d1bd645b3 100644 --- a/packages/deployer/src/cli.ts +++ b/packages/deployer/src/cli.ts @@ -63,9 +63,9 @@ async function onDeployCommandAsync(argv: CliOptions): Promise { networkId, defaults, }; - const deployerArgsString = argv.args; + const deployerArgsString = argv.args as string; const deployerArgs = deployerArgsString.split(','); - await commands.deployAsync(argv.contract, deployerArgs, deployerOpts); + await commands.deployAsync(argv.contract as string, deployerArgs, deployerOpts); } /** * Creates a set of contracts to compile. diff --git a/packages/deployer/src/commands.ts b/packages/deployer/src/commands.ts index 32af7fc3f..8e544a60b 100644 --- a/packages/deployer/src/commands.ts +++ b/packages/deployer/src/commands.ts @@ -5,7 +5,7 @@ import { CompilerOptions, DeployerOptions } from './utils/types'; export const commands = { async compileAsync(opts: CompilerOptions): Promise { const compiler = new Compiler(opts); - await compiler.compileAllAsync(); + await compiler.compileAsync(); }, async deployAsync(contractName: string, args: any[], opts: DeployerOptions): Promise { const deployer = new Deployer(opts); diff --git a/packages/deployer/src/compiler.ts b/packages/deployer/src/compiler.ts index 28232a661..d9571488e 100644 --- a/packages/deployer/src/compiler.ts +++ b/packages/deployer/src/compiler.ts @@ -10,6 +10,14 @@ import solc = require('solc'); import * as Web3 from 'web3'; import { binPaths } from './solc/bin_paths'; +import { + createArtifactsDirIfDoesNotExistAsync, + findImportIfExist, + getContractArtifactIfExistsAsync, + getNormalizedErrMsg, + parseDependencies, + parseSolidityVersionRange, +} from './utils/compiler'; import { constants } from './utils/constants'; import { fsWrapper } from './utils/fs_wrapper'; import { @@ -24,17 +32,13 @@ import { import { utils } from './utils/utils'; const ALL_CONTRACTS_IDENTIFIER = '*'; -const SOLIDITY_VERSION_RANGE_REGEX = /pragma solidity (.*);/; -const SOLIDITY_FILE_EXTENSION_REGEX = /(.*\.sol)/; -const IMPORT_REGEX = /(import\s)/; -const DEPENDENCY_PATH_REGEX = /"([^"]+)"/; // Source: https://github.com/BlockChainCompany/soljitsu/blob/master/lib/shared.js export class Compiler { private _contractsDir: string; private _networkId: number; private _optimizerEnabled: boolean; private _artifactsDir: string; - private _contractSources?: ContractSources; + private _contractSources!: ContractSources; private _solcErrors: Set = new Set(); private _specifiedContracts: Set = new Set(); private _contractSourceData: ContractSourceData = {}; @@ -78,64 +82,6 @@ export class Compiler { } return sources; } - /** - * Gets contract dependendencies and keccak256 hash from source. - * @param source Source code of contract. - * @return Object with contract dependencies and keccak256 hash of source. - */ - private static _getContractSpecificSourceData(source: string): ContractSpecificSourceData { - const dependencies: string[] = []; - const sourceHash = ethUtil.sha3(source); - const solcVersionRange = Compiler._parseSolidityVersionRange(source); - const contractSpecificSourceData: ContractSpecificSourceData = { - dependencies, - solcVersionRange, - sourceHash, - }; - const lines = source.split('\n'); - _.forEach(lines, line => { - if (!_.isNull(line.match(IMPORT_REGEX))) { - const dependencyMatch = line.match(DEPENDENCY_PATH_REGEX); - if (!_.isNull(dependencyMatch)) { - const dependencyPath = dependencyMatch[1]; - const fileName = path.basename(dependencyPath); - contractSpecificSourceData.dependencies.push(fileName); - } - } - }); - return contractSpecificSourceData; - } - /** - * Searches Solidity source code for compiler version range. - * @param source Source code of contract. - * @return Solc compiler version range. - */ - private static _parseSolidityVersionRange(source: string): string { - const solcVersionRangeMatch = source.match(SOLIDITY_VERSION_RANGE_REGEX); - if (_.isNull(solcVersionRangeMatch)) { - throw new Error('Could not find Solidity version range in source'); - } - const solcVersionRange = solcVersionRangeMatch[1]; - return solcVersionRange; - } - /** - * Normalizes the path found in the error message. - * Example: converts 'base/Token.sol:6:46: Warning: Unused local variable' - * to 'Token.sol:6:46: Warning: Unused local variable' - * This is used to prevent logging the same error multiple times. - * @param errMsg An error message from the compiled output. - * @return The error message with directories truncated from the contract path. - */ - private static _getNormalizedErrMsg(errMsg: string): string { - const errPathMatch = errMsg.match(SOLIDITY_FILE_EXTENSION_REGEX); - if (_.isNull(errPathMatch)) { - throw new Error('Could not find a path in error message'); - } - const errPath = errPathMatch[0]; - const baseContract = path.basename(errPath); - const normalizedErrMsg = errMsg.replace(errPath, baseContract); - return normalizedErrMsg; - } /** * Instantiates a new instance of the Compiler class. * @param opts Options specifying directories, network, and optimization settings. @@ -149,20 +95,15 @@ export class Compiler { this._specifiedContracts = opts.specifiedContracts; } /** - * Compiles all Solidity files found in contractsDir and writes JSON artifacts to artifactsDir. + * Compiles selected Solidity files and writes JSON artifacts to artifactsDir. */ - public async compileAllAsync(): Promise { - await this._createArtifactsDirIfDoesNotExistAsync(); + public async compileAsync(): Promise { + await createArtifactsDirIfDoesNotExistAsync(this._artifactsDir); this._contractSources = await Compiler._getContractSourcesAsync(this._contractsDir); - _.forIn(this._contractSources, (source, fileName) => { - this._contractSourceData[fileName] = Compiler._getContractSpecificSourceData(source); - }); + _.forIn(this._contractSources, this._setContractSpecificSourceData.bind(this)); const fileNames = this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER) ? _.keys(this._contractSources) : Array.from(this._specifiedContracts.values()); - _.forEach(fileNames, fileName => { - this._setSourceTreeHash(fileName); - }); for (const fileName of fileNames) { await this._compileContractAsync(fileName); } @@ -179,14 +120,19 @@ export class Compiler { throw new Error('Contract sources not yet initialized'); } const contractSpecificSourceData = this._contractSourceData[fileName]; - const currentArtifactIfExists = (await this._getContractArtifactIfExistsAsync(fileName)) as ContractArtifact; + const currentArtifactIfExists = await getContractArtifactIfExistsAsync(this._artifactsDir, fileName); const sourceHash = `0x${contractSpecificSourceData.sourceHash.toString('hex')}`; - const sourceTreeHash = `0x${contractSpecificSourceData.sourceTreeHashIfExists.toString('hex')}`; + const sourceTreeHash = `0x${contractSpecificSourceData.sourceTreeHash.toString('hex')}`; - const shouldCompile = - _.isUndefined(currentArtifactIfExists) || - currentArtifactIfExists.networks[this._networkId].optimizer_enabled !== this._optimizerEnabled || - currentArtifactIfExists.networks[this._networkId].source_tree_hash !== sourceTreeHash; + let shouldCompile = false; + if (_.isUndefined(currentArtifactIfExists)) { + shouldCompile = true; + } else { + const currentArtifact = currentArtifactIfExists as ContractArtifact; + shouldCompile = + currentArtifact.networks[this._networkId].optimizer_enabled !== this._optimizerEnabled || + currentArtifact.networks[this._networkId].source_tree_hash !== sourceTreeHash; + } if (!shouldCompile) { return; } @@ -221,15 +167,13 @@ export class Compiler { const sourcesToCompile = { sources: input, }; - const compiled = solcInstance.compile( - sourcesToCompile, - Number(this._optimizerEnabled), - this._findImportsIfSourcesExist.bind(this), + const compiled = solcInstance.compile(sourcesToCompile, Number(this._optimizerEnabled), importPath => + findImportIfExist(this._contractSources, importPath), ); if (!_.isUndefined(compiled.errors)) { _.forEach(compiled.errors, errMsg => { - const normalizedErrMsg = Compiler._getNormalizedErrMsg(errMsg); + const normalizedErrMsg = getNormalizedErrMsg(errMsg); this._solcErrors.add(normalizedErrMsg); }); } @@ -263,10 +207,11 @@ export class Compiler { let newArtifact: ContractArtifact; if (!_.isUndefined(currentArtifactIfExists)) { + const currentArtifact = currentArtifactIfExists as ContractArtifact; newArtifact = { - ...currentArtifactIfExists, + ...currentArtifact, networks: { - ...currentArtifactIfExists.networks, + ...currentArtifact.networks, [this._networkId]: contractNetworkData, }, }; @@ -285,79 +230,42 @@ export class Compiler { logUtils.log(`${fileName} artifact saved!`); } /** - * Sets the source tree hash for a file and its dependencies. - * @param fileName Name of contract file. - */ - private _setSourceTreeHash(fileName: string): void { - const contractSpecificSourceData = this._contractSourceData[fileName]; - if (_.isUndefined(contractSpecificSourceData)) { - throw new Error(`Contract data for ${fileName} not yet set`); - } - if (_.isUndefined(contractSpecificSourceData.sourceTreeHashIfExists)) { - const dependencies = contractSpecificSourceData.dependencies; - if (dependencies.length === 0) { - contractSpecificSourceData.sourceTreeHashIfExists = contractSpecificSourceData.sourceHash; - } else { - _.forEach(dependencies, dependency => { - this._setSourceTreeHash(dependency); - }); - const dependencySourceTreeHashes = _.map( - dependencies, - dependency => this._contractSourceData[dependency].sourceTreeHashIfExists, - ); - const sourceTreeHashesBuffer = Buffer.concat([ - contractSpecificSourceData.sourceHash, - ...dependencySourceTreeHashes, - ]); - contractSpecificSourceData.sourceTreeHashIfExists = ethUtil.sha3(sourceTreeHashesBuffer); - } - } - } - /** - * Callback to resolve dependencies with `solc.compile`. - * Throws error if contractSources not yet initialized. - * @param importPath Path to an imported dependency. - * @return Import contents object containing source code of dependency. + * Gets contract dependendencies and keccak256 hash from source. + * @param source Source code of contract. + * @return Object with contract dependencies and keccak256 hash of source. */ - private _findImportsIfSourcesExist(importPath: string): solc.ImportContents { - const fileName = path.basename(importPath); - const source = this._contractSources[fileName]; - if (_.isUndefined(source)) { - throw new Error(`Contract source not found for ${fileName}`); + private _setContractSpecificSourceData(source: string, fileName: string): void { + if (!_.isUndefined(this._contractSourceData[fileName])) { + return; } - const importContents: solc.ImportContents = { - contents: source, + const sourceHash = ethUtil.sha3(source); + const solcVersionRange = parseSolidityVersionRange(source); + const dependencies = parseDependencies(source); + const sourceTreeHash = this._getSourceTreeHash(fileName, sourceHash, dependencies); + this._contractSourceData[fileName] = { + dependencies, + solcVersionRange, + sourceHash, + sourceTreeHash, }; - return importContents; - } - /** - * Creates the artifacts directory if it does not already exist. - */ - private async _createArtifactsDirIfDoesNotExistAsync(): Promise { - if (!fsWrapper.doesPathExistSync(this._artifactsDir)) { - logUtils.log('Creating artifacts directory...'); - await fsWrapper.mkdirAsync(this._artifactsDir); - } } /** - * Gets contract data on network or returns if an artifact does not exist. + * Gets the source tree hash for a file and its dependencies. * @param fileName Name of contract file. - * @return Contract data on network or undefined. */ - private async _getContractArtifactIfExistsAsync(fileName: string): Promise { - let contractArtifact; - const contractName = path.basename(fileName, constants.SOLIDITY_FILE_EXTENSION); - const currentArtifactPath = `${this._artifactsDir}/${contractName}.json`; - try { - const opts = { - encoding: 'utf8', - }; - const contractArtifactString = await fsWrapper.readFileAsync(currentArtifactPath, opts); - contractArtifact = JSON.parse(contractArtifactString); - return contractArtifact; - } catch (err) { - logUtils.log(`Artifact for ${fileName} does not exist`); - return undefined; + private _getSourceTreeHash(fileName: string, sourceHash: Buffer, dependencies: string[]): Buffer { + if (dependencies.length === 0) { + return sourceHash; + } else { + const dependencySourceTreeHashes = _.map(dependencies, dependency => { + const source = this._contractSources[dependency]; + this._setContractSpecificSourceData(source, dependency); + const sourceData = this._contractSourceData[dependency]; + return this._getSourceTreeHash(dependency, sourceData.sourceHash, sourceData.dependencies); + }); + const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]); + const sourceTreeHash = ethUtil.sha3(sourceTreeHashesBuffer); + return sourceTreeHash; } } } diff --git a/packages/deployer/src/utils/compiler.ts b/packages/deployer/src/utils/compiler.ts new file mode 100644 index 000000000..3fb35e9ed --- /dev/null +++ b/packages/deployer/src/utils/compiler.ts @@ -0,0 +1,118 @@ +import { logUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as path from 'path'; +import * as solc from 'solc'; + +import { constants } from './constants'; +import { fsWrapper } from './fs_wrapper'; +import { ContractArtifact, ContractSources } from './types'; + +/** + * Gets contract data on network or returns if an artifact does not exist. + * @param fileName Name of contract file. + * @return Contract data on network or undefined. + */ +export async function getContractArtifactIfExistsAsync( + artifactsDir: string, + fileName: string, +): Promise { + let contractArtifact; + const contractName = path.basename(fileName, constants.SOLIDITY_FILE_EXTENSION); + const currentArtifactPath = `${artifactsDir}/${contractName}.json`; + try { + const opts = { + encoding: 'utf8', + }; + const contractArtifactString = await fsWrapper.readFileAsync(currentArtifactPath, opts); + contractArtifact = JSON.parse(contractArtifactString); + return contractArtifact; + } catch (err) { + logUtils.log(`Artifact for ${fileName} does not exist`); + return undefined; + } +} + +/** + * Creates the artifacts directory if it does not already exist. + */ +export async function createArtifactsDirIfDoesNotExistAsync(artifactsDir: string): Promise { + if (!fsWrapper.doesPathExistSync(artifactsDir)) { + logUtils.log('Creating artifacts directory...'); + await fsWrapper.mkdirAsync(artifactsDir); + } +} + +/** + * Searches Solidity source code for compiler version range. + * @param source Source code of contract. + * @return Solc compiler version range. + */ +export function parseSolidityVersionRange(source: string): string { + const SOLIDITY_VERSION_RANGE_REGEX = /pragma solidity (.*);/; + const solcVersionRangeMatch = source.match(SOLIDITY_VERSION_RANGE_REGEX); + if (_.isNull(solcVersionRangeMatch)) { + throw new Error('Could not find Solidity version range in source'); + } + const solcVersionRange = solcVersionRangeMatch[1]; + return solcVersionRange; +} + +/** + * Normalizes the path found in the error message. + * Example: converts 'base/Token.sol:6:46: Warning: Unused local variable' + * to 'Token.sol:6:46: Warning: Unused local variable' + * This is used to prevent logging the same error multiple times. + * @param errMsg An error message from the compiled output. + * @return The error message with directories truncated from the contract path. + */ +export function getNormalizedErrMsg(errMsg: string): string { + const SOLIDITY_FILE_EXTENSION_REGEX = /(.*\.sol)/; + const errPathMatch = errMsg.match(SOLIDITY_FILE_EXTENSION_REGEX); + if (_.isNull(errPathMatch)) { + throw new Error('Could not find a path in error message'); + } + const errPath = errPathMatch[0]; + const baseContract = path.basename(errPath); + const normalizedErrMsg = errMsg.replace(errPath, baseContract); + return normalizedErrMsg; +} + +export function parseDependencies(source: string): string[] { + // TODO: Use a proper parser + const IMPORT_REGEX = /(import\s)/; + const DEPENDENCY_PATH_REGEX = /"([^"]+)"/; // Source: https://github.com/BlockChainCompany/soljitsu/blob/master/lib/shared.js + const dependencies: string[] = []; + const lines = source.split('\n'); + _.forEach(lines, line => { + if (!_.isNull(line.match(IMPORT_REGEX))) { + const dependencyMatch = line.match(DEPENDENCY_PATH_REGEX); + if (!_.isNull(dependencyMatch)) { + const dependencyPath = dependencyMatch[1]; + const basenName = path.basename(dependencyPath); + dependencies.push(basenName); + } + } + }); + return dependencies; +} + +/** + * Callback to resolve dependencies with `solc.compile`. + * Throws error if contractSources not yet initialized. + * @param importPath Path to an imported dependency. + * @return Import contents object containing source code of dependency. + */ +export function findImportIfExist(contractSources: ContractSources, importPath: string): solc.ImportContents { + const fileName = path.basename(importPath); + if (_.isUndefined(contractSources)) { + throw new Error('Contract sources not yet initialized'); + } + const source = contractSources[fileName]; + if (_.isUndefined(source)) { + throw new Error(`Contract source not found for ${fileName}`); + } + const importContents: solc.ImportContents = { + contents: source, + }; + return importContents; +} diff --git a/packages/deployer/src/utils/types.ts b/packages/deployer/src/utils/types.ts index 13b24b9b7..72ee0df2f 100644 --- a/packages/deployer/src/utils/types.ts +++ b/packages/deployer/src/utils/types.ts @@ -86,7 +86,7 @@ export interface ContractSpecificSourceData { dependencies: string[]; solcVersionRange: string; sourceHash: Buffer; - sourceTreeHashIfExists?: Buffer; + sourceTreeHash: Buffer; } // TODO: Consolidate with 0x.js definitions once types are moved into a separate package. diff --git a/packages/deployer/test/deploy_test.ts b/packages/deployer/test/deploy_test.ts index 26ce337ef..f67cd8234 100644 --- a/packages/deployer/test/deploy_test.ts +++ b/packages/deployer/test/deploy_test.ts @@ -38,7 +38,7 @@ beforeEach(function(done: DoneCallback) { if (fsWrapper.doesPathExistSync(exchangeArtifactPath)) { await fsWrapper.removeFileAsync(exchangeArtifactPath); } - await compiler.compileAllAsync(); + await compiler.compileAsync(); done(); })().catch(done); }); -- cgit v1.2.3