diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 944d14a1..808b3ee9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,4 +24,4 @@ jobs: uses: ./.github/actions/setup - name: Format & Lint - run: turbo fmt-and-lint:ci + run: turbo check:ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f09a0d7..3400eef7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,9 @@ jobs: fi echo "✅ Language version matches: $COMPUTED_LANGUAGE_VERSION" + - name: Run Compact format check + run: turbo format + - name: Compile contracts (with retry on hash mismatch) shell: bash run: | diff --git a/README.md b/README.md index 72bcccbb..408db0b6 100644 --- a/README.md +++ b/README.md @@ -154,8 +154,8 @@ turbo test ### Check/apply Biome formatter ```bash -turbo fmt-and-lint -turbo fmt-and-lint:fix +turbo check +turbo check:fix ``` ### Advanced diff --git a/biome.json b/biome.json index 75086295..559f32e6 100644 --- a/biome.json +++ b/biome.json @@ -52,6 +52,9 @@ "useExhaustiveDependencies": "off", "useJsxKeyInIterable": "off" }, + "complexity": { + "useLiteralKeys": "off" + }, "performance": { "noBarrelFile": "error", "noReExportAll": "error", diff --git a/compact/package.json b/compact/package.json index 461a59ed..5c15b36c 100644 --- a/compact/package.json +++ b/compact/package.json @@ -16,7 +16,8 @@ }, "bin": { "compact-builder": "dist/runBuilder.js", - "compact-compiler": "dist/runCompiler.js" + "compact-compiler": "dist/runCompiler.js", + "compact-formatter": "dist/runFormatter.js" }, "scripts": { "build": "tsc -p .", diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts new file mode 100644 index 00000000..e25ae28a --- /dev/null +++ b/compact/src/BaseServices.ts @@ -0,0 +1,673 @@ +#!/usr/bin/env node + +import { exec as execCallback } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { readdir } from 'node:fs/promises'; +import { join, relative } from 'node:path'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; +import { + CompactCliNotFoundError, + DirectoryNotFoundError, + isPromisifiedChildProcessError, +} from './types/errors.ts'; + +/** + * Default source directory containing .compact files. + * All Compact operations expect source files to be in this directory. + */ +export const SRC_DIR: string = 'src'; + +/** + * Default output directory for compiled artifacts. + * Compilation results are written to subdirectories within this path. + */ +export const ARTIFACTS_DIR: string = 'artifacts'; + +/** + * Function signature for executing shell commands. + * + * Enables dependency injection for testing and allows customization + * of command execution behavior across different environments. + * + * @param command - The shell command to execute + * @returns Promise resolving to command output with stdout and stderr + */ +export type ExecFunction = ( + command: string, +) => Promise<{ stdout: string; stderr: string }>; + +/** + * Abstract base class for validating Compact CLI environment. + * + * Provides common validation logic shared across different Compact operations + * (compilation, formatting, etc.). Subclasses extend this with operation-specific + * validation requirements. + * + * @example + * ```typescript + * class CompilerValidator extends BaseEnvironmentValidator { + * async validate(version?: string) { + * const { devToolsVersion } = await this.validateBase(); + * const toolchainVersion = await this.getToolchainVersion(version); + * return { devToolsVersion, toolchainVersion }; + * } + * } + * ``` + */ +export abstract class BaseEnvironmentValidator { + protected execFn: ExecFunction; + + /** + * @param execFn - Command execution function (defaults to promisified child_process.exec) + */ + constructor(execFn: ExecFunction = promisify(execCallback)) { + this.execFn = execFn; + } + + /** + * Tests whether the Compact CLI is available in the system PATH. + * + * @returns Promise resolving to true if CLI is accessible, false otherwise + */ + async checkCompactAvailable(): Promise { + try { + await this.execFn('compact --version'); + return true; + } catch { + return false; + } + } + + /** + * Retrieves the version string of the installed Compact developer tools. + * + * @returns Promise resolving to the trimmed version output + * @throws Error if the version command fails + */ + async getDevToolsVersion(): Promise { + const { stdout } = await this.execFn('compact --version'); + return stdout.trim(); + } + + /** + * Performs base environment validation that all operations require. + * + * Verifies CLI availability and retrieves version information. + * Subclasses should call this before performing operation-specific validation. + * + * @returns Promise resolving to base validation results + * @throws CompactCliNotFoundError if CLI is not available in PATH + */ + async validateBase(): Promise<{ devToolsVersion: string }> { + const isAvailable = await this.checkCompactAvailable(); + if (!isAvailable) { + throw new CompactCliNotFoundError( + "'compact' CLI not found in PATH. Please install the Compact developer tools.", + ); + } + + const devToolsVersion = await this.getDevToolsVersion(); + return { devToolsVersion }; + } + + /** + * Operation-specific validation logic. + * + * Subclasses must implement this to perform validation requirements + * specific to their operation (e.g., checking formatter availability, + * validating compiler versions). + * + * @param args - Variable arguments for operation-specific validation + * @returns Promise resolving to operation-specific validation results + */ + abstract validate(...args: any[]): Promise; +} + +/** + * Service for discovering .compact files within a directory tree. + * + * Recursively scans directories and returns relative paths to all .compact files + * found. Used by both compilation and formatting operations to identify + * target files for processing. + * + * @example + * ```typescript + * const discovery = new FileDiscovery(); + * const files = await discovery.getCompactFiles('src/contracts'); + * // Returns: ['Token.compact', 'security/AccessControl.compact'] + * ``` + */ +export class FileDiscovery { + /** + * Recursively discovers all .compact files within a directory. + * + * Returns paths relative to SRC_DIR for consistent processing across + * different operations. Gracefully handles access errors by logging + * warnings and continuing with remaining files. + * + * @param dir - Directory path to search (can be relative or absolute) + * @returns Promise resolving to array of relative file paths from SRC_DIR + * + * @example + * ```typescript + * // Search in specific subdirectory + * const files = await discovery.getCompactFiles('src/contracts'); + * + * // Search entire source tree + * const allFiles = await discovery.getCompactFiles('src'); + * ``` + */ + async getCompactFiles(dir: string): Promise { + try { + const dirents = await readdir(dir, { withFileTypes: true }); + const filePromises = dirents.map(async (entry) => { + const fullPath = join(dir, entry.name); + try { + if (entry.isDirectory()) { + return await this.getCompactFiles(fullPath); + } + + if (entry.isFile() && fullPath.endsWith('.compact')) { + return [relative(SRC_DIR, fullPath)]; + } + return []; + } catch (err) { + // biome-ignore lint/suspicious/noConsole: Displaying path + console.warn(`Error accessing ${fullPath}:`, err); + return []; + } + }); + + const results = await Promise.all(filePromises); + return results.flat(); + } catch (err) { + // biome-ignore lint/suspicious/noConsole: Displaying dir + console.error(`Failed to read dir: ${dir}`, err); + return []; + } + } +} + +/** + * Abstract base class for services that execute Compact CLI commands. + * + * Provides common patterns for command execution and error handling. + * Subclasses implement operation-specific command construction while + * inheriting consistent error handling and logging behavior. + * + * @example + * ```typescript + * class FormatterService extends BaseCompactService { + * async formatFiles(files: string[]) { + * const command = `compact format ${files.join(' ')}`; + * return this.executeCompactCommand(command, 'Failed to format files'); + * } + * + * protected createError(message: string, cause?: unknown): Error { + * return new FormatterError(message, cause); + * } + * } + * ``` + */ +export abstract class BaseCompactService { + protected execFn: ExecFunction; + + /** + * @param execFn - Command execution function (defaults to promisified child_process.exec) + */ + constructor(execFn: ExecFunction = promisify(execCallback)) { + this.execFn = execFn; + } + + /** + * Executes a Compact CLI command with consistent error handling. + * + * Catches execution errors and wraps them in operation-specific error types + * using the createError method. Provides consistent error context across + * different operations. + * + * @param command - The complete command string to execute + * @param errorContext - Human-readable context for error messages + * @returns Promise resolving to command output + * @throws Operation-specific error (created by subclass createError method) + * + * @example + * ```typescript + * // In a subclass: + * const result = await this.executeCompactCommand( + * 'compact format --check src/', + * 'Failed to check formatting' + * ); + * ``` + */ + protected async executeCompactCommand( + command: string, + errorContext: string, + ): Promise<{ stdout: string; stderr: string }> { + try { + return await this.execFn(command); + } catch (error: unknown) { + let message: string; + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); + } + + throw this.createError(`${errorContext}: ${message}`, error); + } + } + + /** + * Creates operation-specific error instances. + * + * Subclasses must implement this to return appropriate error types + * (e.g., FormatterError, CompilationError) that provide operation-specific + * context and error handling behavior. + * + * @dev Mostly for edge cases that aren't picked up by the dev tool error handling. + * + * @param message - Error message describing what failed + * @param cause - Original error that triggered this failure (optional) + * @returns Error instance appropriate for the operation + */ + protected abstract createError(message: string, cause?: unknown): Error; +} + +/** + * Shared UI utilities for consistent styling across Compact operations. + * + * Provides common output formatting, progress indicators, and user feedback + * patterns. Ensures all Compact tools have consistent visual appearance + * and behavior. + */ +export const SharedUIService = { + /** + * Formats command output with consistent indentation and coloring. + * + * Filters empty lines and adds 4-space indentation to create visually + * distinct output sections. Used for displaying stdout/stderr from + * Compact CLI commands. + * + * @param output - Raw output text to format + * @param colorFn - Chalk color function for styling the output + * + * @example + * ```typescript + * SharedUIService.printOutput(result.stdout, chalk.cyan); + * SharedUIService.printOutput(result.stderr, chalk.red); + * ``` + */ + printOutput(output: string, colorFn: (text: string) => string): void { + const lines = output + .split('\n') + .filter((line) => line.trim() !== '') + .map((line) => ` ${line}`); + console.log(colorFn(lines.join('\n'))); + }, + + /** + * Displays base environment information common to all operations. + * + * Shows developer tools version and optional target directory. + * Called by operation-specific UI services to provide consistent + * environment context. + * + * @param operation - Operation name for message prefixes (e.g., 'COMPILE', 'FORMAT') + * @param devToolsVersion - Version string of installed Compact tools + * @param targetDir - Optional target directory being processed + */ + displayBaseEnvInfo( + operation: string, + devToolsVersion: string, + targetDir?: string, + ): void { + const spinner = ora(); + + if (targetDir) { + spinner.info(chalk.blue(`[${operation}] TARGET_DIR: ${targetDir}`)); + } + + spinner.info( + chalk.blue(`[${operation}] Compact developer tools: ${devToolsVersion}`), + ); + }, + + /** + * Displays operation start message with file count and location. + * + * Provides user feedback when beginning to process multiple files. + * Shows count of files found and optional location context. + * + * @param operation - Operation name for message prefixes + * @param action - Action being performed (e.g., 'compile', 'format', 'check formatting for') + * @param fileCount - Number of files being processed + * @param targetDir - Optional directory being processed + */ + showOperationStart( + operation: string, + action: string, + fileCount: number, + targetDir?: string, + ): void { + const searchLocation = targetDir ? ` in ${targetDir}/` : ''; + const spinner = ora(); + spinner.info( + chalk.blue( + `[${operation}] Found ${fileCount} .compact file(s) to ${action}${searchLocation}`, + ), + ); + }, + + /** + * Displays warning when no .compact files are found in target location. + * + * Provides clear feedback about search location and reminds users + * where files are expected to be located. + * + * @param operation - Operation name for message prefixes + * @param targetDir - Optional directory that was searched + */ + showNoFiles(operation: string, targetDir?: string): void { + const searchLocation = targetDir ? `${targetDir}/` : 'src/'; + const spinner = ora(); + spinner.warn( + chalk.yellow( + `[${operation}] No .compact files found in ${searchLocation}.`, + ), + ); + }, + + /** + * Shows available directory options when DirectoryNotFoundError occurs. + * + * Provides helpful context about valid directory names that can be + * used with the --dir flag. Displayed after directory not found errors. + * + * @param operation - Operation name for contextualized help text + */ + showAvailableDirectories(operation: string): void { + console.log(chalk.yellow('\nAvailable directories:')); + console.log( + chalk.yellow(` --dir access # ${operation} access control contracts`), + ); + console.log( + chalk.yellow(` --dir archive # ${operation} archive contracts`), + ); + console.log( + chalk.yellow(` --dir security # ${operation} security contracts`), + ); + console.log( + chalk.yellow(` --dir token # ${operation} token contracts`), + ); + console.log( + chalk.yellow(` --dir utils # ${operation} utility contracts`), + ); + }, +}; + +/** + * Abstract base class for Compact operations (compilation, formatting, etc.). + * + * Provides common infrastructure for file discovery, directory validation, + * and argument parsing. Subclasses implement operation-specific logic while + * inheriting shared patterns for working with .compact files. + * + * @example + * ```typescript + * class CompactFormatter extends BaseCompactOperation { + * constructor(writeMode = false, targets: string[] = [], execFn?: ExecFunction) { + * super(targets[0]); // Extract targetDir from targets + * // ... operation-specific setup + * } + * + * async format() { + * await this.validateEnvironment(); + * const { files } = await this.discoverFiles(); + * // ... process files + * } + * } + * ``` + */ +export abstract class BaseCompactOperation { + protected readonly fileDiscovery: FileDiscovery; + protected readonly targetDir?: string; + + /** + * @param targetDir - Optional subdirectory within src/ to limit operation scope + */ + constructor(targetDir?: string) { + this.targetDir = targetDir; + this.fileDiscovery = new FileDiscovery(); + } + + /** + * Validates that the target directory exists (if specified). + * + * Only performs validation when targetDir is set. Throws DirectoryNotFoundError + * if the specified directory doesn't exist, providing clear user feedback. + * + * @param searchDir - Full path to the directory that should exist + * @throws DirectoryNotFoundError if targetDir is set but directory doesn't exist + */ + protected validateTargetDirectory(searchDir: string): void { + if (this.targetDir && !existsSync(searchDir)) { + throw new DirectoryNotFoundError( + `Target directory ${searchDir} does not exist`, + searchDir, + ); + } + } + + /** + * Determines the directory to search based on target configuration. + * + * @returns Full path to search directory (either SRC_DIR or SRC_DIR/targetDir) + */ + protected getSearchDirectory(): string { + return this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; + } + + /** + * Discovers .compact files and handles common validation/feedback. + * + * Performs the complete file discovery workflow: validates directories, + * discovers files, and handles empty results with appropriate user feedback. + * + * @returns Promise resolving to discovered files and search directory + * + * @example + * ```typescript + * const { files, searchDir } = await this.discoverFiles(); + * if (files.length === 0) return; // Already handled by showNoFiles() + * + * // Process discovered files... + * ``` + */ + protected async discoverFiles(): Promise<{ + files: string[]; + searchDir: string; + }> { + const searchDir = this.getSearchDirectory(); + this.validateTargetDirectory(searchDir); + + const files = await this.fileDiscovery.getCompactFiles(searchDir); + + if (files.length === 0) { + this.showNoFiles(); + return { files: [], searchDir }; + } + + return { files, searchDir }; + } + + /** + * Validates the environment for this operation. + * + * Subclasses implement operation-specific validation (CLI availability, + * tool versions, feature availability, etc.). + */ + abstract validateEnvironment(): Promise; + + /** + * Displays operation-specific "no files found" message. + * + * Subclasses implement this to provide operation-appropriate messaging + * when no .compact files are discovered. + */ + abstract showNoFiles(): void; + + /** + * Parses common command-line arguments shared across operations. + * + * Extracts --dir flag and returns remaining arguments for operation-specific + * parsing. Provides consistent argument handling patterns across all tools. + * + * @param args - Raw command-line arguments array + * @returns Parsed base arguments and remaining args for further processing + * @throws Error if --dir flag is malformed + * + * @example + * ```typescript + * static fromArgs(args: string[]) { + * const { targetDir, remainingArgs } = this.parseBaseArgs(args); + * + * // Process operation-specific flags from remainingArgs + * let writeMode = false; + * for (const arg of remainingArgs) { + * if (arg === '--write') writeMode = true; + * } + * + * return new MyOperation(targetDir, writeMode); + * } + * ``` + */ + protected static parseBaseArgs(args: string[]): { + targetDir?: string; + remainingArgs: string[]; + } { + let targetDir: string | undefined; + const remainingArgs: string[] = []; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--dir') { + const dirNameExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); + if (dirNameExists) { + targetDir = args[i + 1]; + i++; + } else { + throw new Error('--dir flag requires a directory name'); + } + } else { + remainingArgs.push(args[i]); + } + } + + return { targetDir, remainingArgs }; + } +} + +/** + * Centralized error handling for CLI applications. + * + * Provides consistent error presentation and user guidance across all + * Compact tools. Handles common error types with appropriate messaging + * and recovery suggestions. + */ +export const BaseErrorHandler = { + /** + * Handles common error types that can occur across all operations. + * + * Processes errors that are shared between compilation, formatting, and + * other operations. Returns true if the error was handled, false if + * operation-specific handling is needed. + * + * @param error - Error that occurred during operation + * @param spinner - Ora spinner instance for consistent UI messaging + * @param operation - Operation name for contextualized error messages + * @returns true if error was handled, false if caller should handle it + * + * @example + * ```typescript + * function handleError(error: unknown, spinner: Ora) { + * if (BaseErrorHandler.handleCommonErrors(error, spinner, 'COMPILE')) { + * return; // Error was handled + * } + * + * // Handle operation-specific errors... + * } + * ``` + */ + handleCommonErrors(error: unknown, spinner: Ora, operation: string): boolean { + // CompactCliNotFoundError + if (error instanceof Error && error.name === 'CompactCliNotFoundError') { + spinner.fail(chalk.red(`[${operation}] Error: ${error.message}`)); + spinner.info( + chalk.blue( + `[${operation}] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, + ), + ); + return true; + } + + // DirectoryNotFoundError + if (error instanceof Error && error.name === 'DirectoryNotFoundError') { + spinner.fail(chalk.red(`[${operation}] Error: ${error.message}`)); + SharedUIService.showAvailableDirectories(operation); + return true; + } + + // Environment validation errors + if (isPromisifiedChildProcessError(error)) { + spinner.fail( + chalk.red( + `[${operation}] Environment validation failed: ${error.message}`, + ), + ); + console.log(chalk.gray('\nTroubleshooting:')); + console.log( + chalk.gray(' • Check that Compact CLI is installed and in PATH'), + ); + console.log( + chalk.gray(' • Verify the specified Compact version exists'), + ); + console.log(chalk.gray(' • Ensure you have proper permissions')); + return true; + } + + // Argument parsing errors + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('--dir flag requires a directory name')) { + spinner.fail( + chalk.red(`[${operation}] Error: --dir flag requires a directory name`), + ); + return false; // Let specific handler show usage + } + + return false; // Not handled, let specific handler deal with it + }, + + /** + * Handles unexpected errors with generic troubleshooting guidance. + * + * Provides fallback error handling for errors not covered by common + * error types. Shows general troubleshooting steps that apply to + * most Compact operations. + * + * @param error - Unexpected error that occurred + * @param spinner - Ora spinner instance for consistent UI messaging + * @param operation - Operation name for contextualized error messages + */ + handleUnexpectedError(error: unknown, spinner: Ora, operation: string): void { + const errorMessage = error instanceof Error ? error.message : String(error); + spinner.fail(chalk.red(`[${operation}] Unexpected error: ${errorMessage}`)); + + console.log(chalk.gray('\nIf this error persists, please check:')); + console.log(chalk.gray(' • Compact CLI is installed and in PATH')); + console.log(chalk.gray(' • Source files exist and are readable')); + console.log(chalk.gray(' • File system permissions are correct')); + }, +}; diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index 5138f93f..f868f27f 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -1,107 +1,56 @@ #!/usr/bin/env node -import { exec as execCallback } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; -import { basename, join, relative } from 'node:path'; -import { promisify } from 'node:util'; +import { basename, join } from 'node:path'; import chalk from 'chalk'; import ora from 'ora'; import { - CompactCliNotFoundError, + ARTIFACTS_DIR, + BaseCompactOperation, + BaseCompactService, + BaseEnvironmentValidator, + type ExecFunction, + SharedUIService, + SRC_DIR, +} from './BaseServices.js'; +import { CompilationError, - DirectoryNotFoundError, isPromisifiedChildProcessError, } from './types/errors.ts'; -/** Source directory containing .compact files */ -const SRC_DIR: string = 'src'; -/** Output directory for compiled artifacts */ -const ARTIFACTS_DIR: string = 'artifacts'; - /** - * Function type for executing shell commands. - * Allows dependency injection for testing and customization. + * Environment validator specialized for Compact compilation operations. * - * @param command - The shell command to execute - * @returns Promise resolving to command output - */ -export type ExecFunction = ( - command: string, -) => Promise<{ stdout: string; stderr: string }>; - -/** - * Service responsible for validating the Compact CLI environment. - * Checks CLI availability, retrieves version information, and ensures - * the toolchain is properly configured before compilation. + * Extends the base validator with compilation-specific requirements including + * toolchain version validation and compatibility checking. Ensures the Compact + * compiler toolchain is available and properly configured before attempting + * compilation operations. * - * @class EnvironmentValidator * @example * ```typescript - * const validator = new EnvironmentValidator(); - * await validator.validate('0.25.0'); - * const version = await validator.getDevToolsVersion(); + * const validator = new CompilerEnvironmentValidator(); + * const { devToolsVersion, toolchainVersion } = await validator.validate('1.2.0'); + * console.log(`Using toolchain ${toolchainVersion}`); * ``` */ -export class EnvironmentValidator { - private execFn: ExecFunction; - +export class CompilerEnvironmentValidator extends BaseEnvironmentValidator { /** - * Creates a new EnvironmentValidator instance. + * Retrieves the version string of the Compact compiler toolchain. * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - */ - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - - /** - * Checks if the Compact CLI is available in the system PATH. + * Queries the Compact CLI for toolchain version information, optionally + * targeting a specific version. This is separate from the dev tools version + * and represents the actual compiler backend being used. * - * @returns Promise resolving to true if CLI is available, false otherwise - * @example - * ```typescript - * const isAvailable = await validator.checkCompactAvailable(); - * if (!isAvailable) { - * throw new Error('Compact CLI not found'); - * } - * ``` - */ - async checkCompactAvailable(): Promise { - try { - await this.execFn('compact --version'); - return true; - } catch { - return false; - } - } - - /** - * Retrieves the version of the Compact developer tools. + * @param version - Optional specific toolchain version to query (e.g., '1.2.0') + * @returns Promise resolving to the trimmed toolchain version string + * @throws Error if the toolchain version command fails or version doesn't exist * - * @returns Promise resolving to the version string - * @throws {Error} If the CLI is not available or command fails * @example * ```typescript - * const version = await validator.getDevToolsVersion(); - * console.log(`Using Compact ${version}`); - * ``` - */ - async getDevToolsVersion(): Promise { - const { stdout } = await this.execFn('compact --version'); - return stdout.trim(); - } - - /** - * Retrieves the version of the Compact toolchain/compiler. + * // Get default toolchain version + * const defaultVersion = await validator.getToolchainVersion(); * - * @param version - Optional specific toolchain version to query - * @returns Promise resolving to the toolchain version string - * @throws {Error} If the CLI is not available or command fails - * @example - * ```typescript - * const toolchainVersion = await validator.getToolchainVersion('0.25.0'); - * console.log(`Toolchain: ${toolchainVersion}`); + * // Get specific toolchain version + * const specificVersion = await validator.getToolchainVersion('1.2.0'); * ``` */ async getToolchainVersion(version?: string): Promise { @@ -113,35 +62,32 @@ export class EnvironmentValidator { } /** - * Validates the entire Compact environment and ensures it's ready for compilation. - * Checks CLI availability and retrieves version information. + * Performs comprehensive environment validation for compilation operations. + * + * Validates both the base Compact CLI environment and compilation-specific + * requirements. Ensures that the specified toolchain version (if any) is + * available and properly installed. * * @param version - Optional specific toolchain version to validate - * @throws {CompactCliNotFoundError} If the Compact CLI is not available - * @throws {Error} If version commands fail + * @returns Promise resolving to validation results including both dev tools and toolchain versions + * @throws CompactCliNotFoundError if CLI is not available + * @throws Error if specified toolchain version is not available + * * @example * ```typescript - * try { - * await validator.validate('0.25.0'); - * console.log('Environment validated successfully'); - * } catch (error) { - * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); - * } - * } + * // Validate with default toolchain + * const result = await validator.validate(); + * + * // Validate with specific toolchain version + * const result = await validator.validate('1.2.0'); + * console.log(`Dev tools: ${result.devToolsVersion}, Toolchain: ${result.toolchainVersion}`); * ``` */ - async validate( - version?: string, - ): Promise<{ devToolsVersion: string; toolchainVersion: string }> { - const isAvailable = await this.checkCompactAvailable(); - if (!isAvailable) { - throw new CompactCliNotFoundError( - "'compact' CLI not found in PATH. Please install the Compact developer tools.", - ); - } - - const devToolsVersion = await this.getDevToolsVersion(); + async validate(version?: string): Promise<{ + devToolsVersion: string; + toolchainVersion: string; + }> { + const { devToolsVersion } = await this.validateBase(); const toolchainVersion = await this.getToolchainVersion(version); return { devToolsVersion, toolchainVersion }; @@ -149,66 +95,12 @@ export class EnvironmentValidator { } /** - * Service responsible for discovering .compact files in the source directory. - * Recursively scans directories and filters for .compact file extensions. + * Service for executing Compact compilation commands. * - * @class FileDiscovery - * @example - * ```typescript - * const discovery = new FileDiscovery(); - * const files = await discovery.getCompactFiles('src/security'); - * console.log(`Found ${files.length} .compact files`); - * ``` - */ -export class FileDiscovery { - /** - * Recursively discovers all .compact files in a directory. - * Returns relative paths from the SRC_DIR for consistent processing. - * - * @param dir - Directory path to search (relative or absolute) - * @returns Promise resolving to array of relative file paths - * @example - * ```typescript - * const files = await discovery.getCompactFiles('src'); - * // Returns: ['contracts/Token.compact', 'security/AccessControl.compact'] - * ``` - */ - async getCompactFiles(dir: string): Promise { - try { - const dirents = await readdir(dir, { withFileTypes: true }); - const filePromises = dirents.map(async (entry) => { - const fullPath = join(dir, entry.name); - try { - if (entry.isDirectory()) { - return await this.getCompactFiles(fullPath); - } - - if (entry.isFile() && fullPath.endsWith('.compact')) { - return [relative(SRC_DIR, fullPath)]; - } - return []; - } catch (err) { - // biome-ignore lint/suspicious/noConsole: Needed to display error and file path - console.warn(`Error accessing ${fullPath}:`, err); - return []; - } - }); - - const results = await Promise.all(filePromises); - return results.flat(); - } catch (err) { - // biome-ignore lint/suspicious/noConsole: Needed to display error and dir path - console.error(`Failed to read dir: ${dir}`, err); - return []; - } - } -} - -/** - * Service responsible for compiling individual .compact files. - * Handles command construction, execution, and error processing. + * Handles the construction and execution of compilation commands for individual + * .compact files. Manages input/output path resolution, command flag application, + * and version targeting. Provides consistent error handling for compilation failures. * - * @class CompilerService * @example * ```typescript * const compiler = new CompilerService(); @@ -220,41 +112,32 @@ export class FileDiscovery { * console.log('Compilation output:', result.stdout); * ``` */ -export class CompilerService { - private execFn: ExecFunction; - - /** - * Creates a new CompilerService instance. - * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - */ - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - +export class CompilerService extends BaseCompactService { /** * Compiles a single .compact file using the Compact CLI. - * Constructs the appropriate command with flags and version, then executes it. * - * @param file - Relative path to the .compact file from SRC_DIR - * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') - * @param version - Optional specific toolchain version to use - * @returns Promise resolving to compilation output (stdout/stderr) - * @throws {CompilationError} If compilation fails for any reason + * Constructs the appropriate compilation command with input/output paths, + * applies the specified flags and version, then executes the compilation. + * Input files are resolved relative to SRC_DIR, and output is written to + * a subdirectory in ARTIFACTS_DIR named after the input file. + * + * @param file - Relative path to the .compact file from SRC_DIR (e.g., 'Token.compact') + * @param flags - Compilation flags to apply (e.g., '--skip-zk') + * @param version - Optional specific toolchain version to use (e.g., '1.2.0') + * @returns Promise resolving to command execution results with stdout and stderr + * @throws CompilationError if the compilation fails + * * @example * ```typescript - * try { - * const result = await compiler.compileFile( - * 'security/AccessControl.compact', - * '--skip-zk', - * '0.25.0' - * ); - * console.log('Success:', result.stdout); - * } catch (error) { - * if (error instanceof CompilationError) { - * console.error('Compilation failed for', error.file); - * } - * } + * // Basic compilation + * await compiler.compileFile('Token.compact', '', undefined); + * + * // Compilation with flags and version + * await compiler.compileFile( + * 'contracts/security/AccessControl.compact', + * '--skip-zk', + * '1.2.0' + * ); * ``` */ async compileFile( @@ -269,74 +152,71 @@ export class CompilerService { const flagsStr = flags ? ` ${flags}` : ''; const command = `compact compile${versionFlag ? ` ${versionFlag}` : ''}${flagsStr} "${inputPath}" "${outputDir}"`; - try { - return await this.execFn(command); - } catch (error: unknown) { - let message: string; - - if (error instanceof Error) { - message = error.message; - } else { - message = String(error); // fallback for strings, objects, numbers, etc. - } - - throw new CompilationError( - `Failed to compile ${file}: ${message}`, - file, - error, - ); - } + return this.executeCompactCommand(command, `Failed to compile ${file}`); } -} -/** - * Utility service for handling user interface output and formatting. - * Provides consistent styling and formatting for compiler messages and output. - * - * @class UIService - * @example - * ```typescript - * UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.25.0', 'security'); - * UIService.printOutput('Compilation successful', chalk.green); - * ``` - */ -export const UIService = { /** - * Prints formatted output with consistent indentation and coloring. - * Filters empty lines and adds consistent indentation for readability. + * Creates compilation-specific error instances. + * + * Wraps compilation failures in CompilationError instances that provide + * additional context including the file that failed to compile. Extracts + * the filename from error messages when possible for better error reporting. + * + * @param message - Error message describing the compilation failure + * @param cause - Original error that caused the compilation failure (optional) + * @returns CompilationError instance with file context and cause information * - * @param output - Raw output text to format - * @param colorFn - Chalk color function for styling * @example * ```typescript - * UIService.printOutput(stdout, chalk.cyan); - * UIService.printOutput(stderr, chalk.red); + * // This method is called automatically by executeCompactCommand + * // when compilation fails, creating errors like: + * // CompilationError: Failed to compile Token.compact: syntax error * ``` */ - printOutput(output: string, colorFn: (text: string) => string): void { - const lines = output - .split('\n') - .filter((line) => line.trim() !== '') - .map((line) => ` ${line}`); - console.log(colorFn(lines.join('\n'))); - }, + protected createError(message: string, cause?: unknown): Error { + // Extract file name from error message for CompilationError + const match = message.match(/Failed to compile (.+?):/); + const file = match ? match[1] : 'unknown'; + return new CompilationError(message, file, cause); + } +} + +/** + * UI service specialized for compilation operations. + * + * Provides compilation-specific user interface elements and messaging. + * Extends the shared UI service with compilation-focused information display, + * progress reporting, and status messaging. Ensures consistent visual presentation + * across compilation operations. + */ +export const CompilerUIService = { + ...SharedUIService, /** - * Displays environment information including tool versions and configuration. - * Shows developer tools version, toolchain version, and optional settings. + * Displays comprehensive compilation environment information. + * + * Shows both developer tools and toolchain versions, along with optional + * target directory and version override information. Provides users with + * clear visibility into the compilation environment configuration. + * + * @param devToolsVersion - Version of the installed Compact developer tools + * @param toolchainVersion - Version of the Compact compiler toolchain being used + * @param targetDir - Optional target directory being compiled (relative to src/) + * @param version - Optional specific toolchain version being used * - * @param devToolsVersion - Version string of the Compact developer tools - * @param toolchainVersion - Version string of the Compact toolchain/compiler - * @param targetDir - Optional target directory being compiled - * @param version - Optional specific version being used * @example * ```typescript - * UIService.displayEnvInfo( - * 'compact 0.1.0', - * 'Compactc version: 0.25.0', - * 'security', - * '0.25.0' + * CompilerUIService.displayEnvInfo( + * 'compact-dev-tools 2.1.0', + * 'compact-toolchain 1.8.0', + * 'contracts', + * '1.8.0' * ); + * // Output: + * // ℹ [COMPILE] TARGET_DIR: contracts + * // ℹ [COMPILE] Compact developer tools: compact-dev-tools 2.1.0 + * // ℹ [COMPILE] Compact toolchain: compact-toolchain 1.8.0 + * // ℹ [COMPILE] Using toolchain version: 1.8.0 * ``` */ displayEnvInfo( @@ -345,15 +225,9 @@ export const UIService = { targetDir?: string, version?: string, ): void { - const spinner = ora(); - - if (targetDir) { - spinner.info(chalk.blue(`[COMPILE] TARGET_DIR: ${targetDir}`)); - } + SharedUIService.displayBaseEnvInfo('COMPILE', devToolsVersion, targetDir); - spinner.info( - chalk.blue(`[COMPILE] Compact developer tools: ${devToolsVersion}`), - ); + const spinner = ora(); spinner.info( chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`), ); @@ -364,110 +238,101 @@ export const UIService = { }, /** - * Displays compilation start message with file count and optional location. + * Displays compilation start message with file count and location context. + * + * Informs users about the scope of the compilation operation, including + * the number of files found and the directory being processed. Provides + * clear expectations about the work to be performed. * - * @param fileCount - Number of files to be compiled + * @param fileCount - Number of .compact files discovered for compilation * @param targetDir - Optional target directory being compiled + * * @example * ```typescript - * UIService.showCompilationStart(5, 'security'); - * // Output: "Found 5 .compact file(s) to compile in security/" + * CompilerUIService.showCompilationStart(3, 'contracts'); + * // Output: ℹ [COMPILE] Found 3 .compact file(s) to compile in contracts/ + * + * CompilerUIService.showCompilationStart(1); + * // Output: ℹ [COMPILE] Found 1 .compact file(s) to compile * ``` */ showCompilationStart(fileCount: number, targetDir?: string): void { - const searchLocation = targetDir ? ` in ${targetDir}/` : ''; - const spinner = ora(); - spinner.info( - chalk.blue( - `[COMPILE] Found ${fileCount} .compact file(s) to compile${searchLocation}`, - ), + SharedUIService.showOperationStart( + 'COMPILE', + 'compile', + fileCount, + targetDir, ); }, /** - * Displays a warning message when no .compact files are found. + * Displays warning when no .compact files are found for compilation. + * + * Provides clear feedback when the compilation operation cannot proceed + * because no source files were discovered in the target location. + * Helps users understand where files are expected to be located. * * @param targetDir - Optional target directory that was searched + * * @example * ```typescript - * UIService.showNoFiles('security'); - * // Output: "No .compact files found in security/." + * CompilerUIService.showNoFiles('contracts'); + * // Output: ⚠ [COMPILE] No .compact files found in contracts/. + * + * CompilerUIService.showNoFiles(); + * // Output: ⚠ [COMPILE] No .compact files found in src/. * ``` */ showNoFiles(targetDir?: string): void { - const searchLocation = targetDir ? `${targetDir}/` : ''; - const spinner = ora(); - spinner.warn( - chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`), - ); + SharedUIService.showNoFiles('COMPILE', targetDir); }, }; /** - * Main compiler class that orchestrates the compilation process. - * Coordinates environment validation, file discovery, and compilation services - * to provide a complete .compact file compilation solution. + * Main compiler orchestrator for Compact compilation operations. * - * Features: - * - Dependency injection for testability - * - Structured error propagation with custom error types - * - Progress reporting and user feedback - * - Support for compiler flags and toolchain versions - * - Environment variable integration + * Coordinates the complete compilation workflow from environment validation + * through file processing. Manages compilation configuration including flags, + * toolchain versions, and target directories. Provides progress reporting + * and error handling for batch compilation operations. * - * @class CompactCompiler * @example * ```typescript - * // Basic usage - * const compiler = new CompactCompiler('--skip-zk', 'security', '0.25.0'); + * // Basic compilation of all files in src/ + * const compiler = new CompactCompiler(); * await compiler.compile(); * - * // Factory method usage - * const compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); + * // Compilation with optimization flags + * const compiler = new CompactCompiler('--skip-zk'); * await compiler.compile(); * - * // With environment variables - * process.env.SKIP_ZK = 'true'; - * const compiler = CompactCompiler.fromArgs(['--dir', 'token']); + * // Compilation of specific directory with version override + * const compiler = new CompactCompiler('', 'contracts', '1.2.0'); * await compiler.compile(); * ``` */ -export class CompactCompiler { - /** Environment validation service */ - private readonly environmentValidator: EnvironmentValidator; - /** File discovery service */ - private readonly fileDiscovery: FileDiscovery; - /** Compilation execution service */ +export class CompactCompiler extends BaseCompactOperation { + private readonly environmentValidator: CompilerEnvironmentValidator; private readonly compilerService: CompilerService; - - /** Compiler flags to pass to the Compact CLI */ private readonly flags: string; - /** Optional target directory to limit compilation scope */ - private readonly targetDir?: string; - /** Optional specific toolchain version to use */ private readonly version?: string; /** * Creates a new CompactCompiler instance with specified configuration. * - * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') - * @param targetDir - Optional subdirectory within src/ to compile (e.g., 'security', 'token') - * @param version - Optional toolchain version to use (e.g., '0.25.0') - * @param execFn - Optional custom exec function for dependency injection - * @example - * ```typescript - * // Compile all files with flags - * const compiler = new CompactCompiler('--skip-zk --verbose'); - * - * // Compile specific directory - * const compiler = new CompactCompiler('', 'security'); + * Initializes the compiler with compilation flags, target directory scope, + * and optional toolchain version override. Sets up the necessary services + * for environment validation and command execution. * - * // Compile with specific version - * const compiler = new CompactCompiler('--skip-zk', undefined, '0.25.0'); + * @param flags - Compilation flags to apply to all files (e.g., '--skip-zk') + * @param targetDir - Optional subdirectory within src/ to limit compilation scope + * @param version - Optional specific toolchain version to use (e.g., '1.2.0') + * @param execFn - Optional command execution function for testing/customization * - * // For testing with custom exec function - * const mockExec = vi.fn(); - * const compiler = new CompactCompiler('', undefined, undefined, mockExec); + * @example + * ```typescript + * // Compile all files with default settings + * const compiler = new CompactCompiler(); * ``` */ constructor( @@ -476,52 +341,48 @@ export class CompactCompiler { version?: string, execFn?: ExecFunction, ) { + super(targetDir); this.flags = flags.trim(); - this.targetDir = targetDir; this.version = version; - this.environmentValidator = new EnvironmentValidator(execFn); - this.fileDiscovery = new FileDiscovery(); + this.environmentValidator = new CompilerEnvironmentValidator(execFn); this.compilerService = new CompilerService(execFn); } /** * Factory method to create a CompactCompiler from command-line arguments. - * Parses various argument formats including flags, directories, versions, and environment variables. - * - * Supported argument patterns: - * - `--dir ` - Target specific directory - * - `+` - Use specific toolchain version - * - Other arguments - Treated as compiler flags - * - `SKIP_ZK=true` environment variable - Adds --skip-zk flag - * - * @param args - Array of command-line arguments - * @param env - Environment variables (defaults to process.env) - * @returns New CompactCompiler instance configured from arguments - * @throws {Error} If --dir flag is provided without a directory name + * + * Parses command-line arguments and environment variables to construct + * a properly configured CompactCompiler instance. Handles flag processing, + * directory targeting, version specification, and environment-based configuration. + * + * @param args - Raw command-line arguments array + * @param env - Process environment variables (defaults to process.env) + * @returns Configured CompactCompiler instance ready for execution + * @throws Error if arguments are malformed (e.g., --dir without directory name) + * * @example * ```typescript - * // Parse command line: compact-compiler --dir security --skip-zk +0.25.0 + * // Parse from command line: ['--dir', 'contracts', '+1.2.0'] * const compiler = CompactCompiler.fromArgs([ - * '--dir', 'security', - * '--skip-zk', - * '+0.25.0' + * '--dir', 'contracts', + * '+1.2.0' * ]); * - * // With environment variable + * // With environment variable for skipping ZK proofs * const compiler = CompactCompiler.fromArgs( - * ['--dir', 'token'], * { SKIP_ZK: 'true' } * ); * - * // Empty args with environment - * const compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); + * // Parse from actual process arguments + * const compiler = CompactCompiler.fromArgs(process.argv.slice(2)); * ``` */ static fromArgs( args: string[], env: NodeJS.ProcessEnv = process.env, ): CompactCompiler { - let targetDir: string | undefined; + const { targetDir, remainingArgs } = CompactCompiler.parseBaseArgs(args); + const flags: string[] = []; let version: string | undefined; @@ -529,22 +390,13 @@ export class CompactCompiler { flags.push('--skip-zk'); } - for (let i = 0; i < args.length; i++) { - if (args[i] === '--dir') { - const dirNameExists = - i + 1 < args.length && !args[i + 1].startsWith('--'); - if (dirNameExists) { - targetDir = args[i + 1]; - i++; - } else { - throw new Error('--dir flag requires a directory name'); - } - } else if (args[i].startsWith('+')) { - version = args[i].slice(1); + for (const arg of remainingArgs) { + if (arg.startsWith('+')) { + version = arg.slice(1); } else { // Only add flag if it's not already present - if (!flags.includes(args[i])) { - flags.push(args[i]); + if (!flags.includes(arg)) { + flags.push(arg); } } } @@ -553,25 +405,23 @@ export class CompactCompiler { } /** - * Validates the compilation environment and displays version information. - * Performs environment validation, retrieves toolchain versions, and shows configuration details. + * Validates the compilation environment and displays configuration information. * - * Process: + * Performs comprehensive environment validation including CLI availability, + * toolchain version verification, and configuration display. Must be called + * before attempting compilation operations. * - * 1. Validates CLI availability and toolchain compatibility - * 2. Retrieves developer tools and compiler versions - * 3. Displays environment configuration information + * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws Error if specified toolchain version is not available * - * @throws {CompactCliNotFoundError} If Compact CLI is not available in PATH - * @throws {Error} If version retrieval or other validation steps fail * @example * ```typescript * try { * await compiler.validateEnvironment(); - * console.log('Environment ready for compilation'); + * // Environment is valid, proceed with compilation * } catch (error) { * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); + * console.error('Please install Compact CLI first'); * } * } * ``` @@ -579,7 +429,8 @@ export class CompactCompiler { async validateEnvironment(): Promise { const { devToolsVersion, toolchainVersion } = await this.environmentValidator.validate(this.version); - UIService.displayEnvInfo( + + CompilerUIService.displayEnvInfo( devToolsVersion, toolchainVersion, this.targetDir, @@ -588,29 +439,36 @@ export class CompactCompiler { } /** - * Main compilation method that orchestrates the entire compilation process. + * Displays warning message when no .compact files are found. + * + * Shows operation-specific messaging when file discovery returns no results. + * Provides clear feedback about the search location and expected file locations. + */ + showNoFiles(): void { + CompilerUIService.showNoFiles(this.targetDir); + } + + /** + * Executes the complete compilation workflow. + * + * Orchestrates the full compilation process: validates environment, discovers + * source files, and compiles each file with progress reporting. Handles batch + * compilation of multiple files with individual error isolation. * - * Process flow: - * 1. Validates environment and shows configuration - * 2. Discovers .compact files in target directory - * 3. Compiles each file with progress reporting - * 4. Handles errors and provides user feedback + * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws DirectoryNotFoundError if target directory doesn't exist + * @throws CompilationError if any file fails to compile * - * @throws {CompactCliNotFoundError} If Compact CLI is not available - * @throws {DirectoryNotFoundError} If target directory doesn't exist - * @throws {CompilationError} If any file compilation fails * @example * ```typescript - * const compiler = new CompactCompiler('--skip-zk', 'security'); + * const compiler = new CompactCompiler('--skip-zk'); * * try { * await compiler.compile(); - * console.log('All files compiled successfully'); + * console.log('Compilation completed successfully'); * } catch (error) { - * if (error instanceof DirectoryNotFoundError) { - * console.error(`Directory not found: ${error.directory}`); - * } else if (error instanceof CompilationError) { - * console.error(`Failed to compile: ${error.file}`); + * if (error instanceof CompilationError) { + * console.error(`Failed to compile ${error.file}: ${error.message}`); * } * } * ``` @@ -618,39 +476,34 @@ export class CompactCompiler { async compile(): Promise { await this.validateEnvironment(); - const searchDir = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; - - // Validate target directory exists - if (this.targetDir && !existsSync(searchDir)) { - throw new DirectoryNotFoundError( - `Target directory ${searchDir} does not exist`, - searchDir, - ); - } - - const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); + const { files } = await this.discoverFiles(); + if (files.length === 0) return; - if (compactFiles.length === 0) { - UIService.showNoFiles(this.targetDir); - return; - } + CompilerUIService.showCompilationStart(files.length, this.targetDir); - UIService.showCompilationStart(compactFiles.length, this.targetDir); - - for (const [index, file] of compactFiles.entries()) { - await this.compileFile(file, index, compactFiles.length); + for (const [index, file] of files.entries()) { + await this.compileFile(file, index, files.length); } } /** * Compiles a single file with progress reporting and error handling. - * Private method used internally by the main compile() method. * - * @param file - Relative path to the .compact file - * @param index - Current file index (0-based) for progress tracking + * Handles the compilation of an individual .compact file with visual progress + * indicators, output formatting, and comprehensive error reporting. Provides + * detailed feedback about compilation status and results. + * + * @param file - Relative path to the .compact file from SRC_DIR + * @param index - Current file index in the batch (0-based) * @param total - Total number of files being compiled - * @throws {CompilationError} If compilation fails - * @private + * @throws CompilationError if the file fails to compile + * + * @example + * ```typescript + * // This method is typically called internally by compile() + * // but can be used for individual file compilation: + * await compiler.compileFile('Token.compact', 0, 1); + * ``` */ private async compileFile( file: string, @@ -670,28 +523,18 @@ export class CompactCompiler { ); spinner.succeed(chalk.green(`[COMPILE] ${step} Compiled ${file}`)); - // Filter out compactc version output from compact compile - const filteredOutput = result.stdout.split('\n').slice(1).join('\n'); - - if (filteredOutput) { - UIService.printOutput(filteredOutput, chalk.cyan); - } - UIService.printOutput(result.stderr, chalk.yellow); + SharedUIService.printOutput(result.stdout, chalk.cyan); + SharedUIService.printOutput(result.stderr, chalk.yellow); } catch (error) { spinner.fail(chalk.red(`[COMPILE] ${step} Failed ${file}`)); if ( error instanceof CompilationError && - isPromisifiedChildProcessError(error) + isPromisifiedChildProcessError(error.cause) ) { - const execError = error; - // Filter out compactc version output from compact compile - const filteredOutput = execError.stdout.split('\n').slice(1).join('\n'); - - if (filteredOutput) { - UIService.printOutput(filteredOutput, chalk.cyan); - } - UIService.printOutput(execError.stderr, chalk.red); + const execError = error.cause; + SharedUIService.printOutput(execError.stdout, chalk.cyan); + SharedUIService.printOutput(execError.stderr, chalk.red); } throw error; @@ -699,7 +542,7 @@ export class CompactCompiler { } /** - * For testing + * For testing - expose internal state */ get testFlags(): string { return this.flags; diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts new file mode 100644 index 00000000..ecb70751 --- /dev/null +++ b/compact/src/Formatter.ts @@ -0,0 +1,396 @@ +#!/usr/bin/env node + +import { join } from 'node:path'; +import { + BaseCompactOperation, + BaseCompactService, + BaseEnvironmentValidator, + type ExecFunction, + SharedUIService, + SRC_DIR, +} from './BaseServices.js'; +import { + FormatterError, + FormatterNotAvailableError, + isPromisifiedChildProcessError, +} from './types/errors.ts'; + +/** + * Environment validator for Compact formatting operations. + * + * Validates that both the Compact CLI and formatter are available before + * attempting formatting operations. The formatter requires Compact compiler + * version 0.25.0 or later to be installed and accessible. + * + * @example + * ```typescript + * const validator = new FormatterEnvironmentValidator(); + * const { devToolsVersion } = await validator.validate(); + * console.log(`Formatter ready with ${devToolsVersion}`); + * ``` + */ +export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { + /** + * Verifies that the Compact formatter is available and accessible. + * + * Tests formatter availability by attempting to access the format help command. + * Throws a specific error with recovery instructions when the formatter is not + * available in the current toolchain. + * + * @throws FormatterNotAvailableError if formatter requires compiler update + * @throws Error if help command fails for other reasons + * + * @example + * ```typescript + * try { + * await validator.checkFormatterAvailable(); + * } catch (error) { + * if (error instanceof FormatterNotAvailableError) { + * console.error('Run: compact update'); + * } + * } + * ``` + */ + async checkFormatterAvailable(): Promise { + try { + await this.execFn('compact help format'); + } catch (error) { + if ( + isPromisifiedChildProcessError(error) && + error.stderr?.includes('formatter not available') + ) { + throw new FormatterNotAvailableError( + 'Formatter not available. Please update your Compact compiler with: compact update', + ); + } + throw error; + } + } + + /** + * Performs complete environment validation for formatting operations. + * + * Validates both base CLI environment and formatter-specific requirements. + * Must be called before attempting any formatting operations. + * + * @returns Promise resolving to validation results with dev tools version + * @throws CompactCliNotFoundError if CLI is not available + * @throws FormatterNotAvailableError if formatter is not available + * + * @example + * ```typescript + * const { devToolsVersion } = await validator.validate(); + * console.log(`Environment ready: ${devToolsVersion}`); + * ``` + */ + async validate(): Promise<{ devToolsVersion: string }> { + const { devToolsVersion } = await this.validateBase(); + await this.checkFormatterAvailable(); + return { devToolsVersion }; + } +} + +/** + * Service for executing Compact formatting commands. + * + * Lightweight wrapper around `compact format` that constructs commands with + * appropriate flags and target paths, then delegates all formatting work and + * user feedback to the underlying tool. + * + * @example + * ```typescript + * const service = new FormatterService(); + * + * // Check formatting without modifications + * await service.format(['src/contracts'], true); + * + * // Format and write changes + * await service.format(['src/contracts'], false); + * ``` + */ +export class FormatterService extends BaseCompactService { + /** + * Executes compact format command with specified targets and mode. + * + * Constructs the appropriate `compact format` command and executes it, + * allowing the underlying tool to handle all user feedback, progress + * reporting, and error messaging. + * + * @param targets - Array of target paths (files or directories) to format + * @param checkMode - If true, uses --check flag to validate without writing + * @returns Promise resolving to command execution results + * @throws FormatterError if the formatting command fails + * + * @example + * ```typescript + * // Check all files in src/ + * await service.format(['src'], true); + * + * // Format specific files + * await service.format(['src/Token.compact', 'src/Utils.compact'], false); + * + * // Format entire project + * await service.format([], false); + * ``` + */ + async format( + targets: string[] = [], + checkMode = true, + ): Promise<{ stdout: string; stderr: string }> { + const checkFlag = checkMode ? ' --check' : ''; + const targetArgs = + targets.length > 0 ? ` ${targets.map((t) => `"${t}"`).join(' ')}` : ''; + + const command = `compact format${checkFlag}${targetArgs}`; + return this.executeCompactCommand(command, 'Formatting failed'); + } + + /** + * Creates formatting-specific error instances. + * + * Wraps formatting failures in FormatterError instances for consistent + * error handling and reporting throughout the application. + * + * @param message - Error message describing the formatting failure + * @param cause - Original error that caused the formatting failure (optional) + * @returns FormatterError instance with cause information + */ + protected createError(message: string, cause?: unknown): Error { + return new FormatterError(message, undefined, cause); + } +} + +/** + * UI service for formatting operations. + * + * Provides minimal UI elements specific to the formatting wrapper, + * since most user feedback is handled by the underlying `compact format` tool. + */ +export const FormatterUIService = { + ...SharedUIService, + + /** + * Displays formatting environment information. + * + * Shows developer tools version and optional target directory information + * to provide context about the formatting environment. + * + * @param devToolsVersion - Version of the installed Compact developer tools + * @param targetDir - Optional target directory being formatted + * + * @example + * ```typescript + * FormatterUIService.displayEnvInfo('compact 0.2.0', 'contracts'); + * // Output: + * // ℹ [FORMAT] TARGET_DIR: contracts + * // ℹ [FORMAT] Compact developer tools: compact 0.2.0 + * ``` + */ + displayEnvInfo(devToolsVersion: string, targetDir?: string): void { + SharedUIService.displayBaseEnvInfo('FORMAT', devToolsVersion, targetDir); + }, + + /** + * Displays warning when no .compact files are found. + * + * Provides feedback when the formatting operation cannot proceed because + * no source files were discovered in the target location. + * + * @param targetDir - Optional target directory that was searched + * + * @example + * ```typescript + * FormatterUIService.showNoFiles('contracts'); + * // Output: ⚠ [FORMAT] No .compact files found in contracts/. + * ``` + */ + showNoFiles(targetDir?: string): void { + SharedUIService.showNoFiles('FORMAT', targetDir); + }, +}; + +/** + * Main formatter coordinator for Compact formatting operations. + * + * Lightweight orchestrator that validates environment, discovers files within + * the project's src/ structure, then delegates to `compact format` for actual + * formatting work. Acts as a bridge between project-specific configuration + * and the underlying formatter tool. + * + * @example + * ```typescript + * // Check formatting of all files + * const formatter = new CompactFormatter(true); + * await formatter.format(); + * + * // Format specific files + * const formatter = new CompactFormatter(false, ['Token.compact']); + * await formatter.format(); + * + * // Format specific directory + * const formatter = new CompactFormatter(false, [], 'contracts'); + * await formatter.format(); + * ``` + */ +export class CompactFormatter extends BaseCompactOperation { + private readonly environmentValidator: FormatterEnvironmentValidator; + private readonly formatterService: FormatterService; + private readonly checkMode: boolean; + private readonly specificFiles: string[]; + + /** + * Creates a new CompactFormatter instance. + * + * Initializes the formatter with operation mode and target configuration. + * Sets up environment validation and command execution services. + * + * @param checkMode - If true, validates formatting without writing changes + * @param specificFiles - Array of specific .compact files to target + * @param targetDir - Optional directory within src/ to limit scope + * @param execFn - Optional command execution function for testing + * + * @example + * ```typescript + * // Check mode for CI/CD + * const formatter = new CompactFormatter(true); + * + * // Format specific files + * const formatter = new CompactFormatter(false, ['Token.compact']); + * + * // Format directory + * const formatter = new CompactFormatter(false, [], 'contracts'); + * ``` + */ + constructor( + checkMode = true, + specificFiles: string[] = [], + targetDir?: string, + execFn?: ExecFunction, + ) { + super(targetDir); + this.checkMode = checkMode; + this.specificFiles = specificFiles; + this.environmentValidator = new FormatterEnvironmentValidator(execFn); + this.formatterService = new FormatterService(execFn); + } + + /** + * Factory method to create CompactFormatter from command-line arguments. + * + * Parses command-line arguments to construct a properly configured formatter. + * Handles --check flag, --dir targeting, and specific file arguments. + * + * @param args - Raw command-line arguments array + * @returns Configured CompactFormatter instance + * @throws Error if arguments are malformed + * + * @example + * ```typescript + * // Parse: ['--check', '--dir', 'contracts'] + * const formatter = CompactFormatter.fromArgs(['--check', '--dir', 'contracts']); + * + * // Parse: ['Token.compact', 'Utils.compact'] + * const formatter = CompactFormatter.fromArgs(['Token.compact', 'Utils.compact']); + * ``` + */ + static fromArgs(args: string[]): CompactFormatter { + const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); + + let checkMode = true; // Default to check mode + const specificFiles: string[] = []; + + for (const arg of remainingArgs) { + if (arg === '--check') { + checkMode = true; + } else if (arg === '--write') { + checkMode = false; // Write mode + } else if (!arg.startsWith('--')) { + specificFiles.push(arg); + } + } + + return new CompactFormatter(checkMode, specificFiles, targetDir); + } + + /** + * Validates formatting environment and displays configuration. + * + * Ensures both CLI and formatter are available before proceeding with + * formatting operations. Displays environment information for user feedback. + * + * @throws CompactCliNotFoundError if CLI is not available + * @throws FormatterNotAvailableError if formatter is not available + */ + async validateEnvironment(): Promise { + const { devToolsVersion } = await this.environmentValidator.validate(); + FormatterUIService.displayEnvInfo(devToolsVersion, this.targetDir); + } + + /** + * Displays warning when no .compact files are found. + * + * Provides user feedback when file discovery returns no results. + */ + showNoFiles(): void { + FormatterUIService.showNoFiles(this.targetDir); + } + + /** + * Executes the formatting workflow. + * + * Validates environment, then either formats specific files or discovers + * and formats files within the target directory. Delegates actual formatting + * to the underlying `compact format` command. + * + * @throws CompactCliNotFoundError if CLI is not available + * @throws FormatterNotAvailableError if formatter is not available + * @throws DirectoryNotFoundError if target directory doesn't exist + * @throws FormatterError if formatting command fails + * + * @example + * ```typescript + * try { + * await formatter.format(); + * } catch (error) { + * if (error instanceof FormatterNotAvailableError) { + * console.error('Update compiler: compact update'); + * } + * } + * ``` + */ + async format(): Promise { + await this.validateEnvironment(); + + // Handle specific files + if (this.specificFiles.length > 0) { + const filePaths = this.specificFiles.map((file) => join(SRC_DIR, file)); + await this.formatterService.format(filePaths, this.checkMode); + return; + } + + // Handle directory or entire project + const { files } = await this.discoverFiles(); + if (files.length === 0) return; + + const mode = this.checkMode ? 'check formatting for' : 'format'; + SharedUIService.showOperationStart( + 'FORMAT', + mode, + files.length, + this.targetDir, + ); + + const targetPath = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; + await this.formatterService.format([targetPath], this.checkMode); + } + + /** + * For testing - expose internal state + */ + get testCheckMode(): boolean { + return this.checkMode; + } + get testSpecificFiles(): string[] { + return this.specificFiles; + } +} diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index 93cbe1b2..de5b597e 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import ora, { type Ora } from 'ora'; +import { BaseErrorHandler } from './BaseServices.js'; import { CompactCompiler } from './Compiler.js'; import { type CompilationError, @@ -9,38 +10,23 @@ import { } from './types/errors.js'; /** - * Executes the Compact compiler CLI with improved error handling and user feedback. + * Main entry point for the Compact compiler CLI application. * - * Error Handling Architecture: + * Orchestrates the complete compilation workflow from command-line argument + * parsing through execution and error handling. Provides user-friendly feedback + * and comprehensive error reporting for compilation operations. * - * This CLI follows a layered error handling approach: + * The function handles the full lifecycle: * - * - Business logic (Compiler.ts) throws structured errors with context. - * - CLI layer (runCompiler.ts) handles all user-facing error presentation. - * - Custom error types (types/errors.ts) provide semantic meaning and context. + * 1. Parses command-line arguments into compiler configuration. + * 2. Executes the compilation process with progress indicators. + * 3. Handles errors with detailed, actionable feedback. + * 4. Exits with appropriate status codes for CI/CD integration. * - * Benefits: Better testability, consistent UI, separation of concerns. - * - * Note: This compiler uses fail-fast error handling. - * Compilation stops on the first error encountered. - * This provides immediate feedback but doesn't attempt to compile remaining files after a failure. - * - * @example Individual module compilation - * ```bash - * npx compact-compiler --dir security --skip-zk - * turbo compact:access -- --skip-zk - * turbo compact:security -- --skip-zk --other-flag - * ``` - * - * @example Full compilation with environment variables + * @example * ```bash - * SKIP_ZK=true turbo compact - * turbo compact - * ``` - * - * @example Version specification - * ```bash - * npx compact-compiler --dir security --skip-zk +0.25.0 + * # Called from command line as: + * compact-compiler --dir ./contracts/src/security --skip-zk +0.25.0 * ``` */ async function runCompiler(): Promise { @@ -57,43 +43,39 @@ async function runCompiler(): Promise { } /** - * Centralized error handling with specific error types and user-friendly messages. + * Comprehensive error handler for compilation-specific failures. + * + * Provides layered error handling that first attempts common error resolution + * before falling back to compilation-specific error types. Ensures users receive + * actionable feedback for all failure scenarios with appropriate visual styling + * and contextual information. * - * Handles different error types with appropriate user feedback: + * Error handling priority: * - * - `CompactCliNotFoundError`: Shows installation instructions. - * - `DirectoryNotFoundError`: Shows available directories. - * - `CompilationError`: Shows file-specific error details with context. - * - Environment validation errors: Shows troubleshooting tips. - * - Argument parsing errors: Shows usage help. - * - Generic errors: Shows general troubleshooting guidance. + * 1. Common errors (CLI not found, directory issues, environment problems). + * 2. Compilation-specific errors (file compilation failures). + * 3. Argument parsing errors (malformed command-line usage). + * 4. Unexpected errors (with troubleshooting guidance). * * @param error - The error that occurred during compilation - * @param spinner - Ora spinner instance for consistent UI messaging + * @param spinner - Ora spinner instance for consistent UI feedback + * + * @example + * ```typescript + * // This function handles errors like: + * // - CompilationError: Failed to compile Token.compact + * // - CompactCliNotFoundError: 'compact' CLI not found in PATH + * // - DirectoryNotFoundError: Target directory contracts/ does not exist + * ``` */ function handleError(error: unknown, spinner: Ora): void { - // CompactCliNotFoundError - if (error instanceof Error && error.name === 'CompactCliNotFoundError') { - spinner.fail(chalk.red(`[COMPILE] Error: ${error.message}`)); - spinner.info( - chalk.blue( - `[COMPILE] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, - ), - ); + // Try common error handling first + if (BaseErrorHandler.handleCommonErrors(error, spinner, 'COMPILE')) { return; } - // DirectoryNotFoundError - if (error instanceof Error && error.name === 'DirectoryNotFoundError') { - spinner.fail(chalk.red(`[COMPILE] Error: ${error.message}`)); - showAvailableDirectories(); - return; - } - - // CompilationError + // CompilationError - specific to compilation if (error instanceof Error && error.name === 'CompilationError') { - // The compilation error details (file name, stdout/stderr) are already displayed - // by `compileFile`; therefore, this just handles the final err state const compilationError = error as CompilationError; spinner.fail( chalk.red( @@ -116,55 +98,38 @@ function handleError(error: unknown, spinner: Ora): void { return; } - // Env validation errors (non-CLI errors) - if (isPromisifiedChildProcessError(error)) { - spinner.fail( - chalk.red(`[COMPILE] Environment validation failed: ${error.message}`), - ); - console.log(chalk.gray('\nTroubleshooting:')); - console.log( - chalk.gray(' • Check that Compact CLI is installed and in PATH'), - ); - console.log(chalk.gray(' • Verify the specified Compact version exists')); - console.log(chalk.gray(' • Ensure you have proper permissions')); - return; - } - - // Arg parsing + // Argument parsing specific to compilation const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('--dir flag requires a directory name')) { - spinner.fail( - chalk.red('[COMPILE] Error: --dir flag requires a directory name'), - ); showUsageHelp(); return; } // Unexpected errors - spinner.fail(chalk.red(`[COMPILE] Unexpected error: ${errorMessage}`)); - console.log(chalk.gray('\nIf this error persists, please check:')); - console.log(chalk.gray(' • Compact CLI is installed and in PATH')); - console.log(chalk.gray(' • Source files exist and are readable')); - console.log(chalk.gray(' • Specified Compact version exists')); - console.log(chalk.gray(' • File system permissions are correct')); + BaseErrorHandler.handleUnexpectedError(error, spinner, 'COMPILE'); } /** - * Shows available directories when `DirectoryNotFoundError` occurs. - */ -function showAvailableDirectories(): void { - console.log(chalk.yellow('\nAvailable directories:')); - console.log( - chalk.yellow(' --dir access # Compile access control contracts'), - ); - console.log(chalk.yellow(' --dir archive # Compile archive contracts')); - console.log(chalk.yellow(' --dir security # Compile security contracts')); - console.log(chalk.yellow(' --dir token # Compile token contracts')); - console.log(chalk.yellow(' --dir utils # Compile utility contracts')); -} - -/** - * Shows usage help with examples for different scenarios. + * Displays comprehensive usage help for the Compact compiler CLI. + * + * Provides detailed documentation of all available command-line options, + * practical usage examples, and integration patterns. Helps users understand + * both basic and advanced compilation scenarios, including environment variable + * usage and toolchain version management. + * + * The help includes: + * + * - Complete option descriptions with parameter details. + * - Practical examples for common compilation tasks. + * - Integration patterns with build tools like Turbo. + * - Environment variable configuration options. + * + * @example + * ```typescript + * // Called automatically when argument parsing fails: + * // compact-compiler --dir # Missing directory name + * // Shows full usage help to guide correct usage + * ``` */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-compiler [options]')); @@ -185,7 +150,7 @@ function showUsageHelp(): void { console.log(chalk.yellow('\nExamples:')); console.log( chalk.yellow( - ' compact-compiler # Compile all files', + ' compact-compiler # Compile all files', ), ); console.log( diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts new file mode 100644 index 00000000..6b9d6846 --- /dev/null +++ b/compact/src/runFormatter.ts @@ -0,0 +1,207 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; +import { BaseErrorHandler } from './BaseServices.js'; +import { CompactFormatter } from './Formatter.js'; +import { + type FormatterError, + isPromisifiedChildProcessError, +} from './types/errors.js'; + +/** + * Main entry point for the Compact formatter CLI binary. + * + * This file serves as the executable binary defined in package.json and is + * invoked through build scripts via Turbo, Yarn, or direct command execution. + * Acts as a lightweight wrapper around the `compact format` command, providing + * environment validation and project-specific file discovery before delegating + * to the underlying formatter tool. + * + * The function manages the wrapper lifecycle: + * + * 1. Validates environment (CLI availability, formatter compatibility). + * 2. Discovers files within the project's src/ structure. + * 3. Constructs and executes appropriate `compact format` commands. + * 4. Handles environment errors while letting format errors pass through. + * + * @example + * ```bash + * # Direct binary execution: + * ./node_modules/.bin/compact-formatter --check --dir security + * ./node_modules/.bin/compact-formatter Token.compact AccessControl.compact + * + * # Via package.json scripts: + * yarn format + * yarn format:fix + * + * # Via Turbo: + * turbo format + * turbo format:fix + * ``` + */ +async function runFormatter(): Promise { + const spinner = ora(chalk.blue('[FORMAT] Compact formatter started')).info(); + + try { + const args = process.argv.slice(2); + const formatter = CompactFormatter.fromArgs(args); + await formatter.format(); + } catch (error) { + handleError(error, spinner); + process.exit(1); + } +} + +/** + * Streamlined error handler focused on environment and setup issues. + * + * Since the underlying `compact format` command handles most user-facing errors + * and feedback (including formatting differences and file processing failures), + * this handler primarily focuses on environment validation errors and setup + * issues that prevent the formatter from running. + * + * Error handling priority: + * + * 1. Common errors (CLI not found, directory issues, permissions). + * 2. Formatter availability errors (toolchain version compatibility). + * 3. Argument parsing errors (malformed command-line usage). + * 4. Formatting errors (let the underlying tool's output show through). + * + * @param error - The error that occurred during formatter execution + * @param spinner - Ora spinner instance for consistent UI feedback + * + * @example + * ```typescript + * // This function primarily handles setup errors like: + * // - FormatterNotAvailableError: Formatter requires compiler 0.25.0+ + * // - CompactCliNotFoundError: 'compact' CLI not found in PATH + * // - DirectoryNotFoundError: Target directory security/ does not exist + * + * // Formatting errors from `compact format` are displayed directly + * ``` + */ +function handleError(error: unknown, spinner: Ora): void { + // Try common error handling first + if (BaseErrorHandler.handleCommonErrors(error, spinner, 'FORMAT')) { + return; + } + + // FormatterNotAvailableError - specific to formatting + if (error instanceof Error && error.name === 'FormatterNotAvailableError') { + spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); + spinner.info(chalk.blue('[FORMAT] Update compiler with: compact update')); + spinner.info( + chalk.blue('[FORMAT] Update dev tools with: compact self update'), + ); + return; + } + + // FormatterError - let the underlying tool's output show through + if (error instanceof Error && error.name === 'FormatterError') { + const formatterError = error as FormatterError; + + // For most formatting errors, the underlying `compact format` command + // already provides good user feedback, so we just show a simple failure message + spinner.fail(chalk.red('[FORMAT] Formatting operation failed')); + + // Show additional details if available + if (isPromisifiedChildProcessError(formatterError.cause)) { + const execError = formatterError.cause; + + // The underlying compact format command output is usually sufficient, + // but show additional details if they're helpful + if (execError.stderr && !execError.stderr.includes('compact format')) { + console.log(chalk.red(` ${execError.stderr}`)); + } + if (execError.stdout?.trim()) { + console.log(chalk.yellow(` ${execError.stdout}`)); + } + } + return; + } + + // Argument parsing specific to formatting + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('--dir flag requires a directory name')) { + showUsageHelp(); + return; + } + + // Unexpected errors + BaseErrorHandler.handleUnexpectedError(error, spinner, 'FORMAT'); +} + +/** + * Displays comprehensive usage documentation for the Compact formatter CLI binary. + * + * Provides complete reference documentation for the package.json binary, + * including all command-line options and integration examples. Emphasizes that + * this is a wrapper around `compact format` that adds project-specific file + * discovery and environment validation. + * + * The help documentation includes: + * + * - Wrapper-specific options (--dir for project structure). + * - Direct binary execution examples. + * - Package.json script integration patterns. + * - Turbo and Yarn workflow examples. + * - Reference to underlying `compact format` capabilities. + * + * @example + * ```typescript + * // Automatically displayed when argument parsing fails: + * // compact-formatter --dir # Missing directory name + * // Shows complete usage guide including script integration examples + * ``` + */ +function showUsageHelp(): void { + console.log(chalk.yellow('\nUsage: compact-formatter [options] [files...]')); + console.log(chalk.yellow('\nOptions:')); + console.log( + chalk.yellow( + ' --check Check if files are properly formatted (default)', + ), + ); + console.log( + chalk.yellow(' --write Write formatting changes to files'), + ); + console.log( + chalk.yellow( + ' --dir Format specific directory (access, archive, security, token, utils)', + ), + ); + console.log(chalk.yellow('\nExamples:')); + console.log( + chalk.yellow( + ' compact-formatter # Check all files (default)', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --write # Format all files', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --write --dir security # Format security directory', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --dir access --check # Check access directory', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --write f1.compact f2.compact # Format specific files', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --check file1.compact # Check specific file', + ), + ); +} + +runFormatter(); diff --git a/compact/src/types/errors.ts b/compact/src/types/errors.ts index 877a61f2..de62b0a0 100644 --- a/compact/src/types/errors.ts +++ b/compact/src/types/errors.ts @@ -10,6 +10,7 @@ * @prop {string} stderr stderr of a child process */ export interface PromisifiedChildProcessError extends Error { + code?: number; stdout: string; stderr: string; } @@ -24,7 +25,11 @@ export interface PromisifiedChildProcessError extends Error { export function isPromisifiedChildProcessError( error: unknown, ): error is PromisifiedChildProcessError { - return error instanceof Error && 'stdout' in error && 'stderr' in error; + return ( + error instanceof Error && + typeof (error as any).stdout === 'string' && + typeof (error as any).stderr === 'string' + ); } /** @@ -57,7 +62,7 @@ export class CompactCliNotFoundError extends Error { */ export class CompilationError extends Error { public readonly file?: string; - + public readonly cause?: unknown; /** * Creates a new CompilationError instance. * @@ -69,6 +74,8 @@ export class CompilationError extends Error { this.file = file; this.name = 'CompilationError'; + this.file = file; + this.cause = cause; } } @@ -95,3 +102,32 @@ export class DirectoryNotFoundError extends Error { this.name = 'DirectoryNotFoundError'; } } + +/** + * Error thrown when the formatter is not available. + * This typically occurs when the Compact compiler version is too old (< 0.25.0). + */ +export class FormatterNotAvailableError extends Error { + constructor(message: string) { + super(message); + this.name = 'FormatterNotAvailableError'; + } +} + +/** + * Error thrown when formatting operations fail. + * Includes the specific target (file or directory) that failed for context. + */ +export class FormatterError extends Error { + /** The target file or directory that failed to format */ + public readonly target?: string; + /** The underlying cause of the formatting failure */ + public readonly cause?: unknown; + + constructor(message: string, target?: string, cause?: unknown) { + super(message); + this.name = 'FormatterError'; + this.target = target; + this.cause = cause; + } +} diff --git a/compact/test/BaseServices.test.ts b/compact/test/BaseServices.test.ts new file mode 100644 index 00000000..07900cc2 --- /dev/null +++ b/compact/test/BaseServices.test.ts @@ -0,0 +1,631 @@ +import { join } from 'node:path'; +import ora from 'ora'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { + BaseCompactOperation, + BaseCompactService, + BaseEnvironmentValidator, + BaseErrorHandler, + FileDiscovery, + SharedUIService, + SRC_DIR, +} from '../src/BaseServices.js'; +import { + CompactCliNotFoundError, + DirectoryNotFoundError, +} from '../src/types/errors.js'; + +// Mock dependencies +vi.mock('node:fs', () => ({ + existsSync: vi.fn(), +})); + +vi.mock('node:fs/promises', () => ({ + readdir: vi.fn(), +})); + +vi.mock('chalk', () => ({ + default: { + blue: vi.fn((text) => text), + yellow: vi.fn((text) => text), + red: vi.fn((text) => text), + gray: vi.fn((text) => text), + }, +})); + +vi.mock('ora', () => ({ + default: vi.fn(() => ({ + info: vi.fn(), + warn: vi.fn(), + fail: vi.fn(), + })), +})); + +// Mock spinner +const mockSpinner = { + start: () => ({ succeed: vi.fn(), fail: vi.fn(), text: '' }), + info: vi.fn(), + warn: vi.fn(), + fail: vi.fn(), + succeed: vi.fn(), +}; + +// Impls for abstract classes +class TestEnvironmentValidator extends BaseEnvironmentValidator { + async validate(): Promise<{ devToolsVersion: string }> { + return this.validateBase(); + } +} + +class TestCompactService extends BaseCompactService { + async testCommand( + command: string, + ): Promise<{ stdout: string; stderr: string }> { + return this.executeCompactCommand(command, 'Test operation failed'); + } + + protected createError(message: string, _cause?: unknown): Error { + return new Error(message); + } +} + +class TestCompactOperation extends BaseCompactOperation { + async validateEnvironment(): Promise {} + + showNoFiles(): void { + SharedUIService.showNoFiles('TEST', this.targetDir); + } +} + +describe('BaseEnvironmentValidator', () => { + let validator: TestEnvironmentValidator; + let mockExec: Mock; + + beforeEach(() => { + mockExec = vi.fn(); + validator = new TestEnvironmentValidator(mockExec); + }); + + describe('checkCompactAvailable', () => { + it('returns true when compact CLI is available', async () => { + mockExec.mockResolvedValue({ stdout: 'compact 0.2.0', stderr: '' }); + + const result = await validator.checkCompactAvailable(); + + expect(result).toBe(true); + expect(mockExec).toHaveBeenCalledWith('compact --version'); + }); + + it('returns false when compact CLI is not available', async () => { + mockExec.mockRejectedValue(new Error('Command not found')); + + const result = await validator.checkCompactAvailable(); + + expect(result).toBe(false); + }); + }); + + describe('getDevToolsVersion', () => { + it('returns trimmed version string', async () => { + mockExec.mockResolvedValue({ stdout: ' compact 0.2.0 \n', stderr: '' }); + + const version = await validator.getDevToolsVersion(); + + expect(version).toBe('compact 0.2.0'); + }); + }); + + describe('validateBase', () => { + it('returns version when CLI is available', async () => { + mockExec.mockResolvedValue({ stdout: 'compact 0.2.0', stderr: '' }); + + const result = await validator.validateBase(); + + expect(result).toEqual({ devToolsVersion: 'compact 0.2.0' }); + }); + + it('throws CompactCliNotFoundError when CLI is not available', async () => { + mockExec.mockRejectedValue(new Error('Command not found')); + + await expect(validator.validateBase()).rejects.toThrow( + CompactCliNotFoundError, + ); + }); + }); +}); + +describe('FileDiscovery', () => { + let fileDiscovery: FileDiscovery; + let mockReaddir: Mock; + + beforeEach(async () => { + fileDiscovery = new FileDiscovery(); + mockReaddir = vi.mocked(await import('node:fs/promises')).readdir; + }); + + it('discovers .compact files recursively', async () => { + mockReaddir + .mockResolvedValueOnce([ + { + name: 'MyToken.compact', + isFile: () => true, + isDirectory: () => false, + }, + { name: 'access', isFile: () => false, isDirectory: () => true }, + ] as any) + .mockResolvedValueOnce([ + { + name: 'AccessControl.compact', + isFile: () => true, + isDirectory: () => false, + }, + ] as any); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual(['MyToken.compact', 'access/AccessControl.compact']); + }); + + it('filters out non-compact files', async () => { + mockReaddir.mockResolvedValue([ + { name: 'MyToken.compact', isFile: () => true, isDirectory: () => false }, + { name: 'README.md', isFile: () => true, isDirectory: () => false }, + { name: 'package.json', isFile: () => true, isDirectory: () => false }, + ] as any); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual(['MyToken.compact']); + }); + + it('handles empty directories', async () => { + mockReaddir.mockResolvedValue([]); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual([]); + }); + + it('handles readdir errors gracefully', async () => { + mockReaddir.mockRejectedValue(new Error('You shall not pass!')); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual([]); + }); +}); + +describe('BaseCompactService', () => { + let service: TestCompactService; + let mockExec: Mock; + + beforeEach(() => { + mockExec = vi.fn(); + service = new TestCompactService(mockExec); + }); + + it('executes command successfully', async () => { + const expectedResult = { stdout: 'Success', stderr: '' }; + mockExec.mockResolvedValue(expectedResult); + + const result = await service.testCommand('compact test'); + + expect(result).toEqual(expectedResult); + expect(mockExec).toHaveBeenCalledWith('compact test'); + }); + + it('handles command execution errors', async () => { + const errMsg = 'Command failed'; + mockExec.mockRejectedValue(new Error(errMsg)); + + await expect(service.testCommand('compact test')).rejects.toThrow( + `Test operation failed: ${errMsg}`, + ); + }); + + it('handles non-Error rejections', async () => { + const otherMsg = 'String error'; + mockExec.mockRejectedValue(otherMsg); + + await expect(service.testCommand('compact test')).rejects.toThrow( + `Test operation failed: ${otherMsg}`, + ); + }); +}); + +describe('SharedUIService', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('printOutput', () => { + it('formats output with indentation', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const colorFn = (text: string) => `colored: ${text}`; + + // Split, filter, map + SharedUIService.printOutput('line1\nline2\n\nline3', colorFn); + + expect(consoleSpy).toHaveBeenCalledWith( + 'colored: line1\n line2\n line3', + ); + consoleSpy.mockRestore(); + }); + + it('filters empty lines', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const colorFn = (text: string) => text; + + SharedUIService.printOutput('line1\n\n\nline2', colorFn); + + expect(consoleSpy).toHaveBeenCalledWith(' line1\n line2'); + consoleSpy.mockRestore(); + }); + }); + + describe('displayBaseEnvInfo', () => { + const testData = { + operation: 'TEST', + version: 'compact 0.2.0', + targetDir: 'security', + }; + const { operation, version, targetDir } = testData; + + it('displays environment info with target directory', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.displayBaseEnvInfo(operation, version, targetDir); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledTimes(2); + expect(mockSpinner.info).toHaveBeenNthCalledWith( + 1, + `[${operation}] TARGET_DIR: ${targetDir}`, + ); + expect(mockSpinner.info).toHaveBeenNthCalledWith( + 2, + `[${operation}] Compact developer tools: ${version}`, + ); + }); + + it('displays environment info without target directory', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.displayBaseEnvInfo(operation, version); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith( + `[${operation}] Compact developer tools: ${version}`, + ); + }); + }); + + describe('showOperationStart', () => { + const testData = { + operation: 'TEST', + action: 'compact 0.2.0', + fileCount: 5, + targetDir: 'security', + }; + const { operation, action, fileCount, targetDir } = testData; + + it('displays operation start message with targetDir', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showOperationStart( + operation, + action, + fileCount, + targetDir, + ); + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith( + `[${operation}] Found ${fileCount} .compact file(s) to ${action} in ${targetDir}/`, + ); + }); + + it('displays operation start message without targetDir', () => { + const mockSpinner = { info: vi.fn() }; + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showOperationStart(operation, action, fileCount); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith( + `[${operation}] Found ${fileCount} .compact file(s) to ${action}`, + ); + }); + }); + + describe('showNoFiles', () => { + const testData = { + operation: 'TEST', + targetDir: 'security', + }; + const { operation, targetDir } = testData; + + it('shows no files warning with targetDir', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showNoFiles(operation, targetDir); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith( + `[${operation}] No .compact files found in ${targetDir}/.`, + ); + }); + + it('shows no files warning without targetDir', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showNoFiles(operation); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith( + `[${operation}] No .compact files found in src/.`, + ); + }); + }); + + describe('showAvailableDirectories', () => { + it('shows available directories', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const operation = 'TEST'; + + SharedUIService.showAvailableDirectories(operation); + + expect(consoleSpy).toHaveBeenNthCalledWith(1, '\nAvailable directories:'); + expect(consoleSpy).toHaveBeenNthCalledWith( + 2, + ` --dir access # ${operation} access control contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 3, + ` --dir archive # ${operation} archive contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 4, + ` --dir security # ${operation} security contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 5, + ` --dir token # ${operation} token contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 6, + ` --dir utils # ${operation} utility contracts`, + ); + consoleSpy.mockRestore(); + }); + }); +}); + +describe('BaseCompactOperation', () => { + let operation: TestCompactOperation; + let mockExistsSync: Mock; + + beforeEach(async () => { + operation = new TestCompactOperation('security'); + mockExistsSync = vi.mocked(await import('node:fs')).existsSync; + }); + + describe('validateTargetDirectory', () => { + it('passes when target directory exists', () => { + mockExistsSync.mockReturnValue(true); + + expect(() => + operation['validateTargetDirectory']('src/security'), + ).not.toThrow(); + }); + + it('throws when target directory does not exist', () => { + mockExistsSync.mockReturnValue(false); + const missingDir = 'src/missingDir'; + const expErr = new DirectoryNotFoundError( + `Target directory ${missingDir} does not exist`, + missingDir, + ); + + expect(() => operation['validateTargetDirectory'](missingDir)).toThrow( + expErr, + ); + }); + + it('does not validate when no target directory is set', () => { + const noTargetOperation = new TestCompactOperation(); + mockExistsSync.mockReturnValue(false); + + expect(() => + noTargetOperation['validateTargetDirectory']('src'), + ).not.toThrow(); + }); + }); + + describe('getSearchDirectory', () => { + it('returns targetDir path when set', () => { + const result = operation['getSearchDirectory'](); + expect(result).toBe(join(SRC_DIR, 'security')); + }); + + it('returns SRC_DIR when no targetDir', () => { + const noTargetOperation = new TestCompactOperation(); + const result = noTargetOperation['getSearchDirectory'](); + expect(result).toBe(SRC_DIR); + }); + }); + + describe('parseBaseArgs', () => { + it('parses --dir argument correctly', () => { + const args = ['--dir', 'security', '--other-flag']; + + const result = BaseCompactOperation['parseBaseArgs'](args); + + expect(result).toEqual({ + targetDir: 'security', + remainingArgs: ['--other-flag'], + }); + }); + + it('throws error when --dir has no value', () => { + const args = ['--dir']; + + expect(() => BaseCompactOperation['parseBaseArgs'](args)).toThrow( + '--dir flag requires a directory name', + ); + }); + + it('throws error when --dir value starts with --', () => { + const args = ['--dir', '--other-flag']; + + expect(() => BaseCompactOperation['parseBaseArgs'](args)).toThrow( + '--dir flag requires a directory name', + ); + }); + + it('handles arguments without --dir', () => { + const args = ['--flag1', 'value1', '--flag2']; + + const result = BaseCompactOperation['parseBaseArgs'](args); + + expect(result).toEqual({ + targetDir: undefined, + remainingArgs: ['--flag1', 'value1', '--flag2'], + }); + }); + }); +}); + +describe('BaseErrorHandler', () => { + let mockSpinner: any; + + beforeEach(() => { + mockSpinner = { + fail: vi.fn(), + info: vi.fn(), + }; + }); + + describe('handleCommonErrors', () => { + const operation = 'TEST'; + + it('handles CompactCliNotFoundError', () => { + const error = new CompactCliNotFoundError('CLI not found'); + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); + + expect(result).toBe(true); + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Error: CLI not found`), + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Install with:`), + ); + }); + + it('handles DirectoryNotFoundError', () => { + const error = new DirectoryNotFoundError('Directory not found', '/path'); + + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); + + expect(result).toBe(true); + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Error: Directory not found`), + ); + }); + + it('handles promisified child process errors', () => { + const error = Object.assign(new Error('Command failed'), { + stdout: 'stdout', + stderr: 'stderr', + code: 1, + }); + + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); + + expect(result).toBe(true); + expect(mockSpinner.fail).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining(`[${operation}] Environment validation failed`), + ); + }); + + it('handles --dir argument parsing errors', () => { + const error = new Error('--dir flag requires a directory name'); + + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); + + expect(result).toBe(false); // Should let specific handler show usage + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining( + `[${operation}] Error: --dir flag requires a directory name`, + ), + ); + }); + + it('returns false for unhandled errors', () => { + const error = new Error('Some other error'); + + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); + + expect(result).toBe(false); + }); + }); + + describe('handleUnexpectedError', () => { + const operation = 'TEST'; + + it('handles Error objects', () => { + const error = new Error('Unexpected error'); + + BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining( + `[${operation}] Unexpected error: Unexpected error`, + ), + ); + }); + + it('handles non-Error values', () => { + const error = 'String error'; + + BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining( + `[${operation}] Unexpected error: String error`, + ), + ); + }); + + it('displays troubleshooting information', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const error = new Error('Test error'); + + BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('If this error persists'), + ); + consoleSpy.mockRestore(); + }); + }); +}); diff --git a/compact/test/Compiler.test.ts b/compact/test/Compiler.test.ts index 8dcc4dd1..22128427 100644 --- a/compact/test/Compiler.test.ts +++ b/compact/test/Compiler.test.ts @@ -1,318 +1,217 @@ -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; -import { - beforeEach, - describe, - expect, - it, - type MockedFunction, - vi, -} from 'vitest'; +import { join } from 'node:path'; +import ora from 'ora'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { ARTIFACTS_DIR, SRC_DIR } from '../src/BaseServices.js'; import { CompactCompiler, + CompilerEnvironmentValidator, CompilerService, - EnvironmentValidator, - type ExecFunction, - FileDiscovery, - UIService, + CompilerUIService, } from '../src/Compiler.js'; -import { - CompactCliNotFoundError, - CompilationError, - DirectoryNotFoundError, -} from '../src/types/errors.js'; - -// Mock Node.js modules -vi.mock('node:fs'); -vi.mock('node:fs/promises'); +import { CompilationError } from '../src/types/errors.js'; + +// Mock dependencies vi.mock('chalk', () => ({ default: { - blue: (text: string) => text, - green: (text: string) => text, - red: (text: string) => text, - yellow: (text: string) => text, - cyan: (text: string) => text, - gray: (text: string) => text, + blue: vi.fn((text) => text), + green: vi.fn((text) => text), + red: vi.fn((text) => text), + yellow: vi.fn((text) => text), + cyan: vi.fn((text) => text), }, })); -// Mock spinner -const mockSpinner = { - start: () => ({ succeed: vi.fn(), fail: vi.fn(), text: '' }), - info: vi.fn(), - warn: vi.fn(), - fail: vi.fn(), - succeed: vi.fn(), -}; - vi.mock('ora', () => ({ - default: () => mockSpinner, + default: vi.fn(() => ({ + start: vi.fn().mockReturnThis(), + succeed: vi.fn(), + fail: vi.fn(), + info: vi.fn(), + })), })); -const mockExistsSync = vi.mocked(existsSync); -const mockReaddir = vi.mocked(readdir); - -describe('EnvironmentValidator', () => { - let mockExec: MockedFunction; - let validator: EnvironmentValidator; +describe('CompilerEnvironmentValidator', () => { + let validator: CompilerEnvironmentValidator; + let mockExec: Mock; beforeEach(() => { - vi.clearAllMocks(); mockExec = vi.fn(); - validator = new EnvironmentValidator(mockExec); - }); - - describe('checkCompactAvailable', () => { - it('should return true when compact CLI is available', async () => { - mockExec.mockResolvedValue({ stdout: 'compact 0.1.0', stderr: '' }); - - const result = await validator.checkCompactAvailable(); - - expect(result).toBe(true); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - - it('should return false when compact CLI is not available', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - - const result = await validator.checkCompactAvailable(); - - expect(result).toBe(false); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - }); - - describe('getDevToolsVersion', () => { - it('should return trimmed version string', async () => { - mockExec.mockResolvedValue({ stdout: ' compact 0.1.0 \n', stderr: '' }); - - const version = await validator.getDevToolsVersion(); - - expect(version).toBe('compact 0.1.0'); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - - it('should throw error when command fails', async () => { - mockExec.mockRejectedValue(new Error('Command failed')); - - await expect(validator.getDevToolsVersion()).rejects.toThrow( - 'Command failed', - ); - }); + validator = new CompilerEnvironmentValidator(mockExec); }); describe('getToolchainVersion', () => { - it('should get version without specific version flag', async () => { + it('returns default toolchain version', async () => { + const testData = { + expectedOutput: 'Compactc version: 0.25.0', + expectedCommand: 'compact compile --version', + }; + mockExec.mockResolvedValue({ - stdout: 'Compactc version: 0.25.0', + stdout: ` ${testData.expectedOutput} \n`, stderr: '', }); const version = await validator.getToolchainVersion(); - expect(version).toBe('Compactc version: 0.25.0'); - expect(mockExec).toHaveBeenCalledWith('compact compile --version'); + expect(version).toBe(testData.expectedOutput); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should get version with specific version flag', async () => { + it('returns toolchain version with specific version', async () => { + const testData = { + version: '0.25.0', + expectedOutput: 'Compactc version: 0.25.0', + expectedCommand: 'compact compile +0.25.0 --version', + }; + mockExec.mockResolvedValue({ - stdout: 'Compactc version: 0.25.0', + stdout: testData.expectedOutput, stderr: '', }); - const version = await validator.getToolchainVersion('0.25.0'); + const version = await validator.getToolchainVersion(testData.version); - expect(version).toBe('Compactc version: 0.25.0'); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.25.0 --version', - ); + expect(version).toBe(testData.expectedOutput); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); }); describe('validate', () => { - it('should validate successfully when CLI is available', async () => { - mockExec.mockResolvedValue({ stdout: 'compact 0.1.0', stderr: '' }); - - await expect(validator.validate()).resolves.not.toThrow(); - }); - - it('should throw CompactCliNotFoundError when CLI is not available', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - - await expect(validator.validate()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - }); -}); - -describe('FileDiscovery', () => { - let discovery: FileDiscovery; - - beforeEach(() => { - vi.clearAllMocks(); - discovery = new FileDiscovery(); - }); - - describe('getCompactFiles', () => { - it('should find .compact files in directory', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => true, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - { name: 'README.md', isFile: () => true, isDirectory: () => false }, - { name: 'utils', isFile: () => false, isDirectory: () => true }, - ]; - - mockReaddir - .mockResolvedValueOnce(mockDirents as any) - .mockResolvedValueOnce([ - { - name: 'Utils.compact', - isFile: () => true, - isDirectory: () => false, - }, - ] as any); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual([ - 'MyToken.compact', - 'Ownable.compact', - 'utils/Utils.compact', - ]); - }); + it('returns both dev tools and toolchain versions', async () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.25.0', + }; - it('should handle empty directories', async () => { - mockReaddir.mockResolvedValue([]); + mockExec + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ + stdout: testData.toolchainVersion, + stderr: '', + }); - const files = await discovery.getCompactFiles('src'); + const result = await validator.validate(); - expect(files).toEqual([]); + expect(result).toEqual({ + devToolsVersion: testData.devToolsVersion, + toolchainVersion: testData.toolchainVersion, + }); }); - it('should handle directory read errors gracefully', async () => { - const consoleSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); + it('passes version parameter to getToolchainVersion', async () => { + const testData = { + version: '0.25.0', + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.25.0', + }; - mockReaddir.mockRejectedValueOnce(new Error('Permission denied')); + mockExec + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ + stdout: testData.toolchainVersion, + stderr: '', + }); - const files = await discovery.getCompactFiles('src'); + await validator.validate(testData.version); - expect(files).toEqual([]); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to read dir: src', - expect.any(Error), + expect(mockExec).toHaveBeenCalledWith( + 'compact compile +0.25.0 --version', ); - consoleSpy.mockRestore(); - }); - - it('should handle file access errors gracefully', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => { - throw new Error('Access denied'); - }, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - - mockReaddir.mockResolvedValue(mockDirents as any); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual(['Ownable.compact']); }); }); }); describe('CompilerService', () => { - let mockExec: MockedFunction; let service: CompilerService; + let mockExec: Mock; beforeEach(() => { - vi.clearAllMocks(); mockExec = vi.fn(); service = new CompilerService(mockExec); }); describe('compileFile', () => { - it('should compile file successfully with basic flags', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); + it('constructs correct command with all parameters', async () => { + const testData = { + file: 'contracts/MyToken.compact', + flags: '--skip-zk --verbose', + version: '0.25.0', + expectedInputPath: join(SRC_DIR, 'contracts/MyToken.compact'), + expectedOutputDir: join(ARTIFACTS_DIR, 'MyToken'), + expectedCommand: + 'compact compile +0.25.0 --skip-zk --verbose "src/contracts/MyToken.compact" "artifacts/MyToken"', + }; - const result = await service.compileFile('MyToken.compact', '--skip-zk'); + mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', + await service.compileFile( + testData.file, + testData.flags, + testData.version, ); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should compile file with version flag', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); + it('constructs command without version flag', async () => { + const testData = { + file: 'MyToken.compact', + flags: '--skip-zk', + expectedCommand: + 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', + }; - const result = await service.compileFile( - 'MyToken.compact', - '--skip-zk', - '0.25.0', - ); + mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.25.0 --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); + await service.compileFile(testData.file, testData.flags); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should handle empty flags', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); + it('constructs command without flags', async () => { + const testData = { + file: 'MyToken.compact', + flags: '', + expectedCommand: + 'compact compile "src/MyToken.compact" "artifacts/MyToken"', + }; - const result = await service.compileFile('MyToken.compact', ''); + mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile "src/MyToken.compact" "artifacts/MyToken"', - ); + await service.compileFile(testData.file, testData.flags); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should throw CompilationError when compilation fails', async () => { - mockExec.mockRejectedValue(new Error('Syntax error on line 10')); + it('throws CompilationError on failure', async () => { + const testData = { + file: 'MyToken.compact', + flags: '--skip-zk', + errorMessage: 'Syntax error on line 10', + }; + + mockExec.mockRejectedValue(new Error(testData.errorMessage)); await expect( - service.compileFile('MyToken.compact', '--skip-zk'), + service.compileFile(testData.file, testData.flags), ).rejects.toThrow(CompilationError); }); - it('should include file path in CompilationError', async () => { - mockExec.mockRejectedValue(new Error('Syntax error')); + it('CompilationError includes file name', async () => { + const testData = { + file: 'contracts/MyToken.compact', + flags: '--skip-zk', + }; + + mockExec.mockRejectedValue(new Error('Compilation failed')); try { - await service.compileFile('MyToken.compact', '--skip-zk'); + await service.compileFile(testData.file, testData.flags); } catch (error) { expect(error).toBeInstanceOf(CompilationError); - expect((error as CompilationError).file).toBe('MyToken.compact'); + expect((error as CompilationError).file).toBe(testData.file); } }); @@ -328,69 +227,90 @@ describe('CompilerService', () => { } }); }); -}); -describe('UIService', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - - describe('printOutput', () => { - it('should format output with indentation', () => { - const mockColorFn = vi.fn((text: string) => `colored(${text})`); + describe('createError', () => { + it('extracts file name from error message', () => { + const testData = { + message: 'Failed to compile contracts/MyToken.compact: Syntax error', + expectedFile: 'contracts/MyToken.compact', + }; - UIService.printOutput('line 1\nline 2\n\nline 3', mockColorFn); + const error = service['createError'](testData.message); - expect(mockColorFn).toHaveBeenCalledWith( - ' line 1\n line 2\n line 3', - ); - expect(console.log).toHaveBeenCalledWith( - 'colored( line 1\n line 2\n line 3)', - ); + expect(error).toBeInstanceOf(CompilationError); + expect((error as CompilationError).file).toBe(testData.expectedFile); }); - it('should handle empty output', () => { - const mockColorFn = vi.fn((text: string) => `colored(${text})`); + it('uses "unknown" when file name cannot be extracted', () => { + const testData = { + message: 'Some generic error message', + expectedFile: 'unknown', + }; - UIService.printOutput('', mockColorFn); + const error = service['createError'](testData.message); - expect(mockColorFn).toHaveBeenCalledWith(''); - expect(console.log).toHaveBeenCalledWith('colored()'); + expect(error).toBeInstanceOf(CompilationError); + expect((error as CompilationError).file).toBe(testData.expectedFile); }); }); +}); + +describe('CompilerUIService', () => { + let mockSpinner: any; + + beforeEach(() => { + mockSpinner = { + info: vi.fn(), + }; + vi.mocked(ora).mockReturnValue(mockSpinner); + }); describe('displayEnvInfo', () => { - it('should display environment information with all parameters', () => { - UIService.displayEnvInfo( - 'compact 0.1.0', - 'Compactc 0.25.0', - 'security', - '0.25.0', + it('displays all environment information', () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.25.0', + targetDir: 'security', + version: '0.25.0', + }; + + CompilerUIService.displayEnvInfo( + testData.devToolsVersion, + testData.toolchainVersion, + testData.targetDir, + testData.version, ); expect(mockSpinner.info).toHaveBeenCalledWith( '[COMPILE] TARGET_DIR: security', ); expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact developer tools: compact 0.1.0', + '[COMPILE] Compact developer tools: compact 0.2.0', ); expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact toolchain: Compactc 0.25.0', + '[COMPILE] Compact toolchain: Compactc version: 0.25.0', ); expect(mockSpinner.info).toHaveBeenCalledWith( '[COMPILE] Using toolchain version: 0.25.0', ); }); - it('should display environment information without optional parameters', () => { - UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.25.0'); + it('displays minimal environment information', () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.25.0', + }; + + CompilerUIService.displayEnvInfo( + testData.devToolsVersion, + testData.toolchainVersion, + ); expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact developer tools: compact 0.1.0', + '[COMPILE] Compact developer tools: compact 0.2.0', ); expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact toolchain: Compactc 0.25.0', + '[COMPILE] Compact toolchain: Compactc version: 0.25.0', ); expect(mockSpinner.info).not.toHaveBeenCalledWith( expect.stringContaining('TARGET_DIR'), @@ -400,81 +320,59 @@ describe('UIService', () => { ); }); }); - - describe('showCompilationStart', () => { - it('should show file count without target directory', () => { - UIService.showCompilationStart(5); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Found 5 .compact file(s) to compile', - ); - }); - - it('should show file count with target directory', () => { - UIService.showCompilationStart(3, 'security'); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Found 3 .compact file(s) to compile in security/', - ); - }); - }); - - describe('showNoFiles', () => { - it('should show no files message with target directory', () => { - UIService.showNoFiles('security'); - - expect(mockSpinner.warn).toHaveBeenCalledWith( - '[COMPILE] No .compact files found in security/.', - ); - }); - - it('should show no files message without target directory', () => { - UIService.showNoFiles(); - - expect(mockSpinner.warn).toHaveBeenCalledWith( - '[COMPILE] No .compact files found in .', - ); - }); - }); }); describe('CompactCompiler', () => { - let mockExec: MockedFunction; let compiler: CompactCompiler; + let mockExec: Mock; beforeEach(() => { - vi.clearAllMocks(); - mockExec = vi.fn().mockResolvedValue({ stdout: 'success', stderr: '' }); - mockExistsSync.mockReturnValue(true); - mockReaddir.mockResolvedValue([]); + mockExec = vi.fn(); }); describe('constructor', () => { - it('should create instance with default parameters', () => { + it('creates instance with default parameters', () => { compiler = new CompactCompiler(); expect(compiler).toBeInstanceOf(CompactCompiler); + expect(compiler.testFlags).toBe(''); + expect(compiler.testTargetDir).toBeUndefined(); + expect(compiler.testVersion).toBeUndefined(); }); - it('should create instance with all parameters', () => { + it('creates instance with all parameters', () => { + const testData = { + flags: '--skip-zk --verbose', + targetDir: 'security', + version: '0.25.0', + }; + compiler = new CompactCompiler( - '--skip-zk', - 'security', - '0.25.0', + testData.flags, + testData.targetDir, + testData.version, mockExec, ); - expect(compiler).toBeInstanceOf(CompactCompiler); + expect(compiler.testFlags).toBe(testData.flags); + expect(compiler.testTargetDir).toBe(testData.targetDir); + expect(compiler.testVersion).toBe(testData.version); }); - it('should trim flags', () => { - compiler = new CompactCompiler(' --skip-zk --verbose '); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + it('trims flags parameter', () => { + const testData = { + inputFlags: ' --skip-zk --verbose ', + expectedFlags: '--skip-zk --verbose', + }; + + compiler = new CompactCompiler(testData.inputFlags); + + expect(compiler.testFlags).toBe(testData.expectedFlags); }); }); describe('fromArgs', () => { - it('should parse empty arguments', () => { + it('parses empty arguments', () => { compiler = CompactCompiler.fromArgs([]); expect(compiler.testFlags).toBe(''); @@ -482,82 +380,75 @@ describe('CompactCompiler', () => { expect(compiler.testVersion).toBeUndefined(); }); - it('should handle SKIP_ZK environment variable', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - - expect(compiler.testFlags).toBe('--skip-zk'); - }); + it('parses SKIP_ZK environment variable', () => { + const testData = { + env: { SKIP_ZK: 'true' }, + expectedFlags: '--skip-zk', + }; - it('should ignore SKIP_ZK when not "true"', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'false' }); + compiler = CompactCompiler.fromArgs([], testData.env); - expect(compiler.testFlags).toBe(''); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should parse --dir flag', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'security']); - - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe(''); - }); + it('ignores SKIP_ZK when not "true"', () => { + const testData = { + env: { SKIP_ZK: 'false' }, + expectedFlags: '', + }; - it('should parse --dir flag with additional flags', () => { - compiler = CompactCompiler.fromArgs([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - ]); + compiler = CompactCompiler.fromArgs([], testData.env); - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should parse version flag', () => { - compiler = CompactCompiler.fromArgs(['+0.25.0']); - - expect(compiler.testVersion).toBe('0.25.0'); - expect(compiler.testFlags).toBe(''); - }); + it('parses complex arguments with all options', () => { + const testData = { + args: ['--dir', 'security', '--skip-zk', '--verbose', '+0.25.0'], + env: {}, + expectedTargetDir: 'security', + expectedFlags: '--skip-zk --verbose', + expectedVersion: '0.25.0', + }; - it('should parse complex arguments', () => { - compiler = CompactCompiler.fromArgs([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.25.0', - ]); + compiler = CompactCompiler.fromArgs(testData.args, testData.env); - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - expect(compiler.testVersion).toBe('0.25.0'); + expect(compiler.testTargetDir).toBe(testData.expectedTargetDir); + expect(compiler.testFlags).toBe(testData.expectedFlags); + expect(compiler.testVersion).toBe(testData.expectedVersion); }); - it('should combine environment variables with CLI flags', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'access', '--verbose'], { - SKIP_ZK: 'true', - }); + it('combines environment variables with CLI flags', () => { + const testData = { + args: ['--dir', 'access', '--verbose'], + env: { SKIP_ZK: 'true' }, + expectedFlags: '--skip-zk --verbose', + }; + + compiler = CompactCompiler.fromArgs(testData.args, testData.env); - expect(compiler.testTargetDir).toBe('access'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should deduplicate flags when both env var and CLI flag are present', () => { - compiler = CompactCompiler.fromArgs(['--skip-zk', '--verbose'], { - SKIP_ZK: 'true', - }); + it('deduplicates flags from environment and CLI', () => { + const testData = { + args: ['--skip-zk', '--verbose'], + env: { SKIP_ZK: 'true' }, + expectedFlags: '--skip-zk --verbose', + }; - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + compiler = CompactCompiler.fromArgs(testData.args, testData.env); + + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should throw error for --dir without argument', () => { + it('throws error for --dir without argument', () => { expect(() => CompactCompiler.fromArgs(['--dir'])).toThrow( '--dir flag requires a directory name', ); }); - it('should throw error for --dir followed by another flag', () => { + it('throws error for --dir followed by another flag', () => { expect(() => CompactCompiler.fromArgs(['--dir', '--skip-zk'])).toThrow( '--dir flag requires a directory name', ); @@ -565,316 +456,43 @@ describe('CompactCompiler', () => { }); describe('validateEnvironment', () => { - it('should validate successfully and display environment info', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // getDevToolsVersion - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.25.0', - stderr: '', - }); // getToolchainVersion - - compiler = new CompactCompiler( - '--skip-zk', - 'security', - '0.25.0', - mockExec, - ); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await expect(compiler.validateEnvironment()).resolves.not.toThrow(); - - // Check steps - expect(mockExec).toHaveBeenCalledTimes(3); - expect(mockExec).toHaveBeenNthCalledWith(1, 'compact --version'); // validate() calls - expect(mockExec).toHaveBeenNthCalledWith(2, 'compact --version'); // getDevToolsVersion() - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.25.0 --version', - ); // getToolchainVersion() - - // Verify passed args - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.25.0', - 'security', - '0.25.0', - ); - - displaySpy.mockRestore(); - }); - - it('should handle CompactCliNotFoundError with installation instructions', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - - it('should handle version retrieval failures after successful CLI check', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // validate() succeeds - .mockRejectedValueOnce(new Error('Version command failed')); // getDevToolsVersion() fails - - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - 'Version command failed', - ); - }); - - it('should handle PromisifiedChildProcessError specifically', async () => { - const childProcessError = new Error('Command execution failed') as any; - childProcessError.stdout = 'some output'; - childProcessError.stderr = 'some error'; - - mockExec.mockRejectedValue(childProcessError); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - "'compact' CLI not found in PATH. Please install the Compact developer tools.", - ); - }); - - it('should handle non-Error exceptions gracefully', async () => { - mockExec.mockRejectedValue('String error message'); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - - it('should validate with specific version flag', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.25.0', - stderr: '', - }); - - compiler = new CompactCompiler('', undefined, '0.25.0', mockExec); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await compiler.validateEnvironment(); - - // Verify version-specific toolchain call - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.25.0 --version', - ); - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.25.0', - undefined, // no targetDir - '0.25.0', - ); - - displaySpy.mockRestore(); - }); + it('calls validator and displays environment info', async () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.25.0', + targetDir: 'security', + version: '0.25.0', + }; - it('should validate without target directory or version', async () => { mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.25.0', + stdout: testData.toolchainVersion, stderr: '', }); - compiler = new CompactCompiler('', undefined, undefined, mockExec); const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') + .spyOn(CompilerUIService, 'displayEnvInfo') .mockImplementation(() => {}); - await compiler.validateEnvironment(); - - // Verify default toolchain call (no version flag) - expect(mockExec).toHaveBeenNthCalledWith(3, 'compact compile --version'); - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.25.0', - undefined, - undefined, - ); - - displaySpy.mockRestore(); - }); - }); - - describe('compile', () => { - it('should handle empty source directory', async () => { - mockReaddir.mockResolvedValue([]); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.compile()).resolves.not.toThrow(); - }); - - it('should throw error if target directory does not exist', async () => { - mockExistsSync.mockReturnValue(false); - compiler = new CompactCompiler('', 'nonexistent', undefined, mockExec); - - await expect(compiler.compile()).rejects.toThrow(DirectoryNotFoundError); - }); - - it('should compile files successfully', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => true, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - mockReaddir.mockResolvedValue(mockDirents as any); compiler = new CompactCompiler( '--skip-zk', - undefined, - undefined, + testData.targetDir, + testData.version, mockExec, ); - await compiler.compile(); - - expect(mockExec).toHaveBeenCalledWith( - expect.stringContaining('compact compile --skip-zk'), - ); - }); - - it('should handle compilation errors gracefully', async () => { - const brokenDirent = { - name: 'Broken.compact', - isFile: () => true, - isDirectory: () => false, - }; - - const mockDirents = [brokenDirent]; - mockReaddir.mockResolvedValue(mockDirents as any); - mockExistsSync.mockReturnValue(true); - - const testMockExec = vi - .fn() - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // getDevToolsVersion - .mockResolvedValueOnce({ stdout: 'Compactc 0.25.0', stderr: '' }) // getToolchainVersion - .mockRejectedValueOnce(new Error('Compilation failed')); // compileFile execution - - compiler = new CompactCompiler('', undefined, undefined, testMockExec); - - // Test that compilation errors are properly propagated - let thrownError: unknown; - try { - await compiler.compile(); - expect.fail('Expected compilation to throw an error'); - } catch (error) { - thrownError = error; - } + await compiler.validateEnvironment(); - expect(thrownError).toBeInstanceOf(Error); - expect((thrownError as Error).message).toBe( - `Failed to compile ${brokenDirent.name}: Compilation failed`, + expect(displaySpy).toHaveBeenCalledWith( + testData.devToolsVersion, + testData.toolchainVersion, + testData.targetDir, + testData.version, ); - expect(testMockExec).toHaveBeenCalledTimes(4); - }); - }); - - describe('Real-world scenarios', () => { - beforeEach(() => { - const mockDirents = [ - { - name: 'AccessControl.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - mockReaddir.mockResolvedValue(mockDirents as any); - }); - - it('should handle turbo compact command', () => { - compiler = CompactCompiler.fromArgs([]); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBeUndefined(); - }); - - it('should handle SKIP_ZK=true turbo compact command', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - - expect(compiler.testFlags).toBe('--skip-zk'); - }); - - it('should handle turbo compact:access command', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'access']); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBe('access'); - }); - - it('should handle turbo compact:security -- --skip-zk command', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); - - expect(compiler.testFlags).toBe('--skip-zk'); - expect(compiler.testTargetDir).toBe('security'); - }); - - it('should handle version specification', () => { - compiler = CompactCompiler.fromArgs(['+0.25.0']); - expect(compiler.testVersion).toBe('0.25.0'); - }); - - it.each([ - { - name: 'with skip zk env var only', - args: [ - '--dir', - 'security', - '--no-communications-commitment', - '+0.25.0', - ], - env: { SKIP_ZK: 'true' }, - }, - { - name: 'with skip-zk flag only', - args: [ - '--dir', - 'security', - '--skip-zk', - '--no-communications-commitment', - '+0.25.0', - ], - env: { SKIP_ZK: 'false' }, - }, - { - name: 'with both skip-zk flag and env var', - args: [ - '--dir', - 'security', - '--skip-zk', - '--no-communications-commitment', - '+0.25.0', - ], - env: { SKIP_ZK: 'true' }, - }, - ])('should handle complex command $name', ({ args, env }) => { - compiler = CompactCompiler.fromArgs(args, env); - - expect(compiler.testFlags).toBe( - '--skip-zk --no-communications-commitment', - ); - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testVersion).toBe('0.25.0'); + displaySpy.mockRestore(); }); }); }); diff --git a/compact/test/Formatter.test.ts b/compact/test/Formatter.test.ts new file mode 100644 index 00000000..40f7cc45 --- /dev/null +++ b/compact/test/Formatter.test.ts @@ -0,0 +1,295 @@ +import { join } from 'node:path'; +import ora from 'ora'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { SRC_DIR } from '../src/BaseServices.js'; +import { + CompactFormatter, + FormatterEnvironmentValidator, + FormatterService, + FormatterUIService, +} from '../src/Formatter.js'; +import { FormatterError } from '../src/types/errors.js'; + +// Mock dependencies +vi.mock('chalk', () => ({ + default: { + blue: vi.fn((text) => text), + green: vi.fn((text) => text), + red: vi.fn((text) => text), + yellow: vi.fn((text) => text), + cyan: vi.fn((text) => text), + white: vi.fn((text) => text), + }, +})); + +vi.mock('ora', () => ({ + default: vi.fn(() => ({ + start: vi.fn().mockReturnThis(), + succeed: vi.fn(), + fail: vi.fn(), + info: vi.fn(), + })), +})); + +describe('FormatterEnvironmentValidator', () => { + let validator: FormatterEnvironmentValidator; + let mockExec: Mock; + + beforeEach(() => { + vi.clearAllMocks(); + mockExec = vi.fn(); + validator = new FormatterEnvironmentValidator(mockExec); + }); + + describe('checkFormatterAvailable', () => { + it('succeeds when formatter is available', async () => { + mockExec.mockResolvedValue({ stdout: 'Format help text', stderr: '' }); + + await expect(validator.checkFormatterAvailable()).resolves.not.toThrow(); + expect(mockExec).toHaveBeenCalledWith('compact help format'); + }); + + it('throws FormatterNotAvailableError when formatter not available', async () => { + const error = Object.assign(new Error('Command failed'), { + stderr: 'formatter not available', + stdout: '', + code: 1, + }); + + mockExec.mockRejectedValue(error); + await expect(validator.checkFormatterAvailable()).rejects.toThrow( + 'Formatter not available', + ); + }); + }); + + describe('validate', () => { + it('returns dev tools version when validation succeeds', async () => { + const devToolsVersion = 'compact 0.2.0'; + + mockExec + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) // checkCompactAvailable + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) // getDevToolsVersion + .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); // checkFormatterAvailable + + const result = await validator.validate(); + + expect(result).toEqual({ devToolsVersion }); + }); + }); +}); + +describe('FormatterService', () => { + let service: FormatterService; + let mockExec: Mock; + + beforeEach(() => { + vi.clearAllMocks(); + mockExec = vi.fn(); + service = new FormatterService(mockExec); + }); + + describe('format', () => { + it('constructs command for check mode', async () => { + const targets = ['security']; + const response = { stdout: 'Check complete', stderr: '' }; + + mockExec.mockResolvedValue(response); + + const result = await service.format(targets, true); + + expect(result).toEqual(response); + expect(mockExec).toHaveBeenCalledWith( + 'compact format --check "security"', + ); + }); + + it('constructs command for write mode', async () => { + const targets = ['security']; + const response = { stdout: 'Format complete', stderr: '' }; + + mockExec.mockResolvedValue(response); + + await service.format(targets, false); + + expect(mockExec).toHaveBeenCalledWith('compact format "security"'); + }); + + it('constructs command without targets', async () => { + mockExec.mockResolvedValue({ stdout: '', stderr: '' }); + + await service.format([], false); + + expect(mockExec).toHaveBeenCalledWith('compact format'); + }); + + it('constructs command with multiple targets', async () => { + const targets = ['src/contracts', 'src/utils']; + mockExec.mockResolvedValue({ stdout: '', stderr: '' }); + + await service.format(targets, false); + + expect(mockExec).toHaveBeenCalledWith( + 'compact format "src/contracts" "src/utils"', + ); + }); + + it('throws FormatterError on failure', async () => { + mockExec.mockRejectedValue(new Error('Format failed')); + + await expect(service.format(['security'], false)).rejects.toThrow( + FormatterError, + ); + }); + }); + + describe('createError', () => { + it('creates FormatterError', () => { + const message = 'Failed to format'; + const error = service['createError'](message); + + expect(error).toBeInstanceOf(FormatterError); + expect(error.message).toBe(message); + }); + }); +}); + +describe('FormatterUIService', () => { + let mockSpinner: any; + + beforeEach(() => { + vi.clearAllMocks(); + mockSpinner = { + info: vi.fn(), + succeed: vi.fn(), + fail: vi.fn(), + }; + vi.mocked(ora).mockReturnValue(mockSpinner); + }); + + describe('displayEnvInfo', () => { + it('displays environment information', () => { + const devToolsVersion = 'compact 0.2.0'; + const targetDir = 'security'; + + FormatterUIService.displayEnvInfo(devToolsVersion, targetDir); + + expect(mockSpinner.info).toHaveBeenCalledWith( + '[FORMAT] TARGET_DIR: security', + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[FORMAT] Compact developer tools: compact 0.2.0', + ); + }); + }); +}); + +describe('CompactFormatter', () => { + let formatter: CompactFormatter; + let mockExec: Mock; + + beforeEach(() => { + vi.clearAllMocks(); + mockExec = vi.fn(); + }); + + describe('constructor', () => { + it('creates instance with default parameters', () => { + formatter = new CompactFormatter(); + + expect(formatter.testCheckMode).toBe(true); + expect(formatter.testSpecificFiles).toEqual([]); + }); + + it('creates instance with parameters', () => { + const checkMode = false; + const specificFiles = ['Token.compact']; + const targetDir = 'security'; + + formatter = new CompactFormatter( + checkMode, + specificFiles, + targetDir, + mockExec, + ); + + expect(formatter.testCheckMode).toBe(checkMode); + expect(formatter.testSpecificFiles).toEqual(specificFiles); + }); + }); + + describe('fromArgs', () => { + it('parses check mode', () => { + formatter = CompactFormatter.fromArgs(['--check']); + + expect(formatter.testCheckMode).toBe(true); + }); + + it('parses specific files', () => { + const args = ['Token.compact', 'AccessControl.compact']; + formatter = CompactFormatter.fromArgs(args); + + expect(formatter.testSpecificFiles).toEqual(args); + }); + + it('parses directory and check mode', () => { + formatter = CompactFormatter.fromArgs(['--dir', 'security', '--check']); + + expect(formatter.testCheckMode).toBe(true); + }); + }); + + describe('validateEnvironment', () => { + it('validates environment successfully', async () => { + const devToolsVersion = 'compact 0.2.0'; + + mockExec + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); + + const displaySpy = vi + .spyOn(FormatterUIService, 'displayEnvInfo') + .mockImplementation(() => {}); + + formatter = new CompactFormatter(false, [], 'security', mockExec); + + await formatter.validateEnvironment(); + + expect(displaySpy).toHaveBeenCalledWith(devToolsVersion, 'security'); + displaySpy.mockRestore(); + }); + }); + + describe('format', () => { + it('formats specific files', async () => { + const specificFiles = ['Token.compact']; + formatter = new CompactFormatter( + false, + specificFiles, + undefined, + mockExec, + ); + + // Mock environment validation + mockExec + .mockResolvedValueOnce({ stdout: 'compact 0.2.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: 'compact 0.2.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }) + // Mock format command + .mockResolvedValueOnce({ stdout: 'Formatted', stderr: '' }); + + const displaySpy = vi + .spyOn(FormatterUIService, 'displayEnvInfo') + .mockImplementation(() => {}); + + await formatter.format(); + + // Should call format with the specific file path + expect(mockExec).toHaveBeenCalledWith( + `compact format "${join(SRC_DIR, 'Token.compact')}"`, + ); + displaySpy.mockRestore(); + }); + }); +}); diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts index 35ea17c2..26338f81 100644 --- a/compact/test/runCompiler.test.ts +++ b/compact/test/runCompiler.test.ts @@ -1,21 +1,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BaseErrorHandler } from '../src/BaseServices.js'; import { CompactCompiler } from '../src/Compiler.js'; import { CompactCliNotFoundError, CompilationError, - DirectoryNotFoundError, isPromisifiedChildProcessError, - type PromisifiedChildProcessError, } from '../src/types/errors.js'; -// Mock CompactCompiler +// Mock dependencies vi.mock('../src/Compiler.js', () => ({ CompactCompiler: { fromArgs: vi.fn(), }, })); -// Mock error utilities +vi.mock('../src/BaseServices.js', () => ({ + BaseErrorHandler: { + handleCommonErrors: vi.fn(), + handleUnexpectedError: vi.fn(), + }, +})); + vi.mock('../src/types/errors.js', async () => { const actual = await vi.importActual('../src/types/errors.js'); return { @@ -55,6 +60,8 @@ const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); describe('runCompiler CLI', () => { let mockCompile: ReturnType; let mockFromArgs: ReturnType; + let mockHandleCommonErrors: ReturnType; + let mockHandleUnexpectedError: ReturnType; let originalArgv: string[]; beforeEach(() => { @@ -66,6 +73,10 @@ describe('runCompiler CLI', () => { mockCompile = vi.fn(); mockFromArgs = vi.mocked(CompactCompiler.fromArgs); + mockHandleCommonErrors = vi.mocked(BaseErrorHandler.handleCommonErrors); + mockHandleUnexpectedError = vi.mocked( + BaseErrorHandler.handleUnexpectedError, + ); // Mock CompactCompiler instance mockFromArgs.mockReturnValue({ @@ -86,306 +97,211 @@ describe('runCompiler CLI', () => { }); describe('successful compilation', () => { - it('should compile successfully with no arguments', async () => { + it('compiles successfully with no arguments', async () => { + const testData = { + expectedArgs: [], + }; + mockCompile.mockResolvedValue(undefined); // Import and run the CLI await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([]); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); expect(mockCompile).toHaveBeenCalled(); expect(mockExit).not.toHaveBeenCalled(); }); - it('should compile successfully with arguments', async () => { - process.argv = [ - 'node', - 'runCompiler.js', - '--dir', - 'security', - '--skip-zk', - ]; + it('compiles successfully with arguments', async () => { + const testData = { + args: ['--dir', 'security', '--skip-zk'], + processArgv: [ + 'node', + 'runCompiler.js', + '--dir', + 'security', + '--skip-zk', + ], + }; + + process.argv = testData.processArgv; mockCompile.mockResolvedValue(undefined); await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'security', - '--skip-zk', - ]); + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); expect(mockCompile).toHaveBeenCalled(); expect(mockExit).not.toHaveBeenCalled(); }); }); - describe('error handling', () => { - it('should handle CompactCliNotFoundError with installation instructions', async () => { - const error = new CompactCliNotFoundError('CLI not found'); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: CLI not found', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - "[COMPILE] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh", - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); + describe('error handling delegation', () => { + it('delegates to BaseErrorHandler.handleCommonErrors first', async () => { + const testData = { + error: new CompactCliNotFoundError('CLI not found'), + operation: 'COMPILE', + }; - it('should handle DirectoryNotFoundError with helpful message', async () => { - const error = new DirectoryNotFoundError( - 'Directory not found', - 'src/nonexistent', - ); - mockCompile.mockRejectedValue(error); + mockHandleCommonErrors.mockReturnValue(true); // Indicates error was handled + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: Directory not found', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nAvailable directories:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir access # Compile access control contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir archive # Compile archive contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir security # Compile security contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir token # Compile token contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir utils # Compile utility contracts', + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation, ); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should handle CompilationError with file context and cause', async () => { - const mockIsPromisifiedChildProcessError = vi.mocked( - isPromisifiedChildProcessError, - ); - - const childProcessError = { - message: 'Syntax error', - stdout: 'some output', - stderr: 'error details', + it('handles compiler-specific errors when BaseErrorHandler returns false', async () => { + const testData = { + error: new CompilationError('Compilation failed', 'MyToken.compact'), + expectedMessage: + '[COMPILE] Compilation failed for file: MyToken.compact', }; - // Return true for this specific error - mockIsPromisifiedChildProcessError.mockImplementation( - (err) => err === childProcessError, - ); - - const error = new CompilationError( - 'Compilation failed', - 'MyToken.compact', - childProcessError, - ); - mockCompile.mockRejectedValue(error); + mockHandleCommonErrors.mockReturnValue(false); // Not handled by base + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Compilation failed for file: MyToken.compact', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ` Additional error details: ${(error.cause as PromisifiedChildProcessError).stderr}`, - ); + expect(mockHandleCommonErrors).toHaveBeenCalled(); + expect(mockSpinner.fail).toHaveBeenCalledWith(testData.expectedMessage); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should handle CompilationError with unknown file', async () => { - const error = new CompilationError('Compilation failed'); - mockCompile.mockRejectedValue(error); + it('handles CompilationError with unknown file', async () => { + const testData = { + error: new CompilationError('Compilation failed', ''), + expectedMessage: '[COMPILE] Compilation failed for file: unknown', + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Compilation failed for file: unknown', - ); - expect(mockExit).toHaveBeenCalledWith(1); + expect(mockSpinner.fail).toHaveBeenCalledWith(testData.expectedMessage); }); - it('should handle argument parsing errors', async () => { - const error = new Error('--dir flag requires a directory name'); - mockFromArgs.mockImplementation(() => { - throw error; - }); + it('shows usage help for argument parsing errors', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: --dir flag requires a directory name', - ); expect(mockConsoleLog).toHaveBeenCalledWith( '\nUsage: compact-compiler [options]', ); + expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should handle unexpected errors', async () => { - const msg = 'Something unexpected happened'; - const error = new Error(msg); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - `[COMPILE] Unexpected error: ${msg}`, - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nIf this error persists, please check:', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Compact CLI is installed and in PATH', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Source files exist and are readable', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Specified Compact version exists', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • File system permissions are correct', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); + it('delegates unexpected errors to BaseErrorHandler', async () => { + const testData = { + error: new Error('Unexpected error'), + operation: 'COMPILE', + }; - it('should handle non-Error exceptions', async () => { - const msg = 'String error'; - mockCompile.mockRejectedValue(msg); + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - `[COMPILE] Unexpected error: ${msg}`, + expect(mockHandleUnexpectedError).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation, ); - expect(mockExit).toHaveBeenCalledWith(1); }); }); - describe('environment validation errors', () => { - it('should handle promisified child process errors', async () => { - const mockIsPromisifiedChildProcessError = vi.mocked( - isPromisifiedChildProcessError, - ); - - const error = { - message: 'Command failed', - stdout: 'some output', - stderr: 'error details', + describe('CompilationError handling', () => { + it('displays stderr output when available', async () => { + const testData = { + execError: { + stderr: 'Detailed error output', + stdout: 'some output', + }, }; - // Return true for this specific error - mockIsPromisifiedChildProcessError.mockImplementation( - (err) => err === error, + const compilationError = new CompilationError( + 'Compilation failed', + 'MyToken.compact', + testData.execError, ); - mockCompile.mockRejectedValue(error); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockCompile.mockRejectedValue(compilationError); await import('../src/runCompiler.js'); - expect(mockIsPromisifiedChildProcessError).toHaveBeenCalledWith(error); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Environment validation failed: Command failed', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nTroubleshooting:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Check that Compact CLI is installed and in PATH', - ); expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Verify the specified Compact version exists', + expect.stringContaining( + 'Additional error details: Detailed error output', + ), ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Ensure you have proper permissions', - ); - expect(mockExit).toHaveBeenCalledWith(1); }); - }); - describe('usage help', () => { - it('should show complete usage help for argument parsing errors', async () => { - const error = new Error('--dir flag requires a directory name'); - mockFromArgs.mockImplementation(() => { - throw error; - }); + it('skips stderr output when it contains stdout/stderr keywords', async () => { + const testData = { + execError: { + stderr: 'Error: stdout and stderr already displayed', + stdout: 'some output', + }, + }; + + const compilationError = new CompilationError( + 'Compilation failed', + 'MyToken.compact', + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockCompile.mockRejectedValue(compilationError); await import('../src/runCompiler.js'); - // Verify all sections of help are shown - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nUsage: compact-compiler [options]', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir Compile specific directory (access, archive, security, token, utils)', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --skip-zk Skip zero-knowledge proof generation', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' + Use specific toolchain version (e.g., +0.25.0)', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nExamples:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler # Compile all files', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --dir security # Compile security directory', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --dir access --skip-zk # Compile access with flags', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --skip-zk +0.25.0 # Use specific version', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nTurbo integration:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' turbo compact # Full build', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' turbo compact:security -- --skip-zk # Directory with flags', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' SKIP_ZK=true turbo compact # Environment variables', + expect(mockConsoleLog).not.toHaveBeenCalledWith( + expect.stringContaining('Additional error details'), ); }); }); - describe('directory error help', () => { - it('should show all available directories', async () => { - const error = new DirectoryNotFoundError( - 'Directory not found', - 'src/invalid', - ); - mockCompile.mockRejectedValue(error); + describe('argument parsing error handling', () => { + it('shows complete usage help', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + expectedSections: [ + '\nUsage: compact-compiler [options]', + '\nOptions:', + ' --dir Compile specific directory (access, archive, security, token, utils)', + ' --skip-zk Skip zero-knowledge proof generation', + ' + Use specific toolchain version (e.g., +0.25.0)', + '\nExamples:', + ' compact-compiler # Compile all files', + ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', + '\nTurbo integration:', + ' turbo compact # Full build', + ], + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockConsoleLog).toHaveBeenCalledWith('\nAvailable directories:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir access # Compile access control contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir archive # Compile archive contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir security # Compile security contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir token # Compile token contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir utils # Compile utility contracts', - ); + testData.expectedSections.forEach((section) => { + expect(mockConsoleLog).toHaveBeenCalledWith(section); + }); }); }); @@ -394,85 +310,101 @@ describe('runCompiler CLI', () => { mockCompile.mockResolvedValue(undefined); }); - it('should handle turbo compact', async () => { - process.argv = ['node', 'runCompiler.js']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([]); - }); + it('handles turbo compact', async () => { + const testData = { + processArgv: ['node', 'runCompiler.js'], + expectedArgs: [], + }; - it('should handle turbo compact:security', async () => { - process.argv = ['node', 'runCompiler.js', '--dir', 'security']; + process.argv = testData.processArgv; await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith(['--dir', 'security']); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); }); - it('should handle turbo compact:access -- --skip-zk', async () => { - process.argv = ['node', 'runCompiler.js', '--dir', 'access', '--skip-zk']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'access', - '--skip-zk', - ]); - }); + it('handles turbo compact:security', async () => { + const testData = { + processArgv: ['node', 'runCompiler.js', '--dir', 'security'], + expectedArgs: ['--dir', 'security'], + }; - it('should handle version specification', async () => { - process.argv = ['node', 'runCompiler.js', '+0.25.0', '--skip-zk']; + process.argv = testData.processArgv; await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith(['+0.25.0', '--skip-zk']); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); }); - it('should handle complex command', async () => { - process.argv = [ - 'node', - 'runCompiler.js', - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.25.0', - ]; + it('handles complex command with multiple flags', async () => { + const testData = { + processArgv: [ + 'node', + 'runCompiler.js', + '--dir', + 'security', + '--skip-zk', + '--verbose', + '+0.25.0', + ], + expectedArgs: [ + '--dir', + 'security', + '--skip-zk', + '--verbose', + '+0.25.0', + ], + }; + + process.argv = testData.processArgv; await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.25.0', - ]); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); }); }); describe('integration with CompactCompiler', () => { - it('should pass arguments correctly to CompactCompiler.fromArgs', async () => { - const args = ['--dir', 'token', '--skip-zk', '+0.25.0']; - process.argv = ['node', 'runCompiler.js', ...args]; + it('passes arguments correctly to CompactCompiler.fromArgs', async () => { + const testData = { + args: ['--dir', 'token', '--skip-zk', '+0.25.0'], + processArgv: [ + 'node', + 'runCompiler.js', + '--dir', + 'token', + '--skip-zk', + '+0.25.0', + ], + }; + + process.argv = testData.processArgv; mockCompile.mockResolvedValue(undefined); await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith(args); + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); expect(mockFromArgs).toHaveBeenCalledTimes(1); expect(mockCompile).toHaveBeenCalledTimes(1); }); - it('should handle empty arguments', async () => { - process.argv = ['node', 'runCompiler.js']; - mockCompile.mockResolvedValue(undefined); + it('handles fromArgs throwing errors', async () => { + const testData = { + error: new Error('Invalid arguments'), + }; + + mockFromArgs.mockImplementation(() => { + throw testData.error; + }); await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([]); + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), + 'COMPILE', + ); + expect(mockExit).toHaveBeenCalledWith(1); }); }); }); diff --git a/compact/test/runFormatter.test.ts b/compact/test/runFormatter.test.ts new file mode 100644 index 00000000..2b0c0fdf --- /dev/null +++ b/compact/test/runFormatter.test.ts @@ -0,0 +1,492 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BaseErrorHandler } from '../src/BaseServices.js'; +import { CompactFormatter } from '../src/Formatter.js'; +import { + FormatterError, + FormatterNotAvailableError, + isPromisifiedChildProcessError, +} from '../src/types/errors.js'; + +// Mock dependencies +vi.mock('../src/Formatter.js', () => ({ + CompactFormatter: { + fromArgs: vi.fn(), + }, +})); + +vi.mock('../src/BaseServices.js', () => ({ + BaseErrorHandler: { + handleCommonErrors: vi.fn(), + handleUnexpectedError: vi.fn(), + }, +})); + +vi.mock('../src/types/errors.js', async () => { + const actual = await vi.importActual('../src/types/errors.js'); + return { + ...actual, + isPromisifiedChildProcessError: vi.fn(), + }; +}); + +// Mock chalk +vi.mock('chalk', () => ({ + default: { + blue: (text: string) => text, + red: (text: string) => text, + yellow: (text: string) => text, + gray: (text: string) => text, + }, +})); + +// Mock ora +const mockSpinner = { + info: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), +}; +vi.mock('ora', () => ({ + default: vi.fn(() => mockSpinner), +})); + +// Mock process.exit +const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + +// Mock console methods +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + +describe('runFormatter CLI', () => { + let mockFormat: ReturnType; + let mockFromArgs: ReturnType; + let mockHandleCommonErrors: ReturnType; + let mockHandleUnexpectedError: ReturnType; + let originalArgv: string[]; + + beforeEach(() => { + // Store original argv + originalArgv = [...process.argv]; + + vi.clearAllMocks(); + vi.resetModules(); + + mockFormat = vi.fn(); + mockFromArgs = vi.mocked(CompactFormatter.fromArgs); + mockHandleCommonErrors = vi.mocked(BaseErrorHandler.handleCommonErrors); + mockHandleUnexpectedError = vi.mocked( + BaseErrorHandler.handleUnexpectedError, + ); + + // Mock CompactFormatter instance + mockFromArgs.mockReturnValue({ + format: mockFormat, + } as any); + + // Clear all mock calls + mockSpinner.info.mockClear(); + mockSpinner.fail.mockClear(); + mockSpinner.succeed.mockClear(); + mockConsoleLog.mockClear(); + mockExit.mockClear(); + }); + + afterEach(() => { + // Restore original argv + process.argv = originalArgv; + }); + + describe('successful formatting', () => { + it('formats successfully with no arguments', async () => { + const testData = { + expectedArgs: [], + }; + + mockFormat.mockResolvedValue(undefined); + + // Import and run the CLI + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + expect(mockFormat).toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + + it('formats successfully with arguments', async () => { + const testData = { + args: ['--dir', 'security', '--check'], + processArgv: [ + 'node', + 'runFormatter.js', + '--dir', + 'security', + '--check', + ], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); + expect(mockFormat).toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + }); + + describe('error handling delegation', () => { + it('delegates to BaseErrorHandler.handleCommonErrors first', async () => { + const testData = { + error: new Error('Directory not found'), + operation: 'FORMAT', + }; + + mockHandleCommonErrors.mockReturnValue(true); // Indicates error was handled + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation, + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('handles FormatterNotAvailableError when BaseErrorHandler returns false', async () => { + const testData = { + error: new FormatterNotAvailableError('Formatter not available'), + expectedFailMessage: '[FORMAT] Error: Formatter not available', + expectedUpdateMessages: [ + '[FORMAT] Update compiler with: compact update', + '[FORMAT] Update dev tools with: compact self update', + ], + }; + + mockHandleCommonErrors.mockReturnValue(false); // Not handled by base + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockHandleCommonErrors).toHaveBeenCalled(); + expect(mockSpinner.fail).toHaveBeenCalledWith( + testData.expectedFailMessage, + ); + testData.expectedUpdateMessages.forEach((message) => { + expect(mockSpinner.info).toHaveBeenCalledWith(message); + }); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('handles FormatterError with child process details', async () => { + const testData = { + execError: { + stderr: 'Formatting error details', + stdout: 'Some output', + }, + expectedFailMessage: '[FORMAT] Formatting operation failed', + }; + + const formatterError = new FormatterError( + 'Formatting failed', + 'Token.compact', + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockFormat.mockRejectedValue(formatterError); + + await import('../src/runFormatter.js'); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + testData.expectedFailMessage, + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + ' Formatting error details', + ); + expect(mockConsoleLog).toHaveBeenCalledWith(' Some output'); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('skips stderr output when it contains compact format keyword', async () => { + const testData = { + execError: { + stderr: 'Error: compact format failed', + stdout: 'some output', + }, + }; + + const formatterError = new FormatterError( + 'Formatting failed', + undefined, + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockFormat.mockRejectedValue(formatterError); + + await import('../src/runFormatter.js'); + + expect(mockConsoleLog).not.toHaveBeenCalledWith( + expect.stringContaining('Error: compact format failed'), + ); + expect(mockConsoleLog).toHaveBeenCalledWith(' some output'); + }); + + it('shows usage help for argument parsing errors', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '\nUsage: compact-formatter [options] [files...]', + ); + expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('delegates unexpected errors to BaseErrorHandler', async () => { + const testData = { + error: new Error('Unexpected error'), + operation: 'FORMAT', + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockHandleUnexpectedError).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation, + ); + }); + }); + + describe('FormatterError handling details', () => { + it('handles FormatterError without child process details', async () => { + const testData = { + error: new FormatterError('Simple formatting error'), + expectedMessage: '[FORMAT] Formatting operation failed', + }; + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockSpinner.fail).toHaveBeenCalledWith(testData.expectedMessage); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + + it('handles empty stdout and stderr gracefully', async () => { + const testData = { + execError: { + stderr: '', + stdout: '', + }, + }; + + const formatterError = new FormatterError( + 'Formatting failed', + undefined, + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockFormat.mockRejectedValue(formatterError); + + await import('../src/runFormatter.js'); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + '[FORMAT] Formatting operation failed', + ); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + }); + + describe('argument parsing error handling', () => { + it('shows complete usage help', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + expectedSections: [ + '\nUsage: compact-formatter [options] [files...]', + '\nOptions:', + ' --check Check if files are properly formatted (default)', + ' --write Write formatting changes to files', + ' --dir Format specific directory (access, archive, security, token, utils)', + '\nExamples:', + ' compact-formatter # Check all files (default)', + ' compact-formatter --write # Format all files', + ' compact-formatter --write --dir security # Format security directory', + ' compact-formatter --write f1.compact f2.compact # Format specific files', + ], + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + testData.expectedSections.forEach((section) => { + expect(mockConsoleLog).toHaveBeenCalledWith(section); + }); + }); + }); + + describe('real-world command scenarios', () => { + beforeEach(() => { + mockFormat.mockResolvedValue(undefined); + }); + + it('handles yarn format (check mode)', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js'], + expectedArgs: [], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles yarn format:fix (write mode)', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js', '--write'], + expectedArgs: ['--write'], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles directory-specific formatting', async () => { + const testData = { + processArgv: [ + 'node', + 'runFormatter.js', + '--write', + '--dir', + 'security', + ], + expectedArgs: ['--write', '--dir', 'security'], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles specific file formatting', async () => { + const testData = { + processArgv: [ + 'node', + 'runFormatter.js', + '--write', + 'Token.compact', + 'AccessControl.compact', + ], + expectedArgs: ['--write', 'Token.compact', 'AccessControl.compact'], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + }); + + describe('integration with CompactFormatter', () => { + it('passes arguments correctly to CompactFormatter.fromArgs', async () => { + const testData = { + args: ['--dir', 'contracts', '--check'], + processArgv: [ + 'node', + 'runFormatter.js', + '--dir', + 'contracts', + '--check', + ], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); + expect(mockFromArgs).toHaveBeenCalledTimes(1); + expect(mockFormat).toHaveBeenCalledTimes(1); + }); + + it('handles fromArgs throwing errors', async () => { + const testData = { + error: new Error('Invalid arguments'), + }; + + mockFromArgs.mockImplementation(() => { + throw testData.error; + }); + + await import('../src/runFormatter.js'); + + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), + 'FORMAT', + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + }); + + describe('edge cases', () => { + it('handles mixed check and write flags', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js', '--check', '--write'], + expectedArgs: ['--check', '--write'], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles empty file list', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js', '--write'], + expectedArgs: ['--write'], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + expect(mockFormat).toHaveBeenCalled(); + }); + }); +}); diff --git a/contracts/package.json b/contracts/package.json index 7112a1d0..bdd10887 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -22,6 +22,8 @@ "compact:utils": "compact-compiler --dir utils", "build": "compact-builder && tsc", "test": "compact-compiler --skip-zk && vitest run", + "format": "compact-formatter", + "format:fix": "compact-formatter --write", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, diff --git a/contracts/src/access/AccessControl.compact b/contracts/src/access/AccessControl.compact index 6e4c1792..dd618470 100644 --- a/contracts/src/access/AccessControl.compact +++ b/contracts/src/access/AccessControl.compact @@ -81,7 +81,8 @@ module AccessControl { * @type {Map} * @type {Map, Map, Boolean>} _operatorRoles  */ - export ledger _operatorRoles: Map, Map, Boolean>>; + export ledger _operatorRoles: Map, + Map, Boolean>>; /** * @description Mapping from a role identifier to an admin role identifier. @@ -94,27 +95,22 @@ module AccessControl { export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; - /** - * @description Returns `true` if `account` has been granted `roleId`. - * - * @circuitInfo k=10, rows=487 - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - The account to query. - * @return {Boolean} - Whether the account has the specified role. + /** + * @description Returns `true` if `account` has been granted `roleId`. + * + * @circuitInfo k=10, rows=487 + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - The account to query. + * @return {Boolean} - Whether the account has the specified role.   */ export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { - if ( - _operatorRoles.member(disclose(roleId)) && - _operatorRoles - .lookup(roleId) - .member(disclose(account)) - ) { - return _operatorRoles - .lookup(roleId) - .lookup(disclose(account)); - } else { - return false; + if (_operatorRoles.member(disclose(roleId)) && + _operatorRoles.lookup(roleId).member(disclose(account))) { + return _operatorRoles.lookup(roleId).lookup(disclose(account)); + } + else { + return false; } } @@ -132,7 +128,7 @@ module AccessControl { * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: Bytes<32>): [] { - _checkRole(roleId, left(ownPublicKey())); + _checkRole(roleId, left(ownPublicKey())); } /** @@ -207,26 +203,29 @@ module AccessControl { _revokeRole(roleId, account); } - /** - * @description Revokes `roleId` from the calling account. - * - * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's - * purpose is to provide a mechanism for accounts to lose their privileges - * if they are compromised (such as when a trusted device is misplaced). - * - * @circuitInfo k=10, rows=640 - * - * Requirements: - * - * - The caller must be `callerConfirmation`. - * - The caller must not be a `ContractAddress`. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. - * @return {[]} - Empty tuple. - */ - export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { - assert(callerConfirmation == left(ownPublicKey()), "AccessControl: bad confirmation"); + /** + * @description Revokes `roleId` from the calling account. + * + * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * @circuitInfo k=10, rows=640 + * + * Requirements: + * + * - The caller must be `callerConfirmation`. + * - The caller must not be a `ContractAddress`. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. + * @return {[]} - Empty tuple. + */ + export circuit renounceRole( + roleId: Bytes<32>, + callerConfirmation: Either + ): [] { + assert(callerConfirmation == left(ownPublicKey()), "AccessControl: bad confirmation"); _revokeRole(roleId, callerConfirmation); } @@ -276,22 +275,16 @@ module AccessControl { * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. */ - export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { + export circuit _unsafeGrantRole( + roleId: Bytes<32>, account: Either): Boolean { if (hasRole(roleId, account)) { return false; } if (!_operatorRoles.member(disclose(roleId))) { _operatorRoles.insert( - disclose(roleId), - default, - Boolean - >> - ); - _operatorRoles - .lookup(roleId) - .insert(disclose(account), true); + disclose(roleId), default, Boolean>>); + _operatorRoles.lookup(roleId).insert(disclose(account), true); return true; } @@ -309,14 +302,13 @@ module AccessControl { * @param {Bytes<32>} adminRole - The admin role identifier. * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { + export circuit _revokeRole( + roleId: Bytes<32>, account: Either): Boolean { if (!hasRole(roleId, account)) { return false; } - _operatorRoles - .lookup(roleId) - .insert(disclose(account), false); + _operatorRoles.lookup(roleId).insert(disclose(account), false); return true; } } diff --git a/contracts/src/access/Ownable.compact b/contracts/src/access/Ownable.compact index 7e6613fe..8eb6c79d 100644 --- a/contracts/src/access/Ownable.compact +++ b/contracts/src/access/Ownable.compact @@ -206,7 +206,8 @@ module Ownable { * @param {Either} newOwner - The new owner. * @returns {[]} Empty tuple. */ - export circuit _unsafeUncheckedTransferOwnership(newOwner: Either): [] { + export circuit _unsafeUncheckedTransferOwnership( + newOwner: Either): [] { Initializable_assertInitialized(); _owner = disclose(newOwner); } diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 10201a3d..d68d662a 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -191,11 +191,10 @@ module ZOwnablePK { Initializable_assertInitialized(); const nonce = wit_secretNonce(); - const callerAsEither = Either { - is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } - }; + const callerAsEither = + Either { is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } }; const id = _computeOwnerId(callerAsEither, nonce); assert(_ownerCommitment == _computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); } @@ -232,19 +231,10 @@ module ZOwnablePK { * after every transfer to prevent duplicate commitments given the same `id`. * @returns {Bytes<32>} The commitment derived from `id` and `counter`. */ - export circuit _computeOwnerCommitment( - id: Bytes<32>, - counter: Uint<64>, - ): Bytes<32> { + export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>,): Bytes<32> { Initializable_assertInitialized(); return persistentHash>>( - [ - id, - _instanceSalt, - counter as Field as Bytes<32>, - pad(32, "ZOwnablePK:shield:") - ] - ); + [id, _instanceSalt, counter as Field as Bytes<32>, pad(32, "ZOwnablePK:shield:")]); } /** @@ -280,9 +270,7 @@ module ZOwnablePK { * @returns {Bytes<32>} The computed owner ID. */ export pure circuit _computeOwnerId( - pk: Either, - nonce: Bytes<32> - ): Bytes<32> { + pk: Either, nonce: Bytes<32>): Bytes<32> { assert(pk.is_left, "ZOwnablePK: contract address owners are not yet supported"); return persistentHash>>([pk.left.bytes, nonce]); diff --git a/contracts/src/access/test/mocks/MockAccessControl.compact b/contracts/src/access/test/mocks/MockAccessControl.compact index 48a6ea7a..20491651 100644 --- a/contracts/src/access/test/mocks/MockAccessControl.compact +++ b/contracts/src/access/test/mocks/MockAccessControl.compact @@ -32,7 +32,8 @@ export circuit revokeRole(roleId: Bytes<32>, account: Either, callerConfirmation: Either): [] { +export circuit renounceRole( + roleId: Bytes<32>, callerConfirmation: Either): [] { AccessControl_renounceRole(roleId, callerConfirmation); } @@ -44,7 +45,8 @@ export circuit _grantRole(roleId: Bytes<32>, account: Either, account: Either): Boolean { +export circuit _unsafeGrantRole( + roleId: Bytes<32>, account: Either): Boolean { return AccessControl__unsafeGrantRole(roleId, account); } diff --git a/contracts/src/access/test/mocks/MockOwnable.compact b/contracts/src/access/test/mocks/MockOwnable.compact index 051ea24d..fc040c60 100644 --- a/contracts/src/access/test/mocks/MockOwnable.compact +++ b/contracts/src/access/test/mocks/MockOwnable.compact @@ -45,6 +45,7 @@ export circuit _transferOwnership(newOwner: Either): [] { +export circuit _unsafeUncheckedTransferOwnership( + newOwner: Either): [] { return Ownable__unsafeUncheckedTransferOwnership(newOwner); } diff --git a/contracts/src/access/test/mocks/MockZOwnablePK.compact b/contracts/src/access/test/mocks/MockZOwnablePK.compact index 51de1633..9d2df1dc 100644 --- a/contracts/src/access/test/mocks/MockZOwnablePK.compact +++ b/contracts/src/access/test/mocks/MockZOwnablePK.compact @@ -3,9 +3,11 @@ pragma language_version >= 0.17.0; import CompactStandardLibrary; + import "../../ZOwnablePK" prefix ZOwnablePK_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; + export { ZOwnablePK__ownerCommitment, ZOwnablePK__counter }; /** @@ -41,7 +43,8 @@ export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes< return ZOwnablePK__computeOwnerCommitment(id, counter); } -export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { +export pure circuit _computeOwnerId( + pk: Either, nonce: Bytes<32>): Bytes<32> { return ZOwnablePK__computeOwnerId(pk, nonce); } diff --git a/contracts/src/archive/ShieldedToken.compact b/contracts/src/archive/ShieldedToken.compact index 7c16169b..55dc4975 100644 --- a/contracts/src/archive/ShieldedToken.compact +++ b/contracts/src/archive/ShieldedToken.compact @@ -51,11 +51,11 @@ module ShieldedToken { // DO NOT USE IN PRODUCTION! * @return {[]} - Empty tuple. */ export circuit initializer( - initNonce: Bytes<32>, - name_: Maybe>, - symbol_: Maybe>, - decimals_ :Uint<8> - ): [] { + initNonce: Bytes<32>, + name_: Maybe>, + symbol_: Maybe>, + decimals_: Uint<8> + ): [] { _nonce = disclose(initNonce); _domain = pad(32, "ShieldedToken"); _name = disclose(name_); diff --git a/contracts/src/archive/test/mocks/MockShieldedToken.compact b/contracts/src/archive/test/mocks/MockShieldedToken.compact index 2f2ee0f0..698644e8 100644 --- a/contracts/src/archive/test/mocks/MockShieldedToken.compact +++ b/contracts/src/archive/test/mocks/MockShieldedToken.compact @@ -3,29 +3,17 @@ pragma language_version >= 0.17.0; import CompactStandardLibrary; + import "../../ShieldedToken" prefix ShieldedToken_; -export { - ZswapCoinPublicKey, - ContractAddress, - Either, - Maybe, - CoinInfo, - SendResult -}; - -export { - ShieldedToken__counter, - ShieldedToken__nonce, - ShieldedToken__domain -}; - -constructor( - _nonce: Bytes<32>, - _name: Maybe>, - _symbol: Maybe>, - _decimals:Uint<8> -) { +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, CoinInfo, SendResult }; + +export { ShieldedToken__counter, ShieldedToken__nonce, ShieldedToken__domain }; + +constructor(_nonce: Bytes<32>, + _name: Maybe>, + _symbol: Maybe>, + _decimals: Uint<8>) { ShieldedToken_initializer(_nonce, _name, _symbol, _decimals); } diff --git a/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts b/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts index 13f93026..6abf905d 100644 --- a/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts +++ b/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts @@ -31,7 +31,8 @@ import type { IContractSimulator } from '../types/test.js'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class ShieldedTokenSimulator - implements IContractSimulator { + implements IContractSimulator +{ /** @description The underlying contract instance managing contract logic. */ readonly contract: MockShielded; diff --git a/contracts/src/security/test/mocks/MockInitializable.compact b/contracts/src/security/test/mocks/MockInitializable.compact index 6ecca8e4..8f41320f 100644 --- a/contracts/src/security/test/mocks/MockInitializable.compact +++ b/contracts/src/security/test/mocks/MockInitializable.compact @@ -1,18 +1,19 @@ pragma language_version >= 0.17.0; import CompactStandardLibrary; + import "../../Initializable" prefix Initializable_; export { Initializable__isInitialized }; export circuit initialize(): [] { - return Initializable_initialize(); + return Initializable_initialize(); } export circuit assertInitialized(): [] { - return Initializable_assertInitialized(); + return Initializable_assertInitialized(); } export circuit assertNotInitialized(): [] { - return Initializable_assertNotInitialized(); + return Initializable_assertNotInitialized(); } diff --git a/contracts/src/security/test/mocks/MockPausable.compact b/contracts/src/security/test/mocks/MockPausable.compact index cf7a4197..850b672e 100644 --- a/contracts/src/security/test/mocks/MockPausable.compact +++ b/contracts/src/security/test/mocks/MockPausable.compact @@ -1,6 +1,7 @@ pragma language_version >= 0.17.0; import CompactStandardLibrary; + import "../../Pausable" prefix Pausable_; export { Pausable__isPaused }; diff --git a/contracts/src/token/FungibleToken.compact b/contracts/src/token/FungibleToken.compact index 0fdc4d6e..89a56ca8 100644 --- a/contracts/src/token/FungibleToken.compact +++ b/contracts/src/token/FungibleToken.compact @@ -60,7 +60,8 @@ module FungibleToken { * @type {Map>} * @type {Map, Map, Uint<128>>>} _allowances */ - export ledger _allowances: Map, Map, Uint<128>>>; + export ledger _allowances: Map, + Map, Uint<128>>>; export ledger _totalSupply: Uint<128>; @@ -80,11 +81,7 @@ module FungibleToken { * @param {Uint<8>} decimals_ - The number of decimals used to get the user representation. * @return {[]} - Empty tuple. */ - export circuit initialize( - name_: Opaque<"string">, - symbol_: Opaque<"string">, - decimals_:Uint<8> - ): [] { + export circuit initialize(name_: Opaque<"string">, symbol_: Opaque<"string">, decimals_: Uint<8>): [] { Initializable_initialize(); _name = disclose(name_); _symbol = disclose(symbol_); @@ -247,9 +244,9 @@ module FungibleToken { * @return {Uint<128>} - The `spender`'s allowance over `owner`'s tokens. */ export circuit allowance( - owner: Either, - spender: Either - ): Uint<128> { + owner: Either, + spender: Either + ): Uint<128> { Initializable_assertInitialized(); if (!_allowances.member(disclose(owner)) || !_allowances.lookup(owner).member(disclose(spender))) { return 0; @@ -305,10 +302,10 @@ module FungibleToken { * @return {Boolean} - As per the IERC20 spec, this MUST return true. */ export circuit transferFrom( - from: Either, - to: Either, - value: Uint<128> - ): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "FungibleToken: Unsafe Transfer"); return _unsafeTransferFrom(from, to, value); @@ -337,10 +334,10 @@ module FungibleToken { * @return {Boolean} - As per the IERC20 spec, this MUST return true. */ export circuit _unsafeTransferFrom( - from: Either, - to: Either, - value: Uint<128> - ): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { Initializable_assertInitialized(); const spender = left(ownPublicKey()); @@ -374,10 +371,10 @@ module FungibleToken { * @return {[]} - Empty tuple. */ export circuit _transfer( - from: Either, - to: Either, - value: Uint<128> - ): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "FungibleToken: Unsafe Transfer"); _unsafeUncheckedTransfer(from, to, value); @@ -404,10 +401,10 @@ module FungibleToken { * @return {[]} - Empty tuple. */ export circuit _unsafeUncheckedTransfer( - from: Either, - to: Either, - value: Uint<128> - ): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(from), "FungibleToken: invalid sender"); assert(!Utils_isKeyOrAddressZero(to), "FungibleToken: invalid receiver"); @@ -431,30 +428,29 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens moved from `from` to `to`. * @return {[]} - Empty tuple. */ - circuit _update( - from: Either, - to: Either, - value: Uint<128> - ): [] { + circuit _update(from: Either, + to: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); if (Utils_isKeyOrAddressZero(disclose(from))) { - // Mint - const MAX_UINT128 = 340282366920938463463374607431768211455; - assert(MAX_UINT128 - _totalSupply >= value, "FungibleToken: arithmetic overflow"); + // Mint + const MAX_UINT128 = 340282366920938463463374607431768211455; + assert(MAX_UINT128 - _totalSupply >= value, "FungibleToken: arithmetic overflow"); - _totalSupply = disclose(_totalSupply + value as Uint<128>); + _totalSupply = disclose(_totalSupply + value as Uint<128>); } else { - const fromBal = balanceOf(from); - assert(fromBal >= value, "FungibleToken: insufficient balance"); - _balances.insert(disclose(from), disclose(fromBal - value as Uint<128>)); + const fromBal = balanceOf(from); + assert(fromBal >= value, "FungibleToken: insufficient balance"); + _balances.insert(disclose(from), disclose(fromBal - value as Uint<128>)); } if (Utils_isKeyOrAddressZero(disclose(to))) { - // Burn - _totalSupply = disclose(_totalSupply - value as Uint<128>); + // Burn + _totalSupply = disclose(_totalSupply - value as Uint<128>); } else { - const toBal = balanceOf(to); - _balances.insert(disclose(to), disclose(toBal + value as Uint<128>)); + const toBal = balanceOf(to); + _balances.insert(disclose(to), disclose(toBal + value as Uint<128>)); } } @@ -478,10 +474,7 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens minted. * @return {[]} - Empty tuple. */ - export circuit _mint( - account: Either, - value: Uint<128> - ): [] { + export circuit _mint(account: Either, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(account), "FungibleToken: Unsafe Transfer"); _unsafeMint(account, value); @@ -505,10 +498,7 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens minted. * @return {[]} - Empty tuple. */ - export circuit _unsafeMint( - account: Either, - value: Uint<128> - ): [] { + export circuit _unsafeMint(account: Either, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(account), "FungibleToken: invalid receiver"); _update(burnAddress(), account, value); @@ -530,10 +520,7 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens to burn. * @return {[]} - Empty tuple. */ - export circuit _burn( - account: Either, - value: Uint<128> - ): [] { + export circuit _burn(account: Either, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(account), "FungibleToken: invalid sender"); _update(account, burnAddress(), value); @@ -558,17 +545,17 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens `spender` may spend on behalf of `owner`. * @return {[]} - Empty tuple. */ - export circuit _approve( - owner: Either, - spender: Either, - value: Uint<128> - ): [] { + export circuit _approve(owner: Either, + spender: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(owner), "FungibleToken: invalid owner"); assert(!Utils_isKeyOrAddressZero(spender), "FungibleToken: invalid spender"); if (!_allowances.member(disclose(owner))) { // If owner doesn't exist, create and insert a new sub-map directly - _allowances.insert(disclose(owner), default, Uint<128>>>); + _allowances.insert( + disclose(owner), default, Uint<128>>>); } _allowances.lookup(owner).insert(disclose(spender), disclose(value)); } @@ -590,12 +577,13 @@ module FungibleToken { * @return {[]} - Empty tuple. */ export circuit _spendAllowance( - owner: Either, - spender: Either, - value: Uint<128> - ): [] { + owner: Either, + spender: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); - assert((_allowances.member(disclose(owner)) && _allowances.lookup(owner).member(disclose(spender))), "FungibleToken: insufficient allowance"); + assert((_allowances.member(disclose(owner)) && + _allowances.lookup(owner).member(disclose(spender))), "FungibleToken: insufficient allowance"); const currentAllowance = _allowances.lookup(owner).lookup(disclose(spender)); const MAX_UINT128 = 340282366920938463463374607431768211455; diff --git a/contracts/src/token/MultiToken.compact b/contracts/src/token/MultiToken.compact index 49946bb3..b72d0dc3 100644 --- a/contracts/src/token/MultiToken.compact +++ b/contracts/src/token/MultiToken.compact @@ -76,7 +76,8 @@ module MultiToken { * @type {Map>} * @type {Map, Map, Uint<128>>>} _balances */ - export ledger _balances: Map, Map, Uint<128>>>; + export ledger _balances: Map, + Map, Uint<128>>>; /** * @description Mapping from account to operator approvals. @@ -86,7 +87,8 @@ module MultiToken { * @type {Map>} * @type {Map, Map, Boolean>>} */ - export ledger _operatorApprovals: Map, Map, Boolean>>; + export ledger _operatorApprovals: Map, + Map, Boolean>>; /** * @description Base URI for computing token URIs. @@ -172,7 +174,8 @@ module MultiToken { * caller's assets. * @return {[]} - Empty tuple. */ - export circuit setApprovalForAll(operator: Either, approved: Boolean): [] { + export circuit setApprovalForAll( + operator: Either, approved: Boolean): [] { Initializable_assertInitialized(); // TODO: Contract-to-contract calls not yet supported. @@ -194,12 +197,13 @@ module MultiToken { * @return {Boolean} - Whether or not `operator` has permission to handle `account`'s assets. */ export circuit isApprovedForAll( - account: Either, - operator: Either - ): Boolean { + account: Either, + operator: Either + ): Boolean { Initializable_assertInitialized(); - if (!_operatorApprovals.member(disclose(account)) || !_operatorApprovals.lookup(account).member(disclose(operator))) { + if (!_operatorApprovals.member(disclose(account)) || + !_operatorApprovals.lookup(account).member(disclose(operator))) { return false; } @@ -235,11 +239,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit transferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { assert(!Utils_isContractAddress(to), "MultiToken: unsafe transfer"); _unsafeTransferFrom(from, to, id, value); } @@ -270,11 +274,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _transfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { assert(!Utils_isContractAddress(to), "MultiToken: unsafe transfer"); _unsafeTransfer(from, to, id, value); } @@ -296,12 +300,11 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens to transfer. * @return {[]} - Empty tuple. */ - circuit _update( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + circuit _update(from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); if (!Utils_isKeyOrAddressZero(disclose(from))) { @@ -315,12 +318,14 @@ module MultiToken { if (!Utils_isKeyOrAddressZero(disclose(to))) { // id not initialized if (!_balances.member(disclose(id))) { - _balances.insert(disclose(id), default, Uint<128>>>); - _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); - } else { - const toBalance = balanceOf(to, id), MAX_UINT128 = 340282366920938463463374607431768211455; - assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); - _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); + _balances.insert( + disclose(id), default, Uint<128>>>); + _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); + } + else { + const toBalance = balanceOf(to, id), MAX_UINT128 = 340282366920938463463374607431768211455; + assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); + _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); } } } @@ -350,11 +355,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); // TODO: Contract-to-contract calls not yet supported. @@ -392,11 +397,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(from), "MultiToken: invalid sender"); @@ -453,7 +458,10 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`. * @return {[]} - Empty tuple. */ - export circuit _mint(to: Either, id: Uint<128>, value: Uint<128>): [] { + export circuit _mint(to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { assert(!Utils_isContractAddress(to), "MultiToken: unsafe transfer"); _unsafeMint(to, id, value); } @@ -477,7 +485,8 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`. * @return {[]} - Empty tuple. */ - export circuit _unsafeMint(to: Either, id: Uint<128>, value: Uint<128>): [] { + export circuit _unsafeMint( + to: Either, id: Uint<128>, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "MultiToken: invalid receiver"); @@ -500,7 +509,10 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens that will be destroyed from `from`. * @return {[]} - Empty tuple. */ - export circuit _burn(from: Either, id: Uint<128>, value: Uint<128>): [] { + export circuit _burn(from: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(from), "MultiToken: invalid sender"); @@ -528,15 +540,16 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _setApprovalForAll( - owner: Either, - operator: Either, - approved: Boolean - ): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(operator), "MultiToken: invalid operator"); if (!_operatorApprovals.member(disclose(owner))) { - _operatorApprovals.insert(disclose(owner), default, Boolean>>); + _operatorApprovals.insert( + disclose(owner), default, Boolean>>); } _operatorApprovals.lookup(owner).insert(disclose(operator), disclose(approved)); diff --git a/contracts/src/token/NonFungibleToken.compact b/contracts/src/token/NonFungibleToken.compact index b2456d3c..1622af47 100644 --- a/contracts/src/token/NonFungibleToken.compact +++ b/contracts/src/token/NonFungibleToken.compact @@ -83,7 +83,8 @@ module NonFungibleToken { * @type {Map>} * @type {Map, Map, Boolean>>} _operatorApprovals */ - export ledger _operatorApprovals: Map, Map, Boolean>>; + export ledger _operatorApprovals: Map, + Map, Boolean>>; /** * @description Mapping from token IDs to their metadata URIs. @@ -258,17 +259,10 @@ module NonFungibleToken { * @param {Uint<128>} tokenId - The token `to` may be permitted to transfer * @return {[]} - Empty tuple. */ - export circuit approve( - to: Either, - tokenId: Uint<128> - ): [] { + export circuit approve(to: Either, tokenId: Uint<128>): [] { Initializable_assertInitialized(); - const auth = left(ownPublicKey()); - _approve( - to, - tokenId, - auth - ); + const auth = left(ownPublicKey()); + _approve(to, tokenId, auth); } /** @@ -307,16 +301,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit setApprovalForAll( - operator: Either, - approved: Boolean - ): [] { + operator: Either, approved: Boolean): [] { Initializable_assertInitialized(); - const owner = left(ownPublicKey()); - _setApprovalForAll( - owner, - operator, - approved - ); + const owner = left(ownPublicKey()); + _setApprovalForAll(owner, operator, approved); } /** @@ -333,14 +321,16 @@ module NonFungibleToken { * @return {Boolean} - A boolean determining if `operator` is allowed to manage all of the tokens of `owner` */ export circuit isApprovedForAll( - owner: Either, - operator: Either - ): Boolean { + owner: Either, + operator: Either + ): Boolean { Initializable_assertInitialized(); - if (_operatorApprovals.member(disclose(owner)) && _operatorApprovals.lookup(owner).member(disclose(operator))) { - return _operatorApprovals.lookup(owner).lookup(disclose(operator)); - } else { - return false; + if (_operatorApprovals.member(disclose(owner)) && + _operatorApprovals.lookup(owner).member(disclose(operator))) { + return _operatorApprovals.lookup(owner).lookup(disclose(operator)); + } + else { + return false; } } @@ -368,10 +358,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit transferFrom( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "NonFungibleToken: Unsafe Transfer"); @@ -401,20 +391,16 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransferFrom( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); // Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists // (from != 0). Therefore, it is not needed to verify that the return value is not 0 here. - const auth = left(ownPublicKey()); - const previousOwner = _update( - to, - tokenId, - auth - ); + const auth = left(ownPublicKey()); + const previousOwner = _update(to, tokenId, auth); assert(previousOwner == from, "NonFungibleToken: Incorrect Owner"); } @@ -478,15 +464,15 @@ module NonFungibleToken { * @return {Boolean} - A boolean determining if `spender` may manage `tokenId` */ export circuit _isAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> - ): Boolean { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): Boolean { Initializable_assertInitialized(); - return ( - !Utils_isKeyOrAddressZero(disclose(spender)) && - (disclose(owner) == disclose(spender) || isApprovedForAll(owner, spender) || _getApproved(tokenId) == disclose(spender)) - ); + return (!Utils_isKeyOrAddressZero(disclose(spender)) && + (disclose(owner) == disclose(spender) || + isApprovedForAll(owner, spender) || + _getApproved(tokenId) == disclose(spender))); } /** @@ -508,10 +494,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _checkAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> - ): [] { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); if (!_isAuthorized(owner, spender, tokenId)) { assert(!Utils_isKeyOrAddressZero(owner), "NonFungibleToken: Nonexistent Token"); @@ -536,17 +522,16 @@ module NonFungibleToken { * @param {Either} auth - An account authorized to transfer the token * @return {Either} - Owner of the token before it was transfered */ - circuit _update( - to: Either, - tokenId: Uint<128>, - auth: Either - ): Either { + circuit _update(to: Either, + tokenId: Uint<128>, + auth: Either + ): Either { Initializable_assertInitialized(); const from = _ownerOf(tokenId); // Perform (optional) operator check if (!Utils_isKeyOrAddressZero(disclose(auth))) { - _checkAuthorized(from, auth, tokenId); + _checkAuthorized(from, auth, tokenId); } // Execute the update @@ -586,10 +571,7 @@ module NonFungibleToken { * @param {Uint<128>} tokenId - The token to transfer * @return {[]} - Empty tuple. */ - export circuit _mint( - to: Either, - tokenId: Uint<128> - ): [] { + export circuit _mint(to: Either, tokenId: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "NonFungibleToken: Unsafe Transfer"); @@ -615,10 +597,7 @@ module NonFungibleToken { * @param {Uint<128>} tokenId - The token to transfer * @return {[]} - Empty tuple. */ - export circuit _unsafeMint( - to: Either, - tokenId: Uint<128> - ): [] { + export circuit _unsafeMint(to: Either, tokenId: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); @@ -671,10 +650,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _transfer( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "NonFungibleToken: Unsafe Transfer"); @@ -704,10 +683,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransfer( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); @@ -733,11 +712,10 @@ module NonFungibleToken { * @param {Either} auth - An account authorized to operate on all tokens held by the owner the token * @return {[]} - Empty tuple. */ - export circuit _approve( - to: Either, - tokenId: Uint<128>, - auth: Either - ): [] { + export circuit _approve(to: Either, + tokenId: Uint<128>, + auth: Either + ): [] { Initializable_assertInitialized(); if (!Utils_isKeyOrAddressZero(disclose(auth))) { const owner = _requireOwned(tokenId); @@ -765,18 +743,16 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _setApprovalForAll( - owner: Either, - operator: Either, - approved: Boolean - ): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(operator), "NonFungibleToken: Invalid Operator"); if (!_operatorApprovals.member(disclose(owner))) { _operatorApprovals.insert( - disclose(owner), - default, Boolean>> - ); + disclose(owner), default, Boolean>>); } _operatorApprovals.lookup(owner).insert(disclose(operator), disclose(approved)); diff --git a/contracts/src/token/test/mocks/MockFungibleToken.compact b/contracts/src/token/test/mocks/MockFungibleToken.compact index f85d6d76..c05c8a16 100644 --- a/contracts/src/token/test/mocks/MockFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockFungibleToken.compact @@ -15,12 +15,7 @@ export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; * Otherwise, the contract will not initialize and we can test * the contract when it is not initialized properly. */ -constructor( - _name: Opaque<"string">, - _symbol: Opaque<"string">, - _decimals:Uint<8>, - init: Boolean -) { +constructor(_name: Opaque<"string">, _symbol: Opaque<"string">, _decimals: Uint<8>, init: Boolean) { if (disclose(init)) { FungibleToken_initialize(_name, _symbol, _decimals); } @@ -47,9 +42,9 @@ export circuit balanceOf(account: Either): } export circuit allowance( - owner: Either, - spender: Either -): Uint<128> { + owner: Either, + spender: Either + ): Uint<128> { return FungibleToken_allowance(owner, spender); } @@ -62,18 +57,18 @@ export circuit _unsafeTransfer(to: Either, } export circuit transferFrom( - from: Either, - to: Either, - value: Uint<128> -): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { return FungibleToken_transferFrom(from, to, value); } export circuit _unsafeTransferFrom( - from: Either, - to: Either, - value: Uint<128> -): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { return FungibleToken__unsafeTransferFrom(from, to, value); } @@ -81,55 +76,45 @@ export circuit approve(spender: Either, val return FungibleToken_approve(spender, value); } -export circuit _approve( - owner: Either, - spender: Either, - value: Uint<128> -): [] { +export circuit _approve(owner: Either, + spender: Either, + value: Uint<128> + ): [] { return FungibleToken__approve(owner, spender, value); } export circuit _transfer( - from: Either, - to: Either, - value: Uint<128> -): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { return FungibleToken__transfer(from, to, value); } export circuit _unsafeUncheckedTransfer( - from: Either, - to: Either, - value: Uint<128> -): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { return FungibleToken__unsafeUncheckedTransfer(from, to, value); } -export circuit _mint( - account: Either, - value: Uint<128> -): [] { +export circuit _mint(account: Either, value: Uint<128>): [] { return FungibleToken__mint(account, value); } -export circuit _unsafeMint( - account: Either, - value: Uint<128> -): [] { +export circuit _unsafeMint(account: Either, value: Uint<128>): [] { return FungibleToken__unsafeMint(account, value); } -export circuit _burn( - account: Either, - value: Uint<128> -): [] { +export circuit _burn(account: Either, value: Uint<128>): [] { return FungibleToken__burn(account, value); } export circuit _spendAllowance( - owner: Either, - spender: Either, - value: Uint<128> -): [] { + owner: Either, + spender: Either, + value: Uint<128> + ): [] { return FungibleToken__spendAllowance(owner, spender, value); } diff --git a/contracts/src/token/test/mocks/MockMultiToken.compact b/contracts/src/token/test/mocks/MockMultiToken.compact index 2549559c..062152c1 100644 --- a/contracts/src/token/test/mocks/MockMultiToken.compact +++ b/contracts/src/token/test/mocks/MockMultiToken.compact @@ -1,9 +1,11 @@ pragma language_version >= 0.17.0; import CompactStandardLibrary; + import "../../MultiToken" prefix MultiToken_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; + export { MultiToken__balances, MultiToken__operatorApprovals, MultiToken__uri }; /** @@ -12,9 +14,7 @@ export { MultiToken__balances, MultiToken__operatorApprovals, MultiToken__uri }; * Otherwise, the contract will not initialize and we can test * the contract when it is not initialized properly. */ -constructor( - _uri: Maybe> -) { +constructor(_uri: Maybe>) { if (disclose(_uri.is_some)) { MultiToken_initialize(_uri.value); } @@ -32,50 +32,51 @@ export circuit balanceOf(account: Either, i return MultiToken_balanceOf(account, id); } -export circuit setApprovalForAll(operator: Either, approved: Boolean): [] { +export circuit setApprovalForAll( + operator: Either, approved: Boolean): [] { return MultiToken_setApprovalForAll(operator, approved); } export circuit isApprovedForAll( - account: Either, - operator: Either -): Boolean { + account: Either, + operator: Either + ): Boolean { return MultiToken_isApprovedForAll(account, operator); } export circuit transferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken_transferFrom(from, to, id, value); } export circuit _unsafeTransferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__unsafeTransferFrom(from, to, id, value); } export circuit _transfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__transfer(from, to, id, value); } export circuit _unsafeTransfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__unsafeTransfer(from, to, id, value); } @@ -83,22 +84,29 @@ export circuit _setURI(newURI: Opaque<"string">): [] { return MultiToken__setURI(newURI); } -export circuit _mint(to: Either, id: Uint<128>, value: Uint<128>): [] { +export circuit _mint(to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__mint(to, id, value); } -export circuit _unsafeMint(to: Either, id: Uint<128>, value: Uint<128>): [] { +export circuit _unsafeMint( + to: Either, id: Uint<128>, value: Uint<128>): [] { return MultiToken__unsafeMint(to, id, value); } -export circuit _burn(from: Either, id: Uint<128>, value: Uint<128>): [] { +export circuit _burn(from: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__burn(from, id, value); } export circuit _setApprovalForAll( - owner: Either, - operator: Either, - approved: Boolean -): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { return MultiToken__setApprovalForAll(owner, operator, approved); } diff --git a/contracts/src/token/test/mocks/MockNonFungibleToken.compact b/contracts/src/token/test/mocks/MockNonFungibleToken.compact index 4b6064cb..79aacdb1 100644 --- a/contracts/src/token/test/mocks/MockNonFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockNonFungibleToken.compact @@ -12,11 +12,7 @@ export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; * Otherwise, the contract will not initialize and we can test the * contract when it is not initialized properly. */ -constructor( - _name: Opaque<"string">, - _symbol: Opaque<"string">, - init: Boolean -) { +constructor(_name: Opaque<"string">, _symbol: Opaque<"string">, init: Boolean) { if (disclose(init)) { NonFungibleToken_initialize(_name, _symbol); } @@ -42,10 +38,7 @@ export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { return NonFungibleToken_tokenURI(tokenId); } -export circuit approve( - to: Either, - tokenId: Uint<128> -): [] { +export circuit approve(to: Either, tokenId: Uint<128>): [] { return NonFungibleToken_approve(to, tokenId); } @@ -54,24 +47,22 @@ export circuit getApproved(tokenId: Uint<128>): Either, - approved: Boolean -): [] { + operator: Either, approved: Boolean): [] { return NonFungibleToken_setApprovalForAll(operator, approved); } export circuit isApprovedForAll( - owner: Either, - operator: Either -): Boolean { + owner: Either, + operator: Either + ): Boolean { return NonFungibleToken_isApprovedForAll(owner, operator); } export circuit transferFrom( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken_transferFrom(from, to, tokenId); } @@ -83,27 +74,26 @@ export circuit _ownerOf(tokenId: Uint<128>): Either, - tokenId: Uint<128>, - auth: Either -): [] { +export circuit _approve(to: Either, + tokenId: Uint<128>, + auth: Either + ): [] { return NonFungibleToken__approve(to, tokenId, auth); } export circuit _checkAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> -): [] { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__checkAuthorized(owner, spender, tokenId); } export circuit _isAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> -): Boolean { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): Boolean { return NonFungibleToken__isAuthorized(owner, spender, tokenId); } @@ -112,17 +102,14 @@ export circuit _getApproved(tokenId: Uint<128>): Either, - operator: Either, - approved: Boolean -): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { return NonFungibleToken__setApprovalForAll(owner, operator, approved); } -export circuit _mint( - to: Either, - tokenId: Uint<128> -): [] { +export circuit _mint(to: Either, tokenId: Uint<128>): [] { return NonFungibleToken__mint(to, tokenId); } @@ -131,10 +118,10 @@ export circuit _burn(tokenId: Uint<128>): [] { } export circuit _transfer( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__transfer(from, to, tokenId); } @@ -143,24 +130,21 @@ export circuit _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">): [] } export circuit _unsafeTransferFrom( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__unsafeTransferFrom(from, to, tokenId); } export circuit _unsafeTransfer( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__unsafeTransfer(from, to, tokenId); } -export circuit _unsafeMint( - to: Either, - tokenId: Uint<128> -): [] { +export circuit _unsafeMint(to: Either, tokenId: Uint<128>): [] { return NonFungibleToken__unsafeMint(to, tokenId); } diff --git a/contracts/src/token/test/simulators/MultiTokenSimulator.ts b/contracts/src/token/test/simulators/MultiTokenSimulator.ts index b15779ef..34b559ee 100644 --- a/contracts/src/token/test/simulators/MultiTokenSimulator.ts +++ b/contracts/src/token/test/simulators/MultiTokenSimulator.ts @@ -28,7 +28,8 @@ import type { IContractSimulator } from '../types/test.js'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class MultiTokenSimulator - implements IContractSimulator { + implements IContractSimulator +{ /** @description The underlying contract instance managing contract logic. */ readonly contract: MockMultiToken; diff --git a/contracts/src/utils/Utils.compact b/contracts/src/utils/Utils.compact index 80dab95f..bec4fe8d 100644 --- a/contracts/src/utils/Utils.compact +++ b/contracts/src/utils/Utils.compact @@ -21,8 +21,8 @@ module Utils { */ export pure circuit isKeyOrAddressZero(keyOrAddress: Either): Boolean { return isContractAddress(keyOrAddress) - ? default == keyOrAddress.right - : default == keyOrAddress.left; + ? default == keyOrAddress.right + : default == keyOrAddress.left; } /** @@ -45,16 +45,17 @@ module Utils { * @return {Boolean} - Returns true if `keyOrAddress` is is equal to `other`. */ export pure circuit isKeyOrAddressEqual( - keyOrAddress: Either, - other: Either - ): Boolean { + keyOrAddress: Either, + other: Either + ): Boolean { if (keyOrAddress.is_left && other.is_left) { - return keyOrAddress.left == other.left; - } else if (!keyOrAddress.is_left && !other.is_left) { - return keyOrAddress.right == other.right; - } else { - return false; - } + return keyOrAddress.left == other.left; + } else + if (!keyOrAddress.is_left && !other.is_left) { + return keyOrAddress.right == other.right; + } else { + return false; + } } /** diff --git a/contracts/src/utils/test/mocks/MockUtils.compact b/contracts/src/utils/test/mocks/MockUtils.compact index 3f3ee9d7..1e237f91 100644 --- a/contracts/src/utils/test/mocks/MockUtils.compact +++ b/contracts/src/utils/test/mocks/MockUtils.compact @@ -11,9 +11,9 @@ export pure circuit isKeyOrAddressZero(keyOrAddress: Either, - other: Either -): Boolean { + keyOrAddress: Either, + other: Either + ): Boolean { return Utils_isKeyOrAddressEqual(keyOrAddress, other); } diff --git a/package.json b/package.json index ebc3ba70..f6b6578c 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "compact": "turbo run compact --filter=@openzeppelin-compact/contracts --log-prefix=none", "build": "turbo run build --filter=!'docs' --log-prefix=none", "test": "turbo run test --filter=@openzeppelin-compact/contracts --log-prefix=none", - "fmt-and-lint": "biome check . --changed", - "fmt-and-lint:fix": "biome check . --changed --write", - "fmt-and-lint:ci": "biome ci . --changed --no-errors-on-unmatched", + "check": "biome check . --changed", + "check:fix": "biome check . --changed --write", + "check:ci": "biome ci . --changed --no-errors-on-unmatched", "types": "turbo run types --filter=!'docs'", "clean": "turbo run clean --filter=!'docs'" }, diff --git a/turbo.json b/turbo.json index b52c9c45..261221fc 100644 --- a/turbo.json +++ b/turbo.json @@ -73,6 +73,13 @@ ], "outputs": ["dist/**"] }, + "format": { + "dependsOn": ["^build"], + "cache": false + }, + "format:fix": { + "dependsOn": ["^build"] + }, "types": { "dependsOn": [ "@openzeppelin-compact/compact#build", @@ -81,9 +88,9 @@ "outputs": [], "cache": false }, - "//#fmt-and-lint": {}, - "//#fmt-and-lint:ci": {}, - "//#fmt-and-lint:fix": { + "//#check": {}, + "//#check:ci": {}, + "//#check:fix": { "cache": false }, "clean": { diff --git a/yarn.lock b/yarn.lock index 082ea687..508605f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,6 +409,7 @@ __metadata: bin: compact-builder: dist/runBuilder.js compact-compiler: dist/runCompiler.js + compact-formatter: dist/runFormatter.js languageName: unknown linkType: soft