@truffle/compile-solidity
Version:
Compiler helper and artifact manager for Solidity files
462 lines (418 loc) • 13.4 kB
JavaScript
const debug = require("debug")("compile:run");
const OS = require("os");
const semver = require("semver");
const Common = require("@truffle/compile-common");
const CompilerSupplier = require("./compilerSupplier");
// this function returns a Compilation - legacy/index.js and ./index.js
// both check to make sure rawSources exist before calling this method
// however, there is a check here that returns null if no sources exist
async function run(rawSources, options, language = "Solidity") {
if (Object.keys(rawSources).length === 0) {
return null;
}
// Ensure sources have operating system independent paths
// i.e., convert backslashes to forward slashes; things like C: are left intact.
const {
sources,
targets,
originalSourcePaths
} = Common.Sources.collectSources(rawSources, options.compilationTargets);
// construct solc compiler input
const compilerInput = prepareCompilerInput({
sources,
targets,
language,
settings: options.compilers.solc.settings,
modelCheckerSettings: options.compilers.solc.modelCheckerSettings
});
// perform compilation
const { compilerOutput, solcVersion } = await invokeCompiler({
compilerInput,
options
});
debug("compilerOutput: %O", compilerOutput);
// handle warnings as errors if options.strict
// log if not options.quiet
const { warnings, errors } = detectErrors({
compilerOutput,
options,
solcVersion
});
if (warnings.length > 0) {
options.events.emit("compile:warnings", { warnings });
}
if (errors.length > 0) {
if (!options.quiet) {
options.logger.log("");
}
throw new Common.Errors.CompileError(errors);
}
// success case
// returns Compilation - see @truffle/compile-common
const outputSources = processAllSources({
sources,
compilerOutput,
originalSourcePaths,
language
});
const sourceIndexes = outputSources
? outputSources.map(source => source.sourcePath)
: undefined; //leave undefined if sources undefined
return {
sourceIndexes,
contracts: processContracts({
sources,
compilerOutput,
solcVersion,
originalSourcePaths
}),
sources: outputSources,
compiler: {
name: "solc",
version: solcVersion
}
};
}
function orderABI({ abi, contractName, ast }) {
if (!abi) {
return []; //Yul doesn't return ABIs, but we require something
}
if (!ast || !ast.nodes) {
return abi;
}
// AST can have multiple contract definitions, make sure we have the
// one that matches our contract
const contractDefinition = ast.nodes.find(
({ nodeType, name }) =>
nodeType === "ContractDefinition" && name === contractName
);
if (!contractDefinition || !contractDefinition.nodes) {
return abi;
}
// Find all function definitions
const orderedFunctionNames = contractDefinition.nodes
.filter(({ nodeType }) => nodeType === "FunctionDefinition")
.map(({ name: functionName }) => functionName);
// Put function names in a hash with their order, lowest first, for speed.
const functionIndexes = orderedFunctionNames
.map((functionName, index) => ({ [functionName]: index }))
.reduce((a, b) => Object.assign({}, a, b), {});
// Construct new ABI with functions at the end in source order
return [
...abi.filter(({ name }) => functionIndexes[name] === undefined),
// followed by the functions in the source order
...abi
.filter(({ name }) => functionIndexes[name] !== undefined)
.sort(
({ name: a }, { name: b }) => functionIndexes[a] - functionIndexes[b]
)
];
}
/**
* @param sources - { [sourcePath]: contents }
* @param targets - sourcePath[]
* @param setings - subset of Solidity settings
* @return solc compiler input JSON
*/
function prepareCompilerInput({
sources,
targets,
language,
settings,
modelCheckerSettings
}) {
return {
language,
sources: prepareSources({ sources }),
settings: {
evmVersion: settings.evmVersion,
optimizer: settings.optimizer,
remappings: settings.remappings,
debug: settings.debug,
metadata: settings.metadata,
libraries: settings.libraries,
viaIR: settings.viaIR,
modelChecker: settings.modelChecker,
// Specify compilation targets. Each target uses defaultSelectors,
// defaulting to single target `*` if targets are unspecified
outputSelection: prepareOutputSelection({ targets })
},
modelCheckerSettings
};
}
/**
* Convert sources into solc compiler input format
* @param sources - { [sourcePath]: string }
* @return { [sourcePath]: { content: string } }
*/
function prepareSources({ sources }) {
return Object.entries(sources)
.map(([sourcePath, content]) => ({ [sourcePath]: { content } }))
.reduce((a, b) => Object.assign({}, a, b), {});
}
/**
* If targets are specified, specify output selectors for each individually.
* Otherwise, just use "*" selector
* @param targets - sourcePath[] | undefined
*/
function prepareOutputSelection({ targets = [] }) {
const defaultSelectors = {
"": ["legacyAST", "ast"],
"*": [
"abi",
"metadata",
"evm.bytecode.object",
"evm.bytecode.linkReferences",
"evm.bytecode.sourceMap",
"evm.bytecode.generatedSources",
"evm.deployedBytecode.object",
"evm.deployedBytecode.linkReferences",
"evm.deployedBytecode.sourceMap",
"evm.deployedBytecode.immutableReferences",
"evm.deployedBytecode.generatedSources",
"userdoc",
"devdoc"
]
};
if (!targets.length) {
return {
"*": defaultSelectors
};
}
return targets
.map(target => ({ [target]: defaultSelectors }))
.reduce((a, b) => Object.assign({}, a, b), {});
}
/**
* Load solc and perform compilation
*/
async function invokeCompiler({ compilerInput, options }) {
const supplierOptions = {
parser: options.parser,
events: options.events,
solcConfig: options.compilers.solc
};
const supplier = new CompilerSupplier(supplierOptions);
const { solc } = await supplier.load();
const solcVersion = solc.version();
// perform compilation
const inputString = JSON.stringify(compilerInput);
const outputString = solc.compile(inputString);
const compilerOutput = JSON.parse(outputString);
return {
compilerOutput,
solcVersion
};
}
/**
* Extract errors/warnings from compiler output based on strict mode setting
* @return { errors: string, warnings: string }
*/
function detectErrors({
compilerOutput: { errors: outputErrors },
options,
solcVersion
}) {
outputErrors = outputErrors || [];
const rawErrors = options.strict
? outputErrors
: outputErrors.filter(({ severity }) => severity !== "warning");
const rawWarnings = options.strict
? [] // none of those in strict mode
: outputErrors.filter(({ severity, message }) =>
severity === "warning" &&
message !== "Yul is still experimental. Please use the output with care." //filter out Yul warning
);
// extract messages
let errors = rawErrors.map(({ formattedMessage }) => formattedMessage).join();
const warnings = rawWarnings.map(({ formattedMessage }) => formattedMessage);
if (errors.includes("requires different compiler version")) {
const contractSolcVer = errors.match(/pragma solidity[^;]*/gm)[0];
const configSolcVer =
options.compilers.solc.version || semver.valid(solcVersion);
errors = errors.concat(
[
OS.EOL,
`Error: Truffle is currently using solc ${configSolcVer}, `,
`but one or more of your contracts specify "${contractSolcVer}".`,
OS.EOL,
`Please update your truffle config or pragma statement(s).`,
OS.EOL,
`(See https://trufflesuite.com/docs/truffle/reference/configuration#compiler-configuration `,
`for information on`,
OS.EOL,
`configuring Truffle to use a specific solc compiler version.)`
].join("")
);
}
return { warnings, errors };
}
/**
* aggregate source information based on compiled output;
* this can include sources that do not define any contracts
*/
function processAllSources({ sources, compilerOutput, originalSourcePaths, language }) {
if (!compilerOutput.sources) {
const entries = Object.entries(sources);
if (entries.length === 1) {
//special case for handling Yul
const [sourcePath, contents] = entries[0];
return [{
sourcePath: originalSourcePaths[sourcePath],
contents,
language
}]
} else {
return [];
}
}
let outputSources = [];
for (const [sourcePath, { id, ast, legacyAST }] of Object.entries(
compilerOutput.sources
)) {
outputSources[id] = {
sourcePath: originalSourcePaths[sourcePath],
contents: sources[sourcePath],
ast,
legacyAST,
language
};
}
return outputSources;
}
/**
* Converts compiler-output contracts into @truffle/compile-solidity's return format
* Uses compiler contract output plus other information.
*/
function processContracts({
compilerOutput,
sources,
originalSourcePaths,
solcVersion
}) {
if (!compilerOutput.contracts) return [];
return (
Object.entries(compilerOutput.contracts)
// map to [[{ source, contractName, contract }]]
.map(([sourcePath, sourceContracts]) =>
Object.entries(sourceContracts).map(([contractName, contract]) => ({
contractName,
contract,
source: {
//some versions of Yul don't have sources in output
ast: ((compilerOutput.sources || {})[sourcePath] || {}).ast,
legacyAST: ((compilerOutput.sources || {})[sourcePath] || {}).legacyAST,
contents: sources[sourcePath],
sourcePath
}
}))
)
// and flatten
.reduce((a, b) => [...a, ...b], [])
// All source will have a key, but only the compiled source will have
// the evm output.
.filter(({ contract: { evm } }) => Object.keys(evm).length > 0)
// convert to output format
.map(
({
contractName,
contract: {
evm: {
bytecode: {
sourceMap,
linkReferences,
generatedSources,
object: bytecode
},
deployedBytecode: deployedBytecodeInfo //destructured below
},
abi,
metadata,
devdoc,
userdoc
},
source: {
ast,
legacyAST,
sourcePath: transformedSourcePath,
contents: source
}
}) => ({
contractName,
abi: orderABI({ abi, contractName, ast }),
metadata,
devdoc,
userdoc,
sourcePath: originalSourcePaths[transformedSourcePath],
source,
sourceMap,
deployedSourceMap: (deployedBytecodeInfo || {}).sourceMap,
ast,
legacyAST,
bytecode: zeroLinkReferences({
bytes: bytecode,
linkReferences: formatLinkReferences(linkReferences)
}),
deployedBytecode: zeroLinkReferences({
bytes: (deployedBytecodeInfo || {}).object,
linkReferences: formatLinkReferences((deployedBytecodeInfo || {}).linkReferences)
}),
immutableReferences: (deployedBytecodeInfo || {}).immutableReferences,
//ideally immutable references would be part of the deployedBytecode object,
//but compatibility makes that impossible
generatedSources,
deployedGeneratedSources: (deployedBytecodeInfo || {}).generatedSources,
compiler: {
name: "solc",
version: solcVersion
}
})
)
);
}
function formatLinkReferences(linkReferences) {
if (!linkReferences) {
return [];
}
// convert to flat list
const libraryLinkReferences = Object.values(linkReferences)
.map(fileLinks =>
Object.entries(fileLinks).map(([name, links]) => ({
name,
links
}))
)
.reduce((a, b) => [...a, ...b], []);
// convert to { offsets, length, name } format
return libraryLinkReferences.map(({ name, links }) => ({
offsets: links.map(({ start }) => start),
length: links[0].length, // HACK just assume they're going to be the same
name
}));
}
// takes linkReferences in output format (not Solidity's format)
function zeroLinkReferences({ bytes, linkReferences }) {
if (bytes === undefined) {
return undefined;
}
// inline link references - start by flattening the offsets
const flattenedLinkReferences = linkReferences
// map each link ref to array of link refs with only one offset
.map(({ offsets, length, name }) =>
offsets.map(offset => ({ offset, length, name }))
)
// flatten
.reduce((a, b) => [...a, ...b], []);
// then overwite bytes with zeroes
bytes = flattenedLinkReferences.reduce((bytes, { offset, length }) => {
// length is a byte offset
const characterLength = length * 2;
const start = offset * 2;
const zeroes = "0".repeat(characterLength);
return `${bytes.substring(0, start)}${zeroes}${bytes.substring(
start + characterLength
)}`;
}, bytes);
return { bytes, linkReferences };
}
module.exports = { run };