Skip to content

Squadack/resolverengine #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/sol-compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@0x/dev-utils": "^1.0.24",
"@0x/tslint-config": "^2.0.2",
"@types/chokidar": "^1.7.5",
"@types/glob": "^7.1.1",
"@types/mkdirp": "^0.5.2",
"@types/pluralize": "^0.0.29",
"@types/require-from-string": "^1.2.0",
Expand Down Expand Up @@ -74,11 +75,15 @@
"@0x/typescript-typings": "^3.0.8",
"@0x/utils": "^3.0.1",
"@0x/web3-wrapper": "^3.2.4",
"@resolver-engine/core": "^0.2.1",
"@resolver-engine/imports": "^0.2.2",
"@resolver-engine/imports-fs": "^0.2.2",
"@types/yargs": "^11.0.0",
"chalk": "^2.3.0",
"chokidar": "^2.0.4",
"ethereum-types": "^1.1.6",
"ethereumjs-util": "^5.1.1",
"glob": "^7.1.3",
"lodash": "^4.17.5",
"mkdirp": "^0.5.1",
"pluralize": "^7.0.0",
Expand Down
90 changes: 42 additions & 48 deletions packages/sol-compiler/src/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { assert } from '@0x/assert';
import {
FallthroughResolver,
FSResolver,
NameResolver,
NPMResolver,
RelativeFSResolver,
Resolver,
SpyResolver,
URLResolver,
} from '@0x/sol-resolver';
import { logUtils } from '@0x/utils';
import { logUtils, promisify } from '@0x/utils';
import * as chokidar from 'chokidar';
import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types';
import * as fs from 'fs';
import glob = require('glob');
import * as _ from 'lodash';
import * as path from 'path';
import * as pluralize from 'pluralize';

import { ResolverEngine } from '@resolver-engine/core';
import { gatherSources, ImportFile } from '@resolver-engine/imports';
import { ImportsFsEngine } from '@resolver-engine/imports-fs';
import * as semver from 'semver';
import solc = require('solc');

Expand All @@ -27,14 +22,18 @@ import {
createDirIfDoesNotExistAsync,
getContractArtifactIfExistsAsync,
getSolcAsync,
getSourcesWithDependencies,
getSourceTreeHash,
getSourcesWithDependenciesAsync,
getSourceTreeHashAsync,
parseSolidityVersionRange,
} from './utils/compiler';
import { constants } from './utils/constants';
import { fsWrapper } from './utils/fs_wrapper';
import { NameResolver } from './utils/name_resolver';
import { SpyResolver } from './utils/spy_resolver';
import { utils } from './utils/utils';

const globAsync = promisify<string[]>(glob);

type TYPE_ALL_FILES_IDENTIFIER = '*';
const ALL_CONTRACTS_IDENTIFIER = '*';
const ALL_FILES_IDENTIFIER = '*';
Expand Down Expand Up @@ -77,8 +76,7 @@ interface ContractData {
* to artifact files.
*/
export class Compiler {
private readonly _resolver: Resolver;
private readonly _nameResolver: NameResolver;
private readonly _resolver: ResolverEngine<ImportFile>;
private readonly _contractsDir: string;
private readonly _compilerSettings: solc.CompilerSettings;
private readonly _artifactsDir: string;
Expand All @@ -102,23 +100,16 @@ export class Compiler {
this._compilerSettings = passedOpts.compilerSettings || config.compilerSettings || DEFAULT_COMPILER_SETTINGS;
this._artifactsDir = passedOpts.artifactsDir || config.artifactsDir || DEFAULT_ARTIFACTS_DIR;
this._specifiedContracts = passedOpts.contracts || config.contracts || ALL_CONTRACTS_IDENTIFIER;
this._nameResolver = new NameResolver(path.resolve(this._contractsDir));
const resolver = new FallthroughResolver();
resolver.appendResolver(new URLResolver());
const packagePath = path.resolve('');
resolver.appendResolver(new NPMResolver(packagePath));
resolver.appendResolver(new RelativeFSResolver(this._contractsDir));
resolver.appendResolver(new FSResolver());
resolver.appendResolver(this._nameResolver);
this._resolver = resolver;
this._contractsDir = path.resolve(this._contractsDir);
this._resolver = ImportsFsEngine().addResolver(NameResolver(this._contractsDir));
}
/**
* Compiles selected Solidity files found in `contractsDir` and writes JSON artifacts to `artifactsDir`.
*/
public async compileAsync(): Promise<void> {
await createDirIfDoesNotExistAsync(this._artifactsDir);
await createDirIfDoesNotExistAsync(constants.SOLC_BIN_DIR);
await this._compileContractsAsync(this._getContractNamesToCompile(), true);
await this._compileContractsAsync(await this._getContractNamesToCompileAsync(), true);
}
/**
* Compiles Solidity files specified during instantiation, and returns the
Expand All @@ -129,7 +120,7 @@ export class Compiler {
* that version.
*/
public async getCompilerOutputsAsync(): Promise<StandardOutput[]> {
const promisedOutputs = this._compileContractsAsync(this._getContractNamesToCompile(), false);
const promisedOutputs = this._compileContractsAsync(await this._getContractNamesToCompileAsync(), false);
return promisedOutputs;
}
public async watchAsync(): Promise<void> {
Expand All @@ -154,7 +145,7 @@ export class Compiler {
}
}

const pathsToWatch = this._getPathsToWatch();
const pathsToWatch = await this._getPathsToWatchAsync();
watcher.add(pathsToWatch);
};
await onFileChangedAsync();
Expand All @@ -166,26 +157,28 @@ export class Compiler {
onFileChangedAsync(); // tslint:disable-line no-floating-promises
});
}
private _getPathsToWatch(): string[] {
const contractNames = this._getContractNamesToCompile();

private async _getPathsToWatchAsync(): Promise<string[]> {
const contractNames = await this._getContractNamesToCompileAsync();
const spyResolver = new SpyResolver(this._resolver);
for (const contractName of contractNames) {
const contractSource = spyResolver.resolve(contractName);
const contractSource = await spyResolver.require(contractName);
// NOTE: We ignore the return value here. We don't want to compute the source tree hash.
// We just want to call a SpyResolver on each contracts and it's dependencies and
// this is a convenient way to reuse the existing code that does that.
// We can then get all the relevant paths from the `spyResolver` below.
getSourceTreeHash(spyResolver, contractSource.path);
await getSourceTreeHashAsync(spyResolver, contractSource.url);
}
const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath));
const pathsToWatch: string[] = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.url));
return pathsToWatch;
}
private _getContractNamesToCompile(): string[] {

private async _getContractNamesToCompileAsync(): Promise<string[]> {
let contractNamesToCompile;
if (this._specifiedContracts === ALL_CONTRACTS_IDENTIFIER) {
const allContracts = this._nameResolver.getAll();
const allContracts = await globAsync(`${this._contractsDir}/**/*${constants.SOLIDITY_FILE_EXTENSION}`);
contractNamesToCompile = _.map(allContracts, contractSource =>
path.basename(contractSource.path, constants.SOLIDITY_FILE_EXTENSION),
path.basename(contractSource, constants.SOLIDITY_FILE_EXTENSION),
);
} else {
contractNamesToCompile = this._specifiedContracts.map(specifiedContract =>
Expand All @@ -207,11 +200,9 @@ export class Compiler {
const contractPathToData: ContractPathToData = {};

for (const contractName of contractNames) {
const contractSource = this._resolver.resolve(contractName);
const sourceTreeHashHex = getSourceTreeHash(
this._resolver,
path.join(this._contractsDir, contractSource.path),
).toString('hex');
const contractSource = await this._resolver.require(contractName);
const sourceTreeHash = await getSourceTreeHashAsync(this._resolver, contractSource.url);
const sourceTreeHashHex = sourceTreeHash.toString('hex');
const contractData = {
contractName,
currentArtifactIfExists: await getContractArtifactIfExistsAsync(this._artifactsDir, contractName),
Expand All @@ -220,7 +211,7 @@ export class Compiler {
if (!this._shouldCompile(contractData)) {
continue;
}
contractPathToData[contractSource.path] = contractData;
contractPathToData[contractSource.url] = contractData;
const solcVersion = _.isUndefined(this._solcVersionIfExists)
? semver.maxSatisfying(_.keys(binPaths), parseSolidityVersionRange(contractSource.source))
: this._solcVersionIfExists;
Expand All @@ -235,11 +226,7 @@ export class Compiler {
contractsToCompile: [],
};
}
// add input to the right version batch
versionToInputs[solcVersion].standardInput.sources[contractSource.path] = {
content: contractSource.source,
};
versionToInputs[solcVersion].contractsToCompile.push(contractSource.path);
versionToInputs[solcVersion].contractsToCompile.push(contractSource.url);
}

const compilerOutputs: StandardOutput[] = [];
Expand All @@ -253,8 +240,15 @@ export class Compiler {
}) with Solidity v${solcVersion}...`,
);

const depList = await gatherSources(input.contractsToCompile, process.cwd(), this._resolver);
for (const infile of depList) {
input.standardInput.sources[infile.url] = {
content: infile.source,
};
}

const { solcInstance, fullSolcVersion } = await getSolcAsync(solcVersion);
const compilerOutput = compile(this._resolver, solcInstance, input.standardInput);
const compilerOutput = compile(solcInstance, input.standardInput);
compilerOutputs.push(compilerOutput);

for (const contractPath of input.contractsToCompile) {
Expand Down Expand Up @@ -309,7 +303,7 @@ export class Compiler {
// contains listings for every contract compiled during the compiler invocation that compiled the contract
// to be persisted, which could include many that are irrelevant to the contract at hand. So, gather up only
// the relevant sources:
const { sourceCodes, sources } = getSourcesWithDependencies(
const { sourceCodes, sources } = await getSourcesWithDependenciesAsync(
this._resolver,
contractPath,
compilerOutput.sources,
Expand Down
64 changes: 36 additions & 28 deletions packages/sol-compiler/src/utils/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ContractSource, Resolver } from '@0x/sol-resolver';
import { fetchAsync, logUtils } from '@0x/utils';
import { ResolverEngine } from '@resolver-engine/core';
import { ImportFile } from '@resolver-engine/imports';
import chalk from 'chalk';
import { ContractArtifact } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
Expand Down Expand Up @@ -88,12 +89,13 @@ export function getNormalizedErrMsg(errMsg: string): string {
return normalizedErrMsg;
}

// TODO consider replacing parseDependencies() with resolver-engine's findImports()
/**
* Parses the contract source code and extracts the dendencies
* @param source Contract source code
* @return List of dependendencies
*/
export function parseDependencies(contractSource: ContractSource): string[] {
export function parseDependencies(contractSource: ImportFile): string[] {
// TODO: Use a proper parser
const source = contractSource.source;
const IMPORT_REGEX = /(import\s)/;
Expand All @@ -106,7 +108,7 @@ export function parseDependencies(contractSource: ContractSource): string[] {
if (!_.isNull(dependencyMatch)) {
let dependencyPath = dependencyMatch[1];
if (dependencyPath.startsWith('.')) {
dependencyPath = path.join(path.dirname(contractSource.path), dependencyPath);
dependencyPath = path.join(path.dirname(contractSource.url), dependencyPath); // TODO squadack implicite zamieniłem path na absolutePath(url)
}
dependencies.push(dependencyPath);
}
Expand All @@ -117,19 +119,13 @@ export function parseDependencies(contractSource: ContractSource): string[] {

/**
* Compiles the contracts and prints errors/warnings
* @param resolver Resolver
* @param solcInstance Instance of a solc compiler
* @param standardInput Solidity standard JSON input
*/
export function compile(
resolver: Resolver,
solcInstance: solc.SolcInstance,
standardInput: solc.StandardInput,
): solc.StandardOutput {
export function compile(solcInstance: solc.SolcInstance, standardInput: solc.StandardInput): solc.StandardOutput {
const standardInputStr = JSON.stringify(standardInput);
const standardOutputStr = solcInstance.compileStandardWrapper(standardInputStr, importPath => {
const sourceCodeIfExists = resolver.resolve(importPath);
return { contents: sourceCodeIfExists.source };
throw Error('Used callback. All sources should be resolved beforehand.');
});
const compiled: solc.StandardOutput = JSON.parse(standardOutputStr);
if (!_.isUndefined(compiled.errors)) {
Expand Down Expand Up @@ -163,15 +159,19 @@ function printCompilationErrorsAndWarnings(solcErrors: solc.SolcError[]): void {
* Gets the source tree hash for a file and its dependencies.
* @param fileName Name of contract file.
*/
export function getSourceTreeHash(resolver: Resolver, importPath: string): Buffer {
const contractSource = resolver.resolve(importPath);
const dependencies = parseDependencies(contractSource);
export async function getSourceTreeHashAsync(
resolver: ResolverEngine<ImportFile>,
importPath: string,
): Promise<Buffer> {
const imFile: ImportFile = await resolver.require(importPath);
const contractSource = { source: imFile.source, path: imFile.url, absolutePath: imFile.url };
const dependencies = parseDependencies(imFile);
const sourceHash = ethUtil.sha3(contractSource.source);
if (dependencies.length === 0) {
return sourceHash;
} else {
const dependencySourceTreeHashes = _.map(dependencies, (dependency: string) =>
getSourceTreeHash(resolver, dependency),
const dependencySourceTreeHashes = await Promise.all(
_.map(dependencies, async (dependency: string) => getSourceTreeHashAsync(resolver, dependency)),
);
const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]);
const sourceTreeHash = ethUtil.sha3(sourceTreeHashesBuffer);
Expand All @@ -187,14 +187,15 @@ export function getSourceTreeHash(resolver: Resolver, importPath: string): Buffe
* taken from the corresponding ID's in @param fullSources, and the content for @return sourceCodes is read from
* disk (via the aforementioned `resolver.source`).
*/
export function getSourcesWithDependencies(
resolver: Resolver,
export async function getSourcesWithDependenciesAsync(
resolver: ResolverEngine<ImportFile>,
contractPath: string,
fullSources: { [sourceName: string]: { id: number } },
): { sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } } {
): Promise<{ sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } }> {
const sources = { [contractPath]: { id: fullSources[contractPath].id } };
const sourceCodes = { [contractPath]: resolver.resolve(contractPath).source };
recursivelyGatherDependencySources(
const pamparampam = await resolver.require(contractPath);
const sourceCodes = { [contractPath]: pamparampam.source };
await recursivelyGatherDependencySourcesAsync(
resolver,
contractPath,
sourceCodes[contractPath],
Expand All @@ -205,14 +206,14 @@ export function getSourcesWithDependencies(
return { sourceCodes, sources };
}

function recursivelyGatherDependencySources(
resolver: Resolver,
async function recursivelyGatherDependencySourcesAsync(
resolver: ResolverEngine<ImportFile>,
contractPath: string,
contractSource: string,
fullSources: { [sourceName: string]: { id: number } },
sourcesToAppendTo: { [sourceName: string]: { id: number } },
sourceCodesToAppendTo: { [sourceName: string]: string },
): void {
): Promise<void> {
const importStatementMatches = contractSource.match(/\nimport[^;]*;/g);
if (importStatementMatches === null) {
return;
Expand Down Expand Up @@ -247,17 +248,24 @@ function recursivelyGatherDependencySources(
* while others are absolute ("Token.sol", "@0x/contracts/Wallet.sol")
* And we need to append the base path for relative imports.
*/
importPath = path.resolve(`/${contractFolder}`, importPath).replace('/', '');
importPath = path.resolve(`/${contractFolder}`, importPath);

// NOTE we want to remove leading slash ONLY if the path to contract
// folder is not an absolute path (e.g. path to npm package)
if (!contractFolder.startsWith('/')) {
importPath = importPath.replace('/', '');
}
}

if (_.isUndefined(sourcesToAppendTo[importPath])) {
sourcesToAppendTo[importPath] = { id: fullSources[importPath].id };
sourceCodesToAppendTo[importPath] = resolver.resolve(importPath).source;
const importFile = await resolver.require(importPath);
sourceCodesToAppendTo[importPath] = importFile.source;

recursivelyGatherDependencySources(
await recursivelyGatherDependencySourcesAsync(
resolver,
importPath,
resolver.resolve(importPath).source,
importFile.source,
fullSources,
sourcesToAppendTo,
sourceCodesToAppendTo,
Expand Down
22 changes: 22 additions & 0 deletions packages/sol-compiler/src/utils/name_resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { promisify } from '@0x/utils';
import { SubResolver } from '@resolver-engine/core';

import glob = require('glob');

const globAsync = promisify<string[]>(glob);

const SOLIDITY_FILE_EXTENSION = '.sol';

/**
* This resolver finds and returns (given only contract name) path to contract file in given directory.
*/

export function NameResolver(contractDir: string): SubResolver {
return async (resolvePath: string) => {
const results = await globAsync(`${contractDir}/**/${resolvePath}${SOLIDITY_FILE_EXTENSION}`);
if (results.length === 1) {
return results[0];
}
return null;
};
}
Loading