## 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()