aboutsummaryrefslogtreecommitdiffstats
path: root/packages/deployer/src/utils/compiler.ts
blob: 600495693496359877ea11f042b82e81e8b5bcf8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import { AbiType, ContractAbi, MethodAbi } from '@0xproject/types';
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, FunctionNameToSeenCount } from './types';

/**
 * Constructs a system-wide unique identifier for a source file.
 * @param directoryNamespace Namespace of the source file's root contract directory.
 * @param sourceFilePath Path to a source file, relative to contractBaseDir.
 * @return sourceFileId A system-wide unique identifier for the source file.
 */
export function constructUniqueSourceFileId(directoryNamespace: string, sourceFilePath: string): string {
    const namespacePrefix = !_.isEmpty(directoryNamespace) ? `/${directoryNamespace}` : '';
    const sourceFilePathNoLeadingSlash = sourceFilePath.replace(/^\/+/g, '');
    const sourceFileId = `${namespacePrefix}/${sourceFilePathNoLeadingSlash}`;
    return sourceFileId;
}
/**
 * Constructs a system-wide unique identifier for a dependency file.
 * @param dependencyFilePath Path from a sourceFile to a dependency.
 * @param  contractBaseDir Base contracts directory of search tree.
 * @return sourceFileId A system-wide unique identifier for the source file.
 */
export function constructDependencyFileId(dependencyFilePath: string, sourceFilePath: string): string {
    if (_.startsWith(dependencyFilePath, '/')) {
        // Path of the form /namespace/path/to/dependency.sol
        return dependencyFilePath;
    } else {
        // Dependency is relative to the source file: ./dependency.sol, ../../some/path/dependency.sol, etc.
        // Join the two paths to construct a valid source file id: /namespace/path/to/dependency.sol
        return path.join(path.dirname(sourceFilePath), dependencyFilePath);
    }
}
/**
 * Constructs a system-wide unique identifier for a contract.
 * @param directoryNamespace Namespace of the source file's root contract directory.
 * @param sourceFilePath Path to a source file, relative to contractBaseDir.
 * @return sourceFileId A system-wide unique identifier for contract.
 */
export function constructContractId(directoryNamespace: string, sourceFilePath: string): string {
    const namespacePrefix = !_.isEmpty(directoryNamespace) ? `${directoryNamespace}:` : '';
    const sourceFileName = path.basename(sourceFilePath, constants.SOLIDITY_FILE_EXTENSION);
    const contractId = `${namespacePrefix}${sourceFileName}`;
    return contractId;
}
/**
 * Gets contract data on network or returns if an artifact does not exist.
 * @param artifactsDir Path to the artifacts directory.
 * @param fileName Name of contract file.
 * @return Contract data on network or undefined.
 */
export async function getContractArtifactIfExistsAsync(
    artifactsDir: string,
    fileName: string,
): Promise<ContractArtifact | void> {
    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 a directory if it does not already exist.
 * @param artifactsDir Path to the directory.
 */
export async function createDirIfDoesNotExistAsync(dirPath: string): Promise<void> {
    if (!fsWrapper.doesPathExistSync(dirPath)) {
        logUtils.log(`Creating directory at ${dirPath}...`);
        await fsWrapper.mkdirAsync(dirPath);
    }
}

/**
 * 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\s+solidity\s+(.*);/;
    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;
}

/**
 * Parses the contract source code and extracts the dendencies
 * @param  source Contract source code
 * @param sourceFilePath File path of the source code.
 * @return List of dependendencies
 */
export function parseDependencies(source: string, sourceFileId: 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 dependencyId = constructDependencyFileId(dependencyPath, sourceFileId);
                dependencies.push(dependencyId);
            }
        }
    });
    return dependencies;
}

/**
 * Callback to resolve dependencies with `solc.compile`.
 * Throws error if contractSources not yet initialized.
 * @param  contractSources Source codes of contracts.
 * @param  sourceFileId ID of the source file.
 * @param  importPath Path of dependency source file.
 * @return Import contents object containing source code of dependency.
 */
export function findImportIfExist(
    contractSources: ContractSources,
    sourceFileId: string,
    importPath: string,
): solc.ImportContents {
    const dependencyFileId = constructDependencyFileId(importPath, sourceFileId);
    const source = contractSources[dependencyFileId];
    if (_.isUndefined(source)) {
        throw new Error(`Contract source not found for ${dependencyFileId}`);
    }
    const importContents: solc.ImportContents = {
        contents: source,
    };
    return importContents;
}