Skip to content

Commit

Permalink
feat(driver): Leverage ImportGraph to resolve entry points (#194)
Browse files Browse the repository at this point in the history
* feat(driver): Leverage ImportGraph to resolve entry points

Closes #57

* fix: Paths

* chore: Update changelog

* fix: API uses in tests, paths

* fix(tests): Remove actual outputs from git
  • Loading branch information
jubnzv authored Oct 24, 2024
1 parent 09f0af2 commit 18223c4
Show file tree
Hide file tree
Showing 34 changed files with 622 additions and 2,469 deletions.
12 changes: 6 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ test/all/**/*.dot
test/all/**/*.mmd
test/all/**/*.actual.out
test/detectors/**/*.actual.out
!test/all/**/*.cfg.json
!test/all/**/*.cfg.dot
!test/all/**/*.cfg.mmd
!test/all/**/*.imports.json
!test/all/**/*.imports.dot
!test/all/**/*.imports.mmd
!test/all/**/*.expected.cfg.json
!test/all/**/*.expected.cfg.dot
!test/all/**/*.expected.cfg.mmd
!test/all/**/*.expected.imports.json
!test/all/**/*.expected.imports.dot
!test/all/**/*.expected.imports.mmd

# Generated code
src/version-info.ts
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `UnusedExpressionResult` detector: PR [#190](https://github.com/nowarp/misti/pull/190)
- `--list-detectors` CLI option: PR [#192](https://github.com/nowarp/misti/pull/192)
- Import Graph: PR [#180](https://github.com/nowarp/misti/pull/180)
- Leverage `ImportGraph` to resolve entry points: PR [#194](https://github.com/nowarp/misti/pull/194)

### Changed
- Improved and optimized the test suite: PR [#184](https://github.com/nowarp/misti/pull/184)
Expand Down
2 changes: 1 addition & 1 deletion examples/implicit-init/test/implicitInit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("ImplicitInit Detector Tests", () => {
"tactConfig.json",
);
const config = path.resolve(__dirname, "project", "mistiConfig.json");
const driver = await Driver.create(tactConfigPath, { config });
const driver = await Driver.create([tactConfigPath], { config });

expect(driver.detectors.length).toBe(1);
expect(driver.detectors[0].id).toBe("ImplicitInit");
Expand Down
4 changes: 2 additions & 2 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function createMistiCommand(): Command {
.name("misti")
.description("TON Static Analyzer")
.version(`Misti ${MISTI_VERSION}\nSupported Tact version: ${TACT_VERSION}`)
.arguments("[TACT_CONFIG_PATH|TACT_FILE_PATH]");
.arguments("[paths...]");
cliOptions.forEach((option) => command.addOption(option));
command.action(async (_tactPath, options) => {
const logger = new Logger();
Expand Down Expand Up @@ -56,7 +56,7 @@ export async function runMistiCommand(
command: Command = createMistiCommand(),
): Promise<[Driver, MistiResult]> {
await command.parseAsync(args, { from: "user" });
const driver = await Driver.create(command.args[0], command.opts());
const driver = await Driver.create(command.args, command.opts());
const result = await driver.execute();
return [driver, result];
}
Expand Down
186 changes: 99 additions & 87 deletions src/cli/driver.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { CLIOptions, cliOptionDefaults } from "./options";
import { MistiTactPath } from "./path";
import {
MistiResult,
ToolOutput,
MistiResultTool,
WarningOutput,
} from "./result";
import { SingleContractProjectManager } from "./singleContract";
import { MistiResult, ToolOutput, WarningOutput } from "./result";
import { OutputFormat } from "./types";
import { Detector, findBuiltInDetector } from "../detectors/detector";
import { MistiContext } from "../internals/context";
import { ExecutionException, InternalException } from "../internals/exceptions";
import { CompilationUnit, ProjectName } from "../internals/ir";
import { CompilationUnit, ImportGraph, ProjectName } from "../internals/ir";
import { createIR } from "../internals/ir/builders/";
import { ImportGraphBuilder } from "../internals/ir/builders/imports";
import { Logger } from "../internals/logger";
import { TactConfigManager, parseTactProject } from "../internals/tact";
import { unreachable } from "../internals/util";
import {
MistiTactWarning,
Expand All @@ -35,38 +30,18 @@ export class Driver {
outputPath: string;
disabledDetectors: Set<string>;
colorizeOutput: boolean;
tactConfigPath: string | undefined;
/**
* Compilation units representing the actual entrypoints of the analysis targets
* based on user's input. Might be empty if no paths are specified.
*/
cus: Map<ProjectName, CompilationUnit>;
/** Minimum severity level to report warnings. */
minSeverity: Severity;
outputFormat: OutputFormat;
singleContractProjectManager: SingleContractProjectManager | undefined;

private constructor(tactPath: string | undefined, options: CLIOptions) {
let mistiTactPath: MistiTactPath | undefined;
if (tactPath) {
const singleContract = tactPath.endsWith(".tact");
if (!fs.existsSync(tactPath)) {
throw ExecutionException.make(
`${singleContract ? "Contract" : "Project"} ${tactPath} is not available.`,
);
}
if (singleContract) {
this.singleContractProjectManager =
SingleContractProjectManager.fromContractPath(tactPath);
this.tactConfigPath =
this.singleContractProjectManager.createTempProjectDir();
mistiTactPath = {
kind: "contract",
tempConfigPath: path.resolve(this.tactConfigPath),
originalPath: path.resolve(tactPath),
};
} else {
// Tact supports absolute paths only
this.tactConfigPath = path.resolve(tactPath);
mistiTactPath = { kind: "config", path: path.resolve(tactPath) };
}
}
this.ctx = new MistiContext(mistiTactPath, options);
private constructor(tactPaths: string[], options: CLIOptions) {
this.ctx = new MistiContext(options);
this.cus = this.createCUs(tactPaths);
this.disabledDetectors = new Set(options.disabledDetectors ?? []);
this.colorizeOutput = options.colors;
this.minSeverity = options.minSeverity;
Expand All @@ -79,12 +54,12 @@ export class Driver {
* @param tactPath Path to the Tact project configuration of to a single Tact contract.
*/
public static async create(
tactPath: string | undefined,
tactPaths: string[],
options: Partial<CLIOptions> = {},
): Promise<Driver> {
const mergedOptions: CLIOptions = { ...cliOptionDefaults, ...options };
this.checkCLIOptions(mergedOptions);
const driver = new Driver(tactPath, mergedOptions);
const driver = new Driver(tactPaths, mergedOptions);
await driver.initializeDetectors();
await driver.initializeTools();
if (!driver.ctx.souffleAvailable) {
Expand All @@ -93,6 +68,73 @@ export class Driver {
return driver;
}

/**
* Resolves the filepaths provided as an input to Misti to initialize the
* compilation units which are IR entries to target analysis on.
*
* @param tactPaths Paths received from the user.
* @returns Created compilation units.
*/
private createCUs(tactPaths: string[]): Map<ProjectName, CompilationUnit> {
return [...new Set(tactPaths)]
.filter(
(tactPath) =>
fs.existsSync(tactPath) ||
(this.ctx.logger.error(`${tactPath} is not available`), false),
)
.reduce((acc, tactPath) => {
// TODO: Check on the available import graphs if some of the inputs are already added
let importGraph: ImportGraph;
let configManager: TactConfigManager;
if (tactPath.endsWith(".tact")) {
importGraph = ImportGraphBuilder.make(this.ctx, [tactPath]).build();
let projectRoot = importGraph.resolveProjectRoot();
if (projectRoot === undefined) {
projectRoot = path.dirname(tactPath);
this.ctx.logger.warn(
`Cannot resolve project path. Trying ${projectRoot}`,
);
}
const projectName = path.basename(tactPath, ".tact") as ProjectName;
// TODO: Try to merge them into one of the existing configs.
configManager = TactConfigManager.fromContract(
projectRoot,
tactPath,
projectName,
);
const projectConfig = configManager.findProjectByName(projectName);
if (projectConfig === undefined) {
throw InternalException.make(
[
`Cannot find ${projectName} in the configuration file generated for ${tactPath}:`,
JSON.stringify(configManager.getConfig, null, 2),
].join("\n"),
);
}
const ast = parseTactProject(this.ctx, projectConfig, projectRoot);
const cu = createIR(this.ctx, projectName, ast, importGraph);
acc.set(projectName, cu);
} else {
configManager = TactConfigManager.fromConfig(tactPath);
importGraph = ImportGraphBuilder.make(
this.ctx,
configManager.getEntryPoints(),
).build();
configManager.getProjects().forEach((configProject) => {
const ast = parseTactProject(
this.ctx,
configProject,
configManager.getProjectRoot(),
);
const projectName = configProject.name as ProjectName;
const cu = createIR(this.ctx, projectName, ast, importGraph);
acc.set(projectName, cu);
});
}
return acc;
}, new Map<ProjectName, CompilationUnit>());
}

/**
* Check CLI options for ambiguities.
* @throws If Misti cannot be executed with the given options
Expand Down Expand Up @@ -245,8 +287,19 @@ export class Driver {
* Entry point of code analysis and tools execution.
*/
public async execute(): Promise<MistiResult> {
if (this.cus.size === 0) {
return { kind: "ok" };
}
if (this.detectors.length === 0 && this.tools.length === 0) {
this.ctx.logger.warn(
"Nothing to execute. Please specify at least one detector or tool.",
);
return { kind: "ok" };
}
try {
return await this.executeImpl();
return this.tools.length > 0
? await this.executeTools()
: await this.executeAnalysis();
} catch (err) {
const result = [] as string[];
if (err instanceof Error) {
Expand All @@ -263,62 +316,24 @@ export class Driver {
}
}

/**
* Executes checks on all compilation units and reports found warnings sorted by severity.
*/
private async executeImpl(): Promise<MistiResult> | never {
try {
if (!this.tactConfigPath) {
throw ExecutionException.make(
"Please specify a path to a Tact project or Tact contract",
);
}
const cus: Map<ProjectName, CompilationUnit> = createIR(
this.ctx,
this.tactConfigPath,
);
if (this.detectors.length === 0 && this.tools.length === 0) {
this.ctx.logger.warn(
"Nothing to execute. Please specify at least one detector or tool.",
);
return { kind: "ok" };
}
const result =
this.tools.length > 0
? await this.executeTools(cus)
: await this.executeAnalysis(cus);
return result;
} finally {
if (this.singleContractProjectManager) {
try {
this.singleContractProjectManager.cleanup();
} catch (error) {
throw error;
}
}
}
}

/**
* Executes all the initialized detectors on the compilation units.
* @param cus Map of compilation units
* @returns MistiResult containing detectors output
*/
private async executeAnalysis(
cus: Map<ProjectName, CompilationUnit>,
): Promise<MistiResult> {
private async executeAnalysis(): Promise<MistiResult> {
const allWarnings = await (async () => {
const warningsMap = new Map<ProjectName, MistiTactWarning[]>();
await Promise.all(
Array.from(cus.entries()).map(async ([projectName, cu]) => {
Array.from(this.cus.entries()).map(async ([projectName, cu]) => {
const warnings = await this.checkCU(cu);
warningsMap.set(projectName, warnings);
}),
);
return warningsMap;
})();
const filteredWarnings: Map<ProjectName, MistiTactWarning[]> =
this.filterImportedWarnings(Array.from(cus.keys()), allWarnings);
this.filterImportedWarnings(Array.from(this.cus.keys()), allWarnings);
this.filterSuppressedWarnings(filteredWarnings);
const reported = new Set<string>();
const warningsOutput: WarningOutput[] = [];
Expand Down Expand Up @@ -358,14 +373,11 @@ export class Driver {

/**
* Executes all the initialized tools on the compilation units.
* @param cus Map of compilation units
* @returns MistiResult containing tool outputs
*/
private async executeTools(
cus: Map<ProjectName, CompilationUnit>,
): Promise<MistiResultTool> {
private async executeTools(): Promise<MistiResult> {
const toolOutputs = await Promise.all(
Array.from(cus.values()).flatMap((cu) =>
Array.from(this.cus.values()).flatMap((cu) =>
this.tools.map((tool) => {
try {
return tool.run(cu);
Expand Down Expand Up @@ -432,7 +444,7 @@ export class Driver {
if (
new Set(allProjectNames).size === new Set(projects).size &&
[...new Set(allProjectNames)].every((value) =>
new Set(projects).has(value),
new Set(projects).has(value as ProjectName),
)
) {
projectWarnings.push(warn);
Expand Down
1 change: 0 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export * from "./driver";
export * from "./result";
export * from "./types";
export * from "./options";
export * from "./path";
export * from "./cli";
60 changes: 0 additions & 60 deletions src/cli/path.ts

This file was deleted.

Loading

0 comments on commit 18223c4

Please sign in to comment.