456 lines
14 KiB
Plaintext
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)
|
|
}
|
|
}
|
|
} |