#!/usr/bin/env nu # KCL validation tool - validates and compiles KCL schemas for distribution # # Validates: # - KCL syntax and type checking # - Schema dependencies # - Configuration completeness # - Template rendering compatibility use std log def main [ --kcl-root: string = "" # Root directory for KCL files (auto-detected if empty) --output-dir: string = "dist/kcl" # Output directory for compiled schemas --validate-only: bool = false # Only validate, don't compile --format-code: bool = false # Format KCL code during validation --check-dependencies: bool = true # Check KCL module dependencies --verbose: bool = false # Enable verbose logging ] -> record { let repo_root = ($env.PWD | path dirname | path dirname | path dirname) let kcl_root = if $kcl_root == "" { # Auto-detect KCL directories find_kcl_directories $repo_root } else { [$kcl_root] } let validation_config = { kcl_directories: $kcl_root output_dir: ($output_dir | path expand) validate_only: $validate_only format_code: $format_code check_dependencies: $check_dependencies verbose: $verbose } log info $"Starting KCL validation with config: ($validation_config)" # Ensure output directory exists (if not validate-only) if not $validation_config.validate_only { mkdir ($validation_config.output_dir) } # Validate each KCL directory let validation_results = $validation_config.kcl_directories | each {|kcl_dir| validate_kcl_directory $kcl_dir $validation_config $repo_root } # Check global dependencies if requested let dependency_result = if $validation_config.check_dependencies { check_global_dependencies $validation_config.kcl_directories } else { { status: "skipped", dependencies: [] } } let summary = { total_directories: ($validation_results | length) successful_directories: ($validation_results | where status == "success" | length) failed_directories: ($validation_results | where status == "failed" | length) total_files: ($validation_results | get files_validated | math sum) total_errors: ($validation_results | get errors | flatten | length) dependency_check: $dependency_result validation_config: $validation_config results: $validation_results } if ($summary.failed_directories > 0) or ($summary.total_errors > 0) { log error $"KCL validation completed with errors: ($summary.failed_directories) failed directories, ($summary.total_errors) total errors" exit 1 } else { log info $"KCL validation completed successfully - ($summary.total_files) files validated across ($summary.total_directories) directories" } return $summary } # Find all KCL directories in the repository def find_kcl_directories [repo_root: string] -> list { log info "Auto-detecting KCL directories..." # Look for directories named 'kcl' or containing .k files let kcl_dirs = (find $repo_root -type d -name "kcl" | where {|dir| # Check if directory contains .k files let k_files = (find $dir -name "*.k" -type f) ($k_files | length) > 0 }) if ($kcl_dirs | length) == 0 { log warning "No KCL directories found" return [] } log info $"Found ($kcl_dirs | length) KCL directories: ($kcl_dirs | str join ', ')" return $kcl_dirs } # Validate a single KCL directory def validate_kcl_directory [ kcl_dir: string validation_config: record repo_root: string ] -> record { log info $"Validating KCL directory: ($kcl_dir)" if not ($kcl_dir | path exists) { log warning $"KCL directory does not exist: ($kcl_dir)" return { directory: $kcl_dir status: "skipped" reason: "directory not found" files_validated: 0 errors: [] } } let start_time = (date now) let mut validation_errors = [] let mut files_validated = 0 try { cd $kcl_dir # Find all KCL files let kcl_files = (find . -name "*.k" -type f) if ($kcl_files | length) == 0 { log warning $"No KCL files found in directory: ($kcl_dir)" return { directory: $kcl_dir status: "skipped" reason: "no KCL files" files_validated: 0 errors: [] } } log info $"Found ($kcl_files | length) KCL files to validate" # Validate each KCL file for file in $kcl_files { let file_result = validate_kcl_file $file $validation_config if $file_result.status == "success" { $files_validated = $files_validated + 1 } else { $validation_errors = ($validation_errors | append $file_result.errors) } } # Format code if requested if $validation_config.format_code { format_kcl_files $kcl_files } # Compile KCL module if not validate-only if not $validation_config.validate_only { let compilation_result = compile_kcl_module $kcl_dir $validation_config if $compilation_result.status == "failed" { $validation_errors = ($validation_errors | append $compilation_result.errors) } } let status = if ($validation_errors | length) > 0 { "failed" } else { "success" } { directory: $kcl_dir status: $status files_validated: $files_validated total_files: ($kcl_files | length) errors: $validation_errors duration: ((date now) - $start_time) } } catch {|err| log error $"Failed to validate KCL directory ($kcl_dir): ($err.msg)" { directory: $kcl_dir status: "failed" reason: $err.msg files_validated: $files_validated errors: [{ file: $kcl_dir, error: $err.msg, type: "directory_error" }] duration: ((date now) - $start_time) } } } # Validate a single KCL file def validate_kcl_file [ file: string validation_config: record ] -> record { if $validation_config.verbose { log info $"Validating KCL file: ($file)" } try { # Use kcl tool to validate syntax let validation_output = (kcl vet $file | complete) if $validation_output.exit_code != 0 { let error_msg = $validation_output.stderr log error $"KCL validation failed for ($file): ($error_msg)" return { file: $file status: "failed" errors: [{ file: $file, error: $error_msg, type: "validation_error" }] } } # Additional checks for common patterns let content = (open $file) let additional_checks = check_kcl_content $content $file if ($additional_checks.errors | length) > 0 { return { file: $file status: "failed" errors: $additional_checks.errors } } return { file: $file status: "success" errors: [] } } catch {|err| log error $"Failed to validate KCL file ($file): ($err.msg)" return { file: $file status: "failed" errors: [{ file: $file, error: $err.msg, type: "validation_exception" }] } } } # Check KCL content for common issues def check_kcl_content [content: string, file: string] -> record { let mut errors = [] # Check for TODO/FIXME comments let todo_lines = ($content | lines | enumerate | where {|line| ($line.item | str contains "TODO") or ($line.item | str contains "FIXME") }) if ($todo_lines | length) > 0 { $errors = ($errors | append { file: $file error: $"Found ($todo_lines | length) TODO/FIXME comments" type: "content_warning" details: $todo_lines }) } # Check for missing documentation on schema definitions let schema_lines = ($content | lines | enumerate | where {|line| $line.item =~ "schema\\s+\\w+" }) for schema_line in $schema_lines { let line_num = $schema_line.index # Check if there's a comment in the previous 3 lines let doc_check = ($content | lines | range (($line_num - 3)..$line_num) | any {|line| $line | str starts-with "#" }) if not $doc_check { $errors = ($errors | append { file: $file error: $"Schema at line ($line_num + 1) lacks documentation" type: "documentation_warning" }) } } return { errors: $errors } } # Format KCL files def format_kcl_files [files: list] { log info "Formatting KCL files..." for file in $files { try { kcl fmt $file log info $"Formatted: ($file)" } catch {|err| log warning $"Failed to format ($file): ($err.msg)" } } } # Compile KCL module def compile_kcl_module [ kcl_dir: string validation_config: record ] -> record { log info $"Compiling KCL module: ($kcl_dir)" try { # Determine module name from directory let module_name = ($kcl_dir | path basename) let output_path = ($validation_config.output_dir | path join $module_name) # Ensure output directory exists mkdir ($output_path | path dirname) # Copy KCL files to output directory cp -r $kcl_dir $output_path # Compile using kcl tool cd $output_path let compile_result = (kcl run . | complete) if $compile_result.exit_code != 0 { let error_msg = $compile_result.stderr log error $"KCL compilation failed for ($kcl_dir): ($error_msg)" return { status: "failed" module: $module_name errors: [{ module: $module_name, error: $error_msg, type: "compilation_error" }] } } log info $"Successfully compiled KCL module: ($module_name)" return { status: "success" module: $module_name output_path: $output_path errors: [] } } catch {|err| log error $"Failed to compile KCL module ($kcl_dir): ($err.msg)" return { status: "failed" module: ($kcl_dir | path basename) errors: [{ module: ($kcl_dir | path basename), error: $err.msg, type: "compilation_exception" }] } } } # Check global dependencies across all KCL modules def check_global_dependencies [kcl_directories: list] -> record { log info "Checking global KCL dependencies..." let mut all_dependencies = [] let mut dependency_errors = [] for kcl_dir in $kcl_directories { try { let deps = extract_dependencies $kcl_dir $all_dependencies = ($all_dependencies | append $deps) } catch {|err| $dependency_errors = ($dependency_errors | append { directory: $kcl_dir error: $err.msg }) } } # Check for missing dependencies let unique_deps = ($all_dependencies | flatten | uniq) let missing_deps = $unique_deps | where {|dep| not (check_dependency_exists $dep $kcl_directories) } if ($missing_deps | length) > 0 { $dependency_errors = ($dependency_errors | append ($missing_deps | each {|dep| { dependency: $dep, error: "dependency not found", type: "missing_dependency" } })) } return { status: (if ($dependency_errors | length) > 0 { "failed" } else { "success" }) dependencies: $unique_deps missing_dependencies: $missing_deps errors: $dependency_errors } } # Extract dependencies from a KCL directory def extract_dependencies [kcl_dir: string] -> list { let kcl_mod_file = ($kcl_dir | path join "kcl.mod") if not ($kcl_mod_file | path exists) { return [] } # Parse kcl.mod file for dependencies let mod_content = (open $kcl_mod_file) # Extract import statements (simplified - would need proper parsing) let imports = ($mod_content | lines | where {|line| $line | str starts-with "import" } | each {|line| $line | str replace "import " "" | str trim }) return $imports } # Check if a dependency exists in the available directories def check_dependency_exists [dependency: string, kcl_directories: list] -> bool { # Simple check - in a real implementation, this would be more sophisticated let dep_found = $kcl_directories | any {|dir| let module_name = ($dir | path basename) $module_name == $dependency } return $dep_found } # Show KCL environment info def "main info" [] { let kcl_info = try { { kcl_version: (kcl version) available: true } } catch { { kcl_version: "not available" available: false } } let repo_root = ($env.PWD | path dirname | path dirname | path dirname) let kcl_dirs = (find_kcl_directories $repo_root) { kcl_tool: $kcl_info repository_root: $repo_root kcl_directories: $kcl_dirs total_kcl_files: ($kcl_dirs | each {|dir| find $dir -name "*.k" -type f | length } | math sum) } } # List all KCL files in the repository def "main list" [] { let repo_root = ($env.PWD | path dirname | path dirname | path dirname) let kcl_dirs = (find_kcl_directories $repo_root) $kcl_dirs | each {|dir| let files = (find $dir -name "*.k" -type f) { directory: $dir module_name: ($dir | path basename) file_count: ($files | length) files: $files has_mod_file: (($dir | path join "kcl.mod") | path exists) } } }