Skip to content

Commit

Permalink
feat(all): Improve blueprint integration (#7)
Browse files Browse the repository at this point in the history
Closes #6
  • Loading branch information
jubnzv authored Oct 9, 2024
1 parent dd9e3e9 commit e79179e
Show file tree
Hide file tree
Showing 13 changed files with 2,141 additions and 123 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ jobs:

- name: Run ESLint
run: yarn lint

- name: Run tests
run: yarn test
27 changes: 27 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Building and testing locally
1. Build this plugin:
```
yarn build
```
2. Create a new Blueprint Tact project:
```
npm create ton@latest
cd <project name>
```
3. Add this plugin from the project's directory:
```
yarn add file:/path/to/blueprint-misti
```
4. Add the Blueprint configuration to the project:
```bash
echo "import { MistiPlugin } from '@nowarp/blueprint-misti';
export const config = {
plugins: [
new MistiPlugin(),
],
};" > blueprint.config.ts
```
5. Test the plugin calling the following command in the project directory:
```bash
yarn blueprint misti <options if needed>
```
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"words": [
"nowarp",
"Georgiy",
"compilables",
"Komarov"
],
"flagWords": [],
Expand Down
7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset : "ts-jest",
testEnvironment : "node",
testPathIgnorePatterns : [ "/node_modules/", "/dist/" ],
maxWorkers : 1,
};

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
],
"scripts": {
"build": "tsc",
"test": "jest",
"clean": "rm -rf dist",
"fmt": "prettier --check src",
"lint": "eslint src",
Expand All @@ -30,18 +31,22 @@
},
"devDependencies": {
"@release-it/keep-a-changelog": "^5.0.0",
"@tact-lang/compiler": "~1.5.2",
"@ton/blueprint": "^0.22.0",
"@ton/core": "^0.53.0",
"@ton/crypto": "^3.2.0",
"@ton/ton": "^13.9.0",
"@types/jest": "^29.2.3",
"@types/node": "^20.2.5",
"@typescript-eslint/eslint-plugin": "^7.0.4",
"@typescript-eslint/parser": "^7.0.4",
"cspell": "^8.14.4",
"eslint": "^8.57.0",
"jest": "^29.7.0",
"knip": "^5.30.5",
"prettier": "^3.3.3",
"release-it": "^17.6.0",
"ts-jest": "^29.0.3",
"typescript": "^4.9.5"
},
"peerDependencies": {
Expand All @@ -51,7 +56,7 @@
"@ton/ton": ">=13.4.1"
},
"dependencies": {
"@nowarp/misti": "^0.3.1"
"@nowarp/misti": "~0.4.0"
},
"prettier": {
"semi": true,
Expand Down
83 changes: 83 additions & 0 deletions src/blueprint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Various utilities to work with Blueprint internals and its generated files.
*
* @packageDocumentation
*/

import { Args } from "@ton/blueprint";
import {
getCompilablesDirectory,
COMPILE_END,
} from "@ton/blueprint/dist/compile/compile";
import { CompilerConfig } from "@ton/blueprint/dist/compile/CompilerConfig";
import { ConfigProject } from "@tact-lang/compiler";
import path from "path";

/**
* Tact project info parsed from the Blueprint compilation wrapper.
*/
export type TactProjectInfo = {
projectName: string;
target: string;
options?: ConfigProject["options"];
};

/**
* Extracts the `CompilerConfig` from the given project name.
*
* XXX: Imported from blueprint, since the original function is private:
* https://github.com/ton-org/blueprint/issues/151
*/
async function getCompilerConfigForContract(
name: string,
): Promise<CompilerConfig> {
const compilablesDirectory = await getCompilablesDirectory();
const mod = await import(path.join(compilablesDirectory, name + COMPILE_END));
if (typeof mod.compile !== "object") {
throw new Error(`Object 'compile' is missing`);
}
return mod.compile;
}

/**
* Extracts an information from the TypeScript wrapper file generated by Blueprint.
*/
export async function extractProjectInfo(
blueprintCompilePath: string,
): Promise<TactProjectInfo> | never {
const filePath = path.resolve(__dirname, blueprintCompilePath);
const projectName = path.basename(filePath).replace(".compile.ts", "");
const compilerConfig = await getCompilerConfigForContract(projectName);
switch (compilerConfig.lang) {
case "func":
throw new Error(
"FunC projects are not currently supported: https://github.com/nowarp/misti/issues/56",
);
case "tact":
return {
projectName,
target: compilerConfig.target,
options: compilerConfig.options,
};
default:
// XXX: It might be *anything* according to the Blueprint API
throw new Error(`Please specify \`lang\` property in ${filePath}`);
}
}

/**
* Converts Blueprint arguments to a list of strings.
*/
export function argsToStringList(args: Args): string[] {
const argsList: string[] = args._;
Object.entries(args).forEach(([key, value]) => {
if (key !== "_" && value !== undefined) {
if (typeof value === "boolean") {
argsList.push(key);
} else {
argsList.push(key, value.toString());
}
}
});
return argsList;
}
124 changes: 124 additions & 0 deletions src/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Args, UIProvider } from "@ton/blueprint";
import { findCompiles, selectFile } from "@ton/blueprint/dist/utils";
import { Sym } from "./util";
import {
TactProjectInfo,
extractProjectInfo,
argsToStringList,
} from "./blueprint";
import {
MistiResult,
runMistiCommand,
createMistiCommand,
} from "@nowarp/misti/dist/cli";
import { setStdlibPath } from "./stdlibPaths";
import fs from "fs";
import path from "path";
import os from "os";

/**
* Interactively selects one of the Tact projects available in the Blueprint compile wrapper.
*/
async function selectProject(
ui: UIProvider,
args: Args,
): Promise<TactProjectInfo> | never {
const result = await selectFile(await findCompiles(), {
ui,
hint: args._.length > 1 && args._[1].length > 0 ? args._[1] : undefined,
import: false,
});
if (!fs.existsSync(result.path)) {
throw new Error(
[
`${Sym.ERR} Cannot access ${result.path}`,
"Please specify path to your contract directly: `yarn blueprint misti path/to/contract.tact`",
].join("\n"),
);
}
return await extractProjectInfo(result.path);
}

export class MistiExecutor {
private constructor(
private projectName: string,
private args: string[],
private ui: UIProvider,
) {}
public static async fromArgs(
args: Args,
ui: UIProvider,
): Promise<MistiExecutor> | never {
let argsStr = argsToStringList(args).slice(1);
const command = createMistiCommand();

let tactPathIsDefined = true;
const originalArgsStr = [...argsStr];
try {
await command.parseAsync(argsStr, { from: "user" });
} catch (error) {
tactPathIsDefined = false;
if (error instanceof Error && error.message.includes("is required")) {
const tempPath = "/tmp/contract.tact";
argsStr.push(tempPath);
await command.parseAsync(argsStr, { from: "user" });
} else {
throw error;
}
}
argsStr = originalArgsStr;

if (tactPathIsDefined) {
// The path to the Tact configuration or contract is explicitly specified
// in arguments (e.g. yarn blueprint misti path/to/contract.tact).
const tactPath = command.args[0];
const projectName = path.basename(tactPath).split(".")[0];
return new MistiExecutor(projectName, argsStr, ui);
}

// Interactively select the project
const project = await selectProject(ui, args);
try {
const tactPath = this.generateTactConfig(project, ".");
argsStr.push(tactPath);
return new MistiExecutor(project.projectName, argsStr, ui);
} catch {
throw new Error(`Cannot create a Tact config in current directory`);
}
}

/**
* Generates the Tact configuration file based on the Blueprint compilation output.
*
* @param outDir Directory to save the generated file
* @throws If it is not possible to create a path
* @returns Absolute path to the generated config
*/
private static generateTactConfig(
config: TactProjectInfo,
outDir: string,
): string | never {
const project: any = {
name: config.projectName,
path: config.target,
output: path.join(os.tmpdir(), "tact-output"),
};
if (config.options !== undefined) {
project.options = config.options;
}
const content = JSON.stringify({
projects: [project],
});
const outPath = path.join(outDir, "tact.config.json");
fs.writeFileSync(outPath, content);
return outPath;
}

public async execute(): Promise<MistiResult> {
this.ui.write(`${Sym.WAIT} Checking ${this.projectName}...\n`);
setStdlibPath(this.args);
// ! is safe: it could not be undefined in Misti 0.4+
const result = (await runMistiCommand(this.args))!;
return result[1];
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class MistiPlugin implements Plugin {
help: `Usage: blueprint misti [flags]
Runs the Misti static analyzer to find security flaws in the project.
See more: https://nowarp.github.io/tools/misti`,
See more: https://nowarp.io/tools/misti/docs/tutorial/blueprint`,
},
];
}
Expand Down
Loading

0 comments on commit e79179e

Please sign in to comment.