|
## Bash/Shell validator for Validatrix.
|
|
##
|
|
## Validates Bash/shell scripts, checking structural balance and common issues.
|
|
|
|
import std/[json, strformat, tables]
|
|
{.warning[UnusedImport]:off.}
|
|
import ../core/types, ../core/tokenizerbase, ../core/validatorbase
|
|
import ../tokenizers/bash_tokenizer
|
|
|
|
type
|
|
BashValidator* = ref object of ValidatorBase
|
|
## Validator for Bash/shell scripts.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Construction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc newBashValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): BashValidator =
|
|
## Create a new Bash validator.
|
|
result = BashValidator(
|
|
source: source,
|
|
sourcePath: sourcePath,
|
|
options: options,
|
|
result: newValidationResult(),
|
|
startTime: epochTime()
|
|
)
|
|
result.result.flavor = options.flavor
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tokenizer creation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
method createTokenizer*(self: BashValidator): TokenizerBase =
|
|
## Create the Bash tokenizer.
|
|
result = newBashTokenizer(self.source, self.options)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Structural analysis
|
|
# ---------------------------------------------------------------------------
|
|
|
|
method analyzeTokens*(self: BashValidator) =
|
|
## Analyze Bash-specific patterns.
|
|
when defined(validatrixDebug):
|
|
if self.options.debugMode:
|
|
debugEnter("BASH_ANALYZE", "Analyzing Bash structure")
|
|
|
|
procCall ValidatorBase(self).analyzeTokens()
|
|
|
|
let tokens = self.tokenizer.tokens
|
|
|
|
# Check for unclosed if/fi, do/done, case/esac
|
|
var keywords: seq[string] = @[]
|
|
for tok in tokens:
|
|
if tok.kind == tkKeyword:
|
|
keywords.add(tok.value)
|
|
if tok.value in ["if", "case", "for", "while", "until"]:
|
|
self.result.moduleInfo.functions.add(FunctionInfo(
|
|
name: tok.value & "_block",
|
|
kind: "block",
|
|
position: tok.position
|
|
))
|
|
elif tok.kind == tkComment:
|
|
self.result.moduleInfo.comments += 1
|
|
|
|
# Check for fi/done/esac matching
|
|
var ifCount, forCount, caseCount = 0
|
|
var fiCount, doneCount, esacCount = 0
|
|
for kw in keywords:
|
|
case kw
|
|
of "if": ifCount += 1
|
|
of "fi": fiCount += 1
|
|
of "for", "while", "until": forCount += 1
|
|
of "done": doneCount += 1
|
|
of "case": caseCount += 1
|
|
of "esac": esacCount += 1
|
|
else: discard
|
|
|
|
if ifCount != fiCount:
|
|
if ifCount > fiCount:
|
|
self.result.errors.add(newValidationError(
|
|
esError, &"Unclosed 'if' block: {ifCount} 'if' but {fiCount} 'fi'",
|
|
ErrUnclosedBlock,
|
|
newSourcePosition(1, 1, 0),
|
|
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
|
|
hint = "Add missing 'fi' to close the if block"
|
|
))
|
|
|
|
if forCount != doneCount:
|
|
if forCount > doneCount:
|
|
self.result.errors.add(newValidationError(
|
|
esError, &"Unclosed loop: {forCount} loop starters but {doneCount} 'done'",
|
|
ErrUnclosedBlock,
|
|
newSourcePosition(1, 1, 0),
|
|
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
|
|
hint = "Add missing 'done' to close the loop"
|
|
))
|
|
|
|
if caseCount != esacCount:
|
|
if caseCount > esacCount:
|
|
self.result.errors.add(newValidationError(
|
|
esError, &"Unclosed 'case' block: {caseCount} 'case' but {esacCount} 'esac'",
|
|
ErrUnclosedBlock,
|
|
newSourcePosition(1, 1, 0),
|
|
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
|
|
hint = "Add missing 'esac' to close the case block"
|
|
))
|
|
|
|
when defined(validatrixDebug):
|
|
if self.options.debugMode:
|
|
debugLog("BASH_ANALYZE", &"if/fi: {ifCount}/{fiCount}, for/done: {forCount}/{doneCount}, case/esac: {caseCount}/{esacCount}")
|
|
debugLeave("BASH_ANALYZE")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc init*() =
|
|
## Register the Bash/Shell validator.
|
|
registerValidator("bash", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase =
|
|
result = newBashValidator(source, options, sourcePath)
|
|
)
|
|
registerValidator("shell", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase =
|
|
result = newBashValidator(source, options, sourcePath)
|
|
)
|
|
registerValidator("sh", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase =
|
|
result = newBashValidator(source, options, sourcePath)
|
|
)
|
|
|
|
# Auto-init
|
|
init()
|