|
## Validatrix - Universal Source Code Validation Framework
|
|
##
|
|
## A comprehensive, extensible validation framework for all common
|
|
## programming languages and file formats. Provides tokenizer-based
|
|
## analysis, auto-detection of file flavor, JSON diagnostics, and
|
|
## consistent error reporting.
|
|
##
|
|
## Usage:
|
|
## import validatrix
|
|
## let r = validateSource("print('hello')", flavor=lfPython)
|
|
## let jsonResult = result.toJson()
|
|
##
|
|
## Or with auto-detection:
|
|
## let r = validateFile("script.py")
|
|
##
|
|
## See README.md for full documentation.
|
|
|
|
import std/[json, strutils, os, strformat, sequtils, algorithm]
|
|
|
|
# Core types and base classes — imported and re-exported for public API consumers
|
|
import validatrix/core/types
|
|
import validatrix/core/tokenizerbase
|
|
import validatrix/core/validatorbase
|
|
import validatrix/core/detector
|
|
import validatrix/core/debug
|
|
import validatrix/reporting/errors
|
|
export types
|
|
export tokenizerbase
|
|
export validatorbase
|
|
export errors
|
|
|
|
# Import all language validators (they register themselves on import)
|
|
{.warning[UnusedImport]:off.}
|
|
import validatrix/languages/nim_validator
|
|
import validatrix/languages/bash_validator
|
|
import validatrix/languages/python_validator
|
|
import validatrix/languages/javascript_validator
|
|
import validatrix/languages/php_validator
|
|
import validatrix/languages/html_validator
|
|
import validatrix/languages/jinja_validator
|
|
import validatrix/languages/config_validators
|
|
{.warning[UnusedImport]:on.}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API - Validate from source string
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc validateSource*(
|
|
source: string,
|
|
flavor: LanguageFlavor = lfUnknown,
|
|
options: ValidationOptions = newValidationOptions(),
|
|
sourcePath: string = ""
|
|
): ValidationResult =
|
|
## Validate source code from a string.
|
|
##
|
|
## If flavor is lfUnknown (default), auto-detection is performed.
|
|
## Returns a ValidationResult with errors, diagnostics, and metadata.
|
|
##
|
|
## Example:
|
|
## let r = validateSource("proc hello() = echo 'hi'", flavor=lfNim)
|
|
## let r = validateSource("var x = 1", flavor=lfUnknown) # auto-detect
|
|
|
|
when defined(validatrixDebug):
|
|
if options.debugMode and not debugEnabled:
|
|
enableDebug()
|
|
debugEnter("VALIDATRIX", "validateSource called")
|
|
|
|
try:
|
|
var opts = options
|
|
if flavor != lfUnknown:
|
|
opts.flavor = flavor
|
|
|
|
# Detect flavor if not explicitly set
|
|
var detectedFlavor = flavor
|
|
var detectionMethod = fdmExplicit
|
|
|
|
if detectedFlavor == lfUnknown:
|
|
let (df, dm, _) = detectFlavor(source, sourcePath, lfUnknown)
|
|
detectedFlavor = df
|
|
detectionMethod = dm
|
|
opts.flavor = detectedFlavor
|
|
|
|
if detectedFlavor == lfUnknown:
|
|
# Cannot determine language
|
|
result = newValidationResult()
|
|
result.valid = false
|
|
result.flavor = lfUnknown
|
|
result.detectionMethod = fdmUnknown
|
|
result.errors.add(newValidationError(
|
|
esError,
|
|
"Could not detect language flavor from source content",
|
|
ErrInvalidSyntax,
|
|
newSourcePosition(1, 1, 0),
|
|
newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)),
|
|
hint = "Specify the flavor explicitly, or add a shebang/extension"
|
|
))
|
|
result.durationMs = 0.0
|
|
return result
|
|
|
|
# Create validator and run validation
|
|
let validator = createValidator(detectedFlavor, source, opts, sourcePath)
|
|
result = validator.validate()
|
|
result.flavor = detectedFlavor
|
|
result.detectionMethod = detectionMethod
|
|
|
|
# Ensure module info is populated
|
|
result.moduleInfo.flavor = detectedFlavor
|
|
result.moduleInfo.detectionMethod = detectionMethod
|
|
|
|
except Exception as e:
|
|
result = newValidationResult()
|
|
result.valid = false
|
|
result.errors.add(newValidationError(
|
|
esCritical,
|
|
&"Validation framework error: {e.msg}",
|
|
"E9999",
|
|
newSourcePosition(0, 0, 0),
|
|
newSourceRange(newSourcePosition(0, 0, 0), newSourcePosition(0, 0, 0))
|
|
))
|
|
|
|
when defined(validatrixDebug):
|
|
if options.debugMode:
|
|
debugLeave("VALIDATRIX", &"Validation complete: valid={result.valid}, errors={result.errors.len}")
|
|
result.debugOutput = getDebugOutput()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API - Validate from file
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc validateFile*(
|
|
filePath: string,
|
|
flavor: LanguageFlavor = lfUnknown,
|
|
options: ValidationOptions = newValidationOptions()
|
|
): ValidationResult =
|
|
## Validate a source file from disk.
|
|
##
|
|
## Auto-detects flavor from file extension (and content if needed).
|
|
## Returns ValidationResult with errors, diagnostics, and metadata.
|
|
##
|
|
## Example:
|
|
## let r = validateFile("src/main.nim")
|
|
## let r = validateFile("template.html.jinja", flavor=lfJinja)
|
|
|
|
when defined(validatrixDebug):
|
|
if options.debugMode:
|
|
debugEnter("VALIDATRIX", &"validateFile: {filePath}")
|
|
|
|
try:
|
|
if not fileExists(filePath):
|
|
result = newValidationResult()
|
|
result.valid = false
|
|
result.errors.add(newValidationError(
|
|
esError,
|
|
&"File not found: {filePath}",
|
|
"E0011",
|
|
newSourcePosition(0, 0, 0),
|
|
newSourceRange(newSourcePosition(0, 0, 0), newSourcePosition(0, 0, 0))
|
|
))
|
|
return result
|
|
|
|
let source = readFile(filePath)
|
|
|
|
# If flavor is not set, detect from extension
|
|
var detectedFlavor = flavor
|
|
if detectedFlavor == lfUnknown:
|
|
let ext {.used.} = splitFile(filePath).ext.toLowerAscii()
|
|
let (df, _, _) = detectFlavor(source, filePath, lfUnknown)
|
|
detectedFlavor = df
|
|
|
|
result = validateSource(source, detectedFlavor, options, filePath)
|
|
|
|
when defined(validatrixDebug):
|
|
if options.debugMode:
|
|
debugLeave("VALIDATRIX", &"File validation complete: {result.errors.len} errors")
|
|
|
|
except Exception as e:
|
|
result = newValidationResult()
|
|
result.valid = false
|
|
result.errors.add(newValidationError(
|
|
esCritical, &"File validation error: {e.msg}", "E9999",
|
|
newSourcePosition(0, 0, 0), newSourceRange(newSourcePosition(0, 0, 0), newSourcePosition(0, 0, 0))
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API - Inspection / Diagnostics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc inspectSource*(
|
|
source: string,
|
|
flavor: LanguageFlavor = lfUnknown,
|
|
options: ValidationOptions = newValidationOptions(),
|
|
sourcePath: string = ""
|
|
): JsonNode =
|
|
## Inspect source code and return structural diagnostics as JSON.
|
|
##
|
|
## Returns a JSON object with all variables, functions, classes,
|
|
## imports, and other structural information found in the source.
|
|
##
|
|
## Example:
|
|
## let diag = inspectSource("class Foo: pass", lfPython)
|
|
## echo $diag # JSON with class/function info
|
|
|
|
let r = validateSource(source, flavor, options, sourcePath)
|
|
if r.valid or r.errors.len > 0:
|
|
r.moduleInfo.toJson()
|
|
else:
|
|
%*{"error": "Validation failed", "details": r.errors.mapIt(it.toJson())}
|
|
|
|
proc inspectFile*(
|
|
filePath: string,
|
|
flavor: LanguageFlavor = lfUnknown,
|
|
options: ValidationOptions = newValidationOptions()
|
|
): JsonNode =
|
|
## Inspect a source file and return structural diagnostics as JSON.
|
|
let r = validateFile(filePath, flavor, options)
|
|
if r.valid or r.errors.len > 0:
|
|
r.moduleInfo.toJson()
|
|
else:
|
|
%*{"error": "Validation failed", "details": r.errors.mapIt(it.toJson())}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API - Additional helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc validateSourceJson*(source: string, flavor: LanguageFlavor = lfUnknown): string =
|
|
## Validate source and return result as JSON string.
|
|
validateSource(source, flavor).toJson().pretty()
|
|
|
|
proc validateFileJson*(filePath: string, flavor: LanguageFlavor = lfUnknown): string =
|
|
## Validate file and return result as JSON string.
|
|
validateFile(filePath, flavor).toJson().pretty()
|
|
|
|
proc reportSource*(source: string, flavor: LanguageFlavor = lfUnknown): string =
|
|
## Get a human-readable validation report for source code.
|
|
let r = validateSource(source, flavor)
|
|
formatReport(r, source)
|
|
|
|
proc reportFile*(filePath: string, flavor: LanguageFlavor = lfUnknown): string =
|
|
## Get a human-readable validation report for a file.
|
|
let source = if fileExists(filePath): readFile(filePath) else: ""
|
|
let r = validateFile(filePath, flavor)
|
|
formatReport(r, source)
|
|
|
|
proc detectFlavor*(source: string, filePath: string = ""): LanguageFlavor =
|
|
## Detect language flavor of source code.
|
|
## Convenience wrapper around core detector.
|
|
let (flavor, _, _) = detector.detectFlavor(source, filePath)
|
|
return flavor
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Version information
|
|
# ---------------------------------------------------------------------------
|
|
|
|
const
|
|
validatrixVersion* = "0.1.0"
|
|
validatrixDescription* = "Universal Source Code Validation Framework"
|
|
|
|
proc version*(): string =
|
|
## Get the Validatrix version string.
|
|
validatrixVersion
|
|
|
|
proc supportedFlavors*(): seq[string] =
|
|
## Get list of all supported language flavors.
|
|
result = @[]
|
|
for key in validatorRegistry.keys:
|
|
result.add(key)
|
|
result.sort(system.cmp[string])
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Debug API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc enableDebug*() =
|
|
## Enable debug mode globally.
|
|
debug.enableDebug()
|
|
|
|
proc disableDebug*() =
|
|
## Disable debug mode globally.
|
|
debug.disableDebug()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When compiled as standalone executable
|
|
# ---------------------------------------------------------------------------
|
|
|
|
when isMainModule:
|
|
|
|
proc printUsage() =
|
|
echo &"Validatrix v{validatrixVersion} - {validatrixDescription}"
|
|
echo ""
|
|
echo "Usage:"
|
|
echo " validatrix validate [--file=<path> | --source=<code>] [--flavor=<lang>]"
|
|
echo " validatrix inspect [--file=<path> | --source=<code>] [--flavor=<lang>]"
|
|
echo " validatrix detect [--file=<path> | --source=<code>]"
|
|
echo " validatrix help"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --file=<path> Source file to process"
|
|
echo " --source=<code> Source code string to process"
|
|
echo " --flavor=<lang> Language flavor (auto-detect if omitted)"
|
|
echo " --debug Enable debug mode"
|
|
echo " --json Output as JSON (default for inspect)"
|
|
echo ""
|
|
echo "Supported flavors:"
|
|
for f in supportedFlavors():
|
|
echo &" - {f}"
|
|
|
|
proc main() =
|
|
let args = commandLineParams()
|
|
if args.len == 0:
|
|
printUsage()
|
|
return
|
|
|
|
let cmd = args[0].toLowerAscii()
|
|
case cmd
|
|
of "validate", "val":
|
|
var filePath, source, flavor = ""
|
|
var debugMode = false
|
|
var i = 1
|
|
while i < args.len:
|
|
let a = args[i]
|
|
if a.startsWith("--file="): filePath = a[7..^1]
|
|
elif a.startsWith("--source="): source = a[9..^1]
|
|
elif a.startsWith("--flavor="): flavor = a[9..^1]
|
|
elif a == "--debug": debugMode = true
|
|
i += 1
|
|
|
|
var opts = newValidationOptions()
|
|
if debugMode: opts.debugMode = true
|
|
|
|
var fl = lfUnknown
|
|
if flavor.len > 0:
|
|
try: fl = parseEnum[LanguageFlavor](flavor)
|
|
except: discard
|
|
|
|
var r: ValidationResult
|
|
if filePath.len > 0:
|
|
r = validateFile(filePath, fl, opts)
|
|
elif source.len > 0:
|
|
r = validateSource(source, fl, opts, "stdin")
|
|
else:
|
|
# Read from stdin
|
|
let input = readAll(stdin).strip()
|
|
r = validateSource(input, fl, opts, "stdin")
|
|
|
|
echo formatReport(r, r.moduleInfo.toJson().pretty())
|
|
|
|
of "inspect", "ins":
|
|
var filePath, source, flavor = ""
|
|
var debugMode = false
|
|
var i = 1
|
|
while i < args.len:
|
|
let a = args[i]
|
|
if a.startsWith("--file="): filePath = a[7..^1]
|
|
elif a.startsWith("--source="): source = a[9..^1]
|
|
elif a.startsWith("--flavor="): flavor = a[9..^1]
|
|
elif a == "--debug": debugMode = true
|
|
i += 1
|
|
|
|
var opts = newValidationOptions()
|
|
if debugMode: opts.debugMode = true
|
|
|
|
var fl = lfUnknown
|
|
if flavor.len > 0:
|
|
try: fl = parseEnum[LanguageFlavor](flavor)
|
|
except: discard
|
|
|
|
var jsonResult: JsonNode
|
|
if filePath.len > 0:
|
|
jsonResult = inspectFile(filePath, fl, opts)
|
|
elif source.len > 0:
|
|
jsonResult = inspectSource(source, fl, opts, "stdin")
|
|
else:
|
|
let input = readAll(stdin).strip()
|
|
jsonResult = inspectSource(input, fl, opts, "stdin")
|
|
|
|
echo jsonResult.pretty()
|
|
|
|
of "detect":
|
|
var filePath, source = ""
|
|
var i = 1
|
|
while i < args.len:
|
|
let a = args[i]
|
|
if a.startsWith("--file="): filePath = a[7..^1]
|
|
elif a.startsWith("--source="): source = a[9..^1]
|
|
i += 1
|
|
|
|
var src = source
|
|
if filePath.len > 0 and fileExists(filePath):
|
|
src = readFile(filePath)
|
|
|
|
let (dflavor, dmethod, dconfidence) = detector.detectFlavor(src, filePath)
|
|
echo &"Detected flavor: {dflavor}"
|
|
echo &" Method: {dmethod}"
|
|
echo &" Confidence: {dconfidence * 100:.2f}%"
|
|
|
|
of "version", "--version", "-v":
|
|
echo &"Validatrix v{validatrixVersion}"
|
|
|
|
of "help", "--help", "-h":
|
|
printUsage()
|
|
|
|
else:
|
|
echo &"Unknown command: {cmd}"
|
|
printUsage()
|
|
|
|
main()
|