provisioning/tools/build/validate-kcl.nu
2025-10-07 11:12:02 +01:00

456 lines
14 KiB
Plaintext

#!/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<string> {
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<string>] {
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<string>] -> 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<string>] -> 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)
}
}
}