From b69d7430f1966dfb047554d625b77cd73571e14d Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 17 Dec 2024 14:39:40 +0530 Subject: [PATCH 1/5] feat: add support for browser environment - Replace direct file system usage with virtualFileSystem for browser environments. - Add souffleEnabled option to disable souffle in browser environments. --- src/cli/cli.ts | 3 +- src/cli/driver.ts | 42 +++++++++----- src/cli/options.ts | 6 ++ src/createDetector.ts | 9 +-- src/detectors/detector.ts | 2 +- src/internals/config.ts | 13 ++++- src/internals/context.ts | 8 ++- src/internals/ir/builders/imports.ts | 13 ++--- src/internals/ir/imports.ts | 5 +- src/internals/tact/config.ts | 9 ++- src/internals/tact/parser.ts | 18 ++++-- src/internals/tact/stdlib.ts | 3 +- src/vfs/createNodeFileSystem.ts | 63 +++++++++++++++++++++ src/vfs/createVirtualFileSystem.ts | 82 ++++++++++++++++++++++++++++ src/vfs/virtualFileSystem.ts | 27 +++++++++ 15 files changed, 263 insertions(+), 40 deletions(-) create mode 100644 src/vfs/createNodeFileSystem.ts create mode 100644 src/vfs/createVirtualFileSystem.ts create mode 100644 src/vfs/virtualFileSystem.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 33b9f5bb..3ec2db81 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -13,6 +13,7 @@ import { ResultReport, } from "./result"; import { Logger } from "../internals/logger"; +import { createNodeFileSystem } from "../vfs/createNodeFileSystem"; import { Command } from "commander"; /** @@ -56,7 +57,7 @@ export async function runMistiCommand( command: Command = createMistiCommand(), ): Promise<[Driver, MistiResult]> { await command.parseAsync(args, { from: "user" }); - const driver = await Driver.create(command.args, command.opts()); + const driver = await Driver.create(command.args, { ...command.opts(), fs: createNodeFileSystem(process.cwd()) }); const result = await driver.execute(); return [driver, result]; } diff --git a/src/cli/driver.ts b/src/cli/driver.ts index 5efeb0b9..6292806b 100644 --- a/src/cli/driver.ts +++ b/src/cli/driver.ts @@ -17,11 +17,10 @@ import { Severity, } from "../internals/warnings"; import { Tool, findBuiltInTool } from "../tools/tool"; -import fs from "fs"; +import { VirtualFileSystem } from "../vfs/virtualFileSystem"; import ignore from "ignore"; import JSONbig from "json-bigint"; import path from "path"; -import { setTimeout } from "timers/promises"; /** * Manages the initialization and execution of detectors for analyzing compilation units. @@ -33,6 +32,7 @@ export class Driver { outputPath: string; disabledDetectors: Set; colorizeOutput: boolean; + fs: VirtualFileSystem; /** * Compilation units representing the actual entrypoints of the analysis targets * based on user's input. Might be empty if no paths are specified. @@ -43,6 +43,7 @@ export class Driver { outputFormat: OutputFormat; private constructor(tactPaths: string[], options: CLIOptions) { + this.fs = options.fs; this.ctx = new MistiContext(options); this.cus = this.createCUs(tactPaths); this.disabledDetectors = new Set(options.disabledDetectors ?? []); @@ -81,7 +82,7 @@ export class Driver { private createCUs(tactPaths: string[]): Map { return [...new Set(tactPaths)] .reduce((acc, tactPath) => { - if (fs.statSync(tactPath).isDirectory()) { + if (this.fs.stat(tactPath).isDirectory()) { const tactFiles = this.collectTactFiles(tactPath); this.ctx.logger.debug( `Collected Tact files from ${tactPath}:\n${tactFiles.map((tactFile) => "- " + tactFile).join("\n")}`, @@ -94,7 +95,7 @@ export class Driver { }, [] as string[]) .filter( (tactPath) => - fs.existsSync(tactPath) || + this.fs.exists(tactPath) || (this.ctx.logger.error(`${tactPath} is not available`), false), ) .reduce((acc, tactPath) => { @@ -103,7 +104,7 @@ export class Driver { let configManager: TactConfigManager; if (tactPath.endsWith(".tact")) { importGraph = ImportGraphBuilder.make(this.ctx, [tactPath]).build(); - let projectRoot = importGraph.resolveProjectRoot(); + let projectRoot = importGraph.resolveProjectRoot(this.fs); if (projectRoot === undefined) { projectRoot = path.dirname(tactPath); this.ctx.logger.warn( @@ -116,6 +117,7 @@ export class Driver { projectRoot, tactPath, projectName, + this.fs, ); const projectConfig = configManager.findProjectByName(projectName); if (projectConfig === undefined) { @@ -126,7 +128,12 @@ export class Driver { ].join("\n"), ); } - const ast = parseTactProject(this.ctx, projectConfig, projectRoot); + const ast = parseTactProject( + this.ctx, + projectConfig, + projectRoot, + this.fs, + ); const cu = createIR(this.ctx, projectName, ast, importGraph); acc.set(projectName, cu); } else { @@ -141,6 +148,7 @@ export class Driver { this.ctx, configProject, configManager.getProjectRoot(), + this.fs, ); const projectName = configProject.name as ProjectName; const cu = createIR(this.ctx, projectName, ast, importGraph); @@ -158,20 +166,20 @@ export class Driver { */ private collectTactFiles(dir: string): string[] { let results: string[] = []; - const files = fs.readdirSync(dir); + const files = this.fs.readdir(dir); // If .gitignore exists, use it to ignore files - const gitignorePath = findGitignore(dir); + const gitignorePath = findGitignore(dir, this.fs); let ig = ignore(); if (gitignorePath) { - ig = ignore().add(fs.readFileSync(gitignorePath, "utf8")); + ig = ignore().add(this.fs.readFile(gitignorePath).toString("utf8")); } files.forEach((file) => { const fullPath = path.join(dir, file); const relativePath = path.relative(dir, fullPath); if (!ig.ignores(relativePath) && !fullPath.includes("node_modules")) { - if (fs.statSync(fullPath).isDirectory()) { + if (this.fs.stat(fullPath).isDirectory()) { results = results.concat(this.collectTactFiles(fullPath)); } else if (file.endsWith(".tact")) { results.push(fullPath); @@ -622,9 +630,17 @@ export class Driver { `${cu.projectName}: Running ${detector.id} for ${cu.projectName}`, ); try { + // Conditional import for setTimeout to support both Node.js and browser environments + let setTimeoutPromise: (ms: number, value?: any) => Promise; + + if (typeof window !== 'undefined') { + setTimeoutPromise = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + } else { + setTimeoutPromise = (await import('timers/promises')).setTimeout; + } const warnings = await Promise.race([ detector.check(cu), - setTimeout(MistiEnv.MISTI_TIMEOUT, []).then(() => { + setTimeoutPromise(MistiEnv.MISTI_TIMEOUT, []).then(() => { throw new Error( `Detector ${detector.id} timed out after ${MistiEnv.MISTI_TIMEOUT}ms`, ); @@ -668,11 +684,11 @@ export class Driver { * @param startDir The directory to start searching from. * @returns The path to the .gitignore file or null if not found. */ -function findGitignore(startDir: string): string | null { +function findGitignore(startDir: string, fs: VirtualFileSystem): string | null { let currentDir = startDir; while (currentDir !== path.parse(currentDir).root) { const gitignorePath = path.join(currentDir, ".gitignore"); - if (fs.existsSync(gitignorePath)) { + if (fs.exists(gitignorePath)) { return gitignorePath; } currentDir = path.dirname(currentDir); diff --git a/src/cli/options.ts b/src/cli/options.ts index a982c77d..3277116b 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -1,5 +1,7 @@ import { ToolConfig, OutputFormat } from "./types"; import { Severity, parseSeverity } from "../internals/warnings"; +import { createVirtualFileSystem } from "../vfs/createVirtualFileSystem"; +import { VirtualFileSystem } from "../vfs/virtualFileSystem"; import { Option } from "commander"; export const STDOUT_PATH = "-"; @@ -13,6 +15,7 @@ export interface CLIOptions { soufflePath: string; souffleBinary: string; souffleVerbose: boolean; + souffleEnabled: boolean; tactStdlibPath: string | undefined; verbose: boolean; quiet: boolean; @@ -23,6 +26,7 @@ export interface CLIOptions { config: string | undefined; newDetector: string | undefined; listDetectors: boolean; + fs: VirtualFileSystem; } export const cliOptionDefaults: Required = { @@ -34,6 +38,7 @@ export const cliOptionDefaults: Required = { soufflePath: "/tmp/misti/souffle", souffleBinary: "souffle", souffleVerbose: false, + souffleEnabled: true, tactStdlibPath: undefined, verbose: false, quiet: false, @@ -44,6 +49,7 @@ export const cliOptionDefaults: Required = { config: undefined, newDetector: undefined, listDetectors: false, + fs: createVirtualFileSystem("/", {}), }; export const cliOptions = [ diff --git a/src/createDetector.ts b/src/createDetector.ts index 057ea836..f038a886 100644 --- a/src/createDetector.ts +++ b/src/createDetector.ts @@ -3,7 +3,7 @@ * * @packageDocumentation */ -import fs from "fs-extra"; +import { createNodeFileSystem } from "./vfs/createNodeFileSystem"; import path from "path"; const TEMPLATE_PATH = path.join( @@ -49,6 +49,7 @@ function getFileInfo(str: string): [string, string] { * @return true if the detector was successfully created, false otherwise. */ export async function createDetector(nameOrPath: string): Promise { + const fs = createNodeFileSystem(process.cwd()); const [dir, detectorName] = getFileInfo(nameOrPath); if (!isCamelCase(detectorName)) { console.error( @@ -57,17 +58,17 @@ export async function createDetector(nameOrPath: string): Promise { return false; } const filepath = path.join(dir, `${detectorName}.ts`); - if (await fs.pathExists(filepath)) { + if (fs.exists(filepath)) { console.error(`File already exists at ${filepath}`); return false; } try { - const templateContent = await fs.readFile(TEMPLATE_PATH, "utf8"); + const templateContent = fs.readFile(TEMPLATE_PATH).toString("utf8"); const content = templateContent.replace( /__ClassName__/g, capitalize(detectorName), ); - await fs.outputFile(filepath, content); + fs.writeFile(filepath, content); console.log( [ `Created ${filepath}\n`, diff --git a/src/detectors/detector.ts b/src/detectors/detector.ts index 8a7790b0..fb1dd236 100644 --- a/src/detectors/detector.ts +++ b/src/detectors/detector.ts @@ -33,7 +33,7 @@ export type DetectorKind = "ast" | "dataflow" | "souffle"; * Abstract base class for a detector module, providing an interface for defining various types of detectors. */ export abstract class Detector { - constructor(readonly ctx: MistiContext) {} + constructor(readonly ctx: MistiContext) { } /** * Gets the short identifier of the detector, used in analyzer warnings. diff --git a/src/internals/config.ts b/src/internals/config.ts index d08b3277..c4c351ee 100644 --- a/src/internals/config.ts +++ b/src/internals/config.ts @@ -5,7 +5,8 @@ import { getEnabledDetectors, DetectorName, } from "../detectors/detector"; -import * as fs from "fs"; +import { createVirtualFileSystem } from "../vfs/createVirtualFileSystem"; +import { VirtualFileSystem } from "../vfs/virtualFileSystem"; import { z } from "zod"; const DetectorConfigSchema = z.object({ @@ -57,22 +58,30 @@ export class MistiConfig { public tactStdlibPath?: string; public unusedPrefix: string; public verbosity: "quiet" | "debug" | "default"; + public fs: VirtualFileSystem; constructor({ configPath = undefined, detectors = undefined, tools = undefined, allDetectors = false, + fs = createVirtualFileSystem("/", { + hello: { content: "world", type: "file" }, + }), }: Partial<{ configPath?: string; detectors?: string[]; tools?: ToolConfig[]; allDetectors: boolean; + fs: VirtualFileSystem; }> = {}) { let configData; + this.fs = fs; if (configPath) { try { - const configFileContents = fs.readFileSync(configPath, "utf8"); + const configFileContents = this.fs + .readFile(configPath) + .toString("utf8"); configData = JSON.parse(configFileContents); } catch (err) { if (err instanceof Error) { diff --git a/src/internals/context.ts b/src/internals/context.ts index 108e72d9..82afca21 100644 --- a/src/internals/context.ts +++ b/src/internals/context.ts @@ -20,15 +20,17 @@ export class MistiContext { * Initializes the context for Misti, setting up configuration and appropriate logger. */ constructor(options: CLIOptions = cliOptionDefaults) { - this.souffleAvailable = this.checkSouffleInstallation( - options.souffleBinary, - ); + this.souffleAvailable = options.souffleEnabled + ? this.checkSouffleInstallation(options.souffleBinary) + : false; + try { this.config = new MistiConfig({ detectors: options.enabledDetectors, tools: options.tools, allDetectors: options.allDetectors, configPath: options.config, + fs: options.fs, }); } catch (err) { throwZodError(err, { diff --git a/src/internals/ir/builders/imports.ts b/src/internals/ir/builders/imports.ts index 1cc41fb0..8fc315c5 100644 --- a/src/internals/ir/builders/imports.ts +++ b/src/internals/ir/builders/imports.ts @@ -22,7 +22,6 @@ import { } from "@tact-lang/compiler/dist/grammar/ast"; import { ItemOrigin } from "@tact-lang/compiler/dist/grammar/grammar"; import tactGrammar from "@tact-lang/compiler/dist/grammar/grammar.ohm-bundle"; -import fs from "fs"; import { Node, NonterminalNode } from "ohm-js"; import path from "path"; @@ -30,7 +29,7 @@ export class ImportGraphBuilder { private constructor( private readonly ctx: MistiContext, private readonly entryPoints: string[], - ) {} + ) { } /** * Creates an ImportGraphBuilder. @@ -66,7 +65,7 @@ export class ImportGraphBuilder { let fileContent = ""; try { - fileContent = fs.readFileSync(filePath, "utf8"); + fileContent = this.ctx.config.fs.readFile(filePath).toString("utf8"); } catch { this.ctx.logger.warn( `Cannot find imported file: ${filePath}. The analysis might not work.`, @@ -146,10 +145,10 @@ export class ImportGraphBuilder { : filePath.endsWith(".fc") ? "func" : (() => { - throw ExecutionException.make( - `Cannot determine the target language of import: ${filePath}`, - ); - })(); + throw ExecutionException.make( + `Cannot determine the target language of import: ${filePath}`, + ); + })(); } /** diff --git a/src/internals/ir/imports.ts b/src/internals/ir/imports.ts index 59525cca..635275b5 100644 --- a/src/internals/ir/imports.ts +++ b/src/internals/ir/imports.ts @@ -1,4 +1,5 @@ import { IdxGenerator } from "./indices"; +import { VirtualFileSystem } from "../../vfs/virtualFileSystem"; import { SrcInfo } from "@tact-lang/compiler/dist/grammar/ast"; import { ItemOrigin } from "@tact-lang/compiler/dist/grammar/grammar"; import path from "path"; @@ -97,7 +98,7 @@ export class ImportGraph { * * @returns Project root directory or undefined if there are no user imports. */ - public resolveProjectRoot(): string | undefined { + public resolveProjectRoot(fs: VirtualFileSystem): string | undefined { let projectRoot: string | undefined; this.nodes.forEach((node) => { if (node.origin === "user") { @@ -113,7 +114,7 @@ export class ImportGraph { } } }); - return projectRoot ? path.resolve(projectRoot) : undefined; + return projectRoot ? fs.resolve(projectRoot) : undefined; } /** diff --git a/src/internals/tact/config.ts b/src/internals/tact/config.ts index 1fe4a499..41b431dd 100644 --- a/src/internals/tact/config.ts +++ b/src/internals/tact/config.ts @@ -1,3 +1,4 @@ +import { VirtualFileSystem } from "../../vfs/virtualFileSystem"; import { ExecutionException, throwZodError } from "../exceptions"; import { ProjectName } from "../ir"; import { @@ -24,7 +25,7 @@ export class TactConfigManager { private projectRoot: string, /** Tact config parsed with Zod. */ private config: TactConfig, - ) {} + ) { } /** * Creates a TactConfigManager from a Tact configuration file typically specified by the user. @@ -53,12 +54,16 @@ export class TactConfigManager { contractPath, ".tact", ) as ProjectName, + vfs: VirtualFileSystem, ): TactConfigManager { + const absoluteProjectRoot = vfs.resolve(projectRoot); + const absoluteContractPath = path.resolve(projectRoot, contractPath); + const tactConfig: TactConfig = { projects: [ { name: projectName, - path: path.relative(projectRoot, contractPath), + path: path.relative(absoluteProjectRoot, absoluteContractPath), output: "/tmp/misti/output", // never used options: { debug: false, diff --git a/src/internals/tact/parser.ts b/src/internals/tact/parser.ts index 1b60bcde..24c9fb0f 100644 --- a/src/internals/tact/parser.ts +++ b/src/internals/tact/parser.ts @@ -1,10 +1,13 @@ -import { getStdlibPath } from "./stdlib"; +import { VirtualFileSystem } from "../../vfs/virtualFileSystem"; import { MistiContext } from "../context"; import { TactException } from "../exceptions"; +import { getStdlibPath } from "./stdlib"; +import { createVirtualFileSystem } from "@tact-lang/compiler"; import { ConfigProject } from "@tact-lang/compiler/dist/config/parseConfig"; import { CompilerContext } from "@tact-lang/compiler/dist/context"; import { getRawAST } from "@tact-lang/compiler/dist/grammar/store"; import { AstStore } from "@tact-lang/compiler/dist/grammar/store"; +import stdLibFiles from '@tact-lang/compiler/dist/imports/stdlib'; import { enableFeatures } from "@tact-lang/compiler/dist/pipeline/build"; import { precompile } from "@tact-lang/compiler/dist/pipeline/precompile"; import { createNodeFileSystem } from "@tact-lang/compiler/dist/vfs/createNodeFileSystem"; @@ -21,15 +24,22 @@ export function parseTactProject( mistiCtx: MistiContext, projectConfig: ConfigProject, projectRoot: string, + vfs: VirtualFileSystem ): AstStore | never { - const project = createNodeFileSystem(projectRoot, false); const stdlibPath = mistiCtx.config.tactStdlibPath ?? getStdlibPath(); - const stdlib = createNodeFileSystem(stdlibPath, false); + let stdlib: any; + if (!vfs) { + stdlib = createNodeFileSystem(stdlibPath); + } else { + stdlib = createVirtualFileSystem('@stdlib', stdLibFiles); + } + + mistiCtx.logger.debug(`Parsing project ${projectConfig.name} ...`); try { let ctx = new CompilerContext(); ctx = enableFeatures(ctx, mistiCtx.logger, projectConfig); - ctx = precompile(ctx, project, stdlib, projectConfig.path); + ctx = precompile(ctx, vfs, stdlib, projectConfig.path); return getRawAST(ctx); } catch (error: unknown) { throw TactException.make(error); diff --git a/src/internals/tact/stdlib.ts b/src/internals/tact/stdlib.ts index c31e307f..e613826c 100644 --- a/src/internals/tact/stdlib.ts +++ b/src/internals/tact/stdlib.ts @@ -42,5 +42,6 @@ export function definedInStdlib( ? DEFAULT_STDLIB_PATH_ELEMENTS : stdlibPath.split("/").filter((part) => part !== ""); const filePath = typeof locOrPath === "string" ? locOrPath : locOrPath.file; - return filePath !== null && hasSubdirs(filePath, pathElements); + + return filePath !== null && (filePath.startsWith('@stdlib') || hasSubdirs(filePath, pathElements)); } diff --git a/src/vfs/createNodeFileSystem.ts b/src/vfs/createNodeFileSystem.ts new file mode 100644 index 00000000..c1cdb7d4 --- /dev/null +++ b/src/vfs/createNodeFileSystem.ts @@ -0,0 +1,63 @@ +import { VirtualFileSystem, FileStat } from "./virtualFileSystem"; +import fs from "fs"; +import path from "path"; + +export function createNodeFileSystem( + root: string, + readonly: boolean = true, +): VirtualFileSystem { + let normalizedRoot = path.normalize(root); + if (!normalizedRoot.endsWith(path.sep)) { + normalizedRoot += path.sep; + } + + return { + root: normalizedRoot, + + exists(filePath: string): boolean { + const resolvedPath = this.resolve(filePath); + return fs.existsSync(resolvedPath); + }, + + resolve(...filePath: string[]): string { + return path.normalize(path.resolve(normalizedRoot, ...filePath)); + }, + + readFile(filePath: string): Buffer { + const resolvedPath = this.resolve(filePath); + return fs.readFileSync(resolvedPath); + }, + + writeFile(filePath: string, content: Buffer | string): void { + if (readonly) { + throw new Error("File system is readonly"); + } + const resolvedPath = this.resolve(filePath); + // Ensure the directory exists + const dir = path.dirname(resolvedPath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(resolvedPath, content); + }, + + readdir(dirPath: string): string[] { + const resolvedPath = this.resolve(dirPath); + if (!fs.statSync(resolvedPath).isDirectory()) { + throw new Error(`Path '${resolvedPath}' is not a directory`); + } + return fs.readdirSync(resolvedPath); + }, + + stat(filePath: string): FileStat { + const resolvedPath = this.resolve(filePath); + const stats = fs.statSync(resolvedPath); + + return { + isFile: () => stats.isFile(), + isDirectory: () => stats.isDirectory(), + size: stats.size, + createdAt: stats.birthtime, + updatedAt: stats.mtime, + }; + }, + }; +} diff --git a/src/vfs/createVirtualFileSystem.ts b/src/vfs/createVirtualFileSystem.ts new file mode 100644 index 00000000..0a166af8 --- /dev/null +++ b/src/vfs/createVirtualFileSystem.ts @@ -0,0 +1,82 @@ +import { FileStat, FileSystemTree, VirtualFileSystem } from "./virtualFileSystem"; +import path from "path"; + +/** + * Creates a virtual file system for managing files only (no directories). + */ + +export function createVirtualFileSystem( + root: string, + fileSystemTree: FileSystemTree = {}, + readonly: boolean = true, +): VirtualFileSystem { + let normalizedRoot = path.normalize(root); + if (!normalizedRoot.endsWith(path.sep)) { + normalizedRoot += path.sep; + } + + const memoryFS = fileSystemTree; + + return { + root: normalizedRoot, + + exists(filePath: string): boolean { + const resolvedPath = this.resolve(filePath); + return resolvedPath in memoryFS; + }, + + resolve(...filePath: string[]): string { + return path.normalize(path.resolve(normalizedRoot, ...filePath)); + }, + + readFile(filePath: string): Buffer { + const resolvedPath = this.resolve(filePath); + const file = memoryFS[resolvedPath]; + + if (!file || file.type !== "file") { + throw new Error(`File '${resolvedPath}' does not exist or is not a file`); + } + + const content = file.content ?? ""; // Default to an empty string if content is undefined + return Buffer.from(content, "utf-8"); + }, + + writeFile(filePath: string, content: Buffer | string): void { + if (readonly) { + throw new Error("File system is readonly"); + } + const resolvedPath = this.resolve(filePath); + + // Write the file + memoryFS[resolvedPath] = { + type: "file", + content: content.toString(), + size: typeof content === "string" ? Buffer.byteLength(content) : content.length, + createdAt: memoryFS[resolvedPath]?.createdAt || new Date(), + updatedAt: new Date(), + }; + }, + + readdir(): string[] { + // List all file names in the memory file system + return Object.keys(memoryFS).filter((key) => memoryFS[key].type === "file"); + }, + + stat(filePath: string): FileStat { + const resolvedPath = this.resolve(filePath); + const node = memoryFS[resolvedPath]; + + if (!node) { + throw new Error(`File '${resolvedPath}' does not exist`); + } + + return { + isFile: () => node.type === "file", + isDirectory: () => false, // No directories in this implementation + size: node.size ?? 0, + createdAt: node.createdAt ?? new Date(), + updatedAt: node.updatedAt ?? new Date(), + }; + }, + }; +} diff --git a/src/vfs/virtualFileSystem.ts b/src/vfs/virtualFileSystem.ts new file mode 100644 index 00000000..ce7ef34a --- /dev/null +++ b/src/vfs/virtualFileSystem.ts @@ -0,0 +1,27 @@ +type FileSystemNode = { + type: 'file' | 'directory'; + content?: string; + size?: number; + createdAt?: Date; + updatedAt?: Date; +}; + +export type FileSystemTree = Record; + +export type FileStat = { + isFile: () => boolean; + isDirectory: () => boolean; + size: number; + createdAt: Date; + updatedAt: Date; +}; + +export type VirtualFileSystem = { + root: string; + resolve(...path: string[]): string; + exists(path: string): boolean; + readFile(path: string): Buffer; + writeFile(path: string, content: Buffer | string): void; + readdir: (path: string) => string[]; + stat: (path: string) => FileStat; +}; From 879a66e0fce01da09d085460f4fe28a283f0d52e Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 17 Dec 2024 14:51:06 +0530 Subject: [PATCH 2/5] chore: update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f49a504e..267b39be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ShortCircuitCondition` detector: PR [#202](https://github.com/nowarp/misti/pull/202) - `PreferredStdlibApi` detector now suggest some preferred replacements for cell methods - Add Callgraph: PR [#185](https://github.com/nowarp/misti/pull/185) +- Support for browser environment: Issue [#228](https://github.com/nowarp/misti/issues/228) +- `souffleEnabled` option to disable Souffle check execution: Issue [#228](https://github.com/nowarp/misti/issues/228) ### Changed - `SuspiciousMessageMode` detector now suggests using SendDefaultMode instead of 0 for mode: PR [#199](https://github.com/nowarp/misti/pull/199/) From 062360fa25fb2da0a3a8a8994b9de592c70d2eaa Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 17 Dec 2024 22:10:43 +0530 Subject: [PATCH 3/5] fix formatting --- src/cli/cli.ts | 5 +- src/cli/driver.ts | 7 +- src/detectors/detector.ts | 2 +- src/internals/ir/builders/imports.ts | 10 +- src/internals/tact/config.ts | 2 +- src/internals/tact/parser.ts | 7 +- src/internals/tact/stdlib.ts | 5 +- src/vfs/createNodeFileSystem.ts | 112 +++++++++++------------ src/vfs/createVirtualFileSystem.ts | 131 +++++++++++++++------------ src/vfs/virtualFileSystem.ts | 34 +++---- 10 files changed, 166 insertions(+), 149 deletions(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 3ec2db81..242bcbcd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -57,7 +57,10 @@ export async function runMistiCommand( command: Command = createMistiCommand(), ): Promise<[Driver, MistiResult]> { await command.parseAsync(args, { from: "user" }); - const driver = await Driver.create(command.args, { ...command.opts(), fs: createNodeFileSystem(process.cwd()) }); + const driver = await Driver.create(command.args, { + ...command.opts(), + fs: createNodeFileSystem(process.cwd()), + }); const result = await driver.execute(); return [driver, result]; } diff --git a/src/cli/driver.ts b/src/cli/driver.ts index 6292806b..d4536a09 100644 --- a/src/cli/driver.ts +++ b/src/cli/driver.ts @@ -633,10 +633,11 @@ export class Driver { // Conditional import for setTimeout to support both Node.js and browser environments let setTimeoutPromise: (ms: number, value?: any) => Promise; - if (typeof window !== 'undefined') { - setTimeoutPromise = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + if (typeof window !== "undefined") { + setTimeoutPromise = (ms) => + new Promise((resolve) => setTimeout(resolve, ms)); } else { - setTimeoutPromise = (await import('timers/promises')).setTimeout; + setTimeoutPromise = (await import("timers/promises")).setTimeout; } const warnings = await Promise.race([ detector.check(cu), diff --git a/src/detectors/detector.ts b/src/detectors/detector.ts index fb1dd236..8a7790b0 100644 --- a/src/detectors/detector.ts +++ b/src/detectors/detector.ts @@ -33,7 +33,7 @@ export type DetectorKind = "ast" | "dataflow" | "souffle"; * Abstract base class for a detector module, providing an interface for defining various types of detectors. */ export abstract class Detector { - constructor(readonly ctx: MistiContext) { } + constructor(readonly ctx: MistiContext) {} /** * Gets the short identifier of the detector, used in analyzer warnings. diff --git a/src/internals/ir/builders/imports.ts b/src/internals/ir/builders/imports.ts index 8fc315c5..390a1ceb 100644 --- a/src/internals/ir/builders/imports.ts +++ b/src/internals/ir/builders/imports.ts @@ -29,7 +29,7 @@ export class ImportGraphBuilder { private constructor( private readonly ctx: MistiContext, private readonly entryPoints: string[], - ) { } + ) {} /** * Creates an ImportGraphBuilder. @@ -145,10 +145,10 @@ export class ImportGraphBuilder { : filePath.endsWith(".fc") ? "func" : (() => { - throw ExecutionException.make( - `Cannot determine the target language of import: ${filePath}`, - ); - })(); + throw ExecutionException.make( + `Cannot determine the target language of import: ${filePath}`, + ); + })(); } /** diff --git a/src/internals/tact/config.ts b/src/internals/tact/config.ts index 41b431dd..76738be7 100644 --- a/src/internals/tact/config.ts +++ b/src/internals/tact/config.ts @@ -25,7 +25,7 @@ export class TactConfigManager { private projectRoot: string, /** Tact config parsed with Zod. */ private config: TactConfig, - ) { } + ) {} /** * Creates a TactConfigManager from a Tact configuration file typically specified by the user. diff --git a/src/internals/tact/parser.ts b/src/internals/tact/parser.ts index 24c9fb0f..400376a2 100644 --- a/src/internals/tact/parser.ts +++ b/src/internals/tact/parser.ts @@ -7,7 +7,7 @@ import { ConfigProject } from "@tact-lang/compiler/dist/config/parseConfig"; import { CompilerContext } from "@tact-lang/compiler/dist/context"; import { getRawAST } from "@tact-lang/compiler/dist/grammar/store"; import { AstStore } from "@tact-lang/compiler/dist/grammar/store"; -import stdLibFiles from '@tact-lang/compiler/dist/imports/stdlib'; +import stdLibFiles from "@tact-lang/compiler/dist/imports/stdlib"; import { enableFeatures } from "@tact-lang/compiler/dist/pipeline/build"; import { precompile } from "@tact-lang/compiler/dist/pipeline/precompile"; import { createNodeFileSystem } from "@tact-lang/compiler/dist/vfs/createNodeFileSystem"; @@ -24,17 +24,16 @@ export function parseTactProject( mistiCtx: MistiContext, projectConfig: ConfigProject, projectRoot: string, - vfs: VirtualFileSystem + vfs: VirtualFileSystem, ): AstStore | never { const stdlibPath = mistiCtx.config.tactStdlibPath ?? getStdlibPath(); let stdlib: any; if (!vfs) { stdlib = createNodeFileSystem(stdlibPath); } else { - stdlib = createVirtualFileSystem('@stdlib', stdLibFiles); + stdlib = createVirtualFileSystem("@stdlib", stdLibFiles); } - mistiCtx.logger.debug(`Parsing project ${projectConfig.name} ...`); try { let ctx = new CompilerContext(); diff --git a/src/internals/tact/stdlib.ts b/src/internals/tact/stdlib.ts index e613826c..98e2814f 100644 --- a/src/internals/tact/stdlib.ts +++ b/src/internals/tact/stdlib.ts @@ -43,5 +43,8 @@ export function definedInStdlib( : stdlibPath.split("/").filter((part) => part !== ""); const filePath = typeof locOrPath === "string" ? locOrPath : locOrPath.file; - return filePath !== null && (filePath.startsWith('@stdlib') || hasSubdirs(filePath, pathElements)); + return ( + filePath !== null && + (filePath.startsWith("@stdlib") || hasSubdirs(filePath, pathElements)) + ); } diff --git a/src/vfs/createNodeFileSystem.ts b/src/vfs/createNodeFileSystem.ts index c1cdb7d4..01949463 100644 --- a/src/vfs/createNodeFileSystem.ts +++ b/src/vfs/createNodeFileSystem.ts @@ -3,61 +3,61 @@ import fs from "fs"; import path from "path"; export function createNodeFileSystem( - root: string, - readonly: boolean = true, + root: string, + readonly: boolean = true, ): VirtualFileSystem { - let normalizedRoot = path.normalize(root); - if (!normalizedRoot.endsWith(path.sep)) { - normalizedRoot += path.sep; - } - - return { - root: normalizedRoot, - - exists(filePath: string): boolean { - const resolvedPath = this.resolve(filePath); - return fs.existsSync(resolvedPath); - }, - - resolve(...filePath: string[]): string { - return path.normalize(path.resolve(normalizedRoot, ...filePath)); - }, - - readFile(filePath: string): Buffer { - const resolvedPath = this.resolve(filePath); - return fs.readFileSync(resolvedPath); - }, - - writeFile(filePath: string, content: Buffer | string): void { - if (readonly) { - throw new Error("File system is readonly"); - } - const resolvedPath = this.resolve(filePath); - // Ensure the directory exists - const dir = path.dirname(resolvedPath); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(resolvedPath, content); - }, - - readdir(dirPath: string): string[] { - const resolvedPath = this.resolve(dirPath); - if (!fs.statSync(resolvedPath).isDirectory()) { - throw new Error(`Path '${resolvedPath}' is not a directory`); - } - return fs.readdirSync(resolvedPath); - }, - - stat(filePath: string): FileStat { - const resolvedPath = this.resolve(filePath); - const stats = fs.statSync(resolvedPath); - - return { - isFile: () => stats.isFile(), - isDirectory: () => stats.isDirectory(), - size: stats.size, - createdAt: stats.birthtime, - updatedAt: stats.mtime, - }; - }, - }; + let normalizedRoot = path.normalize(root); + if (!normalizedRoot.endsWith(path.sep)) { + normalizedRoot += path.sep; + } + + return { + root: normalizedRoot, + + exists(filePath: string): boolean { + const resolvedPath = this.resolve(filePath); + return fs.existsSync(resolvedPath); + }, + + resolve(...filePath: string[]): string { + return path.normalize(path.resolve(normalizedRoot, ...filePath)); + }, + + readFile(filePath: string): Buffer { + const resolvedPath = this.resolve(filePath); + return fs.readFileSync(resolvedPath); + }, + + writeFile(filePath: string, content: Buffer | string): void { + if (readonly) { + throw new Error("File system is readonly"); + } + const resolvedPath = this.resolve(filePath); + // Ensure the directory exists + const dir = path.dirname(resolvedPath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(resolvedPath, content); + }, + + readdir(dirPath: string): string[] { + const resolvedPath = this.resolve(dirPath); + if (!fs.statSync(resolvedPath).isDirectory()) { + throw new Error(`Path '${resolvedPath}' is not a directory`); + } + return fs.readdirSync(resolvedPath); + }, + + stat(filePath: string): FileStat { + const resolvedPath = this.resolve(filePath); + const stats = fs.statSync(resolvedPath); + + return { + isFile: () => stats.isFile(), + isDirectory: () => stats.isDirectory(), + size: stats.size, + createdAt: stats.birthtime, + updatedAt: stats.mtime, + }; + }, + }; } diff --git a/src/vfs/createVirtualFileSystem.ts b/src/vfs/createVirtualFileSystem.ts index 0a166af8..d74dc0ec 100644 --- a/src/vfs/createVirtualFileSystem.ts +++ b/src/vfs/createVirtualFileSystem.ts @@ -1,4 +1,8 @@ -import { FileStat, FileSystemTree, VirtualFileSystem } from "./virtualFileSystem"; +import { + FileStat, + FileSystemTree, + VirtualFileSystem, +} from "./virtualFileSystem"; import path from "path"; /** @@ -6,77 +10,84 @@ import path from "path"; */ export function createVirtualFileSystem( - root: string, - fileSystemTree: FileSystemTree = {}, - readonly: boolean = true, + root: string, + fileSystemTree: FileSystemTree = {}, + readonly: boolean = true, ): VirtualFileSystem { - let normalizedRoot = path.normalize(root); - if (!normalizedRoot.endsWith(path.sep)) { - normalizedRoot += path.sep; - } + let normalizedRoot = path.normalize(root); + if (!normalizedRoot.endsWith(path.sep)) { + normalizedRoot += path.sep; + } - const memoryFS = fileSystemTree; + const memoryFS = fileSystemTree; - return { - root: normalizedRoot, + return { + root: normalizedRoot, - exists(filePath: string): boolean { - const resolvedPath = this.resolve(filePath); - return resolvedPath in memoryFS; - }, + exists(filePath: string): boolean { + const resolvedPath = this.resolve(filePath); + return resolvedPath in memoryFS; + }, - resolve(...filePath: string[]): string { - return path.normalize(path.resolve(normalizedRoot, ...filePath)); - }, + resolve(...filePath: string[]): string { + return path.normalize(path.resolve(normalizedRoot, ...filePath)); + }, - readFile(filePath: string): Buffer { - const resolvedPath = this.resolve(filePath); - const file = memoryFS[resolvedPath]; + readFile(filePath: string): Buffer { + const resolvedPath = this.resolve(filePath); + const file = memoryFS[resolvedPath]; - if (!file || file.type !== "file") { - throw new Error(`File '${resolvedPath}' does not exist or is not a file`); - } + if (!file || file.type !== "file") { + throw new Error( + `File '${resolvedPath}' does not exist or is not a file`, + ); + } - const content = file.content ?? ""; // Default to an empty string if content is undefined - return Buffer.from(content, "utf-8"); - }, + const content = file.content ?? ""; // Default to an empty string if content is undefined + return Buffer.from(content, "utf-8"); + }, - writeFile(filePath: string, content: Buffer | string): void { - if (readonly) { - throw new Error("File system is readonly"); - } - const resolvedPath = this.resolve(filePath); + writeFile(filePath: string, content: Buffer | string): void { + if (readonly) { + throw new Error("File system is readonly"); + } + const resolvedPath = this.resolve(filePath); - // Write the file - memoryFS[resolvedPath] = { - type: "file", - content: content.toString(), - size: typeof content === "string" ? Buffer.byteLength(content) : content.length, - createdAt: memoryFS[resolvedPath]?.createdAt || new Date(), - updatedAt: new Date(), - }; - }, + // Write the file + memoryFS[resolvedPath] = { + type: "file", + content: content.toString(), + size: + typeof content === "string" + ? Buffer.byteLength(content) + : content.length, + createdAt: memoryFS[resolvedPath]?.createdAt || new Date(), + updatedAt: new Date(), + }; + }, - readdir(): string[] { - // List all file names in the memory file system - return Object.keys(memoryFS).filter((key) => memoryFS[key].type === "file"); - }, + readdir(): string[] { + // List all file names in the memory file system + return Object.keys(memoryFS).filter( + (key) => memoryFS[key].type === "file", + ); + }, - stat(filePath: string): FileStat { - const resolvedPath = this.resolve(filePath); - const node = memoryFS[resolvedPath]; + stat(filePath: string): FileStat { + const resolvedPath = this.resolve(filePath); + const node = memoryFS[resolvedPath]; - if (!node) { - throw new Error(`File '${resolvedPath}' does not exist`); - } + if (!node) { + throw new Error(`File '${resolvedPath}' does not exist`); + } - return { - isFile: () => node.type === "file", - isDirectory: () => false, // No directories in this implementation - size: node.size ?? 0, - createdAt: node.createdAt ?? new Date(), - updatedAt: node.updatedAt ?? new Date(), - }; - }, - }; + return { + isFile: () => node.type === "file", + isDirectory: () => false, // No directories in this implementation + size: node.size ?? 0, + createdAt: node.createdAt ?? new Date(), + updatedAt: node.updatedAt ?? new Date(), + }; + }, + }; } diff --git a/src/vfs/virtualFileSystem.ts b/src/vfs/virtualFileSystem.ts index ce7ef34a..86f504b9 100644 --- a/src/vfs/virtualFileSystem.ts +++ b/src/vfs/virtualFileSystem.ts @@ -1,27 +1,27 @@ type FileSystemNode = { - type: 'file' | 'directory'; - content?: string; - size?: number; - createdAt?: Date; - updatedAt?: Date; + type: "file" | "directory"; + content?: string; + size?: number; + createdAt?: Date; + updatedAt?: Date; }; export type FileSystemTree = Record; export type FileStat = { - isFile: () => boolean; - isDirectory: () => boolean; - size: number; - createdAt: Date; - updatedAt: Date; + isFile: () => boolean; + isDirectory: () => boolean; + size: number; + createdAt: Date; + updatedAt: Date; }; export type VirtualFileSystem = { - root: string; - resolve(...path: string[]): string; - exists(path: string): boolean; - readFile(path: string): Buffer; - writeFile(path: string, content: Buffer | string): void; - readdir: (path: string) => string[]; - stat: (path: string) => FileStat; + root: string; + resolve(...path: string[]): string; + exists(path: string): boolean; + readFile(path: string): Buffer; + writeFile(path: string, content: Buffer | string): void; + readdir: (path: string) => string[]; + stat: (path: string) => FileStat; }; From 939d7dea4de64936874c3f8f660f53983e8d593c Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 17 Dec 2024 22:17:14 +0530 Subject: [PATCH 4/5] fix: resolve file system issue in test cases --- .../implicit-init/test/implicitInit.spec.ts | 4 ++- src/internals/config.ts | 10 ++----- src/internals/tact/parser.ts | 13 ++++++--- src/vfs/createNodeFileSystem.ts | 1 + src/vfs/createVirtualFileSystem.ts | 1 + src/vfs/virtualFileSystem.ts | 1 + test/config.spec.ts | 29 +++++++++++++------ 7 files changed, 38 insertions(+), 21 deletions(-) diff --git a/examples/implicit-init/test/implicitInit.spec.ts b/examples/implicit-init/test/implicitInit.spec.ts index 3ed9aad9..c8d309aa 100644 --- a/examples/implicit-init/test/implicitInit.spec.ts +++ b/examples/implicit-init/test/implicitInit.spec.ts @@ -1,7 +1,9 @@ import { Driver, MistiResultWarnings } from "../../../src/cli"; import path from "path"; +import { createNodeFileSystem } from "../../../src/vfs/createNodeFileSystem"; describe("ImplicitInit Detector Tests", () => { + const fs = createNodeFileSystem(process.cwd()); it("should detect an issue in the sample contract", async () => { const tactConfigPath = path.resolve( __dirname, @@ -9,7 +11,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, fs }); expect(driver.detectors.length).toBe(1); expect(driver.detectors[0].id).toBe("ImplicitInit"); diff --git a/src/internals/config.ts b/src/internals/config.ts index c4c351ee..288ccbf7 100644 --- a/src/internals/config.ts +++ b/src/internals/config.ts @@ -5,7 +5,7 @@ import { getEnabledDetectors, DetectorName, } from "../detectors/detector"; -import { createVirtualFileSystem } from "../vfs/createVirtualFileSystem"; +import { createNodeFileSystem } from "../vfs/createNodeFileSystem"; import { VirtualFileSystem } from "../vfs/virtualFileSystem"; import { z } from "zod"; @@ -65,9 +65,7 @@ export class MistiConfig { detectors = undefined, tools = undefined, allDetectors = false, - fs = createVirtualFileSystem("/", { - hello: { content: "world", type: "file" }, - }), + fs = createNodeFileSystem(process.cwd()), }: Partial<{ configPath?: string; detectors?: string[]; @@ -79,9 +77,7 @@ export class MistiConfig { this.fs = fs; if (configPath) { try { - const configFileContents = this.fs - .readFile(configPath) - .toString("utf8"); + const configFileContents = fs.readFile(configPath).toString("utf8"); configData = JSON.parse(configFileContents); } catch (err) { if (err instanceof Error) { diff --git a/src/internals/tact/parser.ts b/src/internals/tact/parser.ts index 400376a2..08c348b2 100644 --- a/src/internals/tact/parser.ts +++ b/src/internals/tact/parser.ts @@ -2,7 +2,11 @@ import { VirtualFileSystem } from "../../vfs/virtualFileSystem"; import { MistiContext } from "../context"; import { TactException } from "../exceptions"; import { getStdlibPath } from "./stdlib"; -import { createVirtualFileSystem } from "@tact-lang/compiler"; +import { createNodeFileSystem } from "../../vfs/createNodeFileSystem"; +import { + createVirtualFileSystem, + VirtualFileSystem as TactVirtualFileSystem, +} from "@tact-lang/compiler"; import { ConfigProject } from "@tact-lang/compiler/dist/config/parseConfig"; import { CompilerContext } from "@tact-lang/compiler/dist/context"; import { getRawAST } from "@tact-lang/compiler/dist/grammar/store"; @@ -10,7 +14,6 @@ import { AstStore } from "@tact-lang/compiler/dist/grammar/store"; import stdLibFiles from "@tact-lang/compiler/dist/imports/stdlib"; import { enableFeatures } from "@tact-lang/compiler/dist/pipeline/build"; import { precompile } from "@tact-lang/compiler/dist/pipeline/precompile"; -import { createNodeFileSystem } from "@tact-lang/compiler/dist/vfs/createNodeFileSystem"; /** * Parses the project defined in the Tact configuration file, generating its AST. @@ -27,9 +30,11 @@ export function parseTactProject( vfs: VirtualFileSystem, ): AstStore | never { const stdlibPath = mistiCtx.config.tactStdlibPath ?? getStdlibPath(); - let stdlib: any; - if (!vfs) { + let stdlib: VirtualFileSystem | TactVirtualFileSystem; + + if (vfs.type === "node") { stdlib = createNodeFileSystem(stdlibPath); + vfs = createNodeFileSystem(projectRoot); } else { stdlib = createVirtualFileSystem("@stdlib", stdLibFiles); } diff --git a/src/vfs/createNodeFileSystem.ts b/src/vfs/createNodeFileSystem.ts index 01949463..d033b434 100644 --- a/src/vfs/createNodeFileSystem.ts +++ b/src/vfs/createNodeFileSystem.ts @@ -13,6 +13,7 @@ export function createNodeFileSystem( return { root: normalizedRoot, + type: "node", exists(filePath: string): boolean { const resolvedPath = this.resolve(filePath); diff --git a/src/vfs/createVirtualFileSystem.ts b/src/vfs/createVirtualFileSystem.ts index d74dc0ec..9fce7219 100644 --- a/src/vfs/createVirtualFileSystem.ts +++ b/src/vfs/createVirtualFileSystem.ts @@ -23,6 +23,7 @@ export function createVirtualFileSystem( return { root: normalizedRoot, + type: "memory", exists(filePath: string): boolean { const resolvedPath = this.resolve(filePath); diff --git a/src/vfs/virtualFileSystem.ts b/src/vfs/virtualFileSystem.ts index 86f504b9..25691b34 100644 --- a/src/vfs/virtualFileSystem.ts +++ b/src/vfs/virtualFileSystem.ts @@ -18,6 +18,7 @@ export type FileStat = { export type VirtualFileSystem = { root: string; + type: "node" | "memory"; resolve(...path: string[]): string; exists(path: string): boolean; readFile(path: string): Buffer; diff --git a/test/config.spec.ts b/test/config.spec.ts index 9291166a..333b0e5f 100644 --- a/test/config.spec.ts +++ b/test/config.spec.ts @@ -1,7 +1,5 @@ import { MistiConfig } from "../src/internals/config"; -import * as fs from "fs"; - -jest.mock("fs"); +import { createVirtualFileSystem } from "../src/vfs/createVirtualFileSystem"; describe("Config class", () => { const MOCK_CONFIG_PATH = "./mistiConfig_mock.json"; @@ -13,13 +11,20 @@ describe("Config class", () => { ignoredProjects: ["ignoredProject"], }); + const fs = createVirtualFileSystem(process.cwd(), {}, false); + beforeEach(() => { jest.clearAllMocks(); }); it("should load and parse config file correctly", () => { - (fs.readFileSync as jest.Mock).mockReturnValue(MOCK_CONFIG_CONTENT); - const configInstance = new MistiConfig({ configPath: MOCK_CONFIG_PATH }); + fs.readFile = jest + .fn() + .mockReturnValue(Buffer.from(MOCK_CONFIG_CONTENT, "utf8")); + const configInstance = new MistiConfig({ + configPath: MOCK_CONFIG_PATH, + fs, + }); expect(configInstance.detectors).toEqual([ { className: "ReadOnlyVariables" }, { className: "ZeroAddress" }, @@ -28,10 +33,10 @@ describe("Config class", () => { }); it("throws an error when the config file cannot be read", () => { - (fs.readFileSync as jest.Mock).mockImplementation(() => { + fs.readFile = jest.fn().mockImplementation(() => { throw new Error("Failed to read file"); }); - expect(() => new MistiConfig({ configPath: MOCK_CONFIG_PATH })).toThrow( + expect(() => new MistiConfig({ configPath: MOCK_CONFIG_PATH, fs })).toThrow( "Failed to read file", ); }); @@ -43,8 +48,14 @@ describe("Config class", () => { { detector: "ReadOnlyVariables", position: "file.tact:10:5" }, ], }); - (fs.readFileSync as jest.Mock).mockReturnValue(configWithSuppressions); - const configInstance = new MistiConfig({ configPath: MOCK_CONFIG_PATH }); + fs.readFile = jest + .fn() + .mockReturnValue(Buffer.from(configWithSuppressions, "utf8")); + + const configInstance = new MistiConfig({ + configPath: MOCK_CONFIG_PATH, + fs, + }); expect(configInstance.suppressions).toEqual([ { detector: "ReadOnlyVariables", file: "file.tact", line: 10, col: 5 }, ]); From 8fd2a95a937cbcb2885f8a30fc3c2f4cb9ea861d Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 20 Dec 2024 13:55:27 +0530 Subject: [PATCH 5/5] feat(vfs): add JSDoc comments and refactor implementation --- CHANGELOG.md | 4 +- src/internals/ir/imports.ts | 1 + src/internals/tact/config.ts | 1 + src/internals/tact/parser.ts | 3 +- src/vfs/createNodeFileSystem.ts | 69 ++++++++++++++++++++++++++++-- src/vfs/createVirtualFileSystem.ts | 66 +++++++++++++++++++++++++--- src/vfs/virtualFileSystem.ts | 19 ++++++-- 7 files changed, 145 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73cb4dd3..e3eec3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ShortCircuitCondition` detector: PR [#202](https://github.com/nowarp/misti/pull/202) - `PreferredStdlibApi` detector now suggest some preferred replacements for cell methods - Add Callgraph: PR [#185](https://github.com/nowarp/misti/pull/185) -- Support for browser environment: Issue [#228](https://github.com/nowarp/misti/issues/228) -- `souffleEnabled` option to disable Souffle check execution: Issue [#228](https://github.com/nowarp/misti/issues/228) +- Support for browser environment: PR [#231](https://github.com/nowarp/misti/pull/231) +- `souffleEnabled` option to disable Souffle check execution: PR [#231](https://github.com/nowarp/misti/pull/231) - Add function effects to Callgraph: PR [#227](https://github.com/nowarp/misti/pull/227) ### Changed diff --git a/src/internals/ir/imports.ts b/src/internals/ir/imports.ts index 635275b5..79c76ad1 100644 --- a/src/internals/ir/imports.ts +++ b/src/internals/ir/imports.ts @@ -96,6 +96,7 @@ export class ImportGraph { * Resolves project root based on the import directives. * The project root is a directory including all the imported files. * + * @param fs The virtual file system used to manage and resolve file paths during the operation. * @returns Project root directory or undefined if there are no user imports. */ public resolveProjectRoot(fs: VirtualFileSystem): string | undefined { diff --git a/src/internals/tact/config.ts b/src/internals/tact/config.ts index 76738be7..87de8c8c 100644 --- a/src/internals/tact/config.ts +++ b/src/internals/tact/config.ts @@ -46,6 +46,7 @@ export class TactConfigManager { * @param ctx Misti context. * @param projectName Name of the project. * @param contractPath Path to the Tact contract. + * @param vfs Virtual file system to manage interactions with the project files. */ public static fromContract( projectRoot: string, diff --git a/src/internals/tact/parser.ts b/src/internals/tact/parser.ts index 08c348b2..6c065854 100644 --- a/src/internals/tact/parser.ts +++ b/src/internals/tact/parser.ts @@ -21,6 +21,7 @@ import { precompile } from "@tact-lang/compiler/dist/pipeline/precompile"; * @param mistiCtx Misti context * @param projectRoot Absolute path to the root the project * @param config The Tact configuration object: contents of the existing file or a generated object + * @param vfs Virtual file system to manage file interactions during parsing * @returns A mapping of project names to their corresponding ASTs. */ export function parseTactProject( @@ -32,7 +33,7 @@ export function parseTactProject( const stdlibPath = mistiCtx.config.tactStdlibPath ?? getStdlibPath(); let stdlib: VirtualFileSystem | TactVirtualFileSystem; - if (vfs.type === "node") { + if (vfs.type === "local") { stdlib = createNodeFileSystem(stdlibPath); vfs = createNodeFileSystem(projectRoot); } else { diff --git a/src/vfs/createNodeFileSystem.ts b/src/vfs/createNodeFileSystem.ts index d033b434..8c25d848 100644 --- a/src/vfs/createNodeFileSystem.ts +++ b/src/vfs/createNodeFileSystem.ts @@ -1,7 +1,16 @@ import { VirtualFileSystem, FileStat } from "./virtualFileSystem"; +import { InternalException } from "../internals/exceptions"; import fs from "fs"; import path from "path"; +/** + * Creates a Virtual File System backed by the local file system. + * This file system interacts directly with the host's disk storage. + * + * @param root - The root directory for the virtual file system. + * @param readonly - If true, prevents write operations. Default is true. + * @returns A VirtualFileSystem instance with local file system operations. + */ export function createNodeFileSystem( root: string, readonly: boolean = true, @@ -12,42 +21,94 @@ export function createNodeFileSystem( } return { + /** + * The normalized root directory for the virtual file system. + */ root: normalizedRoot, - type: "node", + /** + * The type of the virtual file system. In this case, it is "local". + */ + type: "local", + + /** + * Checks if a file or directory exists at the specified path. + * + * @param filePath - The path to check existence for. + * @returns True if the file or directory exists, otherwise false. + */ exists(filePath: string): boolean { const resolvedPath = this.resolve(filePath); return fs.existsSync(resolvedPath); }, + /** + * Resolves a given path to an absolute path within the virtual file system's root. + * + * @param filePath - One or more path segments to resolve. + * @returns The resolved absolute path. + */ resolve(...filePath: string[]): string { return path.normalize(path.resolve(normalizedRoot, ...filePath)); }, + /** + * Reads a file from the virtual file system. + * + * @param filePath - The path of the file to read. + * @returns A Buffer containing the file's content. + */ readFile(filePath: string): Buffer { const resolvedPath = this.resolve(filePath); return fs.readFileSync(resolvedPath); }, + /** + * Writes content to a file in the virtual file system. + * Creates necessary directories if they do not exist. + * + * @param filePath - The path of the file to write to. + * @param content - The content to write, as a Buffer or string. + * @throws An exception if the file system is in readonly mode. + */ writeFile(filePath: string, content: Buffer | string): void { if (readonly) { - throw new Error("File system is readonly"); + throw InternalException.make( + `Cannot write to file "${filePath}": The file system is in readonly mode.`, + ); } const resolvedPath = this.resolve(filePath); // Ensure the directory exists const dir = path.dirname(resolvedPath); - fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } fs.writeFileSync(resolvedPath, content); }, + /** + * Reads the contents of a directory in the virtual file system. + * + * @param dirPath - The path of the directory to read. + * @returns An array of filenames in the directory. + * @throws An error if the specified path is not a directory. + */ readdir(dirPath: string): string[] { const resolvedPath = this.resolve(dirPath); if (!fs.statSync(resolvedPath).isDirectory()) { - throw new Error(`Path '${resolvedPath}' is not a directory`); + throw InternalException.make( + `Path '${resolvedPath}' is not a directory`, + ); } return fs.readdirSync(resolvedPath); }, + /** + * Retrieves the statistics of a file or directory. + * + * @param filePath - The path of the file or directory. + * @returns An object containing file/directory metadata. + */ stat(filePath: string): FileStat { const resolvedPath = this.resolve(filePath); const stats = fs.statSync(resolvedPath); diff --git a/src/vfs/createVirtualFileSystem.ts b/src/vfs/createVirtualFileSystem.ts index 9fce7219..84bed926 100644 --- a/src/vfs/createVirtualFileSystem.ts +++ b/src/vfs/createVirtualFileSystem.ts @@ -3,12 +3,18 @@ import { FileSystemTree, VirtualFileSystem, } from "./virtualFileSystem"; +import { InternalException } from "../internals/exceptions"; import path from "path"; /** * Creates a virtual file system for managing files only (no directories). + * This file system is entirely in-memory and does not interact with the host file system. + * + * @param root - The root directory for the virtual file system. + * @param fileSystemTree - The initial structure of the in-memory file system. Default is an empty object. + * @param readonly - If true, prevents write operations. Default is true. + * @returns A VirtualFileSystem instance for managing in-memory files. */ - export function createVirtualFileSystem( root: string, fileSystemTree: FileSystemTree = {}, @@ -22,35 +28,69 @@ export function createVirtualFileSystem( const memoryFS = fileSystemTree; return { + /** + * The normalized root directory for the virtual file system. + */ root: normalizedRoot, - type: "memory", + /** + * The type of the virtual file system. In this case, it is "inMemory". + */ + type: "inMemory", + + /** + * Checks if a file exists at the specified path. + * + * @param filePath - The path to check existence for. + * @returns True if the file exists, otherwise false. + */ exists(filePath: string): boolean { const resolvedPath = this.resolve(filePath); return resolvedPath in memoryFS; }, + /** + * Resolves a given path to an absolute path within the virtual file system's root. + * + * @param filePath - One or more path segments to resolve. + * @returns The resolved absolute path. + */ resolve(...filePath: string[]): string { return path.normalize(path.resolve(normalizedRoot, ...filePath)); }, + /** + * Reads a file from the virtual file system. + * + * @param filePath - The path of the file to read. + * @returns A Buffer containing the file's content. + * @throws An error if the file does not exist or is not a file. + */ readFile(filePath: string): Buffer { const resolvedPath = this.resolve(filePath); const file = memoryFS[resolvedPath]; if (!file || file.type !== "file") { - throw new Error( + throw InternalException.make( `File '${resolvedPath}' does not exist or is not a file`, ); } - const content = file.content ?? ""; // Default to an empty string if content is undefined - return Buffer.from(content, "utf-8"); + return Buffer.from(file.content, "utf-8"); }, + /** + * Writes content to a file in the virtual file system. + * + * @param filePath - The path of the file to write to. + * @param content - The content to write, as a Buffer or string. + * @throws An error if the file system is in readonly mode. + */ writeFile(filePath: string, content: Buffer | string): void { if (readonly) { - throw new Error("File system is readonly"); + throw InternalException.make( + `Cannot write to file "${filePath}": The file system is in readonly mode.`, + ); } const resolvedPath = this.resolve(filePath); @@ -67,6 +107,11 @@ export function createVirtualFileSystem( }; }, + /** + * Lists all file names in the virtual file system. + * + * @returns An array of file names. + */ readdir(): string[] { // List all file names in the memory file system return Object.keys(memoryFS).filter( @@ -74,12 +119,19 @@ export function createVirtualFileSystem( ); }, + /** + * Retrieves the statistics of a file. + * + * @param filePath - The path of the file. + * @returns An object containing file metadata such as size, creation date, and update date. + * @throws An error if the file does not exist. + */ stat(filePath: string): FileStat { const resolvedPath = this.resolve(filePath); const node = memoryFS[resolvedPath]; if (!node) { - throw new Error(`File '${resolvedPath}' does not exist`); + throw InternalException.make(`File '${resolvedPath}' does not exist`); } return { diff --git a/src/vfs/virtualFileSystem.ts b/src/vfs/virtualFileSystem.ts index 25691b34..0083bf6d 100644 --- a/src/vfs/virtualFileSystem.ts +++ b/src/vfs/virtualFileSystem.ts @@ -1,11 +1,20 @@ -type FileSystemNode = { - type: "file" | "directory"; - content?: string; +type FileNode = { + type: "file"; + content: string; size?: number; createdAt?: Date; updatedAt?: Date; }; +type DirectoryNode = { + type: "directory"; + size?: number; + createdAt?: Date; + updatedAt?: Date; +}; + +type FileSystemNode = FileNode | DirectoryNode; + export type FileSystemTree = Record; export type FileStat = { @@ -16,9 +25,11 @@ export type FileStat = { updatedAt: Date; }; +type FileSystemBackend = "local" | "inMemory"; + export type VirtualFileSystem = { root: string; - type: "node" | "memory"; + type: FileSystemBackend; resolve(...path: string[]): string; exists(path: string): boolean; readFile(path: string): Buffer;