diff --git a/src/contract/compiler/Base.ts b/src/contract/compiler/Base.ts index a06e4c733a..289d62d3da 100644 --- a/src/contract/compiler/Base.ts +++ b/src/contract/compiler/Base.ts @@ -19,6 +19,19 @@ export type Aci = Array<{ }; }>; +export type CompileResult = Promise<{ + bytecode: Encoded.ContractBytearray; + aci: Aci; + warnings: Array<{ + message: string; + pos: { + file?: string; + line: number; + col: number; + }; + }>; +}>; + /** * A base class for all compiler implementations */ @@ -29,10 +42,7 @@ export default abstract class CompilerBase { * @param path - Path to contract source code * @returns ACI and bytecode */ - abstract compile(path: string): Promise<{ - bytecode: Encoded.ContractBytearray; - aci: Aci; - }>; + abstract compile(path: string): CompileResult; /** * Compile contract by contract's source code @@ -50,10 +60,7 @@ export default abstract class CompilerBase { abstract compileBySourceCode( sourceCode: string, fileSystem?: Record, - ): Promise<{ - bytecode: Encoded.ContractBytearray; - aci: Aci; - }>; + ): CompileResult; /** * Generate contract's ACI by contract's path diff --git a/src/contract/compiler/Cli.ts b/src/contract/compiler/Cli.ts index 491723d28f..2292d5e9e4 100644 --- a/src/contract/compiler/Cli.ts +++ b/src/contract/compiler/Cli.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'os'; import { resolve, dirname, basename } from 'path'; import { mkdir, writeFile, rm } from 'fs/promises'; import { fileURLToPath } from 'url'; -import CompilerBase, { Aci } from './Base'; +import CompilerBase, { Aci, CompileResult } from './Base'; import { Encoded } from '../../utils/encoder'; import { CompilerError, InternalError, UnsupportedVersionError } from '../../utils/errors'; import semverSatisfies from '../../utils/semver-satisfies'; @@ -45,16 +45,21 @@ export default class CompilerCli extends CompilerBase { } } - async #run(...parameters: string[]): Promise { + async #runWithStderr(...parameters: string[]): Promise<{ stderr: string; stdout: string }> { return new Promise((pResolve, pReject) => { execFile('escript', [this.#path, ...parameters], (error, stdout, stderr) => { if (error != null) pReject(error); - else if (stderr !== '') pReject(new CompilerError(stderr)); - else pResolve(stdout); + else pResolve({ stdout, stderr }); }); }); } + async #run(...parameters: string[]): Promise { + const { stderr, stdout } = await this.#runWithStderr(...parameters); + if (stderr !== '') throw new CompilerError(stderr); + return stdout; + } + static async #saveContractToTmpDir( sourceCode: string, fileSystem: Record = {}, @@ -73,19 +78,29 @@ export default class CompilerCli extends CompilerBase { return sourceCodePath; } - async compile(path: string): Promise<{ - bytecode: Encoded.ContractBytearray; - aci: Aci; - }> { + async compile(path: string): CompileResult { await this.#ensureCompatibleVersion; try { - const [bytecode, aci] = await Promise.all([ - this.#run(path, '--no_warning', 'all'), - this.#run('--create_json_aci', path).then((res) => JSON.parse(res)), + const [compileRes, aci] = await Promise.all([ + this.#runWithStderr(path), + this.generateAci(path), ]); return { - bytecode: bytecode.trimEnd() as Encoded.ContractBytearray, + bytecode: compileRes.stdout.trimEnd() as Encoded.ContractBytearray, aci, + warnings: compileRes.stderr.split('Warning in ').slice(1).map((warning) => { + const reg = /^'(.+)' at line (\d+), col (\d+):\n(.+)$/s; + const match = warning.match(reg); + if (match == null) throw new InternalError(`Can't parse compiler output: "${warning}"`); + return { + message: match[4].trimEnd(), + pos: { + ...match[1] !== path && { file: match[1] }, + line: +match[2], + col: +match[3], + }, + }; + }), }; } catch (error) { ensureError(error); @@ -93,10 +108,10 @@ export default class CompilerCli extends CompilerBase { } } - async compileBySourceCode(sourceCode: string, fileSystem?: Record): Promise<{ - bytecode: Encoded.ContractBytearray; - aci: Aci; - }> { + async compileBySourceCode( + sourceCode: string, + fileSystem?: Record, + ): CompileResult { const tmp = await CompilerCli.#saveContractToTmpDir(sourceCode, fileSystem); try { return await this.compile(tmp); diff --git a/src/contract/compiler/Http.ts b/src/contract/compiler/Http.ts index e4cdf83a07..04fcf7f580 100644 --- a/src/contract/compiler/Http.ts +++ b/src/contract/compiler/Http.ts @@ -5,7 +5,7 @@ import { CompilerError as CompilerErrorApi, } from '../../apis/compiler'; import { genErrorFormatterPolicy, genVersionCheckPolicy } from '../../utils/autorest'; -import CompilerBase, { Aci } from './Base'; +import CompilerBase, { Aci, CompileResult } from './Base'; import { Encoded } from '../../utils/encoder'; import { CompilerError, NotImplementedError } from '../../utils/errors'; @@ -63,11 +63,14 @@ export default class CompilerHttp extends CompilerBase { async compileBySourceCode( sourceCode: string, fileSystem?: Record, - ): Promise<{ bytecode: Encoded.ContractBytearray; aci: Aci }> { + ): CompileResult { try { - const res = await this.api.compileContract({ code: sourceCode, options: { fileSystem } }); + const cmpOut = await this.api.compileContract({ code: sourceCode, options: { fileSystem } }); + cmpOut.warnings ??= []; // TODO: remove after requiring http compiler above or equal to 8.0.0 + const warnings = cmpOut.warnings.map(({ type, ...warning }) => warning); + const res = { ...cmpOut, warnings }; // TODO: should be fixed when the compiledAci interface gets updated - return res as { bytecode: Encoded.ContractBytearray; aci: Aci }; + return res as Awaited; } catch (error) { if (error instanceof RestError && error.statusCode === 400) { throw new CompilerError(error.message); @@ -77,7 +80,7 @@ export default class CompilerHttp extends CompilerBase { } // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars - async compile(path: string): Promise<{ bytecode: Encoded.ContractBytearray; aci: Aci }> { + async compile(path: string): CompileResult { throw new NotImplementedError('File system access, use CompilerHttpNode instead'); } diff --git a/src/contract/compiler/HttpNode.ts b/src/contract/compiler/HttpNode.ts index c4d4541c8f..37d2aa5b49 100644 --- a/src/contract/compiler/HttpNode.ts +++ b/src/contract/compiler/HttpNode.ts @@ -1,6 +1,6 @@ import { readFile } from 'fs/promises'; import HttpBrowser from './Http'; -import { Aci } from './Base'; +import { Aci, CompileResult } from './Base'; import { Encoded } from '../../utils/encoder'; import getFileSystem from './getFileSystem'; @@ -12,7 +12,7 @@ import getFileSystem from './getFileSystem'; * @example CompilerHttpNode('COMPILER_URL') */ export default class CompilerHttpNode extends HttpBrowser { - override async compile(path: string): Promise<{ bytecode: Encoded.ContractBytearray; aci: Aci }> { + override async compile(path: string): CompileResult { const fileSystem = await getFileSystem(path); const sourceCode = await readFile(path, 'utf8'); return this.compileBySourceCode(sourceCode, fileSystem); diff --git a/test/integration/compiler.ts b/test/integration/compiler.ts index 907c8fe028..d48c169261 100644 --- a/test/integration/compiler.ts +++ b/test/integration/compiler.ts @@ -67,15 +67,18 @@ function testCompiler(compiler: CompilerBase, isAesophia7: boolean): void { }); it('compiles and generates aci by path', async () => { - const { bytecode, aci } = await compiler.compile(inclSourceCodePath); + const { bytecode, aci, warnings } = await compiler.compile(inclSourceCodePath); expect(bytecode).to.equal(inclBytecode); expect(aci).to.eql(inclAci); + expect(warnings).to.eql([]); }); it('compiles and generates aci by source code', async () => { - const { bytecode, aci } = await compiler.compileBySourceCode(inclSourceCode, inclFileSystem); + const { bytecode, aci, warnings } = await compiler + .compileBySourceCode(inclSourceCode, inclFileSystem); expect(bytecode).to.equal(inclBytecode); expect(aci).to.eql(inclAci); + expect(warnings).to.eql([]); }); it('throws clear exception if compile broken contract', async () => { @@ -97,6 +100,34 @@ function testCompiler(compiler: CompilerBase, isAesophia7: boolean): void { ); }); + it('returns warnings', async () => { + const { warnings } = await compiler.compileBySourceCode( + 'include "./lib/Library.aes"\n' + + '\n' + + 'main contract Foo =\n' + + ' entrypoint getArg(x: int) =\n' + + ' let t = 42\n' + + ' x\n', + { + './lib/Library.aes': '' + + 'contract Library =\n' + + ' entrypoint getArg() =\n' + + ' 1 / 0\n', + }, + ); + if (isAesophia7 && compiler instanceof CompilerHttpNode) { + expect(warnings).to.eql([]); + return; + } + expect(warnings).to.eql([{ + message: 'The variable `t` is defined but never used.', + pos: { col: 9, line: 5 }, + }, { + message: 'Division by zero.', + pos: { file: './lib/Library.aes', col: 5, line: 3 }, + }]); + }); + it('generates aci by path', async () => { const aci = await compiler.generateAci(interfaceSourceCodePath); expect(aci).to.eql(interfaceAci); diff --git a/tooling/autorest/compiler-prepare.mjs b/tooling/autorest/compiler-prepare.mjs index 6d8f58755e..1ebdb20dc8 100644 --- a/tooling/autorest/compiler-prepare.mjs +++ b/tooling/autorest/compiler-prepare.mjs @@ -1,6 +1,6 @@ import fs from 'fs'; -const swaggerUrl = 'https://raw.githubusercontent.com/aeternity/aesophia_http/v7.4.0/config/swagger.yaml'; +const swaggerUrl = 'https://raw.githubusercontent.com/aeternity/aesophia_http/v8.0.0-rc1/config/swagger.yaml'; const response = await fetch(swaggerUrl); console.assert(response.status === 200, 'Invalid response code', response.status);