322 lines
10 KiB
Plaintext
322 lines
10 KiB
Plaintext
|
|
#!/usr/bin/env nu
|
|||
|
|
# audit-nu-scripts.nu - Automated Nushell script auditor for 0.109+ compatibility
|
|||
|
|
# Usage: nu .claude/guidelines/nushell/audit-nu-scripts.nu [--fix] [--verbose]
|
|||
|
|
|
|||
|
|
def main [
|
|||
|
|
--fix # Automatically fix simple issues
|
|||
|
|
--verbose # Show detailed output
|
|||
|
|
--output: string = "" # Output report to file
|
|||
|
|
] {
|
|||
|
|
print_header
|
|||
|
|
|
|||
|
|
# Find all Nushell scripts
|
|||
|
|
let scripts = glob **/*.nu | where {|path| not ($path | str starts-with "target/")}
|
|||
|
|
|
|||
|
|
print $"Found ($scripts | length) Nushell script(s) to audit\n"
|
|||
|
|
|
|||
|
|
mut issues = []
|
|||
|
|
mut summary = {
|
|||
|
|
total_scripts: ($scripts | length)
|
|||
|
|
scripts_with_issues: 0
|
|||
|
|
total_issues: 0
|
|||
|
|
critical: 0
|
|||
|
|
warning: 0
|
|||
|
|
info: 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Audit each script
|
|||
|
|
for script_path in $scripts {
|
|||
|
|
let script_issues = audit_script $script_path $verbose
|
|||
|
|
|
|||
|
|
if ($script_issues | length) > 0 {
|
|||
|
|
$summary.scripts_with_issues = $summary.scripts_with_issues + 1
|
|||
|
|
$summary.total_issues = $summary.total_issues + ($script_issues | length)
|
|||
|
|
|
|||
|
|
# Count by severity
|
|||
|
|
for issue in $script_issues {
|
|||
|
|
match $issue.severity {
|
|||
|
|
"critical" => { $summary.critical = $summary.critical + 1 }
|
|||
|
|
"warning" => { $summary.warning = $summary.warning + 1 }
|
|||
|
|
"info" => { $summary.info = $summary.info + 1 }
|
|||
|
|
_ => {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Add to issues list
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
script: $script_path
|
|||
|
|
issues: $script_issues
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Print results
|
|||
|
|
print_divider
|
|||
|
|
print_summary $summary
|
|||
|
|
print_divider
|
|||
|
|
|
|||
|
|
if ($issues | length) > 0 {
|
|||
|
|
print "\n📋 DETAILED FINDINGS:\n"
|
|||
|
|
for script_result in $issues {
|
|||
|
|
print_script_issues $script_result.script $script_result.issues
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate report if requested
|
|||
|
|
if $output != "" {
|
|||
|
|
generate_report $issues $summary $output
|
|||
|
|
print $"\n✓ Report saved to: ($output)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Exit code based on critical issues
|
|||
|
|
if $summary.critical > 0 {
|
|||
|
|
exit 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Audit a single script
|
|||
|
|
def audit_script [path: string, verbose: bool] -> list {
|
|||
|
|
if $verbose {
|
|||
|
|
print $"Auditing: ($path)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mut issues = []
|
|||
|
|
|
|||
|
|
# Read script content
|
|||
|
|
let content = try {
|
|||
|
|
open --raw $path
|
|||
|
|
} catch {
|
|||
|
|
return [{
|
|||
|
|
line: 0
|
|||
|
|
severity: "critical"
|
|||
|
|
category: "file_access"
|
|||
|
|
message: $"Cannot read file: ($path)"
|
|||
|
|
suggestion: "Check file permissions"
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let lines = $content | lines
|
|||
|
|
|
|||
|
|
# Check 1: Missing shebang
|
|||
|
|
if (($lines | first) !~ "#!/usr/bin/env nu") {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: 1
|
|||
|
|
severity: "warning"
|
|||
|
|
category: "shebang"
|
|||
|
|
message: "Missing or incorrect shebang"
|
|||
|
|
suggestion: "Add: #!/usr/bin/env nu"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check 2: Old closure syntax (using $in in where/each)
|
|||
|
|
for (idx, line) in ($lines | enumerate) {
|
|||
|
|
let line_num = $idx + 1
|
|||
|
|
|
|||
|
|
# Check for old-style closures
|
|||
|
|
if ($line =~ '\| where \{ \$in') or ($line =~ '\| each \{ \$in') {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: $line_num
|
|||
|
|
severity: "warning"
|
|||
|
|
category: "closure_syntax"
|
|||
|
|
message: "Old-style closure using $in"
|
|||
|
|
suggestion: "Use explicit parameter: {|x| $x ...} instead of { $in ... }"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for missing type annotations on exported functions
|
|||
|
|
if ($line =~ 'export def .+\[') and not ($line =~ ': \w+') {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: $line_num
|
|||
|
|
severity: "info"
|
|||
|
|
category: "type_annotations"
|
|||
|
|
message: "Function parameters missing type annotations"
|
|||
|
|
suggestion: "Add type annotations: [param: type]"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for unwrap-like patterns (get without -i)
|
|||
|
|
if ($line =~ '\| get [a-zA-Z_]') and not ($line =~ 'get -i') {
|
|||
|
|
if not ($line =~ '\| get \w+ \|') {
|
|||
|
|
# Potentially unsafe get (not in pipeline continuation)
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: $line_num
|
|||
|
|
severity: "info"
|
|||
|
|
category: "null_safety"
|
|||
|
|
message: "Potentially unsafe 'get' without -i flag"
|
|||
|
|
suggestion: "Use 'get -i' for safe access or add error handling"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for hardcoded paths (common anti-pattern)
|
|||
|
|
if ($line =~ '/Users/[^/]+/') or ($line =~ 'C:\\\\') {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: $line_num
|
|||
|
|
severity: "warning"
|
|||
|
|
category: "hardcoded_paths"
|
|||
|
|
message: "Hardcoded absolute path detected"
|
|||
|
|
suggestion: "Use $env.HOME or relative paths"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for missing error handling in external commands
|
|||
|
|
if ($line =~ '\^[a-zA-Z_-]+') and not ($line =~ 'try \{') {
|
|||
|
|
# External command not wrapped in try-catch
|
|||
|
|
# Check if previous or next line has try
|
|||
|
|
let has_try = if $idx > 0 {
|
|||
|
|
($lines | get ($idx - 1) | str contains "try")
|
|||
|
|
} else {
|
|||
|
|
false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if not $has_try {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: $line_num
|
|||
|
|
severity: "info"
|
|||
|
|
category: "error_handling"
|
|||
|
|
message: "External command without error handling"
|
|||
|
|
suggestion: "Wrap in try-catch block"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for deprecated mut syntax
|
|||
|
|
if ($line =~ 'let mut ') {
|
|||
|
|
# This is actually fine in 0.109, just noting it
|
|||
|
|
# $issues = append with info if we want to track mutable usage
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for missing return type annotations
|
|||
|
|
if ($line =~ 'export def .+\]') and not ($line =~ '-> \w+') and not ($line =~ '\{$') {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: $line_num
|
|||
|
|
severity: "info"
|
|||
|
|
category: "type_annotations"
|
|||
|
|
message: "Function missing return type annotation"
|
|||
|
|
suggestion: "Add return type: ] -> type {"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check 3: No error handling in main functions
|
|||
|
|
let has_try = $content | str contains "try"
|
|||
|
|
let has_catch = $content | str contains "catch"
|
|||
|
|
if not ($has_try and $has_catch) {
|
|||
|
|
$issues = ($issues | append {
|
|||
|
|
line: 0
|
|||
|
|
severity: "info"
|
|||
|
|
category: "error_handling"
|
|||
|
|
message: "Script may lack error handling (no try-catch found)"
|
|||
|
|
suggestion: "Add try-catch blocks for robust error handling"
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$issues
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Print header
|
|||
|
|
def print_header [] {
|
|||
|
|
print ""
|
|||
|
|
print "╔════════════════════════════════════════════════════════╗"
|
|||
|
|
print "║ Nushell 0.109+ Compatibility Audit Tool ║"
|
|||
|
|
print "║ syntaxis Project - Script Quality Checker ║"
|
|||
|
|
print "╚════════════════════════════════════════════════════════╝"
|
|||
|
|
print ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Print divider
|
|||
|
|
def print_divider [] {
|
|||
|
|
print "─────────────────────────────────────────────────────────"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Print summary
|
|||
|
|
def print_summary [summary: record] {
|
|||
|
|
print "\n📊 AUDIT SUMMARY:\n"
|
|||
|
|
print $" Total scripts: ($summary.total_scripts)"
|
|||
|
|
print $" Scripts with issues: ($summary.scripts_with_issues)"
|
|||
|
|
print $" Total issues: ($summary.total_issues)"
|
|||
|
|
print ""
|
|||
|
|
print " By Severity:"
|
|||
|
|
print $" (ansi red)● Critical: ($summary.critical)(ansi reset)"
|
|||
|
|
print $" (ansi yellow)● Warnings: ($summary.warning)(ansi reset)"
|
|||
|
|
print $" (ansi cyan)● Info: ($summary.info)(ansi reset)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Print issues for a script
|
|||
|
|
def print_script_issues [script: string, issues: list] {
|
|||
|
|
print $"\n(ansi cyan)📄 ($script)(ansi reset)"
|
|||
|
|
|
|||
|
|
for issue in $issues {
|
|||
|
|
let severity_icon = match $issue.severity {
|
|||
|
|
"critical" => $"(ansi red)✗(ansi reset)"
|
|||
|
|
"warning" => $"(ansi yellow)⚠(ansi reset)"
|
|||
|
|
"info" => $"(ansi cyan)ℹ(ansi reset)"
|
|||
|
|
_ => "•"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let line_info = if $issue.line > 0 {
|
|||
|
|
$" (Line ($issue.line))"
|
|||
|
|
} else {
|
|||
|
|
""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $" ($severity_icon) [($issue.category)]($line_info): ($issue.message)"
|
|||
|
|
print $" → ($issue.suggestion)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate markdown report
|
|||
|
|
def generate_report [issues: list, summary: record, output_path: string] {
|
|||
|
|
let timestamp = date now | format date "%Y-%m-%d %H:%M:%S"
|
|||
|
|
|
|||
|
|
mut report = $"# Nushell Script Audit Report
|
|||
|
|
Generated: ($timestamp)
|
|||
|
|
|
|||
|
|
## Summary
|
|||
|
|
|
|||
|
|
- **Total Scripts**: ($summary.total_scripts)
|
|||
|
|
- **Scripts with Issues**: ($summary.scripts_with_issues)
|
|||
|
|
- **Total Issues**: ($summary.total_issues)
|
|||
|
|
- Critical: ($summary.critical)
|
|||
|
|
- Warnings: ($summary.warning)
|
|||
|
|
- Info: ($summary.info)
|
|||
|
|
|
|||
|
|
## Detailed Findings
|
|||
|
|
|
|||
|
|
"
|
|||
|
|
|
|||
|
|
for script_result in $issues {
|
|||
|
|
$report = $report + $"\n### ($script_result.script)\n\n"
|
|||
|
|
|
|||
|
|
for issue in $script_result.issues {
|
|||
|
|
let line_ref = if $issue.line > 0 {
|
|||
|
|
$" (Line ($issue.line))"
|
|||
|
|
} else {
|
|||
|
|
""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$report = $report + $"- **[($issue.severity | str upcase)] ($issue.category)**($line_ref)
|
|||
|
|
- Issue: ($issue.message)
|
|||
|
|
- Suggestion: ($issue.suggestion)
|
|||
|
|
|
|||
|
|
"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$report = $report + $"\n## Recommendations
|
|||
|
|
|
|||
|
|
1. Review all **critical** issues immediately
|
|||
|
|
2. Address **warnings** before next release
|
|||
|
|
3. Consider **info** items for code quality improvements
|
|||
|
|
4. Follow guidelines in `.claude/guidelines/nushell/NUSHELL_0.109_GUIDELINES.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
*Generated by syntaxis Nushell Audit Tool*
|
|||
|
|
"
|
|||
|
|
|
|||
|
|
$report | save --force $output_path
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Run main if executed directly
|
|||
|
|
main
|