UNPKG

@truffle/compile-solidity

Version:
524 lines (484 loc) 15.6 kB
import { zeroLinkReferences, formatLinkReferences } from "./shims"; import debugModule from "debug"; const debug = debugModule("compile:run"); import OS = require("os"); import semver from "semver"; import { CompilerSupplier } from "./compilerSupplier"; import * as Common from "@truffle/compile-common"; import type { Compilation, Source, CompiledContract } from "@truffle/compile-common"; import type { CompilerOutput, Contracts, InternalOptions, ProcessAllSourcesArgs, PrepareCompilerInputArgs, PreparedSources, PrepareSourcesArgs, ProcessContractsArgs, Targets } from "./types"; import type Config from "@truffle/config"; // 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 export async function run( rawSources: { [name: string]: string }, options: Config, internalOptions: InternalOptions = {} ): Promise<Compilation | null> { if (Object.keys(rawSources).length === 0) { return null; } const { language = "Solidity", // could also be "Yul" noTransform = false, // turns off project root transform solc // passing this skips compilerSupplier.load() } = internalOptions; // Ensure sources have operating system independent paths // i.e., convert backslashes to forward slashes; things like C: are left intact. // we also strip the project root (to avoid it appearing in metadata) // and replace it with "project:/" (unless noTransform is set) const { sources, targets, originalSourcePaths } = Common.Sources.collectSources( rawSources, options.compilationTargets, noTransform ? "" : options.working_directory, noTransform ? "" : "project:/" ); // 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, solc, options }); debug("compilerOutput: %O", compilerOutput); // handle warnings as errors if options.strict // log if not options.quiet const { infos, warnings, errors } = detectErrors({ compilerOutput, options, solcVersion }); if (infos.length > 0) { options.events.emit("compile:infos", { infos }); } 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); } const outputSources: Source[] = processAllSources({ sources, compilerOutput, originalSourcePaths, language }); const sourceIndexes: string[] = outputSources ? outputSources.map(source => source.sourcePath) : []; 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: string, index: number) => ({ [functionName]: index })) .reduce( ( a: { [functionName: string]: number }, b: { [functionName: string]: number } ) => 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] ) ]; } function prepareCompilerInput({ sources, targets, language, settings, modelCheckerSettings }: PrepareCompilerInputArgs) { return { language, sources: prepareSources({ sources }), settings: { ...settings, // 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 }: PrepareSourcesArgs): PreparedSources { 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 = [] }: { targets: Targets }) { const defaultSelectors = { "": ["legacyAST", "ast"], "*": [ "abi", "ast", //necessary to get Yul ASTs "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, solc }): Promise<{ compilerOutput: CompilerOutput; solcVersion: string; }> { const supplierOptions = { parser: options.parser, events: options.events, solcConfig: options.compilers.solc }; if (!solc) { const supplier = new CompilerSupplier(supplierOptions); ({ 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 }; } function detectErrors({ compilerOutput, options, solcVersion }: { compilerOutput: CompilerOutput; options: Config; solcVersion: string; }): { errors: string; warnings: string[]; infos: string[] } { const outputErrors = compilerOutput.errors || []; const rawErrors = outputErrors.filter( ({ severity }) => options.strict ? severity !== "info" //strict mode: warnings are errors too : severity === "error" //nonstrict mode: only errors are errors ); const rawWarnings = options.strict ? [] // in strict mode these get classified as errors, not warnings : outputErrors.filter( ({ severity, message }) => severity === "warning" && message !== "Yul is still experimental. Please use the output with care." //filter out Yul warning ); const rawInfos = outputErrors.filter(({ severity }) => severity === "info"); // extract messages let errors = rawErrors .map(({ formattedMessage }) => formattedMessage.replace( /: File import callback not supported/g, //remove this confusing message suffix "" ) ) .join(); const warnings = rawWarnings.map(({ formattedMessage }) => formattedMessage); const infos = rawInfos.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, infos }; } /** * aggregate source information based on compiled output; * this can include sources that do not define any contracts */ function processAllSources({ sources, compilerOutput, originalSourcePaths, language }: ProcessAllSourcesArgs) { if (!compilerOutput.sources) { const entries = Object.entries(sources); if (entries.length === 1) { //special case for handling old Yul versions const [sourcePath, contents] = entries[0]; return [ { sourcePath: originalSourcePaths[sourcePath], contents, language } ]; } else { return []; } } let outputSources: Source[] = []; for (const [sourcePath, { id, ast, legacyAST }] of Object.entries( compilerOutput.sources )) { outputSources[id] = { sourcePath: originalSourcePaths[sourcePath], contents: sources[sourcePath], ast, legacyAST, language }; } //HACK: special case for handling a Yul compilation bug that causes //the ID to be returned as 1 rather than 0 if ( language === "Yul" && outputSources.length === 2 && outputSources[0] === undefined ) { return [outputSources[1]]; } 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 }: ProcessContractsArgs): CompiledContract[] { let { contracts } = compilerOutput; if (!contracts) return []; //HACK: versions of Solidity prior to 0.4.20 are confused by our "project:/" //prefix (or, more generally, by paths containing colons) //and put contracts in a weird form as a result. we detect //this case and repair it. contracts = repairOldContracts(contracts); return ( Object.entries(contracts) // map to [[{ source, contractName, contract }]] .map(([sourcePath, sourceContracts]) => { return 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 } }) => { return { 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 repairOldContracts(contracts: Contracts): Contracts { const contractNames = Object.values(contracts) .map(source => Object.keys(source)) .flat(); if (contractNames.some(name => name.includes(":"))) { //if any of the "contract names" contains a colon... hack invoked! //(notionally we could always apply this hack but let's skip it most of the //time please :P ) let repairedContracts = {}; for (const [sourcePrefix, sourceContracts] of Object.entries(contracts)) { for (const [mixedPath, contract] of Object.entries(sourceContracts)) { let sourcePath: string, contractName: string; const lastColonIndex = mixedPath.lastIndexOf(":"); if (lastColonIndex === -1) { //if there is none sourcePath = sourcePrefix; contractName = mixedPath; } else { contractName = mixedPath.slice(lastColonIndex + 1); //take the part after the final colon sourcePath = sourcePrefix + ":" + mixedPath.slice(0, lastColonIndex); //the part before the final colon } if (!repairedContracts[sourcePath]) { repairedContracts[sourcePath] = {}; } repairedContracts[sourcePath][contractName] = contract; } } debug("repaired contracts: %O", repairedContracts); return repairedContracts; } else { //otherwise just return contracts as-is rather than processing return contracts; } }