Skip to content

Commit

Permalink
feat(compiler): provide compilation warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Feb 9, 2024
1 parent cd2357c commit d0ec012
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 34 deletions.
23 changes: 15 additions & 8 deletions src/contract/compiler/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand All @@ -50,10 +60,7 @@ export default abstract class CompilerBase {
abstract compileBySourceCode(
sourceCode: string,
fileSystem?: Record<string, string>,
): Promise<{
bytecode: Encoded.ContractBytearray;
aci: Aci;
}>;
): CompileResult;

/**
* Generate contract's ACI by contract's path
Expand Down
47 changes: 31 additions & 16 deletions src/contract/compiler/Cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,16 +45,21 @@ export default class CompilerCli extends CompilerBase {
}
}

async #run(...parameters: string[]): Promise<string> {
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<string> {
const { stderr, stdout } = await this.#runWithStderr(...parameters);
if (stderr !== '') throw new CompilerError(stderr);
return stdout;
}

static async #saveContractToTmpDir(
sourceCode: string,
fileSystem: Record<string, string> = {},
Expand All @@ -73,30 +78,40 @@ 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);
throw new CompilerError(error.message);
}
}

async compileBySourceCode(sourceCode: string, fileSystem?: Record<string, string>): Promise<{
bytecode: Encoded.ContractBytearray;
aci: Aci;
}> {
async compileBySourceCode(
sourceCode: string,
fileSystem?: Record<string, string>,
): CompileResult {
const tmp = await CompilerCli.#saveContractToTmpDir(sourceCode, fileSystem);
try {
return await this.compile(tmp);
Expand Down
13 changes: 8 additions & 5 deletions src/contract/compiler/Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -63,11 +63,14 @@ export default class CompilerHttp extends CompilerBase {
async compileBySourceCode(
sourceCode: string,
fileSystem?: Record<string, string>,
): 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<CompileResult>;
} catch (error) {
if (error instanceof RestError && error.statusCode === 400) {
throw new CompilerError(error.message);
Expand All @@ -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');
}

Expand Down
4 changes: 2 additions & 2 deletions src/contract/compiler/HttpNode.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Expand Down
35 changes: 33 additions & 2 deletions test/integration/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tooling/autorest/compiler-prepare.mjs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down

0 comments on commit d0ec012

Please sign in to comment.